Commit b3537d0f4a

Alex Rønne Petersen <alex@alexrp.com>
2025-04-16 02:44:55
compiler: Allow configuring UBSan mode at the module level.
* Accept -fsanitize-c=trap|full in addition to the existing form. * Accept -f(no-)sanitize-trap=undefined in zig cc. * Change type of std.Build.Module.sanitize_c to std.zig.SanitizeC. * Add some missing Compilation.Config fields to the cache. Closes #23216.
1 parent 23440fb
lib/std/Build/Module.zig
@@ -22,7 +22,7 @@ unwind_tables: ?std.builtin.UnwindTables,
 single_threaded: ?bool,
 stack_protector: ?bool,
 stack_check: ?bool,
-sanitize_c: ?bool,
+sanitize_c: ?std.zig.SanitizeC,
 sanitize_thread: ?bool,
 fuzz: ?bool,
 code_model: std.builtin.CodeModel,
@@ -256,7 +256,7 @@ pub const CreateOptions = struct {
     code_model: std.builtin.CodeModel = .default,
     stack_protector: ?bool = null,
     stack_check: ?bool = null,
-    sanitize_c: ?bool = null,
+    sanitize_c: ?std.zig.SanitizeC = null,
     sanitize_thread: ?bool = null,
     fuzz: ?bool = null,
     /// Whether to emit machine code that integrates with Valgrind.
@@ -559,13 +559,18 @@ pub fn appendZigProcessFlags(
     try addFlag(zig_args, m.stack_protector, "-fstack-protector", "-fno-stack-protector");
     try addFlag(zig_args, m.omit_frame_pointer, "-fomit-frame-pointer", "-fno-omit-frame-pointer");
     try addFlag(zig_args, m.error_tracing, "-ferror-tracing", "-fno-error-tracing");
-    try addFlag(zig_args, m.sanitize_c, "-fsanitize-c", "-fno-sanitize-c");
     try addFlag(zig_args, m.sanitize_thread, "-fsanitize-thread", "-fno-sanitize-thread");
     try addFlag(zig_args, m.fuzz, "-ffuzz", "-fno-fuzz");
     try addFlag(zig_args, m.valgrind, "-fvalgrind", "-fno-valgrind");
     try addFlag(zig_args, m.pic, "-fPIC", "-fno-PIC");
     try addFlag(zig_args, m.red_zone, "-mred-zone", "-mno-red-zone");
 
+    if (m.sanitize_c) |sc| switch (sc) {
+        .off => try zig_args.append("-fno-sanitize-c"),
+        .trap => try zig_args.append("-fsanitize-c=trap"),
+        .full => try zig_args.append("-fsanitize-c=full"),
+    };
+
     if (m.dwarf_format) |dwarf_format| {
         try zig_args.append(switch (dwarf_format) {
             .@"32" => "-gdwarf32",
lib/std/zig.zig
@@ -236,6 +236,12 @@ pub fn binNameAlloc(allocator: Allocator, options: BinNameOptions) error{OutOfMe
     }
 }
 
+pub const SanitizeC = enum {
+    off,
+    trap,
+    full,
+};
+
 pub const BuildId = union(enum) {
     none,
     fast,
src/Compilation/Config.zig
@@ -32,7 +32,7 @@ any_non_single_threaded: bool,
 /// per-Module setting.
 any_error_tracing: bool,
 any_sanitize_thread: bool,
-any_sanitize_c: bool,
+any_sanitize_c: std.zig.SanitizeC,
 any_fuzz: bool,
 pie: bool,
 /// If this is true then linker code is responsible for making an LLVM IR
@@ -86,7 +86,7 @@ pub const Options = struct {
     ensure_libcpp_on_non_freestanding: bool = false,
     any_non_single_threaded: bool = false,
     any_sanitize_thread: bool = false,
-    any_sanitize_c: bool = false,
+    any_sanitize_c: std.zig.SanitizeC = .off,
     any_fuzz: bool = false,
     any_unwind_tables: bool = false,
     any_dyn_libs: bool = false,
src/Package/Module.zig
@@ -24,7 +24,7 @@ omit_frame_pointer: bool,
 stack_check: bool,
 stack_protector: u32,
 red_zone: bool,
-sanitize_c: bool,
+sanitize_c: std.zig.SanitizeC,
 sanitize_thread: bool,
 fuzz: bool,
 unwind_tables: std.builtin.UnwindTables,
@@ -92,7 +92,7 @@ pub const CreateOptions = struct {
         stack_protector: ?u32 = null,
         red_zone: ?bool = null,
         unwind_tables: ?std.builtin.UnwindTables = null,
-        sanitize_c: ?bool = null,
+        sanitize_c: ?std.zig.SanitizeC = null,
         sanitize_thread: ?bool = null,
         fuzz: ?bool = null,
         structured_cfg: ?bool = null,
@@ -113,6 +113,7 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
     if (options.inherited.fuzz == true) assert(options.global.any_fuzz);
     if (options.inherited.single_threaded == false) assert(options.global.any_non_single_threaded);
     if (options.inherited.unwind_tables) |uwt| if (uwt != .none) assert(options.global.any_unwind_tables);
+    if (options.inherited.sanitize_c) |sc| if (sc != .off) assert(options.global.any_sanitize_c != .off);
     if (options.inherited.error_tracing == true) assert(options.global.any_error_tracing);
 
     const resolved_target = options.inherited.resolved_target orelse options.parent.?.resolved_target;
@@ -249,10 +250,18 @@ pub fn create(arena: Allocator, options: CreateOptions) !*Package.Module {
         .ReleaseFast, .ReleaseSmall => false,
     };
 
-    const sanitize_c = b: {
+    const sanitize_c: std.zig.SanitizeC = b: {
         if (options.inherited.sanitize_c) |x| break :b x;
         if (options.parent) |p| break :b p.sanitize_c;
-        break :b is_safe_mode;
+        break :b switch (optimize_mode) {
+            .Debug => .full,
+            // It's recommended to use the minimal runtime in production
+            // environments due to the security implications of the full runtime.
+            // The minimal runtime doesn't provide much benefit over simply
+            // trapping, however, so we do that instead.
+            .ReleaseSafe => .trap,
+            .ReleaseFast, .ReleaseSmall => .off,
+        };
     };
 
     const stack_check = b: {
src/clang_options_data.zig
@@ -3682,7 +3682,14 @@ flagpd1("fno-sanitize-stats"),
 flagpd1("fno-sanitize-thread-atomics"),
 flagpd1("fno-sanitize-thread-func-entry-exit"),
 flagpd1("fno-sanitize-thread-memory-access"),
-flagpd1("fno-sanitize-trap"),
+.{
+    .name = "fno-sanitize-trap",
+    .syntax = .flag,
+    .zig_equivalent = .no_sanitize_trap,
+    .pd1 = true,
+    .pd2 = false,
+    .psl = false,
+},
 flagpd1("fno-sanitize-undefined-trap-on-error"),
 flagpd1("fno-save-main-program"),
 flagpd1("fno-save-optimization-record"),
@@ -4024,7 +4031,14 @@ flagpd1("fsanitize-stats"),
 flagpd1("fsanitize-thread-atomics"),
 flagpd1("fsanitize-thread-func-entry-exit"),
 flagpd1("fsanitize-thread-memory-access"),
-flagpd1("fsanitize-trap"),
+.{
+    .name = "fsanitize-trap",
+    .syntax = .flag,
+    .zig_equivalent = .sanitize_trap,
+    .pd1 = true,
+    .pd2 = false,
+    .psl = false,
+},
 flagpd1("fsanitize-undefined-trap-on-error"),
 flagpd1("fsave-main-program"),
 flagpd1("fsave-optimization-record"),
@@ -6592,7 +6606,7 @@ joinpd1("fmacro-prefix-map="),
 .{
     .name = "fno-sanitize-trap=",
     .syntax = .comma_joined,
-    .zig_equivalent = .other,
+    .zig_equivalent = .no_sanitize_trap,
     .pd1 = true,
     .pd2 = false,
     .psl = false,
@@ -6864,7 +6878,7 @@ joinpd1("frecord-marker="),
 .{
     .name = "fsanitize-trap=",
     .syntax = .comma_joined,
-    .zig_equivalent = .other,
+    .zig_equivalent = .sanitize_trap,
     .pd1 = true,
     .pd2 = false,
     .psl = false,
src/Compilation.zig
@@ -1287,7 +1287,14 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
         const any_unwind_tables = options.config.any_unwind_tables or options.root_mod.unwind_tables != .none;
         const any_non_single_threaded = options.config.any_non_single_threaded or !options.root_mod.single_threaded;
         const any_sanitize_thread = options.config.any_sanitize_thread or options.root_mod.sanitize_thread;
-        const any_sanitize_c = options.config.any_sanitize_c or options.root_mod.sanitize_c;
+        const any_sanitize_c: std.zig.SanitizeC = switch (options.config.any_sanitize_c) {
+            .off => options.root_mod.sanitize_c,
+            .trap => if (options.root_mod.sanitize_c == .full)
+                .full
+            else
+                .trap,
+            .full => .full,
+        };
         const any_fuzz = options.config.any_fuzz or options.root_mod.fuzz;
 
         const link_eh_frame_hdr = options.link_eh_frame_hdr or any_unwind_tables;
@@ -1346,7 +1353,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
         // and this reduces unnecessary bloat.
         const ubsan_rt_strat: RtStrat = s: {
             const is_spirv = options.root_mod.resolved_target.result.cpu.arch.isSpirV();
-            const want_ubsan_rt = options.want_ubsan_rt orelse (!is_spirv and any_sanitize_c and is_exe_or_dyn_lib);
+            const want_ubsan_rt = options.want_ubsan_rt orelse (!is_spirv and any_sanitize_c == .full and is_exe_or_dyn_lib);
             if (!want_ubsan_rt) break :s .none;
             if (options.skip_linker_dependencies) break :s .none;
             if (have_zcu) break :s .zcu;
@@ -1418,6 +1425,10 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
         cache.hash.add(options.config.lto);
         cache.hash.add(options.config.link_mode);
         cache.hash.add(options.config.any_unwind_tables);
+        cache.hash.add(options.config.any_non_single_threaded);
+        cache.hash.add(options.config.any_sanitize_thread);
+        cache.hash.add(options.config.any_sanitize_c);
+        cache.hash.add(options.config.any_fuzz);
         cache.hash.add(options.function_sections);
         cache.hash.add(options.data_sections);
         cache.hash.add(link_libc);
@@ -6048,7 +6059,7 @@ pub fn addCCArgs(
             {
                 var san_arg: std.ArrayListUnmanaged(u8) = .empty;
                 const prefix = "-fsanitize=";
-                if (mod.sanitize_c) {
+                if (mod.sanitize_c != .off) {
                     if (san_arg.items.len == 0) try san_arg.appendSlice(arena, prefix);
                     try san_arg.appendSlice(arena, "undefined,");
                 }
@@ -6064,37 +6075,33 @@ pub fn addCCArgs(
                 if (san_arg.pop()) |_| {
                     try argv.append(san_arg.items);
 
-                    // These args have to be added after the `-fsanitize` arg or
-                    // they won't take effect.
-                    if (mod.sanitize_c) {
-                        // This check requires implementing the Itanium C++ ABI.
-                        // We would make it `-fsanitize-trap=vptr`, however this check requires
-                        // a full runtime due to the type hashing involved.
-                        try argv.append("-fno-sanitize=vptr");
-
-                        // It is very common, and well-defined, for a pointer on one side of a C ABI
-                        // to have a different but compatible element type. Examples include:
-                        // `char*` vs `uint8_t*` on a system with 8-bit bytes
-                        // `const char*` vs `char*`
-                        // `char*` vs `unsigned char*`
-                        // Without this flag, Clang would invoke UBSAN when such an extern
-                        // function was called.
-                        try argv.append("-fno-sanitize=function");
-
-                        if (mod.optimize_mode == .ReleaseSafe) {
-                            // It's recommended to use the minimal runtime in production
-                            // environments due to the security implications of the full runtime.
-                            // The minimal runtime doesn't provide much benefit over simply
-                            // trapping, however, so we do that instead.
+                    switch (mod.sanitize_c) {
+                        .off => {},
+                        .trap => {
                             try argv.append("-fsanitize-trap=undefined");
-                        } else {
+                        },
+                        .full => {
+                            // This check requires implementing the Itanium C++ ABI.
+                            // We would make it `-fsanitize-trap=vptr`, however this check requires
+                            // a full runtime due to the type hashing involved.
+                            try argv.append("-fno-sanitize=vptr");
+
+                            // It is very common, and well-defined, for a pointer on one side of a C ABI
+                            // to have a different but compatible element type. Examples include:
+                            // `char*` vs `uint8_t*` on a system with 8-bit bytes
+                            // `const char*` vs `char*`
+                            // `char*` vs `unsigned char*`
+                            // Without this flag, Clang would invoke UBSAN when such an extern
+                            // function was called.
+                            try argv.append("-fno-sanitize=function");
+
                             // This is necessary because, by default, Clang instructs LLVM to embed
                             // a COFF link dependency on `libclang_rt.ubsan_standalone.a` when the
                             // UBSan runtime is used.
                             if (target.os.tag == .windows) {
                                 try argv.append("-fno-rtlib-defaultlib");
                             }
-                        }
+                        },
                     }
                 }
 
@@ -6797,7 +6804,7 @@ pub fn build_crt_file(
             .strip = comp.compilerRtStrip(),
             .stack_check = false,
             .stack_protector = 0,
-            .sanitize_c = false,
+            .sanitize_c = .off,
             .sanitize_thread = false,
             .red_zone = comp.root_mod.red_zone,
             // Some libcs (e.g. musl) are opinionated about -fomit-frame-pointer.
src/glibc.zig
@@ -1231,7 +1231,7 @@ fn buildSharedLib(
             .strip = strip,
             .stack_check = false,
             .stack_protector = 0,
-            .sanitize_c = false,
+            .sanitize_c = .off,
             .sanitize_thread = false,
             .red_zone = comp.root_mod.red_zone,
             .omit_frame_pointer = comp.root_mod.omit_frame_pointer,
src/libcxx.zig
@@ -182,7 +182,7 @@ pub fn buildLibCxx(comp: *Compilation, prog_node: std.Progress.Node) BuildError!
             .strip = strip,
             .stack_check = false,
             .stack_protector = 0,
-            .sanitize_c = false,
+            .sanitize_c = .off,
             .sanitize_thread = comp.config.any_sanitize_thread,
             .red_zone = comp.root_mod.red_zone,
             .omit_frame_pointer = comp.root_mod.omit_frame_pointer,
@@ -396,7 +396,7 @@ pub fn buildLibCxxAbi(comp: *Compilation, prog_node: std.Progress.Node) BuildErr
             .strip = strip,
             .stack_check = false,
             .stack_protector = 0,
-            .sanitize_c = false,
+            .sanitize_c = .off,
             .sanitize_thread = comp.config.any_sanitize_thread,
             .red_zone = comp.root_mod.red_zone,
             .omit_frame_pointer = comp.root_mod.omit_frame_pointer,
src/libtsan.zig
@@ -95,7 +95,7 @@ pub fn buildTsan(comp: *Compilation, prog_node: std.Progress.Node) BuildError!vo
             .strip = strip,
             .stack_check = false,
             .stack_protector = 0,
-            .sanitize_c = false,
+            .sanitize_c = .off,
             .sanitize_thread = false,
             .red_zone = comp.root_mod.red_zone,
             .omit_frame_pointer = optimize_mode != .Debug and !target.os.tag.isDarwin(),
src/libunwind.zig
@@ -64,7 +64,7 @@ pub fn buildStaticLib(comp: *Compilation, prog_node: std.Progress.Node) BuildErr
             .red_zone = comp.root_mod.red_zone,
             .omit_frame_pointer = comp.root_mod.omit_frame_pointer,
             .valgrind = false,
-            .sanitize_c = false,
+            .sanitize_c = .off,
             .sanitize_thread = false,
             // necessary so that libunwind can unwind through its own stack frames
             // The old 32-bit x86 variant of SEH doesn't use tables.
src/main.zig
@@ -526,7 +526,9 @@ const usage_build_generic =
     \\  -fno-stack-protector      Disable stack protection in safe builds
     \\  -fvalgrind                Include valgrind client requests in release builds
     \\  -fno-valgrind             Omit valgrind client requests in debug builds
-    \\  -fsanitize-c              Enable C undefined behavior detection in unsafe builds
+    \\  -fsanitize-c[=mode]       Enable C undefined behavior detection in unsafe builds
+    \\    trap                    Insert trap instructions on undefined behavior
+    \\    full                    (Default) Insert runtime calls on undefined behavior
     \\  -fno-sanitize-c           Disable C undefined behavior detection in safe builds
     \\  -fsanitize-thread         Enable Thread Sanitizer
     \\  -fno-sanitize-thread      Disable Thread Sanitizer
@@ -1464,9 +1466,18 @@ fn buildOutputType(
                     } else if (mem.eql(u8, arg, "-fno-omit-frame-pointer")) {
                         mod_opts.omit_frame_pointer = false;
                     } else if (mem.eql(u8, arg, "-fsanitize-c")) {
-                        mod_opts.sanitize_c = true;
+                        mod_opts.sanitize_c = .full;
+                    } else if (mem.startsWith(u8, arg, "-fsanitize-c=")) {
+                        const mode = arg["-fsanitize-c=".len..];
+                        if (mem.eql(u8, mode, "trap")) {
+                            mod_opts.sanitize_c = .trap;
+                        } else if (mem.eql(u8, mode, "full")) {
+                            mod_opts.sanitize_c = .full;
+                        } else {
+                            fatal("Invalid -fsanitize-c mode: '{s}'. Must be 'trap' or 'full'.", .{mode});
+                        }
                     } else if (mem.eql(u8, arg, "-fno-sanitize-c")) {
-                        mod_opts.sanitize_c = false;
+                        mod_opts.sanitize_c = .off;
                     } else if (mem.eql(u8, arg, "-fvalgrind")) {
                         mod_opts.valgrind = true;
                     } else if (mem.eql(u8, arg, "-fno-valgrind")) {
@@ -2236,7 +2247,7 @@ fn buildOutputType(
                         var recognized_any = false;
                         while (san_it.next()) |sub_arg| {
                             if (mem.eql(u8, sub_arg, "undefined")) {
-                                mod_opts.sanitize_c = enable;
+                                mod_opts.sanitize_c = if (enable) .full else .off;
                                 recognized_any = true;
                             } else if (mem.eql(u8, sub_arg, "thread")) {
                                 mod_opts.sanitize_thread = enable;
@@ -2250,6 +2261,49 @@ fn buildOutputType(
                             try cc_argv.appendSlice(arena, it.other_args);
                         }
                     },
+                    .sanitize_trap, .no_sanitize_trap => |t| {
+                        const enable = t == .sanitize_trap;
+                        var san_it = std.mem.splitScalar(u8, it.only_arg, ',');
+                        var recognized_any = false;
+                        while (san_it.next()) |sub_arg| {
+                            // This logic doesn't match Clang 1:1, but it's probably good enough, and avoids
+                            // significantly complicating the resolution of the options.
+                            if (mem.eql(u8, sub_arg, "undefined")) {
+                                if (mod_opts.sanitize_c) |sc| switch (sc) {
+                                    .off => if (enable) {
+                                        mod_opts.sanitize_c = .trap;
+                                    },
+                                    .trap => if (!enable) {
+                                        mod_opts.sanitize_c = .full;
+                                    },
+                                    .full => if (enable) {
+                                        mod_opts.sanitize_c = .trap;
+                                    },
+                                } else {
+                                    if (enable) {
+                                        mod_opts.sanitize_c = .trap;
+                                    } else {
+                                        // This means we were passed `-fno-sanitize-trap=undefined` and nothing else. In
+                                        // this case, ideally, we should use whatever value `sanitize_c` resolves to by
+                                        // default, except change `trap` to `full`. However, we don't yet know what
+                                        // `sanitize_c` will resolve to! So we either have to pick `off` or `full`.
+                                        //
+                                        // `full` has the potential to be problematic if `optimize_mode` turns out to
+                                        // be `ReleaseFast`/`ReleaseSmall` because the user will get a slower and larger
+                                        // binary than expected. On the other hand, if `optimize_mode` turns out to be
+                                        // `Debug`/`ReleaseSafe`, `off` would mean UBSan would unexpectedly be disabled.
+                                        //
+                                        // `off` seems very slightly less bad, so let's go with that.
+                                        mod_opts.sanitize_c = .off;
+                                    }
+                                }
+                                recognized_any = true;
+                            }
+                        }
+                        if (!recognized_any) {
+                            try cc_argv.appendSlice(arena, it.other_args);
+                        }
+                    },
                     .linker_script => linker_script = it.only_arg,
                     .verbose => {
                         verbose_link = true;
@@ -2766,7 +2820,7 @@ fn buildOutputType(
             }
 
             if (mod_opts.sanitize_c) |wsc| {
-                if (wsc and mod_opts.optimize_mode == .ReleaseFast) {
+                if (wsc != .off and mod_opts.optimize_mode == .ReleaseFast) {
                     mod_opts.optimize_mode = .ReleaseSafe;
                 }
             }
@@ -2915,6 +2969,13 @@ fn buildOutputType(
             create_module.opts.any_non_single_threaded = true;
         if (mod_opts.sanitize_thread == true)
             create_module.opts.any_sanitize_thread = true;
+        if (mod_opts.sanitize_c) |sc| switch (sc) {
+            .off => {},
+            .trap => if (create_module.opts.any_sanitize_c == .off) {
+                create_module.opts.any_sanitize_c = .trap;
+            },
+            .full => create_module.opts.any_sanitize_c = .full,
+        };
         if (mod_opts.fuzz == true)
             create_module.opts.any_fuzz = true;
         if (mod_opts.unwind_tables) |uwt| switch (uwt) {
@@ -5941,6 +6002,8 @@ pub const ClangArgIterator = struct {
         gdwarf64,
         sanitize,
         no_sanitize,
+        sanitize_trap,
+        no_sanitize_trap,
         linker_script,
         dry_run,
         verbose,
@@ -7728,6 +7791,13 @@ fn handleModArg(
         create_module.opts.any_non_single_threaded = true;
     if (mod_opts.sanitize_thread == true)
         create_module.opts.any_sanitize_thread = true;
+    if (mod_opts.sanitize_c) |sc| switch (sc) {
+        .off => {},
+        .trap => if (create_module.opts.any_sanitize_c == .off) {
+            create_module.opts.any_sanitize_c = .trap;
+        },
+        .full => create_module.opts.any_sanitize_c = .full,
+    };
     if (mod_opts.fuzz == true)
         create_module.opts.any_fuzz = true;
     if (mod_opts.unwind_tables) |uwt| switch (uwt) {
src/musl.zig
@@ -231,7 +231,7 @@ pub fn buildCrtFile(comp: *Compilation, in_crt_file: CrtFile, prog_node: std.Pro
                     .strip = strip,
                     .stack_check = false,
                     .stack_protector = 0,
-                    .sanitize_c = false,
+                    .sanitize_c = .off,
                     .sanitize_thread = false,
                     .red_zone = comp.root_mod.red_zone,
                     .omit_frame_pointer = comp.root_mod.omit_frame_pointer,
test/link/glibc_compat/build.zig
@@ -25,7 +25,7 @@ pub fn build(b: *std.Build) void {
         // We disable UBSAN for these tests as the libc being tested here is
         // so old, it doesn't even support compiling our UBSAN implementation.
         exe.bundle_ubsan_rt = false;
-        exe.root_module.sanitize_c = false;
+        exe.root_module.sanitize_c = .off;
         exe.root_module.addCSourceFile(.{ .file = b.path("main.c") });
         // TODO: actually test the output
         _ = exe.getEmittedBin();
@@ -69,7 +69,7 @@ pub fn build(b: *std.Build) void {
         // We disable UBSAN for these tests as the libc being tested here is
         // so old, it doesn't even support compiling our UBSAN implementation.
         exe.bundle_ubsan_rt = false;
-        exe.root_module.sanitize_c = false;
+        exe.root_module.sanitize_c = .off;
         exe.root_module.addCSourceFile(.{ .file = b.path("glibc_runtime_check.c") });
 
         // Only try running the test if the host glibc is known to be good enough.  Ideally, the Zig
@@ -172,7 +172,7 @@ pub fn build(b: *std.Build) void {
         // We disable UBSAN for these tests as the libc being tested here is
         // so old, it doesn't even support compiling our UBSAN implementation.
         exe.bundle_ubsan_rt = false;
-        exe.root_module.sanitize_c = false;
+        exe.root_module.sanitize_c = .off;
 
         // Only try running the test if the host glibc is known to be good enough.  Ideally, the Zig
         // test runner would be able to check this, but see https://github.com/ziglang/zig/pull/17702#issuecomment-1831310453
test/link/elf.zig
@@ -2052,7 +2052,7 @@ fn testLargeBss(b: *Build, opts: Options) *Step {
     exe.linkLibC();
     // Disabled to work around the ELF linker crashing.
     // Can be reproduced on a x86_64-linux host by commenting out the line below.
-    exe.root_module.sanitize_c = false;
+    exe.root_module.sanitize_c = .off;
 
     const run = addRunArtifact(exe);
     run.expectExitCode(0);
@@ -3558,7 +3558,7 @@ fn testTlsLargeTbss(b: *Build, opts: Options) *Step {
     exe.linkLibC();
     // Disabled to work around the ELF linker crashing.
     // Can be reproduced on a x86_64-linux host by commenting out the line below.
-    exe.root_module.sanitize_c = false;
+    exe.root_module.sanitize_c = .off;
 
     const run = addRunArtifact(exe);
     run.expectStdOutEqual("3 0 5 0 0 0\n");
tools/update_clang_options.zig
@@ -288,6 +288,14 @@ const known_options = [_]KnownOpt{
         .name = "fno-sanitize",
         .ident = "no_sanitize",
     },
+    .{
+        .name = "fsanitize-trap",
+        .ident = "sanitize_trap",
+    },
+    .{
+        .name = "fno-sanitize-trap",
+        .ident = "no_sanitize_trap",
+    },
     .{
         .name = "T",
         .ident = "linker_script",