authorgravatar for goon.pri.low@gmail.comKendall Condon <goon.pri.low@gmail.com> 2025-09-18 18:34:22-04:00
committergravatar for goon.pri.low@gmail.comKendall Condon <goon.pri.low@gmail.com> 2025-09-18 18:56:18-04:00
log7c6ccca46d31a69cd6fddfdacd0aa7fba1d1e922
treefd74a260a659baaa045f538c753cf96bcff85e12
parentb905c65661bd0d47e6ac7d537649eef8e29e44fc

fuzzer: remove rodata load tracing

This can be re-evaluated at a later time, but at the moment the performance and stability concerns hold it back. Additionally, it promotes a non-smithing approach to fuzz tests.

2 files changed, 13 insertions(+), 203 deletions(-)

lib/fuzzer.zig+12-202
...@@ -57,9 +57,6 @@ fn bitsetUsizes(elems: usize) usize {...@@ -57,9 +57,6 @@ fn bitsetUsizes(elems: usize) usize {
57const Executable = struct {57const Executable = struct {
58 /// Tracks the hit count for each pc as updated by the process's instrumentation.58 /// Tracks the hit count for each pc as updated by the process's instrumentation.
59 pc_counters: []u8,59 pc_counters: []u8,
60 /// Read-only memory section containing compiled-in constants found from parsing the executable
61 rodata_addr: usize,
62 rodata_size: usize,
6360
64 cache_f: std.fs.Dir,61 cache_f: std.fs.Dir,
65 /// Shared copy of all pcs that have been hit stored in a memory-mapped file that can viewed62 /// Shared copy of all pcs that have been hit stored in a memory-mapped file that can viewed
...@@ -72,80 +69,12 @@ const Executable = struct {...@@ -72,80 +69,12 @@ const Executable = struct {
72 /// Used before this structure is initialized to avoid illegal behavior69 /// Used before this structure is initialized to avoid illegal behavior
73 /// from instrumentation functions being called and using undefined values.70 /// from instrumentation functions being called and using undefined values.
74 pub const preinit: Executable = .{71 pub const preinit: Executable = .{
75 .rodata_addr = 0,
76 .rodata_size = 0,
77 .pc_counters = undefined, // instrumentation works off the __sancov_cntrs section72 .pc_counters = undefined, // instrumentation works off the __sancov_cntrs section
78 .cache_f = undefined,73 .cache_f = undefined,
79 .shared_seen_pcs = undefined,74 .shared_seen_pcs = undefined,
80 .pc_digest = undefined,75 .pc_digest = undefined,
81 };76 };
8277
83 /// Even on error, this initializes rodata_addr and rodata_size to valid values
84 fn initRodata(self: *Executable) !void {
85 errdefer {
86 self.rodata_addr = 0;
87 self.rodata_size = 0;
88 }
89
90 const exec_path = std.fs.selfExePathAlloc(gpa) catch |e|
91 if (e == error.OutOfMemory) @panic("OOM") else return e;
92 defer gpa.free(exec_path);
93 const exec_file = try std.fs.cwd().openFile(exec_path, .{});
94 defer exec_file.close();
95
96 switch (builtin.object_format) {
97 .elf => {
98 // We use two reader instances since the data they respectively read are
99 // not next to each other in the file.
100 //
101 // Multiple instances is safe since Elf.SectionHeaderIterator always calls
102 // seekTo (which we also use to arbitrarily set the index) and we always
103 // call seekTo to arbitrarily read from the string table.
104 var r_buf: [4096]u8 = undefined;
105 var r = exec_file.reader(&r_buf);
106 var str_r_buf: [4096]u8 = undefined;
107 var str_r = exec_file.reader(&str_r_buf);
108
109 const ehdr: std.elf.Header = try .read(&r.interface);
110 if (ehdr.shstrndx == 0) return error.NoElfStringTable;
111 var shdr_it = ehdr.iterateSectionHeaders(&r);
112
113 shdr_it.index = ehdr.shstrndx;
114 const str_tab_shdr = try shdr_it.next() orelse return error.InvalidElfSection;
115 const str_tab_off = str_tab_shdr.sh_offset;
116
117 shdr_it.index = 0;
118 while (try shdr_it.next()) |shdr| {
119 const flags: packed struct {
120 write: bool,
121 alloc: bool,
122 execinstr: bool,
123 } = @bitCast(@as(u3, @truncate(shdr.sh_flags)));
124 if (shdr.sh_addr == 0 or shdr.sh_size == 0 or flags != @TypeOf(flags){
125 .alloc = true,
126 .write = false,
127 .execinstr = false,
128 }) continue;
129
130 const rodata_name = ".rodata\x00";
131 try str_r.seekTo(try math.add(u64, str_tab_off, shdr.sh_name));
132 const section_name = str_r.interface.take(rodata_name.len) catch return r.err.?;
133 if (!std.mem.eql(u8, section_name, rodata_name))
134 continue;
135
136 const addr = math.cast(usize, shdr.sh_addr) orelse return error.Overflow;
137 const size = math.cast(usize, shdr.sh_size) orelse return error.Overflow;
138 _ = try math.add(usize, addr, size); // make sure there is no overflow
139 self.rodata_addr = addr;
140 self.rodata_size = size;
141 return;
142 }
143 return error.NoRodataSection;
144 },
145 else => return error.UnsupportedObjectFormat,
146 }
147 }
148
149 fn getCoverageFile(cache_dir: std.fs.Dir, pcs: []const usize, pc_digest: u64) MemoryMappedList {78 fn getCoverageFile(cache_dir: std.fs.Dir, pcs: []const usize, pc_digest: u64) MemoryMappedList {
150 const pc_bitset_usizes = bitsetUsizes(pcs.len);79 const pc_bitset_usizes = bitsetUsizes(pcs.len);
151 const coverage_file_name = std.fmt.hex(pc_digest);80 const coverage_file_name = std.fmt.hex(pc_digest);
...@@ -284,11 +213,6 @@ const Executable = struct {...@@ -284,11 +213,6 @@ const Executable = struct {
284 .{ self.pc_counters.len, pcs.len },213 .{ self.pc_counters.len, pcs.len },
285 );214 );
286215
287 self.initRodata() catch |e| if (e != error.UnsupportedObjectFormat) std.log.err(
288 \\failed to enumerate read-only memory: {t}
289 \\efficiency will be severly reduced
290 , .{e});
291
292 self.pc_digest = std.hash.Wyhash.hash(0, mem.sliceAsBytes(pcs));216 self.pc_digest = std.hash.Wyhash.hash(0, mem.sliceAsBytes(pcs));
293 self.shared_seen_pcs = getCoverageFile(cache_dir, pcs, self.pc_digest);217 self.shared_seen_pcs = getCoverageFile(cache_dir, pcs, self.pc_digest);
294218
...@@ -322,33 +246,22 @@ const Executable = struct {...@@ -322,33 +246,22 @@ const Executable = struct {
322 };246 };
323};247};
324248
325/// Data gathered from instrumentation functions249/// Data gathered from instrumentation functions.
326/// Seperate from Executable since its state is resetable and changes250/// Seperate from Executable since its state is resetable and changes.
327/// Seperate from Fuzzer since it may be needed before fuzzing starts251/// Seperate from Fuzzer since it may be needed before fuzzing starts.
328const Instrumentation = struct {252const Instrumentation = struct {
329 /// Bitset of seen pcs across all runs excluding fresh pcs.253 /// Bitset of seen pcs across all runs excluding fresh pcs.
330 /// This is seperate then shared_seen_pcs because multiple fuzzing processes are likely using254 /// This is seperate then shared_seen_pcs because multiple fuzzing processes are likely using
331 /// it which causes contention and unrelated pcs to our campaign being set.255 /// it which causes contention and unrelated pcs to our campaign being set.
332 seen_pcs: []usize,256 seen_pcs: []usize,
333 /// Bitset of seen rodata bytes read across all runs
334 seen_rodata_loads: []usize,
335
336 /// Bitset of run's read bytes that weren't present in seen_loads
337 /// Elements are always zero if !any_new_data_loads
338 new_rodata_loads: []usize,
339 any_new_rodata_loads: bool,
340257
341 /// Stores a fresh input's new pcs258 /// Stores a fresh input's new pcs
342 fresh_pcs: []usize,259 fresh_pcs: []usize,
343 /// Stores a fresh input's new reads
344 /// Elements are always zero if !any_fresh_rodata_loads
345 fresh_rodata_loads: []usize,
346 any_fresh_rodata_loads: bool,
347260
348 /// Pcs which __sanitizer_cov_trace_switch and __sanitizer_cov_trace_const_cmpx261 /// Pcs which __sanitizer_cov_trace_switch and __sanitizer_cov_trace_const_cmpx
349 /// have been called from and have had their already been added to const_x_vals262 /// have been called from and have had their already been added to const_x_vals
350 const_pcs: std.AutoArrayHashMapUnmanaged(usize, void) = .empty,263 const_pcs: std.AutoArrayHashMapUnmanaged(usize, void) = .empty,
351 /// Values that have been constant operands in comparisons, switch cases, or memory reads264 /// Values that have been constant operands in comparisons and switch cases.
352 /// There may be duplicates in this array if they came from different addresses, which is265 /// There may be duplicates in this array if they came from different addresses, which is
353 /// fine as they are likely more important and hence more likely to be selected.266 /// fine as they are likely more important and hence more likely to be selected.
354 const_vals2: std.ArrayListUnmanaged(u16) = .empty,267 const_vals2: std.ArrayListUnmanaged(u16) = .empty,
...@@ -361,12 +274,7 @@ const Instrumentation = struct {...@@ -361,12 +274,7 @@ const Instrumentation = struct {
361 /// from instrumentation functions being called and using undefined values.274 /// from instrumentation functions being called and using undefined values.
362 pub const preinit: Instrumentation = .{275 pub const preinit: Instrumentation = .{
363 .seen_pcs = undefined, // currently only updated by `Fuzzer`276 .seen_pcs = undefined, // currently only updated by `Fuzzer`
364 .seen_rodata_loads = undefined,
365 .new_rodata_loads = undefined,
366 .any_new_rodata_loads = undefined,
367 .fresh_pcs = undefined,277 .fresh_pcs = undefined,
368 .fresh_rodata_loads = undefined,
369 .any_fresh_rodata_loads = undefined,
370 };278 };
371279
372 pub fn depreinit(self: *Instrumentation) void {280 pub fn depreinit(self: *Instrumentation) void {
...@@ -379,20 +287,14 @@ const Instrumentation = struct {...@@ -379,20 +287,14 @@ const Instrumentation = struct {
379287
380 pub fn init() Instrumentation {288 pub fn init() Instrumentation {
381 const pc_bitset_usizes = bitsetUsizes(exec.pc_counters.len);289 const pc_bitset_usizes = bitsetUsizes(exec.pc_counters.len);
382 const rodata_bitset_usizes = bitsetUsizes(exec.rodata_size);290 const alloc_usizes = pc_bitset_usizes * 2;
383 const alloc_usizes = pc_bitset_usizes * 2 + rodata_bitset_usizes * 3;
384 const buf = gpa.alloc(u8, alloc_usizes * @sizeOf(usize)) catch @panic("OOM");291 const buf = gpa.alloc(u8, alloc_usizes * @sizeOf(usize)) catch @panic("OOM");
385 var fba_ctx: std.heap.FixedBufferAllocator = .init(buf);292 var fba_ctx: std.heap.FixedBufferAllocator = .init(buf);
386 const fba = fba_ctx.allocator();293 const fba = fba_ctx.allocator();
387294
388 var self: Instrumentation = .{295 var self: Instrumentation = .{
389 .seen_pcs = fba.alloc(usize, pc_bitset_usizes) catch unreachable,296 .seen_pcs = fba.alloc(usize, pc_bitset_usizes) catch unreachable,
390 .seen_rodata_loads = fba.alloc(usize, rodata_bitset_usizes) catch unreachable,
391 .new_rodata_loads = fba.alloc(usize, rodata_bitset_usizes) catch unreachable,
392 .any_new_rodata_loads = undefined,
393 .fresh_pcs = fba.alloc(usize, pc_bitset_usizes) catch unreachable,297 .fresh_pcs = fba.alloc(usize, pc_bitset_usizes) catch unreachable,
394 .fresh_rodata_loads = fba.alloc(usize, rodata_bitset_usizes) catch unreachable,
395 .any_fresh_rodata_loads = undefined,
396 };298 };
397 self.reset();299 self.reset();
398 return self;300 return self;
...@@ -400,12 +302,7 @@ const Instrumentation = struct {...@@ -400,12 +302,7 @@ const Instrumentation = struct {
400302
401 pub fn reset(self: *Instrumentation) void {303 pub fn reset(self: *Instrumentation) void {
402 @memset(self.seen_pcs, 0);304 @memset(self.seen_pcs, 0);
403 @memset(self.seen_rodata_loads, 0);
404 @memset(self.new_rodata_loads, 0);
405 self.any_new_rodata_loads = false;
406 @memset(self.fresh_pcs, 0);305 @memset(self.fresh_pcs, 0);
407 @memset(self.fresh_rodata_loads, 0);
408 self.any_fresh_rodata_loads = false;
409 self.const_pcs.clearRetainingCapacity();306 self.const_pcs.clearRetainingCapacity();
410 self.const_vals2.clearRetainingCapacity();307 self.const_vals2.clearRetainingCapacity();
411 self.const_vals4.clearRetainingCapacity();308 self.const_vals4.clearRetainingCapacity();
...@@ -418,16 +315,7 @@ const Instrumentation = struct {...@@ -418,16 +315,7 @@ const Instrumentation = struct {
418 return (self.const_pcs.getOrPut(gpa, pc) catch @panic("OOM")).found_existing;315 return (self.const_pcs.getOrPut(gpa, pc) catch @panic("OOM")).found_existing;
419 }316 }
420317
421 pub fn clearNewRodataLoads(self: *Instrumentation) void {
422 if (self.any_new_rodata_loads) {
423 @memset(self.new_rodata_loads, 0);
424 self.any_new_rodata_loads = false;
425 }
426 }
427
428 pub fn isFresh(self: *Instrumentation) bool {318 pub fn isFresh(self: *Instrumentation) bool {
429 if (self.any_new_rodata_loads) return true;
430
431 var hit_pcs = exec.pcBitsetIterator();319 var hit_pcs = exec.pcBitsetIterator();
432 for (self.seen_pcs) |seen_pcs| {320 for (self.seen_pcs) |seen_pcs| {
433 if (hit_pcs.next() & ~seen_pcs != 0) return true;321 if (hit_pcs.next() & ~seen_pcs != 0) return true;
...@@ -436,38 +324,24 @@ const Instrumentation = struct {...@@ -436,38 +324,24 @@ const Instrumentation = struct {
436 return false;324 return false;
437 }325 }
438326
439 /// Updates fresh_pcs and fresh_rodata_loads327 /// Updates `fresh_pcs`
440 /// any_new_rodata_loads and elements of new_rodata_loads are unspecified
441 /// afterwards, but still valid.
442 pub fn setFresh(self: *Instrumentation) void {328 pub fn setFresh(self: *Instrumentation) void {
443 var hit_pcs = exec.pcBitsetIterator();329 var hit_pcs = exec.pcBitsetIterator();
444 for (self.seen_pcs, self.fresh_pcs) |seen_pcs, *fresh_pcs| {330 for (self.seen_pcs, self.fresh_pcs) |seen_pcs, *fresh_pcs| {
445 fresh_pcs.* = hit_pcs.next() & ~seen_pcs;331 fresh_pcs.* = hit_pcs.next() & ~seen_pcs;
446 }332 }
447
448 mem.swap([]usize, &self.fresh_rodata_loads, &self.new_rodata_loads);
449 mem.swap(bool, &self.any_fresh_rodata_loads, &self.any_new_rodata_loads);
450 }333 }
451334
452 /// Returns if exec.pc_counters and new_rodata_loads are the same or a superset of fresh_pcs and335 /// Returns if `exec.pc_counters` is a superset of `fresh_pcs`.
453 /// fresh_rodata_loads respectively.
454 pub fn atleastFresh(self: *Instrumentation) bool {336 pub fn atleastFresh(self: *Instrumentation) bool {
455 var hit_pcs = exec.pcBitsetIterator();337 var hit_pcs = exec.pcBitsetIterator();
456 for (self.fresh_pcs) |fresh_pcs| {338 for (self.fresh_pcs) |fresh_pcs| {
457 if (fresh_pcs & hit_pcs.next() != fresh_pcs) return false;339 if (fresh_pcs & hit_pcs.next() != fresh_pcs) return false;
458 }340 }
459
460 if (self.any_fresh_rodata_loads) {
461 if (!self.any_new_rodata_loads) return false;
462 for (self.new_rodata_loads, self.fresh_rodata_loads) |n, f| {
463 if (n & f != f) return false;
464 }
465 }
466
467 return true;341 return true;
468 }342 }
469343
470 /// Updates based off fresh_pcs and fresh_rodata_loads344 /// Updates based off `fresh_pcs`
471 fn updateSeen(self: *Instrumentation) void {345 fn updateSeen(self: *Instrumentation) void {
472 comptime assert(abi.SeenPcsHeader.trailing[0] == .pc_bits_usize);346 comptime assert(abi.SeenPcsHeader.trailing[0] == .pc_bits_usize);
473 const shared_seen_pcs: [*]volatile usize = @ptrCast(347 const shared_seen_pcs: [*]volatile usize = @ptrCast(
...@@ -479,11 +353,6 @@ const Instrumentation = struct {...@@ -479,11 +353,6 @@ const Instrumentation = struct {
479 if (fresh != 0)353 if (fresh != 0)
480 _ = @atomicRmw(usize, shared_seen, .Or, fresh, .monotonic);354 _ = @atomicRmw(usize, shared_seen, .Or, fresh, .monotonic);
481 }355 }
482
483 if (self.any_fresh_rodata_loads) {
484 for (self.seen_rodata_loads, self.fresh_rodata_loads) |*s, f|
485 s.* |= f;
486 }
487 }356 }
488};357};
489358
...@@ -496,8 +365,8 @@ const Fuzzer = struct {...@@ -496,8 +365,8 @@ const Fuzzer = struct {
496 /// input.365 /// input.
497 input: MemoryMappedList,366 input: MemoryMappedList,
498367
499 /// Minimized past inputs leading to new pcs or rodata reads. These are randomly mutated in368 /// Minimized past inputs leading to new pc hits.
500 /// round-robin fashion369 /// These are randomly mutated in round-robin fashion
501 /// Element zero is always an empty input. It is gauraunteed no other elements are empty.370 /// Element zero is always an empty input. It is gauraunteed no other elements are empty.
502 corpus: std.ArrayListUnmanaged([]const u8),371 corpus: std.ArrayListUnmanaged([]const u8),
503 corpus_pos: usize,372 corpus_pos: usize,
...@@ -596,10 +465,9 @@ const Fuzzer = struct {...@@ -596,10 +465,9 @@ const Fuzzer = struct {
596 self.run();465 self.run();
597 inst.setFresh();466 inst.setFresh();
598 inst.updateSeen();467 inst.updateSeen();
599 inst.clearNewRodataLoads();
600 }468 }
601469
602 /// Assumes fresh_pcs and fresh_rodata_loads correspond to the input470 /// Assumes `fresh_pcs` correspond to the input
603 fn minimizeInput(self: *Fuzzer) void {471 fn minimizeInput(self: *Fuzzer) void {
604 // The minimization technique is kept relatively simple, we sequentially try to remove each472 // The minimization technique is kept relatively simple, we sequentially try to remove each
605 // byte and check that the new pcs and memory loads are still hit.473 // byte and check that the new pcs and memory loads are still hit.
...@@ -609,7 +477,6 @@ const Fuzzer = struct {...@@ -609,7 +477,6 @@ const Fuzzer = struct {
609 const old = self.input.orderedRemove(i);477 const old = self.input.orderedRemove(i);
610478
611 @memset(exec.pc_counters, 0);479 @memset(exec.pc_counters, 0);
612 inst.clearNewRodataLoads();
613 self.run();480 self.run();
614481
615 if (!inst.atleastFresh()) {482 if (!inst.atleastFresh()) {
...@@ -623,11 +490,7 @@ const Fuzzer = struct {...@@ -623,11 +490,7 @@ const Fuzzer = struct {
623 }490 }
624491
625 fn run(self: *Fuzzer) void {492 fn run(self: *Fuzzer) void {
626 // We don't need to clear pc_counters here; all we care about is new hits and not already493 // `pc_counters` is not cleared since only new hits are relevant.
627 // seen hits. Ideally, we wouldn't even have these counters and do something similiar to
628 // what we do for tracking memory (i.e. a __sanitizer_cov function that updates a flag on a
629 // new hit.)
630 assert(!inst.any_new_rodata_loads);
631494
632 mem.bytesAsValue(usize, self.input.items[0..8]).* =495 mem.bytesAsValue(usize, self.input.items[0..8]).* =
633 mem.nativeToLittle(usize, self.input.items.len - 8);496 mem.nativeToLittle(usize, self.input.items.len - 8);
...@@ -673,7 +536,6 @@ const Fuzzer = struct {...@@ -673,7 +536,6 @@ const Fuzzer = struct {
673 inst.setFresh();536 inst.setFresh();
674 self.minimizeInput();537 self.minimizeInput();
675 inst.updateSeen();538 inst.updateSeen();
676 inst.clearNewRodataLoads();
677539
678 // An empty-input has always been tried, so if an empty input is fresh then the540 // An empty-input has always been tried, so if an empty input is fresh then the
679 // test has to be non-deterministic. This has to be checked as duplicate empty541 // test has to be non-deterministic. This has to be checked as duplicate empty
...@@ -796,58 +658,6 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases: [*]const u64) void {...@@ -796,58 +658,6 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases: [*]const u64) void {
796 }658 }
797}659}
798660
799fn genericLoad(T: anytype, ptr: *align(1) const T, comptime opt_const_vals_field: ?[]const u8) void {
800 const addr = @intFromPtr(ptr);
801 const off = addr -% exec.rodata_addr;
802 if (off >= exec.rodata_size) {
803 @branchHint(.likely);
804 return;
805 }
806
807 const i = off / @bitSizeOf(usize);
808 // Bits are intentionally truncated since the pointer will almost always be aligned
809 const hit = (@as(usize, (1 << @sizeOf(T)) - 1)) << @intCast(off % @bitSizeOf(usize));
810 const new = hit & ~inst.seen_rodata_loads[i];
811 if (new == 0) {
812 @branchHint(.likely);
813 return;
814 }
815
816 inst.new_rodata_loads[i] |= new;
817 inst.any_new_rodata_loads = true;
818
819 if (opt_const_vals_field) |const_vals_field| {
820 // This may have already been hit and this run is just being used for evaluating the
821 // input, in which case we do not want to readd the same value.
822 if (inst.any_fresh_rodata_loads) {
823 @branchHint(.unlikely);
824 if (new & ~inst.fresh_rodata_loads[i] == 0)
825 return;
826 }
827 @field(inst, const_vals_field).append(gpa, ptr.*) catch @panic("OOM");
828 }
829}
830
831export fn __sanitizer_cov_load1(ptr: *align(1) const u8) void {
832 genericLoad(u8, ptr, null);
833}
834
835export fn __sanitizer_cov_load2(ptr: *align(1) const u16) void {
836 genericLoad(u16, ptr, "const_vals2");
837}
838
839export fn __sanitizer_cov_load4(ptr: *align(1) const u32) void {
840 genericLoad(u32, ptr, "const_vals4");
841}
842
843export fn __sanitizer_cov_load8(ptr: *align(1) const u64) void {
844 genericLoad(u64, ptr, "const_vals8");
845}
846
847export fn __sanitizer_cov_load16(ptr: *align(1) const u128) void {
848 genericLoad(u128, ptr, "const_vals16");
849}
850
851export fn __sanitizer_cov_trace_cmp1(arg1: u8, arg2: u8) void {661export fn __sanitizer_cov_trace_cmp1(arg1: u8, arg2: u8) void {
852 _ = arg1;662 _ = arg1;
853 _ = arg2;663 _ = arg2;
src/codegen/llvm.zig+1-1
...@@ -1115,7 +1115,7 @@ pub const Object = struct {...@@ -1115,7 +1115,7 @@ pub const Object = struct {
1115 .NoPrune = false,1115 .NoPrune = false,
1116 // Workaround for https://github.com/llvm/llvm-project/pull/1064641116 // Workaround for https://github.com/llvm/llvm-project/pull/106464
1117 .StackDepth = true,1117 .StackDepth = true,
1118 .TraceLoads = options.fuzz,1118 .TraceLoads = false,
1119 .TraceStores = false,1119 .TraceStores = false,
1120 .CollectControlFlow = false,1120 .CollectControlFlow = false,
1121 },1121 },