We have introduced __builtin_allow_sanitize_check("name"), a builtin that returns true if the specified sanitizer is instrumenting a given function (after inlining). The builtin respects the no_sanitize attribute, and is independent of global sanitizer enablement state.
While global per-TU enablement state can already be queried with __SANITIZE_{ADDRESS,MEMORY,THREAD}__ pre-processor macros, this is insufficient where instrumented and uninstrumented functions share always_inline (or macro-based) primitives with explicit sanitizer checks in the same TU.
Motivation
This feature provides a mechanism for library developers to insert explicit sanitizer checks (e.g., validating inline assembly inputs) that correctly respect no_sanitize attributes after macro expansion and forced inlining (via always_inline).
The motivating use case comes from the Linux kernel. Low-level primitives often use inline assembly, which is opaque to sanitizers. Developers manually add checks to these primitives to avoid coverage loss. However, these helpers are frequently always_inline and shared with “noinstr” (which are no_sanitize) contexts, which are brittle contexts where any instrumentation is strictly prohibited.
Previously, using an instrumented helper in a no_sanitize context required awkward workarounds to strip the instrumentation. This builtin resolves the issue by allowing the helper to query whether checks are allowed in the current context, and after dead-code elimination no explicit checks remain.
See the Linux kernel discussion for more context.
Background: no_sanitize and Inlining
- Standard
no_sanitizeSemantics: A function markedno_sanitizeindicates that specific instrumentation must not be applied. Currently this is implemented by prohibiting inlining ano_sanitizefunction into an instrumented caller (overriding standard inlining heuristics) and vice versa. always_inlineandno_sanitize:always_inlineforces inlining, effectively causing the function to adopt the sanitization state of its caller. Respectingalways_inlinebehaviour has functional correctness implications in some cases (such as in the Linux kernel), and cannot be ignored.- Inlined into a
no_sanitizefunction → Not instrumented. - Inlined into an instrumented function → Instrumented.
- Unsupported: Specifying
no_sanitize(with either “address”, “thread”, “memory”, “hwaddress”, or their “kernel-” variants) on analways_inlinefunction is unsupported.
- Inlined into a
Specification
Signature:
bool __builtin_allow_sanitize_check(const char *sanitizer_name);
Semantics:
- Arguments: The argument is a string literal known at compile time.
- Supported values:
"address","thread","memory","hwaddress". - Implementations should also support the
"kernel-"prefixed variants (e.g.,"kernel-address") as aliases to matchno_sanitizeattribute conventions.
- Supported values:
- Return Value:
- Returns
trueif explicit checks are allowed for the specified sanitizer in the containing function after inlining. - Returns
falseif explicit checks are prohibited (e.g. sanitizer disabled via command line orno_sanitizeattribute).
- Returns
- Evaluation: The builtin is evaluated to a constant after inlining is complete, but before the final optimization passes (such as constant folding and dead-code elimination). This ensures that any code guarded by the builtin (e.g., in an
ifblock) is removed when the sanitizer is disabled.- Inlining Example: If function
A(always_inline) calls the builtin, andBcallsA, the result depends onB’s sanitizer state.
- Inlining Example: If function
Usage Example
inline __attribute__((always_inline))
void copy_to_device(void *addr, size_t size) {
if (__builtin_allow_sanitize_check("address")) {
// Explicit check using sanitizer runtime state that address range is valid.
}
// ... actual device memory copy logic ...
}
void instrumented() {
copy_to_device(buf, size); // Builtin returns true -> checks execute
}
__attribute__((no_sanitize("address")))
void uninstrumented() {
copy_to_device(buf, size); // Builtin returns false -> checks skipped
}
Implementation Notes
Since the result depends on the context after inlining, Clang cannot always evaluate this statically in the frontend:
- Sanitizer Disabled for TU: If the specified sanitizer is disabled for the TU (via command-line flags), Clang immediately folds the builtin to
false, avoiding any overhead in the middle-end. - Sanitizer Enabled for TU: If the sanitizer is enabled for the TU, Clang lowers the builtin to
llvm.allow.sanitize.*intrinsics. - Intrinsic Lowering: The LLVM intrinsics are resolved by the
LowerAllowCheckPass(running late, after inlining) into boolean constants (true/false) based on the function’s sanitization attributes.
Naming Rationale
The name __builtin_allow_sanitize_check("name") was chosen for consistency with __builtin_allow_runtime_check(..), but also describes the intent (permission to check) rather than just the state (“is active”), resolving ambiguity around global vs. local enablement and future-proofing the API against policy changes (e.g. selective sanitization). The string argument values mirror those of corresponding no_sanitize("name") respectively.