authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-11-19 12:55:17+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-11-20 10:42:20+00:00
log010dcd6a9b64d5bd13579a4b0c4c70a5aee5c967
tree06b820caa177d8ea5dd0b6d19302b7535af054ae
parent0a330d4f947c1b05ac9f7d624443e2f80db2912f
signaturelock-open Commit is signed but in an unrecognized format.

fuzzer: account for runtime address slide

This is relevant to PIEs, which are notably enabled by default on macOS. The build system needs to only see virtual addresses, that is, those which do not have the slide applied; but the fuzzer itself naturally sees relocated addresses (i.e. with the slide applied). We just need to subtract the slide when we communicate addresses to the build system.

7 files changed, 56 insertions(+), 8 deletions(-)

lib/compiler/test_runner.zig+1-1
...@@ -184,7 +184,7 @@ fn mainServer() !void {...@@ -184,7 +184,7 @@ fn mainServer() !void {
184 const test_fn = builtin.test_functions[index];184 const test_fn = builtin.test_functions[index];
185 const entry_addr = @intFromPtr(test_fn.func);185 const entry_addr = @intFromPtr(test_fn.func);
186186
187 try server.serveU64Message(.fuzz_start_addr, entry_addr);187 try server.serveU64Message(.fuzz_start_addr, fuzz_abi.fuzzer_unslide_address(entry_addr));
188 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);188 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
189 is_fuzz_test = false;189 is_fuzz_test = false;
190 fuzz_test_index = index;190 fuzz_test_index = index;
lib/fuzzer.zig+28-6
...@@ -116,13 +116,18 @@ const Executable = struct {...@@ -116,13 +116,18 @@ const Executable = struct {
116 "failed to init memory map for coverage file '{s}': {t}",116 "failed to init memory map for coverage file '{s}': {t}",
117 .{ &coverage_file_name, e },117 .{ &coverage_file_name, e },
118 );118 );
119 map.appendSliceAssumeCapacity(mem.asBytes(&abi.SeenPcsHeader{119 map.appendSliceAssumeCapacity(@ptrCast(&abi.SeenPcsHeader{
120 .n_runs = 0,120 .n_runs = 0,
121 .unique_runs = 0,121 .unique_runs = 0,
122 .pcs_len = pcs.len,122 .pcs_len = pcs.len,
123 }));123 }));
124 map.appendNTimesAssumeCapacity(0, pc_bitset_usizes * @sizeOf(usize));124 map.appendNTimesAssumeCapacity(0, pc_bitset_usizes * @sizeOf(usize));
125 map.appendSliceAssumeCapacity(mem.sliceAsBytes(pcs));125 // Relocations have been applied to `pcs` so it contains runtime addresses (with slide
126 // applied). We need to translate these to the virtual addresses as on disk.
127 for (pcs) |pc| {
128 const pc_vaddr = fuzzer_unslide_address(pc);
129 map.appendSliceAssumeCapacity(@ptrCast(&pc_vaddr));
130 }
126 return map;131 return map;
127 } else {132 } else {
128 const size = coverage_file.getEndPos() catch |e| panic(133 const size = coverage_file.getEndPos() catch |e| panic(
...@@ -215,7 +220,16 @@ const Executable = struct {...@@ -215,7 +220,16 @@ const Executable = struct {
215 .{ self.pc_counters.len, pcs.len },220 .{ self.pc_counters.len, pcs.len },
216 );221 );
217222
218 self.pc_digest = std.hash.Wyhash.hash(0, mem.sliceAsBytes(pcs));223 self.pc_digest = digest: {
224 // Relocations have been applied to `pcs` so it contains runtime addresses (with slide
225 // applied). We need to translate these to the virtual addresses as on disk.
226 var h: std.hash.Wyhash = .init(0);
227 for (pcs) |pc| {
228 const pc_vaddr = fuzzer_unslide_address(pc);
229 h.update(@ptrCast(&pc_vaddr));
230 }
231 break :digest h.final();
232 };
219 self.shared_seen_pcs = getCoverageFile(cache_dir, pcs, self.pc_digest);233 self.shared_seen_pcs = getCoverageFile(cache_dir, pcs, self.pc_digest);
220234
221 return self;235 return self;
...@@ -622,6 +636,14 @@ export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {...@@ -622,6 +636,14 @@ export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {
622 }636 }
623}637}
624638
639export fn fuzzer_unslide_address(addr: usize) usize {
640 const si = std.debug.getSelfDebugInfo() catch @compileError("unsupported");
641 const slide = si.getModuleSlide(std.debug.getDebugInfoAllocator(), addr) catch |err| {
642 std.debug.panic("failed to find virtual address slide: {t}", .{err});
643 };
644 return addr - slide;
645}
646
625/// Helps determine run uniqueness in the face of recursion.647/// Helps determine run uniqueness in the face of recursion.
626/// Currently not used by the fuzzer.648/// Currently not used by the fuzzer.
627export threadlocal var __sancov_lowest_stack: usize = 0;649export threadlocal var __sancov_lowest_stack: usize = 0;
...@@ -1185,13 +1207,13 @@ const Mutation = enum {...@@ -1185,13 +1207,13 @@ const Mutation = enum {
1185 const j = rng.uintAtMostBiased(usize, corpus[splice_i].len - len);1207 const j = rng.uintAtMostBiased(usize, corpus[splice_i].len - len);
1186 out.appendSliceAssumeCapacity(corpus[splice_i][j..][0..len]);1208 out.appendSliceAssumeCapacity(corpus[splice_i][j..][0..len]);
1187 },1209 },
1188 .@"const" => out.appendSliceAssumeCapacity(mem.asBytes(1210 .@"const" => out.appendSliceAssumeCapacity(@ptrCast(
1189 &data_ctx[rng.uintLessThanBiased(usize, data_ctx.len)],1211 &data_ctx[rng.uintLessThanBiased(usize, data_ctx.len)],
1190 )),1212 )),
1191 .small => out.appendSliceAssumeCapacity(mem.asBytes(1213 .small => out.appendSliceAssumeCapacity(@ptrCast(
1192 &mem.nativeTo(data_ctx[0], rng.int(SmallValue), data_ctx[1]),1214 &mem.nativeTo(data_ctx[0], rng.int(SmallValue), data_ctx[1]),
1193 )),1215 )),
1194 .few => out.appendSliceAssumeCapacity(mem.asBytes(1216 .few => out.appendSliceAssumeCapacity(@ptrCast(
1195 &fewValue(rng, data_ctx[0], data_ctx[1]),1217 &fewValue(rng, data_ctx[0], data_ctx[1]),
1196 )),1218 )),
1197 }1219 }
lib/std/Build/abi.zig+1
...@@ -145,6 +145,7 @@ pub const fuzz = struct {...@@ -145,6 +145,7 @@ pub const fuzz = struct {
145 pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void;145 pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void;
146 pub extern fn fuzzer_new_input(bytes: Slice) void;146 pub extern fn fuzzer_new_input(bytes: Slice) void;
147 pub extern fn fuzzer_main(limit_kind: LimitKind, amount: u64) void;147 pub extern fn fuzzer_main(limit_kind: LimitKind, amount: u64) void;
148 pub extern fn fuzzer_unslide_address(addr: usize) usize;
148149
149 pub const Slice = extern struct {150 pub const Slice = extern struct {
150 ptr: [*]const u8,151 ptr: [*]const u8,
lib/std/debug.zig+1-1
...@@ -1367,7 +1367,7 @@ test printLineFromFile {...@@ -1367,7 +1367,7 @@ test printLineFromFile {
13671367
1368/// The returned allocator should be thread-safe if the compilation is multi-threaded, because1368/// The returned allocator should be thread-safe if the compilation is multi-threaded, because
1369/// multiple threads could capture and/or print stack traces simultaneously.1369/// multiple threads could capture and/or print stack traces simultaneously.
1370fn getDebugInfoAllocator() Allocator {1370pub fn getDebugInfoAllocator() Allocator {
1371 // Allow overriding the debug info allocator by exposing `root.debug.getDebugInfoAllocator`.1371 // Allow overriding the debug info allocator by exposing `root.debug.getDebugInfoAllocator`.
1372 if (@hasDecl(root, "debug") and @hasDecl(root.debug, "getDebugInfoAllocator")) {1372 if (@hasDecl(root, "debug") and @hasDecl(root.debug, "getDebugInfoAllocator")) {
1373 return root.debug.getDebugInfoAllocator();1373 return root.debug.getDebugInfoAllocator();
lib/std/debug/SelfInfo/Elf.zig+5
...@@ -80,6 +80,11 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons...@@ -80,6 +80,11 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons
80 if (module.name.len == 0) return error.MissingDebugInfo;80 if (module.name.len == 0) return error.MissingDebugInfo;
81 return module.name;81 return module.name;
82}82}
83pub fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
84 const module = try si.findModule(gpa, address, .shared);
85 defer si.rwlock.unlockShared();
86 return module.load_offset;
87}
8388
84pub const can_unwind: bool = s: {89pub const can_unwind: bool = s: {
85 // The DWARF code can't deal with ILP32 ABIs yet: https://github.com/ziglang/zig/issues/2544790 // The DWARF code can't deal with ILP32 ABIs yet: https://github.com/ziglang/zig/issues/25447
lib/std/debug/SelfInfo/MachO.zig+14
...@@ -82,6 +82,20 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons...@@ -82,6 +82,20 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons
82 defer si.mutex.unlock();82 defer si.mutex.unlock();
83 return module.name;83 return module.name;
84}84}
85pub fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
86 const module = try si.findModule(gpa, address);
87 defer si.mutex.unlock();
88 const header: *std.macho.mach_header_64 = @ptrFromInt(module.text_base);
89 const raw_macho: [*]u8 = @ptrCast(header);
90 var it = macho.LoadCommandIterator.init(header, raw_macho[@sizeOf(macho.mach_header_64)..][0..header.sizeofcmds]) catch unreachable;
91 const text_vmaddr = while (it.next() catch unreachable) |load_cmd| {
92 if (load_cmd.hdr.cmd != .SEGMENT_64) continue;
93 const segment_cmd = load_cmd.cast(macho.segment_command_64).?;
94 if (!mem.eql(u8, segment_cmd.segName(), "__TEXT")) continue;
95 break segment_cmd.vmaddr;
96 } else unreachable;
97 return module.text_base - text_vmaddr;
98}
8599
86pub const can_unwind: bool = true;100pub const can_unwind: bool = true;
87pub const UnwindContext = std.debug.Dwarf.SelfUnwinder;101pub const UnwindContext = std.debug.Dwarf.SelfUnwinder;
lib/std/debug/SelfInfo/Windows.zig+6
...@@ -33,6 +33,12 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons...@@ -33,6 +33,12 @@ pub fn getModuleName(si: *SelfInfo, gpa: Allocator, address: usize) Error![]cons
33 const module = try si.findModule(gpa, address);33 const module = try si.findModule(gpa, address);
34 return module.name;34 return module.name;
35}35}
36pub fn getModuleSlide(si: *SelfInfo, gpa: Allocator, address: usize) Error!usize {
37 si.mutex.lock();
38 defer si.mutex.unlock();
39 const module = try si.findModule(gpa, address);
40 return module.base_address;
41}
3642
37pub const can_unwind: bool = switch (builtin.cpu.arch) {43pub const can_unwind: bool = switch (builtin.cpu.arch) {
38 else => true,44 else => true,