Explicit Sanitizer Checks with `__builtin_allow_sanitize_check()`

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

  1. Standard no_sanitize Semantics: A function marked no_sanitize indicates that specific instrumentation must not be applied. Currently this is implemented by prohibiting inlining a no_sanitize function into an instrumented caller (overriding standard inlining heuristics) and vice versa.
  2. always_inline and no_sanitize: always_inline forces inlining, effectively causing the function to adopt the sanitization state of its caller. Respecting always_inline behaviour has functional correctness implications in some cases (such as in the Linux kernel), and cannot be ignored.
    • Inlined into a no_sanitize function → Not instrumented.
    • Inlined into an instrumented function → Instrumented.
    • Unsupported: Specifying no_sanitize (with either “address”, “thread”, “memory”, “hwaddress”, or their “kernel-” variants) on an always_inline function is unsupported.

Specification

Signature:

bool __builtin_allow_sanitize_check(const char *sanitizer_name);

Semantics:

  1. 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 match no_sanitize attribute conventions.
  2. Return Value:
    • Returns true if explicit checks are allowed for the specified sanitizer in the containing function after inlining.
    • Returns false if explicit checks are prohibited (e.g. sanitizer disabled via command line or no_sanitize attribute).
  3. 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 if block) is removed when the sanitizer is disabled.
    • Inlining Example: If function A (always_inline) calls the builtin, and B calls A, the result depends on B’s sanitizer state.

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:

  1. 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.
  2. Sanitizer Enabled for TU: If the sanitizer is enabled for the TU, Clang lowers the builtin to llvm.allow.sanitize.* intrinsics.
  3. 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.

PR: https://github.com/llvm/llvm-project/pull/172030

Why was this not a RFC first before merging this?

Also you should have given heads up to the GCC folks on this change. Where GCC could have implemented this at the begining of their stage 3.

Why was this not a RFC first before merging this?

Some wires got crossed, and we thought it was trivial enough to be a simple PR. The post-merge post was made after this comment. Sorry for the confusion.

We can certainly revert the whole thing again and try again in a few months.

Also you should have given heads up to the GCC folks on this change. Where GCC could have implemented this at the begining of their stage 3.

Another reason for this post is to suggest this to GCC folks. But it’s relatively new, so not even a month has elapsed (it was a response to a discussion at Linux Plumbers Conference last month).

Anyways I filed Making sure you're not a bot! to add this builtin to GCC. I don’t know who is going to implement them though. I might take some time to implement the builtin over this weekend. GCC stage 4 (regression only fixes) for GCC 16 starts on Monday Jan 12th so it might be a tight squeeze there. Otherwise it is going to have to wait for GCC 17.

1 Like