Author: Marco Elver Date: 2026-01-05T19:38:44+01:00 New Revision: 0e2b16217e10fe7368443ae26647e5cc3ec66410
URL: https://github.com/llvm/llvm-project/commit/0e2b16217e10fe7368443ae26647e5cc3ec66410 DIFF: https://github.com/llvm/llvm-project/commit/0e2b16217e10fe7368443ae26647e5cc3ec66410.diff LOG: [Clang] Add __builtin_allow_sanitize_check() (#172030) Introduce `__builtin_allow_sanitize_check("name")` which returns true if the specified sanitizer is enabled for the function (after inlining). Supported sanitizers are "address", "thread", "memory", "hwaddress", and their "kernel-" variants, matching the names of the `no_sanitize("name")` usage. This builtin enables conditional execution of explicit checks only when the sanitizer is enabled, respecting `no_sanitize` attributes, even when used from `always_inline` functions that may be used in sanitized or no_sanitize functions. Since we must defer until after inlining and cannot determine the result statically, Clang must lower to the `llvm.allow.sanitize.*` intrinsics, which are then resolved by the `LowerAllowCheckPass`. *Original Motivation:* The Linux kernel has a number of low-level primitives that use inline assembly not visible to the sanitizers, but use explicitly inserted checks to avoid coverage loss. Many of those low-level helpers, however, are also used from so-called `noinstr` functions, which use `no_sanitize(..)` to prohibit instrumentation; these are used for very brittle code (such as when the kernel sets up a task context *before* normal memory is accessible), and any instrumentation, incl. from explicit instrumentation, is prohibited. Many such helpers themselves are macros or `always_inline`, however, are unable to be used from such brittle contexts because they contain explicit instrumentation. This requires awkward workarounds to avoid the instrumentation. The ideal solution is this new builtin, that can be used to determine if instrumentation is enabled in a given function or not, which the helper can then use to insert instrumentation only where instrumentation is allowed. A recent such case came up in [1], where file-level instrumentation had already been disabled for KASAN and KCSAN, which had not been necessary if the new builtin were available. [1] https://lore.kernel.org/all/[email protected]/ Added: clang/test/CodeGen/builtin-allow-sanitize-check-lower.c clang/test/CodeGen/builtin-allow-sanitize-check.c clang/test/Sema/builtin-allow-sanitize-check.c Modified: clang/docs/AddressSanitizer.rst clang/docs/MemorySanitizer.rst clang/docs/ReleaseNotes.rst clang/docs/ThreadSanitizer.rst clang/include/clang/Basic/Builtins.td clang/include/clang/Basic/DiagnosticSemaKinds.td clang/lib/CodeGen/BackendUtil.cpp clang/lib/CodeGen/CGBuiltin.cpp clang/lib/Sema/SemaChecking.cpp Removed: ################################################################################ diff --git a/clang/docs/AddressSanitizer.rst b/clang/docs/AddressSanitizer.rst index ed59afa1e0af5..dc0ba79f66cc2 100644 --- a/clang/docs/AddressSanitizer.rst +++ b/clang/docs/AddressSanitizer.rst @@ -254,6 +254,40 @@ AddressSanitizer also supports works similarly to ``__attribute__((no_sanitize("address")))``, but it also prevents instrumentation performed by other sanitizers. +Conditional Sanitizer Checks with ``__builtin_allow_sanitize_check`` +-------------------------------------------------------------------- + +The ``__builtin_allow_sanitize_check("address")`` builtin can be used to +conditionally execute code only when AddressSanitizer is active for the current +function (after inlining). This is particularly useful for inserting explicit, +sanitizer-specific checks around operations like syscalls or inline assembly, +which might otherwise be unchecked by the sanitizer. + +Example: + +.. code-block:: c + + inline __attribute__((always_inline)) + void copy_to_device(void *addr, size_t size) { + if (__builtin_allow_sanitize_check("address")) { + // Custom checks that address range is valid. + } + // ... actual device memory copy logic, potentially a syscall ... + } + + void instrumented_function() { + ... + copy_to_device(buf, sizeof(buf)); // checks are active + ... + } + + __attribute__((no_sanitize("address"))) + void uninstrumented_function() { + ... + copy_to_device(buf, sizeof(buf)); // checks are skipped + ... + } + Disabling container overflow checks ----------------------------------- diff --git a/clang/docs/MemorySanitizer.rst b/clang/docs/MemorySanitizer.rst index 4f581427c36af..54f88d076d36e 100644 --- a/clang/docs/MemorySanitizer.rst +++ b/clang/docs/MemorySanitizer.rst @@ -101,6 +101,40 @@ positives and therefore should be used with care, and only if absolutely required; for example for certain code that cannot tolerate any instrumentation and resulting side-effects. This attribute overrides ``no_sanitize("memory")``. +Conditional Sanitizer Checks with ``__builtin_allow_sanitize_check`` +-------------------------------------------------------------------- + +The ``__builtin_allow_sanitize_check("memory")`` builtin can be used to +conditionally execute code only when MemorySanitizer is active for the current +function (after inlining). This is particularly useful for inserting explicit, +sanitizer-specific checks around operations like syscalls or inline assembly, +which might otherwise be unchecked by the sanitizer. + +Example: + +.. code-block:: c + + inline __attribute__((always_inline)) + void copy_to_device(void *addr, size_t size) { + if (__builtin_allow_sanitize_check("memory")) { + // Custom checks if `data` is initialized. + } + // ... actual device memory copy logic, potentially a syscall ... + } + + void instrumented_function() { + ... + copy_to_device(buf, sizeof(buf)); // checks are active + ... + } + + __attribute__((no_sanitize("memory"))) + void uninstrumented_function() { + ... + copy_to_device(buf, sizeof(buf)); // checks are skipped + ... + } + Ignorelist ---------- diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index 6de27b1366c06..411cc348d4caf 100644 --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -833,6 +833,13 @@ Moved checkers Sanitizers ---------- - Improved documentation for legacy ``no_sanitize`` attributes. +- Added ``__builtin_allow_sanitize_check("name")`` that returns true if the + specified sanitizer is enabled for the current function (after inlining). + This allows for conditional code execution based on sanitizer enablement, + respecting ``no_sanitize`` attributes. It currently supports sanitizers: + "address", "kernel-address", "hwaddress", "kernel-hwaddress", "memory", + "kernel-memory", and "thread". + Python Binding Changes ---------------------- diff --git a/clang/docs/ThreadSanitizer.rst b/clang/docs/ThreadSanitizer.rst index 5dc78fa5a7a56..c90d668600ec9 100644 --- a/clang/docs/ThreadSanitizer.rst +++ b/clang/docs/ThreadSanitizer.rst @@ -110,6 +110,40 @@ and only if absolutely required; for example for certain code that cannot tolerate any instrumentation and resulting side-effects. This attribute overrides ``no_sanitize("thread")``. +Conditional Sanitizer Checks with ``__builtin_allow_sanitize_check`` +-------------------------------------------------------------------- + +The ``__builtin_allow_sanitize_check("thread")`` builtin can be used to +conditionally execute code only when ThreadSanitizer is active for the current +function (after inlining). This is particularly useful for inserting explicit, +sanitizer-specific checks around operations like syscalls or inline assembly, +which might otherwise be unchecked by the sanitizer. + +Example: + +.. code-block:: c + + inline __attribute__((always_inline)) + void copy_to_device(void *addr, size_t size) { + if (__builtin_allow_sanitize_check("thread")) { + // Custom checks that `data` is not concurrently modified. + } + // ... actual device memory copy logic, potentially a syscall ... + } + + void instrumented_function() { + ... + copy_to_device(&shared_data, size); // checks are active + ... + } + + __attribute__((no_sanitize("thread"))) + void uninstrumented_function() { + ... + copy_to_device(&shared_data, size); // checks are skipped + ... + } + Ignorelist ---------- diff --git a/clang/include/clang/Basic/Builtins.td b/clang/include/clang/Basic/Builtins.td index aab2418511399..a5be656cdacf4 100644 --- a/clang/include/clang/Basic/Builtins.td +++ b/clang/include/clang/Basic/Builtins.td @@ -1226,6 +1226,12 @@ def AllowRuntimeCheck : Builtin { let Prototype = "bool(char const*)"; } +def BuiltinAllowSanitizeCheck : Builtin { + let Spellings = ["__builtin_allow_sanitize_check"]; + let Attributes = [NoThrow, Const, CustomTypeChecking]; + let Prototype = "bool(char const*)"; +} + def ShuffleVector : Builtin { let Spellings = ["__builtin_shufflevector"]; let Attributes = [NoThrow, Const, CustomTypeChecking, Constexpr]; diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 4f7934b328d9f..f3c13841c4bf0 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -838,6 +838,8 @@ def warn_redecl_library_builtin : Warning< InGroup<DiagGroup<"incompatible-library-redeclaration">>; def err_builtin_definition : Error<"definition of builtin function %0">; def err_builtin_redeclare : Error<"cannot redeclare builtin function %0">; +def err_invalid_builtin_argument : Error<"invalid argument '%0' to %1">; + def err_arm_invalid_specialreg : Error<"invalid special register for builtin">; def err_arm_invalid_coproc : Error<"coprocessor %0 must be configured as " "%select{GCP|CDE}1">; diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp index c11b13de15d91..e4b64a0c2ff48 100644 --- a/clang/lib/CodeGen/BackendUtil.cpp +++ b/clang/lib/CodeGen/BackendUtil.cpp @@ -810,15 +810,27 @@ static void addSanitizers(const Triple &TargetTriple, // LastEP does not need GlobalsAA. PB.registerOptimizerLastEPCallback(SanitizersCallback); } +} +void addLowerAllowCheckPass(const CodeGenOptions &CodeGenOpts, + const LangOptions &LangOpts, PassBuilder &PB) { // SanitizeSkipHotCutoffs: doubles with range [0, 1] // Opts.cutoffs: unsigned ints with range [0, 1000000] auto ScaledCutoffs = CodeGenOpts.SanitizeSkipHotCutoffs.getAllScaled(1000000); uint64_t AllowRuntimeCheckSkipHotCutoff = CodeGenOpts.AllowRuntimeCheckSkipHotCutoff.value_or(0.0) * 1000000; + // Only register the pass if one of the relevant sanitizers is enabled. + // This avoids pipeline overhead for builds that do not use these sanitizers. + bool LowerAllowSanitize = LangOpts.Sanitize.hasOneOf( + SanitizerKind::Address | SanitizerKind::KernelAddress | + SanitizerKind::Thread | SanitizerKind::Memory | + SanitizerKind::KernelMemory | SanitizerKind::HWAddress | + SanitizerKind::KernelHWAddress); + // TODO: remove IsRequested() if (LowerAllowCheckPass::IsRequested() || ScaledCutoffs.has_value() || - CodeGenOpts.AllowRuntimeCheckSkipHotCutoff.has_value()) { + CodeGenOpts.AllowRuntimeCheckSkipHotCutoff.has_value() || + LowerAllowSanitize) { // We want to call it after inline, which is about OptimizerEarlyEPCallback. PB.registerOptimizerEarlyEPCallback( [ScaledCutoffs, AllowRuntimeCheckSkipHotCutoff]( @@ -1084,6 +1096,7 @@ void EmitAssemblyHelper::RunOptimizationPipeline( // Most sanitizers only run during PreLink stage. addSanitizers(TargetTriple, CodeGenOpts, LangOpts, PB); addKCFIPass(TargetTriple, LangOpts, PB); + addLowerAllowCheckPass(CodeGenOpts, LangOpts, PB); PB.registerPipelineStartEPCallback( [&](ModulePassManager &MPM, OptimizationLevel Level) { diff --git a/clang/lib/CodeGen/CGBuiltin.cpp b/clang/lib/CodeGen/CGBuiltin.cpp index b12f48618335e..f650fb3700669 100644 --- a/clang/lib/CodeGen/CGBuiltin.cpp +++ b/clang/lib/CodeGen/CGBuiltin.cpp @@ -3552,6 +3552,41 @@ RValue CodeGenFunction::EmitBuiltinExpr(const GlobalDecl GD, unsigned BuiltinID, llvm::MetadataAsValue::get(Ctx, llvm::MDString::get(Ctx, Kind))); return RValue::get(Allow); } + case Builtin::BI__builtin_allow_sanitize_check: { + Intrinsic::ID IntrID = Intrinsic::not_intrinsic; + StringRef Name = + cast<StringLiteral>(E->getArg(0)->IgnoreParenCasts())->getString(); + + // We deliberately allow the use of kernel- and non-kernel names + // interchangably, even when one or the other is enabled. This is consistent + // with the no_sanitize-attribute, which allows either kernel- or non-kernel + // name to disable instrumentation (see CodeGenFunction::StartFunction). + if (getLangOpts().Sanitize.hasOneOf(SanitizerKind::Address | + SanitizerKind::KernelAddress) && + (Name == "address" || Name == "kernel-address")) { + IntrID = Intrinsic::allow_sanitize_address; + } else if (getLangOpts().Sanitize.has(SanitizerKind::Thread) && + Name == "thread") { + IntrID = Intrinsic::allow_sanitize_thread; + } else if (getLangOpts().Sanitize.hasOneOf(SanitizerKind::Memory | + SanitizerKind::KernelMemory) && + (Name == "memory" || Name == "kernel-memory")) { + IntrID = Intrinsic::allow_sanitize_memory; + } else if (getLangOpts().Sanitize.hasOneOf( + SanitizerKind::HWAddress | SanitizerKind::KernelHWAddress) && + (Name == "hwaddress" || Name == "kernel-hwaddress")) { + IntrID = Intrinsic::allow_sanitize_hwaddress; + } + + if (IntrID != Intrinsic::not_intrinsic) { + llvm::Value *Allow = Builder.CreateCall(CGM.getIntrinsic(IntrID)); + return RValue::get(Allow); + } + // If the checked sanitizer is not enabled, we can safely lower to false + // right away. This is also more efficient, since the LowerAllowCheckPass + // must not always be enabled if none of the above sanitizers are enabled. + return RValue::get(Builder.getFalse()); + } case Builtin::BI__arithmetic_fence: { // Create the builtin call if FastMath is selected, and the target // supports the builtin, otherwise just return the argument. diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp index 2ba5dc37eb8a4..ba4b25961d70d 100644 --- a/clang/lib/Sema/SemaChecking.cpp +++ b/clang/lib/Sema/SemaChecking.cpp @@ -3587,6 +3587,30 @@ Sema::CheckBuiltinFunctionCall(FunctionDecl *FDecl, unsigned BuiltinID, } break; } + + case Builtin::BI__builtin_allow_sanitize_check: { + Expr *Arg = TheCall->getArg(0); + // Check if the argument is a string literal. + const StringLiteral *SanitizerName = + dyn_cast<StringLiteral>(Arg->IgnoreParenImpCasts()); + if (!SanitizerName) { + Diag(TheCall->getBeginLoc(), diag::err_expr_not_string_literal) + << Arg->getSourceRange(); + return ExprError(); + } + // Validate the sanitizer name. + if (!llvm::StringSwitch<bool>(SanitizerName->getString()) + .Cases({"address", "thread", "memory", "hwaddress", + "kernel-address", "kernel-memory", "kernel-hwaddress"}, + true) + .Default(false)) { + Diag(TheCall->getBeginLoc(), diag::err_invalid_builtin_argument) + << SanitizerName->getString() << "__builtin_allow_sanitize_check" + << Arg->getSourceRange(); + return ExprError(); + } + break; + } case Builtin::BI__builtin_counted_by_ref: if (BuiltinCountedByRef(TheCall)) return ExprError(); diff --git a/clang/test/CodeGen/builtin-allow-sanitize-check-lower.c b/clang/test/CodeGen/builtin-allow-sanitize-check-lower.c new file mode 100644 index 0000000000000..5e52f77f55573 --- /dev/null +++ b/clang/test/CodeGen/builtin-allow-sanitize-check-lower.c @@ -0,0 +1,23 @@ +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=address -emit-llvm -O0 -o - %s | FileCheck %s +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=address -emit-llvm -O1 -o - %s | FileCheck %s +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=address -emit-llvm -O2 -o - %s | FileCheck %s + +// CHECK-NOT: call{{.*}} @llvm.allow.sanitize.address + +__attribute__((always_inline)) +_Bool check() { + return __builtin_allow_sanitize_check("address"); +} + +// CHECK-LABEL: @test_sanitize +// CHECK: ret i1 true +_Bool test_sanitize() { + return check(); +} + +// CHECK-LABEL: @test_no_sanitize +// CHECK: ret i1 false +__attribute__((no_sanitize("address"))) +_Bool test_no_sanitize() { + return check(); +} diff --git a/clang/test/CodeGen/builtin-allow-sanitize-check.c b/clang/test/CodeGen/builtin-allow-sanitize-check.c new file mode 100644 index 0000000000000..d535885bd2ddb --- /dev/null +++ b/clang/test/CodeGen/builtin-allow-sanitize-check.c @@ -0,0 +1,78 @@ +// RUN: %clang_cc1 -triple x86_64-linux-gnu -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,NONE +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=address -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,ASAN +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=kernel-address -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,ASAN +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=thread -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,TSAN +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=memory -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,MSAN +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=kernel-memory -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,MSAN +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=hwaddress -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,HWASAN +// RUN: %clang_cc1 -triple x86_64-linux-gnu -fsanitize=kernel-hwaddress -emit-llvm -disable-llvm-passes -o - %s | FileCheck %s --check-prefixes=CHECK,HWASAN + +// CHECK-LABEL: @test_address +// NONE: ret i1 false +// ASAN: call i1 @llvm.allow.sanitize.address() +// TSAN: ret i1 false +// MSAN: ret i1 false +// HWASAN: ret i1 false +_Bool test_address() { + return __builtin_allow_sanitize_check("address"); +} + +// CHECK-LABEL: @test_kernel_address +// NONE: ret i1 false +// ASAN: call i1 @llvm.allow.sanitize.address() +// TSAN: ret i1 false +// MSAN: ret i1 false +// HWASAN: ret i1 false +_Bool test_kernel_address() { + return __builtin_allow_sanitize_check("kernel-address"); +} + +// CHECK-LABEL: @test_thread +// NONE: ret i1 false +// ASAN: ret i1 false +// TSAN: call i1 @llvm.allow.sanitize.thread() +// MSAN: ret i1 false +// HWASAN: ret i1 false +_Bool test_thread() { + return __builtin_allow_sanitize_check("thread"); +} + +// CHECK-LABEL: @test_memory +// NONE: ret i1 false +// ASAN: ret i1 false +// TSAN: ret i1 false +// MSAN: call i1 @llvm.allow.sanitize.memory() +// HWASAN: ret i1 false +_Bool test_memory() { + return __builtin_allow_sanitize_check("memory"); +} + +// CHECK-LABEL: @test_kernel_memory +// NONE: ret i1 false +// ASAN: ret i1 false +// TSAN: ret i1 false +// MSAN: call i1 @llvm.allow.sanitize.memory() +// HWASAN: ret i1 false +_Bool test_kernel_memory() { + return __builtin_allow_sanitize_check("kernel-memory"); +} + +// CHECK-LABEL: @test_hwaddress +// NONE: ret i1 false +// ASAN: ret i1 false +// TSAN: ret i1 false +// MSAN: ret i1 false +// HWASAN: call i1 @llvm.allow.sanitize.hwaddress() +_Bool test_hwaddress() { + return __builtin_allow_sanitize_check("hwaddress"); +} + +// CHECK-LABEL: @test_kernel_hwaddress +// NONE: ret i1 false +// ASAN: ret i1 false +// TSAN: ret i1 false +// MSAN: ret i1 false +// HWASAN: call i1 @llvm.allow.sanitize.hwaddress() +_Bool test_kernel_hwaddress() { + return __builtin_allow_sanitize_check("kernel-hwaddress"); +} diff --git a/clang/test/Sema/builtin-allow-sanitize-check.c b/clang/test/Sema/builtin-allow-sanitize-check.c new file mode 100644 index 0000000000000..6e0e21a869461 --- /dev/null +++ b/clang/test/Sema/builtin-allow-sanitize-check.c @@ -0,0 +1,24 @@ +// RUN: %clang_cc1 -fsyntax-only -verify %s + +void test_builtin_allow_sanitize_check() { + // Test with non-string literal argument. + char str[] = "address"; + (void)__builtin_allow_sanitize_check(str); // expected-error {{expression is not a string literal}} + (void)__builtin_allow_sanitize_check(123); // expected-error {{expression is not a string literal}} + + // Test with unsupported sanitizer name. + (void)__builtin_allow_sanitize_check("unsupported"); // expected-error {{invalid argument 'unsupported' to __builtin_allow_sanitize_check}} + + // Test with supported sanitizer names. + (void)__builtin_allow_sanitize_check("address"); + (void)__builtin_allow_sanitize_check("thread"); + (void)__builtin_allow_sanitize_check("memory"); + (void)__builtin_allow_sanitize_check("hwaddress"); + (void)__builtin_allow_sanitize_check("kernel-address"); + (void)__builtin_allow_sanitize_check("kernel-memory"); + (void)__builtin_allow_sanitize_check("kernel-hwaddress"); +} + +#if !__has_builtin(__builtin_allow_sanitize_check) +#error "missing __builtin_allow_sanitize_check" +#endif _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
