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 {
184184 const test_fn = builtin.test_functions[index];
185185 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));
188188 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
189189 is_fuzz_test = false;
190190 fuzz_test_index = index;
lib/fuzzer.zig+28-6
......@@ -116,13 +116,18 @@ const Executable = struct {
116116 "failed to init memory map for coverage file '{s}': {t}",
117117 .{ &coverage_file_name, e },
118118 );
119 map.appendSliceAssumeCapacity(mem.asBytes(&abi.SeenPcsHeader{
119 map.appendSliceAssumeCapacity(@ptrCast(&abi.SeenPcsHeader{
120120 .n_runs = 0,
121121 .unique_runs = 0,
122122 .pcs_len = pcs.len,
123123 }));
124124 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 }
126131 return map;
127132 } else {
128133 const size = coverage_file.getEndPos() catch |e| panic(
......@@ -215,7 +220,16 @@ const Executable = struct {
215220 .{ self.pc_counters.len, pcs.len },
216221 );
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 };
219233 self.shared_seen_pcs = getCoverageFile(cache_dir, pcs, self.pc_digest);
220234
221235 return self;
......@@ -622,6 +636,14 @@ export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {
622636 }
623637}
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
625647/// Helps determine run uniqueness in the face of recursion.
626648/// Currently not used by the fuzzer.
627649export threadlocal var __sancov_lowest_stack: usize = 0;
......@@ -1185,13 +1207,13 @@ const Mutation = enum {
11851207 const j = rng.uintAtMostBiased(usize, corpus[splice_i].len - len);
11861208 out.appendSliceAssumeCapacity(corpus[splice_i][j..][0..len]);
11871209 },
1188 .@"const" => out.appendSliceAssumeCapacity(mem.asBytes(
1210 .@"const" => out.appendSliceAssumeCapacity(@ptrCast(
11891211 &data_ctx[rng.uintLessThanBiased(usize, data_ctx.len)],
11901212 )),
1191 .small => out.appendSliceAssumeCapacity(mem.asBytes(
1213 .small => out.appendSliceAssumeCapacity(@ptrCast(
11921214 &mem.nativeTo(data_ctx[0], rng.int(SmallValue), data_ctx[1]),
11931215 )),
1194 .few => out.appendSliceAssumeCapacity(mem.asBytes(
1216 .few => out.appendSliceAssumeCapacity(@ptrCast(
11951217 &fewValue(rng, data_ctx[0], data_ctx[1]),
11961218 )),
11971219 }
lib/std/Build/abi.zig+1
......@@ -145,6 +145,7 @@ pub const fuzz = struct {
145145 pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void;
146146 pub extern fn fuzzer_new_input(bytes: Slice) void;
147147 pub extern fn fuzzer_main(limit_kind: LimitKind, amount: u64) void;
148 pub extern fn fuzzer_unslide_address(addr: usize) usize;
148149
149150 pub const Slice = extern struct {
150151 ptr: [*]const u8,
lib/std/debug.zig+1-1
......@@ -1367,7 +1367,7 @@ test printLineFromFile {
13671367
13681368/// The returned allocator should be thread-safe if the compilation is multi-threaded, because
13691369/// multiple threads could capture and/or print stack traces simultaneously.
1370fn getDebugInfoAllocator() Allocator {
1370pub fn getDebugInfoAllocator() Allocator {
13711371 // Allow overriding the debug info allocator by exposing `root.debug.getDebugInfoAllocator`.
13721372 if (@hasDecl(root, "debug") and @hasDecl(root.debug, "getDebugInfoAllocator")) {
13731373 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
8080 if (module.name.len == 0) return error.MissingDebugInfo;
8181 return module.name;
8282}
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
8489pub const can_unwind: bool = s: {
8590 // 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
8282 defer si.mutex.unlock();
8383 return module.name;
8484}
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
86100pub const can_unwind: bool = true;
87101pub 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
3333 const module = try si.findModule(gpa, address);
3434 return module.name;
3535}
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
3743pub const can_unwind: bool = switch (builtin.cpu.arch) {
3844 else => true,