authorgravatar for goon.pri.low@gmail.comKendall Condon <goon.pri.low@gmail.com> 2025-05-01 16:52:56-04:00
committergravatar for goon.pri.low@gmail.comKendall Condon <goon.pri.low@gmail.com> 2025-09-18 18:56:10-04:00
loge66b269333b5c400d76078318ac92354753bcf7b
treec198e686748f59bd025c4d4ced7210a8d3bdb62f
parenta0ec4e270e680960290642468f6df3ce7e7d7664

greatly improve capabilities of the fuzzer

This PR significantly improves the capabilities of the fuzzer. The changes made to the fuzzer to accomplish this feat mostly include tracking memory reads from .rodata to determine fresh inputs, new mutations (especially the ones that insert const values from .rodata reads and __sanitizer_conv_const_cmp), and minimizing found inputs. Additionally, the runs per second has greatly been increased due to generating smaller inputs and avoiding clearing the 8-bit pc counters. An additional feature added is that the length of the input file is now stored and the old input file is rerun upon start. Other changes made to the fuzzer include more logical initialization, using one shared file `in` for inputs, creating corpus files with proper sizes, and using hexadecimal-numbered corpus files for simplicity. Furthermore, I added several new fuzz tests to gauge the fuzzer's efficiency. I also tried to add a test for zstandard decompression, which it crashed within 60,000 runs (less than a second.) Bug fixes include: * Fixed a race conditions when multiple fuzzer processes needed to use the same coverage file. * Web interface stats now update even when unique runs is not changing. * Fixed tokenizer.testPropertiesUpheld to allow stray carriage returns since they are valid whitespace.

7 files changed, 1458 insertions(+), 487 deletions(-)

lib/compiler/test_runner.zig+18-29
...@@ -4,6 +4,7 @@ const builtin = @import("builtin");...@@ -4,6 +4,7 @@ const builtin = @import("builtin");
4const std = @import("std");4const std = @import("std");
5const testing = std.testing;5const testing = std.testing;
6const assert = std.debug.assert;6const assert = std.debug.assert;
7const fuzz_abi = std.Build.abi.fuzz;
78
8pub const std_options: std.Options = .{9pub const std_options: std.Options = .{
9 .logFn = log,10 .logFn = log,
...@@ -57,7 +58,7 @@ pub fn main() void {...@@ -57,7 +58,7 @@ pub fn main() void {
57 fba.reset();58 fba.reset();
58 if (builtin.fuzz) {59 if (builtin.fuzz) {
59 const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument");60 const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument");
60 fuzzer_init(FuzzerSlice.fromSlice(cache_dir));61 fuzz_abi.fuzzer_init(.fromSlice(cache_dir));
61 }62 }
6263
63 if (listen) {64 if (listen) {
...@@ -78,7 +79,7 @@ fn mainServer() !void {...@@ -78,7 +79,7 @@ fn mainServer() !void {
78 });79 });
7980
80 if (builtin.fuzz) {81 if (builtin.fuzz) {
81 const coverage_id = fuzzer_coverage_id();82 const coverage_id = fuzz_abi.fuzzer_coverage_id();
82 try server.serveU64Message(.coverage_id, coverage_id);83 try server.serveU64Message(.coverage_id, coverage_id);
83 }84 }
8485
...@@ -152,14 +153,19 @@ fn mainServer() !void {...@@ -152,14 +153,19 @@ fn mainServer() !void {
152 });153 });
153 },154 },
154 .start_fuzzing => {155 .start_fuzzing => {
156 // This ensures that this code won't be analyzed and hence reference fuzzer symbols
157 // since they are not present.
155 if (!builtin.fuzz) unreachable;158 if (!builtin.fuzz) unreachable;
159
156 const index = try server.receiveBody_u32();160 const index = try server.receiveBody_u32();
157 const test_fn = builtin.test_functions[index];161 const test_fn = builtin.test_functions[index];
158 const entry_addr = @intFromPtr(test_fn.func);162 const entry_addr = @intFromPtr(test_fn.func);
163
159 try server.serveU64Message(.fuzz_start_addr, entry_addr);164 try server.serveU64Message(.fuzz_start_addr, entry_addr);
160 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);165 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
161 is_fuzz_test = false;166 is_fuzz_test = false;
162 fuzzer_set_name(test_fn.name.ptr, test_fn.name.len);167 fuzz_test_index = index;
168
163 test_fn.func() catch |err| switch (err) {169 test_fn.func() catch |err| switch (err) {
164 error.SkipZigTest => return,170 error.SkipZigTest => return,
165 else => {171 else => {
...@@ -184,6 +190,8 @@ fn mainServer() !void {...@@ -184,6 +190,8 @@ fn mainServer() !void {
184190
185fn mainTerminal() void {191fn mainTerminal() void {
186 @disableInstrumentation();192 @disableInstrumentation();
193 if (builtin.fuzz) @panic("fuzz test requires server");
194
187 const test_fn_list = builtin.test_functions;195 const test_fn_list = builtin.test_functions;
188 var ok_count: usize = 0;196 var ok_count: usize = 0;
189 var skip_count: usize = 0;197 var skip_count: usize = 0;
...@@ -333,28 +341,8 @@ pub fn mainSimple() anyerror!void {...@@ -333,28 +341,8 @@ pub fn mainSimple() anyerror!void {
333 if (failed != 0) std.process.exit(1);341 if (failed != 0) std.process.exit(1);
334}342}
335343
336const FuzzerSlice = extern struct {
337 ptr: [*]const u8,
338 len: usize,
339
340 /// Inline to avoid fuzzer instrumentation.
341 inline fn toSlice(s: FuzzerSlice) []const u8 {
342 return s.ptr[0..s.len];
343 }
344
345 /// Inline to avoid fuzzer instrumentation.
346 inline fn fromSlice(s: []const u8) FuzzerSlice {
347 return .{ .ptr = s.ptr, .len = s.len };
348 }
349};
350
351var is_fuzz_test: bool = undefined;344var is_fuzz_test: bool = undefined;
352345var fuzz_test_index: u32 = undefined;
353extern fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void;
354extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
355extern fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void;
356extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.c) void) void;
357extern fn fuzzer_coverage_id() u64;
358346
359pub fn fuzz(347pub fn fuzz(
360 context: anytype,348 context: anytype,
...@@ -385,12 +373,12 @@ pub fn fuzz(...@@ -385,12 +373,12 @@ pub fn fuzz(
385 const global = struct {373 const global = struct {
386 var ctx: @TypeOf(context) = undefined;374 var ctx: @TypeOf(context) = undefined;
387375
388 fn fuzzer_one(input_ptr: [*]const u8, input_len: usize) callconv(.c) void {376 fn test_one(input: fuzz_abi.Slice) callconv(.c) void {
389 @disableInstrumentation();377 @disableInstrumentation();
390 testing.allocator_instance = .{};378 testing.allocator_instance = .{};
391 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);379 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
392 log_err_count = 0;380 log_err_count = 0;
393 testOne(ctx, input_ptr[0..input_len]) catch |err| switch (err) {381 testOne(ctx, input.toSlice()) catch |err| switch (err) {
394 error.SkipZigTest => return,382 error.SkipZigTest => return,
395 else => {383 else => {
396 std.debug.lockStdErr();384 std.debug.lockStdErr();
...@@ -411,10 +399,11 @@ pub fn fuzz(...@@ -411,10 +399,11 @@ pub fn fuzz(
411 testing.allocator_instance = .{};399 testing.allocator_instance = .{};
412 defer testing.allocator_instance = prev_allocator_state;400 defer testing.allocator_instance = prev_allocator_state;
413401
414 for (options.corpus) |elem| fuzzer_init_corpus_elem(elem.ptr, elem.len);
415
416 global.ctx = context;402 global.ctx = context;
417 fuzzer_start(&global.fuzzer_one);403 fuzz_abi.fuzzer_init_test(&global.test_one, .fromSlice(builtin.test_functions[fuzz_test_index].name));
404 for (options.corpus) |elem|
405 fuzz_abi.fuzzer_new_input(.fromSlice(elem));
406 fuzz_abi.fuzzer_main();
418 return;407 return;
419 }408 }
420409
lib/fuzzer.zig+1381-433
...@@ -1,532 +1,1465 @@...@@ -1,532 +1,1465 @@
1const builtin = @import("builtin");1const builtin = @import("builtin");
2const std = @import("std");2const std = @import("std");
3const Allocator = std.mem.Allocator;3const mem = std.mem;
4const math = std.math;
5const Allocator = mem.Allocator;
4const assert = std.debug.assert;6const assert = std.debug.assert;
5const fatal = std.process.fatal;7const panic = std.debug.panic;
6const SeenPcsHeader = std.Build.abi.fuzz.SeenPcsHeader;8const abi = std.Build.abi.fuzz;
9const native_endian = builtin.cpu.arch.endian();
710
8pub const std_options = std.Options{11pub const std_options = std.Options{
9 .logFn = logOverride,12 .logFn = logOverride,
10};13};
1114
12var log_file_buffer: [256]u8 = undefined;
13var log_file_writer: ?std.fs.File.Writer = null;
14
15fn logOverride(15fn logOverride(
16 comptime level: std.log.Level,16 comptime level: std.log.Level,
17 comptime scope: @Type(.enum_literal),17 comptime scope: @Type(.enum_literal),
18 comptime format: []const u8,18 comptime format: []const u8,
19 args: anytype,19 args: anytype,
20) void {20) void {
21 const fw = if (log_file_writer) |*f| f else f: {21 const f = log_f orelse
22 const f = fuzzer.cache_dir.createFile("tmp/libfuzzer.log", .{}) catch22 panic("attempt to use log before initialization, message:\n" ++ format, args);
23 @panic("failed to open fuzzer log file");23 f.lock(.exclusive) catch |e| panic("failed to lock logging file: {t}", .{e});
24 log_file_writer = f.writer(&log_file_buffer);24 defer f.unlock();
25 break :f &log_file_writer.?;25
26 };26 var buf: [256]u8 = undefined;
27 var fw = f.writer(&buf);
28 const end = f.getEndPos() catch |e| panic("failed to get fuzzer log file end: {t}", .{e});
29 fw.seekTo(end) catch |e| panic("failed to seek to fuzzer log file end: {t}", .{e});
30
27 const prefix1 = comptime level.asText();31 const prefix1 = comptime level.asText();
28 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";32 const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
29 fw.interface.print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch33 fw.interface.print(
30 @panic("failed to write to fuzzer log");34 "[{s}] " ++ prefix1 ++ prefix2 ++ format ++ "\n",
31 fw.interface.flush() catch @panic("failed to flush fuzzer log");35 .{current_test_name orelse "setup"} ++ args,
36 ) catch panic("failed to write to fuzzer log: {t}", .{fw.err.?});
37 fw.interface.flush() catch panic("failed to write to fuzzer log: {t}", .{fw.err.?});
32}38}
3339
34/// Helps determine run uniqueness in the face of recursion.40var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
35export threadlocal var __sancov_lowest_stack: usize = 0;41const gpa = switch (builtin.mode) {
42 .Debug => debug_allocator.allocator(),
43 .ReleaseFast, .ReleaseSmall, .ReleaseSafe => std.heap.smp_allocator,
44};
3645
37export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void {46/// Part of `exec`, however seperate to allow it to be set before `exec` is.
38 handleCmp(@returnAddress(), arg1, arg2);47var log_f: ?std.fs.File = null;
39}48var exec: Executable = .preinit;
49var inst: Instrumentation = .preinit;
50var fuzzer: Fuzzer = undefined;
51var current_test_name: ?[]const u8 = null;
4052
41export fn __sanitizer_cov_trace_cmp1(arg1: u8, arg2: u8) void {53fn bitsetUsizes(elems: usize) usize {
42 handleCmp(@returnAddress(), arg1, arg2);54 return math.divCeil(usize, elems, @bitSizeOf(usize)) catch unreachable;
43}55}
4456
45export fn __sanitizer_cov_trace_const_cmp2(arg1: u16, arg2: u16) void {57const Executable = struct {
46 handleCmp(@returnAddress(), arg1, arg2);58 /// Tracks the hit count for each pc as updated by the process's instrumentation.
47}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,
63
64 cache_f: std.fs.Dir,
65 /// Shared copy of all pcs that have been hit stored in a memory-mapped file that can viewed
66 /// while the fuzzer is running.
67 shared_seen_pcs: MemoryMappedList,
68 /// Hash of pcs used to uniquely identify the shared coverage file
69 pc_digest: u64,
70
71 /// A minimal state for this struct which instrumentation can function on.
72 /// Used before this structure is initialized to avoid illegal behavior
73 /// from instrumentation functions being called and using undefined values.
74 pub const preinit: Executable = .{
75 .rodata_addr = 0,
76 .rodata_size = 0,
77 .pc_counters = undefined, // instrumentation works off the __sancov_cntrs section
78 .cache_f = undefined,
79 .shared_seen_pcs = undefined,
80 .pc_digest = undefined,
81 };
4882
49export fn __sanitizer_cov_trace_cmp2(arg1: u16, arg2: u16) void {83 /// Even on error, this initializes rodata_addr and rodata_size to valid values
50 handleCmp(@returnAddress(), arg1, arg2);84 fn initRodata(self: *Executable) !void {
51}85 errdefer {
86 self.rodata_addr = 0;
87 self.rodata_size = 0;
88 }
5289
53export fn __sanitizer_cov_trace_const_cmp4(arg1: u32, arg2: u32) void {90 const exec_path = std.fs.selfExePathAlloc(gpa) catch |e|
54 handleCmp(@returnAddress(), arg1, arg2);91 if (e == error.OutOfMemory) @panic("OOM") else return e;
55}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 }
56148
57export fn __sanitizer_cov_trace_cmp4(arg1: u32, arg2: u32) void {149 fn getCoverageFile(cache_dir: std.fs.Dir, pcs: []const usize, pc_digest: u64) MemoryMappedList {
58 handleCmp(@returnAddress(), arg1, arg2);150 const pc_bitset_usizes = bitsetUsizes(pcs.len);
59}151 const coverage_file_name = std.fmt.hex(pc_digest);
152 comptime assert(abi.SeenPcsHeader.trailing[0] == .pc_bits_usize);
153 comptime assert(abi.SeenPcsHeader.trailing[1] == .pc_addr);
60154
61export fn __sanitizer_cov_trace_const_cmp8(arg1: u64, arg2: u64) void {155 var v = cache_dir.makeOpenPath("v", .{}) catch |e|
62 handleCmp(@returnAddress(), arg1, arg2);156 panic("failed to create directory 'v': {t}", .{e});
63}157 defer v.close();
158 const coverage_file, const populate = if (v.createFile(&coverage_file_name, .{
159 .read = true,
160 // If we create the file, we want to block other processes while we populate it
161 .lock = .exclusive,
162 .exclusive = true,
163 })) |f|
164 .{ f, true }
165 else |e| switch (e) {
166 error.PathAlreadyExists => .{ v.openFile(&coverage_file_name, .{
167 .mode = .read_write,
168 .lock = .shared,
169 }) catch |e2| panic(
170 "failed to open existing coverage file '{s}': {t}",
171 .{ &coverage_file_name, e2 },
172 ), false },
173 else => panic("failed to create coverage file '{s}': {t}", .{ &coverage_file_name, e }),
174 };
64175
65export fn __sanitizer_cov_trace_cmp8(arg1: u64, arg2: u64) void {176 const coverage_file_len = @sizeOf(abi.SeenPcsHeader) +
66 handleCmp(@returnAddress(), arg1, arg2);177 pc_bitset_usizes * @sizeOf(usize) +
67}178 pcs.len * @sizeOf(usize);
179 if (populate) {
180 defer coverage_file.lock(.shared) catch |e| panic(
181 "failed to demote lock for coverage file '{s}': {t}",
182 .{ &coverage_file_name, e },
183 );
184 var map = MemoryMappedList.create(coverage_file, 0, coverage_file_len) catch |e| panic(
185 "failed to init memory map for coverage file '{s}': {t}",
186 .{ &coverage_file_name, e },
187 );
188 map.appendSliceAssumeCapacity(mem.asBytes(&abi.SeenPcsHeader{
189 .n_runs = 0,
190 .unique_runs = 0,
191 .pcs_len = pcs.len,
192 }));
193 map.appendNTimesAssumeCapacity(0, pc_bitset_usizes * @sizeOf(usize));
194 map.appendSliceAssumeCapacity(mem.sliceAsBytes(pcs));
195 return map;
196 } else {
197 const size = coverage_file.getEndPos() catch |e| panic(
198 "failed to stat coverage file '{s}': {t}",
199 .{ &coverage_file_name, e },
200 );
201 if (size != coverage_file_len) panic(
202 "incompatible existing coverage file '{s}' (differing lengths: {} != {})",
203 .{ &coverage_file_name, size, coverage_file_len },
204 );
205
206 const map = MemoryMappedList.init(
207 coverage_file,
208 coverage_file_len,
209 coverage_file_len,
210 ) catch |e| panic(
211 "failed to init memory map for coverage file '{s}': {t}",
212 .{ &coverage_file_name, e },
213 );
214
215 const seen_pcs_header: *const abi.SeenPcsHeader = @ptrCast(@volatileCast(map.items));
216 if (seen_pcs_header.pcs_len != pcs.len) panic(
217 "incompatible existing coverage file '{s}' (differing pcs length: {} != {})",
218 .{ &coverage_file_name, seen_pcs_header.pcs_len, pcs.len },
219 );
220 if (mem.indexOfDiff(usize, seen_pcs_header.pcAddrs(), pcs)) |i| panic(
221 "incompatible existing coverage file '{s}' (differing pc at index {d}: {x} != {x})",
222 .{ &coverage_file_name, i, seen_pcs_header.pcAddrs()[i], pcs[i] },
223 );
224
225 return map;
226 }
227 }
68228
69export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void {229 pub fn init(cache_dir_path: []const u8) Executable {
70 const pc = @returnAddress();230 var self: Executable = undefined;
71 const len = cases_ptr[0];
72 const val_size_in_bits = cases_ptr[1];
73 const cases = cases_ptr[2..][0..len];
74 fuzzer.traceValue(pc ^ val);
75 _ = val_size_in_bits;
76 _ = cases;
77 //std.log.debug("0x{x}: switch on value {d} ({d} bits) with {d} cases", .{
78 // pc, val, val_size_in_bits, cases.len,
79 //});
80}
81231
82export fn __sanitizer_cov_trace_pc_indir(callee: usize) void {232 const cache_dir = std.fs.cwd().makeOpenPath(cache_dir_path, .{}) catch |e| panic(
83 // Not valuable because we already have pc tracing via 8bit counters.233 "failed to open directory '{s}': {t}",
84 _ = callee;234 .{ cache_dir_path, e },
85 //const pc = @returnAddress();235 );
86 //fuzzer.traceValue(pc ^ callee);236 log_f = cache_dir.createFile("tmp/libfuzzer.log", .{ .truncate = false }) catch |e|
87 //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee });237 panic("failed to create file 'tmp/libfuzzer.log': {t}", .{e});
88}238 self.cache_f = cache_dir.makeOpenPath("f", .{}) catch |e|
89export fn __sanitizer_cov_8bit_counters_init(start: usize, end: usize) void {239 panic("failed to open directory 'f': {t}", .{e});
90 // clang will emit a call to this function when compiling with code coverage instrumentation.240
91 // however fuzzer_init() does not need this information, since it directly reads from the symbol table.241 // Linkers are expected to automatically add symbols prefixed with these for the start and
92 _ = start;242 // end of sections whose names are valid C identifiers.
93 _ = end;243 const ofmt = builtin.object_format;
94}244 const section_start_prefix, const section_end_prefix = switch (ofmt) {
95export fn __sanitizer_cov_pcs_init(start: usize, end: usize) void {245 .elf => .{ "__start_", "__stop_" },
96 // clang will emit a call to this function when compiling with code coverage instrumentation.246 .macho => .{ "\x01section$start$__DATA$", "\x01section$end$__DATA$" },
97 // however fuzzer_init() does not need this information, since it directly reads from the symbol table.247 else => @compileError("unsupported fuzzing object format '" ++ @tagName(ofmt) ++ "'"),
98 _ = start;248 };
99 _ = end;
100}
101249
102fn handleCmp(pc: usize, arg1: u64, arg2: u64) void {250 self.pc_counters = blk: {
103 fuzzer.traceValue(pc ^ arg1 ^ arg2);251 const pc_counters_start_name = section_start_prefix ++ "__sancov_cntrs";
104 //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 });252 const pc_counters_start = @extern([*]u8, .{
105}253 .name = pc_counters_start_name,
254 .linkage = .weak,
255 }) orelse panic("missing {s} symbol", .{pc_counters_start_name});
106256
107const Fuzzer = struct {257 const pc_counters_end_name = section_end_prefix ++ "__sancov_cntrs";
108 rng: std.Random.DefaultPrng,258 const pc_counters_end = @extern([*]u8, .{
109 pcs: []const usize,259 .name = pc_counters_end_name,
110 pc_counters: []u8,260 .linkage = .weak,
111 n_runs: usize,261 }) orelse panic("missing {s} symbol", .{pc_counters_end_name});
112 traced_comparisons: std.AutoArrayHashMapUnmanaged(usize, void),
113 /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.
114 /// Stored in a memory-mapped file so that it can be shared with other
115 /// processes and viewed while the fuzzer is running.
116 seen_pcs: MemoryMappedList,
117 cache_dir: std.fs.Dir,
118 /// Identifies the file name that will be used to store coverage
119 /// information, available to other processes.
120 coverage_id: u64,
121 unit_test_name: []const u8,
122
123 /// The index corresponds to the file name within the f/ subdirectory.
124 /// The string is the input.
125 /// This data is read-only; it caches what is on the filesystem.
126 corpus: std.ArrayListUnmanaged(Input),
127 corpus_directory: std.Build.Cache.Directory,
128262
129 /// The next input that will be given to the testOne function. When the263 break :blk pc_counters_start[0 .. pc_counters_end - pc_counters_start];
130 /// current process crashes, this memory-mapped file is used to recover the264 };
131 /// input.
132 ///
133 /// The file size corresponds to the capacity. The length is not stored
134 /// and that is the next thing to work on!
135 input: MemoryMappedList,
136265
137 const Input = struct {266 const pcs = blk: {
138 bytes: []u8,267 const pcs_start_name = section_start_prefix ++ "__sancov_pcs1";
139 last_traced_comparison: usize,268 const pcs_start = @extern([*]usize, .{
140 };269 .name = pcs_start_name,
270 .linkage = .weak,
271 }) orelse panic("missing {s} symbol", .{pcs_start_name});
141272
142 const Slice = extern struct {273 const pcs_end_name = section_end_prefix ++ "__sancov_pcs1";
143 ptr: [*]const u8,274 const pcs_end = @extern([*]usize, .{
144 len: usize,275 .name = pcs_end_name,
276 .linkage = .weak,
277 }) orelse panic("missing {s} symbol", .{pcs_end_name});
145278
146 fn toZig(s: Slice) []const u8 {279 break :blk pcs_start[0 .. pcs_end - pcs_start];
147 return s.ptr[0..s.len];280 };
148 }
149281
150 fn fromZig(s: []const u8) Slice {282 if (self.pc_counters.len != pcs.len) panic(
151 return .{283 "pc counters length and pcs length do not match ({} != {})",
152 .ptr = s.ptr,284 .{ self.pc_counters.len, pcs.len },
153 .len = s.len,285 );
154 };286
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));
293 self.shared_seen_pcs = getCoverageFile(cache_dir, pcs, self.pc_digest);
294
295 return self;
296 }
297
298 pub fn pcBitsetIterator(self: Executable) PcBitsetIterator {
299 return .{ .pc_counters = self.pc_counters };
300 }
301
302 /// Iterates over pc_counters returning a bitset for if each of them have been hit
303 pub const PcBitsetIterator = struct {
304 index: usize = 0,
305 pc_counters: []u8,
306
307 pub fn next(self: *PcBitsetIterator) usize {
308 const rest = self.pc_counters[self.index..];
309 if (rest.len >= @bitSizeOf(usize)) {
310 defer self.index += @bitSizeOf(usize);
311 const V = @Vector(@bitSizeOf(usize), u8);
312 return @as(usize, @bitCast(@as(V, @splat(0)) != rest[0..@bitSizeOf(usize)].*));
313 } else if (rest.len != 0) {
314 defer self.index += rest.len;
315 var res: usize = 0;
316 for (0.., rest) |bit_index, byte| {
317 res |= @shlExact(@as(usize, @intFromBool(byte != 0)), @intCast(bit_index));
318 }
319 return res;
320 } else unreachable;
155 }321 }
156 };322 };
323};
157324
158 fn init(f: *Fuzzer, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []const usize) !void {325/// Data gathered from instrumentation functions
159 f.cache_dir = cache_dir;326/// Seperate from Executable since its state is resetable and changes
160 f.pc_counters = pc_counters;327/// Seperate from Fuzzer since it may be needed before fuzzing starts
161 f.pcs = pcs;328const Instrumentation = struct {
162329 /// Bitset of seen pcs across all runs excluding fresh pcs.
163 // Choose a file name for the coverage based on a hash of the PCs that will be stored within.330 /// This is seperate then shared_seen_pcs because multiple fuzzing processes are likely using
164 const pc_digest = std.hash.Wyhash.hash(0, std.mem.sliceAsBytes(pcs));331 /// it which causes contention and unrelated pcs to our campaign being set.
165 f.coverage_id = pc_digest;332 seen_pcs: []usize,
166 const hex_digest = std.fmt.hex(pc_digest);333 /// Bitset of seen rodata bytes read across all runs
167 const coverage_file_path = "v/" ++ hex_digest;334 seen_rodata_loads: []usize,
168335
169 // Layout of this file:336 /// Bitset of run's read bytes that weren't present in seen_loads
170 // - Header337 /// Elements are always zero if !any_new_data_loads
171 // - list of PC addresses (usize elements)338 new_rodata_loads: []usize,
172 // - list of hit flag, 1 bit per address (stored in u8 elements)339 any_new_rodata_loads: bool,
173 const coverage_file = createFileBail(cache_dir, coverage_file_path, .{340
174 .read = true,341 /// Stores a fresh input's new pcs
175 .truncate = false,342 fresh_pcs: []usize,
176 });343 /// Stores a fresh input's new reads
177 const n_bitset_elems = (pcs.len + @bitSizeOf(usize) - 1) / @bitSizeOf(usize);344 /// Elements are always zero if !any_fresh_rodata_loads
178 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);345 fresh_rodata_loads: []usize,
179 comptime assert(SeenPcsHeader.trailing[1] == .pc_addr);346 any_fresh_rodata_loads: bool,
180 const bytes_len = @sizeOf(SeenPcsHeader) +347
181 n_bitset_elems * @sizeOf(usize) +348 /// Pcs which __sanitizer_cov_trace_switch and __sanitizer_cov_trace_const_cmpx
182 pcs.len * @sizeOf(usize);349 /// have been called from and have had their already been added to const_x_vals
183 const existing_len = coverage_file.getEndPos() catch |err| {350 const_pcs: std.AutoArrayHashMapUnmanaged(usize, void) = .empty,
184 fatal("unable to check len of coverage file: {s}", .{@errorName(err)});351 /// Values that have been constant operands in comparisons, switch cases, or memory reads
352 /// 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.
354 const_vals2: std.ArrayListUnmanaged(u16) = .empty,
355 const_vals4: std.ArrayListUnmanaged(u32) = .empty,
356 const_vals8: std.ArrayListUnmanaged(u64) = .empty,
357 const_vals16: std.ArrayListUnmanaged(u128) = .empty,
358
359 /// A minimal state for this struct which instrumentation can function on.
360 /// Used before this structure is initialized to avoid illegal behavior
361 /// from instrumentation functions being called and using undefined values.
362 pub const preinit: Instrumentation = .{
363 .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,
368 .fresh_rodata_loads = undefined,
369 .any_fresh_rodata_loads = undefined,
370 };
371
372 pub fn depreinit(self: *Instrumentation) void {
373 self.const_vals2.deinit(gpa);
374 self.const_vals4.deinit(gpa);
375 self.const_vals8.deinit(gpa);
376 self.const_vals16.deinit(gpa);
377 self.* = undefined;
378 }
379
380 pub fn init() Instrumentation {
381 const pc_bitset_usizes = bitsetUsizes(exec.pc_counters.len);
382 const rodata_bitset_usizes = bitsetUsizes(exec.rodata_size);
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");
385 var fba_ctx: std.heap.FixedBufferAllocator = .init(buf);
386 const fba = fba_ctx.allocator();
387
388 var self: Instrumentation = .{
389 .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,
394 .fresh_rodata_loads = fba.alloc(usize, rodata_bitset_usizes) catch unreachable,
395 .any_fresh_rodata_loads = undefined,
185 };396 };
186 if (existing_len == 0) {397 self.reset();
187 coverage_file.setEndPos(bytes_len) catch |err| {398 return self;
188 fatal("unable to set len of coverage file: {s}", .{@errorName(err)});399 }
189 };400
190 } else if (existing_len != bytes_len) {401 pub fn reset(self: *Instrumentation) void {
191 fatal("incompatible existing coverage file (differing lengths)", .{});402 @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);
407 @memset(self.fresh_rodata_loads, 0);
408 self.any_fresh_rodata_loads = false;
409 self.const_pcs.clearRetainingCapacity();
410 self.const_vals2.clearRetainingCapacity();
411 self.const_vals4.clearRetainingCapacity();
412 self.const_vals8.clearRetainingCapacity();
413 self.const_vals16.clearRetainingCapacity();
414 }
415
416 /// If false is returned, then the pc is marked as seen
417 pub fn constPcSeen(self: *Instrumentation, pc: usize) bool {
418 return (self.const_pcs.getOrPut(gpa, pc) catch @panic("OOM")).found_existing;
419 }
420
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;
192 }425 }
193 f.seen_pcs = MemoryMappedList.init(coverage_file, existing_len, bytes_len) catch |err| {426 }
194 fatal("unable to init coverage memory map: {s}", .{@errorName(err)});427
195 };428 pub fn isFresh(self: *Instrumentation) bool {
196 if (existing_len != 0) {429 if (self.any_new_rodata_loads) return true;
197 const existing_pcs_bytes = f.seen_pcs.items[@sizeOf(SeenPcsHeader) + @sizeOf(usize) * n_bitset_elems ..][0 .. pcs.len * @sizeOf(usize)];430
198 const existing_pcs = std.mem.bytesAsSlice(usize, existing_pcs_bytes);431 var hit_pcs = exec.pcBitsetIterator();
199 for (existing_pcs, pcs, 0..) |old, new, i| {432 for (self.seen_pcs) |seen_pcs| {
200 if (old != new) {433 if (hit_pcs.next() & ~seen_pcs != 0) return true;
201 fatal("incompatible existing coverage file (differing PC at index {d}: {x} != {x})", .{
202 i, old, new,
203 });
204 }
205 }
206 } else {
207 const header: SeenPcsHeader = .{
208 .n_runs = 0,
209 .unique_runs = 0,
210 .pcs_len = pcs.len,
211 };
212 f.seen_pcs.appendSliceAssumeCapacity(std.mem.asBytes(&header));
213 f.seen_pcs.appendNTimesAssumeCapacity(0, n_bitset_elems * @sizeOf(usize));
214 f.seen_pcs.appendSliceAssumeCapacity(std.mem.sliceAsBytes(pcs));
215 }434 }
435
436 return false;
216 }437 }
217438
218 fn initNextInput(f: *Fuzzer) void {439 /// Updates fresh_pcs and fresh_rodata_loads
219 while (true) {440 /// any_new_rodata_loads and elements of new_rodata_loads are unspecified
220 const i = f.corpus.items.len;441 /// afterwards, but still valid.
221 var buf: [30]u8 = undefined;442 pub fn setFresh(self: *Instrumentation) void {
222 const input_sub_path = std.fmt.bufPrint(&buf, "{d}", .{i}) catch unreachable;443 var hit_pcs = exec.pcBitsetIterator();
223 const input = f.corpus_directory.handle.readFileAlloc(input_sub_path, gpa, .limited(1 << 31)) catch |err| switch (err) {444 for (self.seen_pcs, self.fresh_pcs) |seen_pcs, *fresh_pcs| {
224 error.FileNotFound => {445 fresh_pcs.* = hit_pcs.next() & ~seen_pcs;
225 // Make this one the next input.
226 const input_file = f.corpus_directory.handle.createFile(input_sub_path, .{
227 .exclusive = true,
228 .truncate = false,
229 .read = true,
230 }) catch |e| switch (e) {
231 error.PathAlreadyExists => continue,
232 else => fatal("unable to create '{f}{d}: {s}", .{ f.corpus_directory, i, @errorName(err) }),
233 };
234 errdefer input_file.close();
235 // Initialize the mmap for the current input.
236 f.input = MemoryMappedList.create(input_file, 0, std.heap.page_size_max) catch |e| {
237 fatal("unable to init memory map for input at '{f}{d}': {s}", .{
238 f.corpus_directory, i, @errorName(e),
239 });
240 };
241 break;
242 },
243 else => fatal("unable to read '{f}{d}': {s}", .{ f.corpus_directory, i, @errorName(err) }),
244 };
245 errdefer gpa.free(input);
246 f.corpus.append(gpa, .{
247 .bytes = input,
248 .last_traced_comparison = 0,
249 }) catch |err| oom(err);
250 }446 }
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);
251 }450 }
252451
253 fn addCorpusElem(f: *Fuzzer, input: []const u8) !void {452 /// Returns if exec.pc_counters and new_rodata_loads are the same or a superset of fresh_pcs and
254 try f.corpus.append(gpa, .{453 /// fresh_rodata_loads respectively.
255 .bytes = try gpa.dupe(u8, input),454 pub fn atleastFresh(self: *Instrumentation) bool {
256 .last_traced_comparison = 0,455 var hit_pcs = exec.pcBitsetIterator();
257 });456 for (self.fresh_pcs) |fresh_pcs| {
457 if (fresh_pcs & hit_pcs.next() != fresh_pcs) return false;
458 }
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;
258 }468 }
259469
260 fn start(f: *Fuzzer) !void {470 /// Updates based off fresh_pcs and fresh_rodata_loads
261 const rng = fuzzer.rng.random();471 fn updateSeen(self: *Instrumentation) void {
472 comptime assert(abi.SeenPcsHeader.trailing[0] == .pc_bits_usize);
473 const shared_seen_pcs: [*]volatile usize = @ptrCast(
474 exec.shared_seen_pcs.items[@sizeOf(abi.SeenPcsHeader)..].ptr,
475 );
262476
263 // Grab the corpus which is namespaced based on `unit_test_name`.477 for (self.seen_pcs, shared_seen_pcs, self.fresh_pcs) |*seen, *shared_seen, fresh| {
264 {478 seen.* |= fresh;
265 if (f.unit_test_name.len == 0) fatal("test runner never set unit test name", .{});479 if (fresh != 0)
266 const sub_path = try std.fmt.allocPrint(gpa, "f/{s}", .{f.unit_test_name});480 _ = @atomicRmw(usize, shared_seen, .Or, fresh, .monotonic);
267 f.corpus_directory = .{
268 .handle = f.cache_dir.makeOpenPath(sub_path, .{}) catch |err|
269 fatal("unable to open corpus directory 'f/{s}': {t}", .{ sub_path, err }),
270 .path = sub_path,
271 };
272 initNextInput(f);
273 }481 }
274482
275 assert(f.n_runs == 0);483 if (self.any_fresh_rodata_loads) {
276484 for (self.seen_rodata_loads, self.fresh_rodata_loads) |*s, f|
277 // If the corpus is empty, synthesize one input.485 s.* |= f;
278 if (f.corpus.items.len == 0) {
279 const len = rng.uintLessThanBiased(usize, 200);
280 const slice = try gpa.alloc(u8, len);
281 rng.bytes(slice);
282 f.input.appendSliceAssumeCapacity(slice);
283 try f.corpus.append(gpa, .{
284 .bytes = slice,
285 .last_traced_comparison = 0,
286 });
287 runOne(f, 0);
288 }486 }
487 }
488};
489
490const Fuzzer = struct {
491 arena_ctx: std.heap.ArenaAllocator = .init(gpa),
492 rng: std.Random.DefaultPrng = .init(0),
493 test_one: abi.TestOne,
494 /// The next input that will be given to the testOne function. When the
495 /// current process crashes, this memory-mapped file is used to recover the
496 /// input.
497 input: MemoryMappedList,
498
499 /// Minimized past inputs leading to new pcs or rodata reads. These are randomly mutated in
500 /// round-robin fashion
501 /// Element zero is always an empty input. It is gauraunteed no other elements are empty.
502 corpus: std.ArrayListUnmanaged([]const u8),
503 corpus_pos: usize,
504 /// List of past mutations that have led to new inputs. This way, the mutations that are the
505 /// most effective are the most likely to be selected again. Starts with one of each mutation.
506 mutations: std.ArrayListUnmanaged(Mutation) = .empty,
507
508 /// Filesystem directory containing found inputs for future runs
509 corpus_dir: std.fs.Dir,
510 corpus_dir_idx: usize = 0,
511
512 pub fn init(test_one: abi.TestOne, unit_test_name: []const u8) Fuzzer {
513 var self: Fuzzer = .{
514 .test_one = test_one,
515 .input = undefined,
516 .corpus = .empty,
517 .corpus_pos = 0,
518 .mutations = .empty,
519 .corpus_dir = undefined,
520 };
521 const arena = self.arena_ctx.allocator();
522
523 self.corpus_dir = exec.cache_f.makeOpenPath(unit_test_name, .{}) catch |e|
524 panic("failed to open directory '{s}': {t}", .{ unit_test_name, e });
525 self.input = in: {
526 const f = self.corpus_dir.createFile("in", .{
527 .read = true,
528 .truncate = false,
529 // In case any other fuzz tests are running under the same test name,
530 // the input file is exclusively locked to ensures only one proceeds.
531 .lock = .exclusive,
532 .lock_nonblocking = true,
533 }) catch |e| switch (e) {
534 error.WouldBlock => @panic("input file 'in' is in use by another fuzzing process"),
535 else => panic("failed to create input file 'in': {t}", .{e}),
536 };
537 const size = f.getEndPos() catch |e| panic("failed to stat input file 'in': {t}", .{e});
538 const map = (if (size < std.heap.page_size_max)
539 MemoryMappedList.create(f, 8, std.heap.page_size_max)
540 else
541 MemoryMappedList.init(f, size, size)) catch |e|
542 panic("failed to memory map input file 'in': {t}", .{e});
543
544 // Perform a dry-run of the stored input if there was one in case it might reproduce a
545 // crash.
546 const old_in_len = mem.littleToNative(usize, mem.bytesAsValue(usize, map.items[0..8]).*);
547 if (size >= 8 and old_in_len != 0 and map.items.len - 8 < old_in_len) {
548 test_one(.fromSlice(@volatileCast(map.items[8..][0..old_in_len])));
549 }
550
551 break :in map;
552 };
553 inst.reset();
554
555 self.mutations.appendSlice(gpa, std.meta.tags(Mutation)) catch @panic("OOM");
556 // Ensure there is never an empty corpus. Additionally, an empty input usually leads to
557 // new inputs.
558 self.addInput(&.{});
289559
290 while (true) {560 while (true) {
291 const chosen_index = rng.uintLessThanBiased(usize, f.corpus.items.len);561 var name_buf: [@sizeOf(usize) * 2]u8 = undefined;
292 const modification = rng.enumValue(Mutation);562 const bytes = self.corpus_dir.readFileAlloc(
293 f.mutateAndRunOne(chosen_index, modification);563 std.fmt.bufPrint(&name_buf, "{x}", .{self.corpus_dir_idx}) catch unreachable,
564 arena,
565 .unlimited,
566 ) catch |e| switch (e) {
567 error.FileNotFound => break,
568 else => panic("failed to read corpus file '{x}': {t}", .{ self.corpus_dir_idx, e }),
569 };
570 // No corpus file of length zero will ever be created
571 if (bytes.len == 0)
572 panic("corrupt corpus file '{x}' (len of zero)", .{self.corpus_dir_idx});
573 self.addInput(bytes);
574 self.corpus_dir_idx += 1;
294 }575 }
576
577 return self;
295 }578 }
296579
297 /// `x` represents a possible branch. It is the PC address of the possible580 pub fn deinit(self: *Fuzzer) void {
298 /// branch site, hashed together with the value(s) used that determine to581 self.input.deinit();
299 /// where it branches.582 self.corpus.deinit(gpa);
300 fn traceValue(f: *Fuzzer, x: usize) void {583 self.mutations.deinit(gpa);
301 errdefer |err| oom(err);584 self.corpus_dir.close();
302 try f.traced_comparisons.put(gpa, x, {});585 self.arena_ctx.deinit();
586 self.* = undefined;
303 }587 }
304588
305 const Mutation = enum {589 pub fn addInput(self: *Fuzzer, bytes: []const u8) void {
306 remove_byte,590 self.corpus.append(gpa, bytes) catch @panic("OOM");
307 modify_byte,591 self.input.clearRetainingCapacity();
308 add_byte,592 self.input.ensureTotalCapacity(8 + bytes.len) catch |e|
309 };593 panic("could not resize shared input file: {t}", .{e});
594 self.input.items.len = 8;
595 self.input.appendSliceAssumeCapacity(bytes);
596 self.run();
597 inst.setFresh();
598 inst.updateSeen();
599 inst.clearNewRodataLoads();
600 }
310601
311 fn mutateAndRunOne(f: *Fuzzer, corpus_index: usize, mutation: Mutation) void {602 /// Assumes fresh_pcs and fresh_rodata_loads correspond to the input
312 const rng = fuzzer.rng.random();603 fn minimizeInput(self: *Fuzzer) void {
313 f.input.clearRetainingCapacity();604 // The minimization technique is kept relatively simple, we sequentially try to remove each
314 const old_input = f.corpus.items[corpus_index].bytes;605 // byte and check that the new pcs and memory loads are still hit.
315 f.input.ensureTotalCapacity(old_input.len + 1) catch @panic("mmap file resize failed");606 var i = self.input.items.len;
316 switch (mutation) {607 while (i != 8) {
317 .remove_byte => {608 i -= 1;
318 const omitted_index = rng.uintLessThanBiased(usize, old_input.len);609 const old = self.input.orderedRemove(i);
319 f.input.appendSliceAssumeCapacity(old_input[0..omitted_index]);610
320 f.input.appendSliceAssumeCapacity(old_input[omitted_index + 1 ..]);611 @memset(exec.pc_counters, 0);
321 },612 inst.clearNewRodataLoads();
322 .modify_byte => {613 self.run();
323 const modified_index = rng.uintLessThanBiased(usize, old_input.len);614
324 f.input.appendSliceAssumeCapacity(old_input);615 if (!inst.atleastFresh()) {
325 f.input.items[modified_index] = rng.int(u8);616 self.input.insertAssumeCapacity(i, old);
326 },617 } else {
327 .add_byte => {618 // This removal may have led to new pcs or memory loads being hit, so we need to
328 const modified_index = rng.uintLessThanBiased(usize, old_input.len);619 // update them to avoid duplicates.
329 f.input.appendSliceAssumeCapacity(old_input[0..modified_index]);620 inst.setFresh();
330 f.input.appendAssumeCapacity(rng.int(u8));621 }
331 f.input.appendSliceAssumeCapacity(old_input[modified_index..]);
332 },
333 }622 }
334 runOne(f, corpus_index);
335 }623 }
336624
337 fn runOne(f: *Fuzzer, corpus_index: usize) void {625 fn run(self: *Fuzzer) void {
338 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);626 // We don't need to clear pc_counters here; all we care about is new hits and not already
339627 // seen hits. Ideally, we wouldn't even have these counters and do something similiar to
340 f.traced_comparisons.clearRetainingCapacity();628 // what we do for tracking memory (i.e. a __sanitizer_cov function that updates a flag on a
341 @memset(f.pc_counters, 0);629 // new hit.)
342 __sancov_lowest_stack = std.math.maxInt(usize);630 assert(!inst.any_new_rodata_loads);
343631
344 fuzzer_one(@volatileCast(f.input.items.ptr), f.input.items.len);632 mem.bytesAsValue(usize, self.input.items[0..8]).* =
633 mem.nativeToLittle(usize, self.input.items.len - 8);
634 self.test_one(.fromSlice(@volatileCast(self.input.items[8..])));
345635
346 f.n_runs += 1;636 const header = mem.bytesAsValue(
637 abi.SeenPcsHeader,
638 exec.shared_seen_pcs.items[0..@sizeOf(abi.SeenPcsHeader)],
639 );
347 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);640 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
641 }
348642
349 // Track code coverage from all runs.643 pub fn cycle(self: *Fuzzer) void {
350 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);644 const input = self.corpus.items[self.corpus_pos];
351 const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]);645 self.corpus_pos += 1;
352 const remainder = f.pcs.len % @bitSizeOf(usize);646 if (self.corpus_pos == self.corpus.items.len)
353 const aligned_len = f.pcs.len - remainder;647 self.corpus_pos = 0;
354 const seen_pcs = header_end_ptr[0..aligned_len];648
355 const pc_counters = std.mem.bytesAsSlice([@bitSizeOf(usize)]u8, f.pc_counters[0..aligned_len]);649 const rng = self.rng.random();
356 const V = @Vector(@bitSizeOf(usize), u8);650 while (true) {
357 const zero_v: V = @splat(0);651 const m = self.mutations.items[rng.uintLessThanBiased(usize, self.mutations.items.len)];
358 var fresh = false;652 if (!m.mutate(
359 var superset = true;653 rng,
360654 input,
361 for (header_end_ptr[0..pc_counters.len], pc_counters) |*elem, *array| {655 &self.input,
362 const v: V = array.*;656 self.corpus.items,
363 const mask: usize = @bitCast(v != zero_v);657 inst.const_vals2.items,
364 const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic);658 inst.const_vals4.items,
365 fresh = fresh or (prev | mask) != prev;659 inst.const_vals8.items,
366 superset = superset and (prev | mask) != mask;660 inst.const_vals16.items,
367 }661 )) continue;
368 if (remainder > 0) {662
369 const i = pc_counters.len;663 self.run();
370 const elem = &seen_pcs[i];664 if (inst.isFresh()) {
371 var mask: usize = 0;665 @branchHint(.unlikely);
372 for (f.pc_counters[i * @bitSizeOf(usize) ..][0..remainder], 0..) |byte, bit_index| {666
373 mask |= @as(usize, @intFromBool(byte != 0)) << @intCast(bit_index);667 const header = mem.bytesAsValue(
668 abi.SeenPcsHeader,
669 exec.shared_seen_pcs.items[0..@sizeOf(abi.SeenPcsHeader)],
670 );
671 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
672
673 inst.setFresh();
674 self.minimizeInput();
675 inst.updateSeen();
676 inst.clearNewRodataLoads();
677
678 // 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 empty
680 // entries are not allowed.
681 if (self.input.items.len - 8 == 0) {
682 std.log.warn("non-deterministic test (empty input produces different hits)", .{});
683 _ = @atomicRmw(usize, &header.unique_runs, .Sub, 1, .monotonic);
684 return;
685 }
686
687 const arena = self.arena_ctx.allocator();
688 const bytes = arena.dupe(u8, @volatileCast(self.input.items[8..])) catch @panic("OOM");
689
690 self.corpus.append(gpa, bytes) catch @panic("OOM");
691 self.mutations.appendNTimes(gpa, m, 6) catch @panic("OOM");
692
693 // Write new corpus to cache
694 var name_buf: [@sizeOf(usize) * 2]u8 = undefined;
695 self.corpus_dir.writeFile(.{
696 .sub_path = std.fmt.bufPrint(
697 &name_buf,
698 "{x}",
699 .{self.corpus_dir_idx},
700 ) catch unreachable,
701 .data = bytes,
702 }) catch |e| panic(
703 "failed to write corpus file '{x}': {t}",
704 .{ self.corpus_dir_idx, e },
705 );
706 self.corpus_dir_idx += 1;
374 }707 }
375 const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic);
376 fresh = fresh or (prev | mask) != prev;
377 superset = superset and (prev | mask) != mask;
378 }
379708
380 // First check if this is a better version of an already existing709 break;
381 // input, replacing that input.
382 if (superset or f.traced_comparisons.entries.len >= f.corpus.items[corpus_index].last_traced_comparison) {
383 const new_input = gpa.realloc(f.corpus.items[corpus_index].bytes, f.input.items.len) catch |err| oom(err);
384 f.corpus.items[corpus_index] = .{
385 .bytes = new_input,
386 .last_traced_comparison = f.traced_comparisons.count(),
387 };
388 @memcpy(new_input, @volatileCast(f.input.items));
389 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
390 return;
391 }710 }
711 }
712};
392713
393 if (!fresh) return;714/// Instrumentation must not be triggered before this function is called
715export fn fuzzer_init(cache_dir_path: abi.Slice) void {
716 inst.depreinit();
717 exec = .init(cache_dir_path.toSlice());
718 inst = .init();
719}
394720
395 // Input is already committed to the file system, we just need to open a new file721/// Invalid until `fuzzer_init` is called.
396 // for the next input.722export fn fuzzer_coverage_id() u64 {
397 // Pre-add it to the corpus list so that it does not get redundantly picked up.723 return exec.pc_digest;
398 f.corpus.append(gpa, .{724}
399 .bytes = gpa.dupe(u8, @volatileCast(f.input.items)) catch |err| oom(err),
400 .last_traced_comparison = f.traced_comparisons.entries.len,
401 }) catch |err| oom(err);
402 f.input.deinit();
403 initNextInput(f);
404725
405 // TODO: also mark input as "hot" so it gets prioritized for checking mutations above others.726/// fuzzer_init must be called beforehand
727export fn fuzzer_init_test(test_one: abi.TestOne, unit_test_name: abi.Slice) void {
728 current_test_name = unit_test_name.toSlice();
729 fuzzer = .init(test_one, unit_test_name.toSlice());
730}
406731
407 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);732/// fuzzer_init_test must be called beforehand
733/// The callee owns the memory of bytes and must not free it until the fuzzer is finished.
734export fn fuzzer_new_input(bytes: abi.Slice) void {
735 // An entry of length zero is always added and duplicates of it are not allowed.
736 if (bytes.len != 0)
737 fuzzer.addInput(bytes.toSlice());
738}
739
740/// fuzzer_init_test must be called first
741export fn fuzzer_main() void {
742 while (true) {
743 fuzzer.cycle();
408 }744 }
409};745}
410746
411fn createFileBail(dir: std.fs.Dir, sub_path: []const u8, flags: std.fs.File.CreateFlags) std.fs.File {747/// Helps determine run uniqueness in the face of recursion.
412 return dir.createFile(sub_path, flags) catch |err| switch (err) {748/// Currently not used by the fuzzer.
413 error.FileNotFound => {749export threadlocal var __sancov_lowest_stack: usize = 0;
414 const dir_name = std.fs.path.dirname(sub_path).?;750
415 dir.makePath(dir_name) catch |e| {751/// Inline since the return address of the callee is required
416 fatal("unable to make path '{s}': {s}", .{ dir_name, @errorName(e) });752inline fn genericConstCmp(T: anytype, val: T, comptime const_vals_field: []const u8) void {
417 };753 if (!inst.constPcSeen(@returnAddress())) {
418 return dir.createFile(sub_path, flags) catch |e| {754 @branchHint(.unlikely);
419 fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(e) });755 @field(inst, const_vals_field).append(gpa, val) catch @panic("OOM");
420 };756 }
421 },757}
422 else => fatal("unable to create file '{s}': {s}", .{ sub_path, @errorName(err) }),758
423 };759export fn __sanitizer_cov_trace_const_cmp1(const_arg: u8, arg: u8) void {
760 _ = const_arg;
761 _ = arg;
762}
763
764export fn __sanitizer_cov_trace_const_cmp2(const_arg: u16, arg: u16) void {
765 _ = arg;
766 genericConstCmp(u16, const_arg, "const_vals2");
767}
768
769export fn __sanitizer_cov_trace_const_cmp4(const_arg: u32, arg: u32) void {
770 _ = arg;
771 genericConstCmp(u32, const_arg, "const_vals4");
772}
773
774export fn __sanitizer_cov_trace_const_cmp8(const_arg: u64, arg: u64) void {
775 _ = arg;
776 genericConstCmp(u64, const_arg, "const_vals8");
424}777}
425778
426fn oom(err: anytype) noreturn {779export fn __sanitizer_cov_trace_switch(val: u64, cases: [*]const u64) void {
427 switch (err) {780 _ = val;
428 error.OutOfMemory => @panic("out of memory"),781 if (!inst.constPcSeen(@returnAddress())) {
782 @branchHint(.unlikely);
783 const case_bits = cases[1];
784 const cases_slice = cases[2..][0..cases[0]];
785 switch (case_bits) {
786 // 8-bit cases are ignored because they are likely to be randomly generated
787 0...8 => {},
788 9...16 => for (cases_slice) |c|
789 inst.const_vals2.append(gpa, @truncate(c)) catch @panic("OOM"),
790 17...32 => for (cases_slice) |c|
791 inst.const_vals4.append(gpa, @truncate(c)) catch @panic("OOM"),
792 33...64 => for (cases_slice) |c|
793 inst.const_vals8.append(gpa, @truncate(c)) catch @panic("OOM"),
794 else => {}, // Should be impossible
795 }
429 }796 }
430}797}
431798
432var debug_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;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 }
433806
434const gpa = switch (builtin.mode) {807 const i = off / @bitSizeOf(usize);
435 .Debug => debug_allocator.allocator(),808 // Bits are intentionally truncated since the pointer will almost always be aligned
436 .ReleaseFast, .ReleaseSmall, .ReleaseSafe => std.heap.smp_allocator,809 const hit = (@as(usize, (1 << @sizeOf(T)) - 1)) << @intCast(off % @bitSizeOf(usize));
437};810 const new = hit & ~inst.seen_rodata_loads[i];
811 if (new == 0) {
812 @branchHint(.likely);
813 return;
814 }
438815
439var fuzzer: Fuzzer = .{816 inst.new_rodata_loads[i] |= new;
440 .rng = std.Random.DefaultPrng.init(0),817 inst.any_new_rodata_loads = true;
441 .input = undefined,
442 .pcs = undefined,
443 .pc_counters = undefined,
444 .n_runs = 0,
445 .cache_dir = undefined,
446 .seen_pcs = undefined,
447 .coverage_id = undefined,
448 .unit_test_name = &.{},
449 .corpus = .empty,
450 .corpus_directory = undefined,
451 .traced_comparisons = .empty,
452};
453818
454/// Invalid until `fuzzer_init` is called.819 if (opt_const_vals_field) |const_vals_field| {
455export fn fuzzer_coverage_id() u64 {820 // This may have already been hit and this run is just being used for evaluating the
456 return fuzzer.coverage_id;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 }
457}829}
458830
459var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.c) void = undefined;831export fn __sanitizer_cov_load1(ptr: *align(1) const u8) void {
832 genericLoad(u8, ptr, null);
833}
460834
461export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void {835export fn __sanitizer_cov_load2(ptr: *align(1) const u16) void {
462 fuzzer_one = testOne;836 genericLoad(u16, ptr, "const_vals2");
463 fuzzer.start() catch |err| oom(err);
464}837}
465838
466export fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void {839export fn __sanitizer_cov_load4(ptr: *align(1) const u32) void {
467 fuzzer.unit_test_name = name_ptr[0..name_len];840 genericLoad(u32, ptr, "const_vals4");
468}841}
469842
470export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {843export fn __sanitizer_cov_load8(ptr: *align(1) const u64) void {
471 // Linkers are expected to automatically add `__start_<section>` and844 genericLoad(u64, ptr, "const_vals8");
472 // `__stop_<section>` symbols when section names are valid C identifiers.845}
473846
474 const ofmt = builtin.object_format;847export fn __sanitizer_cov_load16(ptr: *align(1) const u128) void {
475848 genericLoad(u128, ptr, "const_vals16");
476 const start_symbol_prefix: []const u8 = if (ofmt == .macho)849}
477 "\x01section$start$__DATA$__"850
478 else851export fn __sanitizer_cov_trace_cmp1(arg1: u8, arg2: u8) void {
479 "__start___";852 _ = arg1;
480 const end_symbol_prefix: []const u8 = if (ofmt == .macho)853 _ = arg2;
481 "\x01section$end$__DATA$__"854}
482 else855
483 "__stop___";856export fn __sanitizer_cov_trace_cmp2(arg1: u16, arg2: u16) void {
484857 _ = arg1;
485 const pc_counters_start_name = start_symbol_prefix ++ "sancov_cntrs";858 _ = arg2;
486 const pc_counters_start = @extern([*]u8, .{859}
487 .name = pc_counters_start_name,860
488 .linkage = .weak,861export fn __sanitizer_cov_trace_cmp4(arg1: u32, arg2: u32) void {
489 }) orelse fatal("missing {s} symbol", .{pc_counters_start_name});862 _ = arg1;
490863 _ = arg2;
491 const pc_counters_end_name = end_symbol_prefix ++ "sancov_cntrs";864}
492 const pc_counters_end = @extern([*]u8, .{865
493 .name = pc_counters_end_name,866export fn __sanitizer_cov_trace_cmp8(arg1: u64, arg2: u64) void {
494 .linkage = .weak,867 _ = arg1;
495 }) orelse fatal("missing {s} symbol", .{pc_counters_end_name});868 _ = arg2;
496869}
497 const pc_counters = pc_counters_start[0 .. pc_counters_end - pc_counters_start];870
498871export fn __sanitizer_cov_trace_pc_indir(callee: usize) void {
499 const pcs_start_name = start_symbol_prefix ++ "sancov_pcs1";872 // Not valuable because we already have pc tracing via 8bit counters.
500 const pcs_start = @extern([*]usize, .{873 _ = callee;
501 .name = pcs_start_name,874}
502 .linkage = .weak,875export fn __sanitizer_cov_8bit_counters_init(start: usize, end: usize) void {
503 }) orelse fatal("missing {s} symbol", .{pcs_start_name});876 // clang will emit a call to this function when compiling with code coverage instrumentation.
504877 // however, fuzzer_init() does not need this information since it directly reads from the
505 const pcs_end_name = end_symbol_prefix ++ "sancov_pcs1";878 // symbol table.
506 const pcs_end = @extern([*]usize, .{879 _ = start;
507 .name = pcs_end_name,880 _ = end;
508 .linkage = .weak,881}
509 }) orelse fatal("missing {s} symbol", .{pcs_end_name});882export fn __sanitizer_cov_pcs_init(start: usize, end: usize) void {
510883 // clang will emit a call to this function when compiling with code coverage instrumentation.
511 const pcs = pcs_start[0 .. pcs_end - pcs_start];884 // however, fuzzer_init() does not need this information since it directly reads from the
512885 // symbol table.
513 const cache_dir_path = cache_dir_struct.toZig();886 _ = start;
514 const cache_dir = if (cache_dir_path.len == 0)887 _ = end;
515 std.fs.cwd()888}
516 else
517 std.fs.cwd().makeOpenPath(cache_dir_path, .{ .iterate = true }) catch |err| {
518 fatal("unable to open fuzz directory '{s}': {s}", .{ cache_dir_path, @errorName(err) });
519 };
520889
521 fuzzer.init(cache_dir, pc_counters, pcs) catch |err|890/// Copy all of source into dest at position 0.
522 fatal("unable to init fuzzer: {s}", .{@errorName(err)});891/// If the slices overlap, dest.ptr must be <= src.ptr.
892fn volatileCopyForwards(comptime T: type, dest: []volatile T, source: []const volatile T) void {
893 for (dest, source) |*d, s| d.* = s;
523}894}
524895
525export fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void {896/// Copy all of source into dest at position 0.
526 fuzzer.addCorpusElem(input_ptr[0..input_len]) catch |err|897/// If the slices overlap, dest.ptr must be >= src.ptr.
527 fatal("failed to add corpus element: {s}", .{@errorName(err)});898fn volatileCopyBackwards(comptime T: type, dest: []volatile T, source: []const volatile T) void {
899 var i = source.len;
900 while (i > 0) {
901 i -= 1;
902 dest[i] = source[i];
903 }
528}904}
529905
906const Mutation = enum {
907 /// Applies .insert_*_span, .push_*_span
908 /// For wtf-8, this limits code units, not code points
909 const max_insert_len = 12;
910 /// Applies to .insert_large_*_span and .push_large_*_span
911 /// 4096 is used as it is a common sector size
912 const max_large_insert_len = 4096;
913 /// Applies to .delete_span and .pop_span
914 const max_delete_len = 16;
915 /// Applies to .set_*span, .move_span, .set_existing_span
916 const max_set_len = 12;
917 const max_replicate_len = 64;
918 const AddValue = i6;
919 const SmallValue = i10;
920
921 delete_byte,
922 delete_span,
923 /// Removes the last byte from the input
924 pop_byte,
925 pop_span,
926 /// Inserts a group of bytes which is already in the input and removes the original copy.
927 move_span,
928 /// Replaces a group of bytes in the input with another group of bytes in the input
929 set_existing_span,
930 insert_existing_span,
931 push_existing_span,
932 set_rng_byte,
933 set_rng_span,
934 insert_rng_byte,
935 insert_rng_span,
936 /// Adds a byte to the end of the input
937 push_rng_byte,
938 push_rng_span,
939 set_zero_byte,
940 set_zero_span,
941 insert_zero_byte,
942 insert_zero_span,
943 push_zero_byte,
944 push_zero_span,
945 /// Inserts a lot of zeros to the end of the input
946 /// This is intended to work with fuzz tests that require data in (large) blocks
947 push_large_zero_span,
948 /// Inserts a group of ascii printable character
949 insert_print_span,
950 /// Inserts a group of character from a...z, A...Z, 0...9, _, and ' '
951 insert_common_span,
952 /// Inserts a group of ascii digits possibly preceded by a `-`
953 insert_integer,
954 /// Code units are evenly distributed between one to four
955 insert_wtf8_char,
956 insert_wtf8_span,
957 /// Inserts a group of bytes from another input
958 insert_splice_span,
959 // utf16 is not yet included since insertion of random bytes should adaquetly check
960 // BMP character, surrogate handling, and occasionally chacters outside of the BMP.
961 set_print_span,
962 set_common_span,
963 set_splice_span,
964 /// Similar to set_splice_span, but the bytes are copied to the same index instead of a random
965 replicate_splice_span,
966 push_print_span,
967 push_common_span,
968 push_integer,
969 push_wtf8_char,
970 push_wtf8_span,
971 push_splice_span,
972 /// Clears a random amount of high bits of a byte
973 truncate_8,
974 truncate_16le,
975 truncate_16be,
976 truncate_32le,
977 truncate_32be,
978 truncate_64le,
979 truncate_64be,
980 /// Flips a random bit
981 xor_1,
982 /// Swaps up to three bits of a byte biased to less bits
983 xor_few_8,
984 /// Swaps up to six bits of a 16-bit value biased to less bits
985 xor_few_16,
986 /// Swaps up to nine bits of a 32-bit value biased to less bits
987 xor_few_32,
988 /// Swaps up to twelve bits of 64-bit value biased to less bits
989 xor_few_64,
990 /// Adds to a byte a value of type AddValue
991 add_8,
992 add_16le,
993 add_16be,
994 add_32le,
995 add_32be,
996 add_64le,
997 add_64be,
998 /// Sets a 16-bit little-endian value to a value of type SmallValue
999 set_small_16le,
1000 set_small_16be,
1001 set_small_32le,
1002 set_small_32be,
1003 set_small_64le,
1004 set_small_64be,
1005 insert_small_16le,
1006 insert_small_16be,
1007 insert_small_32le,
1008 insert_small_32be,
1009 insert_small_64le,
1010 insert_small_64be,
1011 push_small_16le,
1012 push_small_16be,
1013 push_small_32le,
1014 push_small_32be,
1015 push_small_64le,
1016 push_small_64be,
1017 set_const_16,
1018 set_const_32,
1019 set_const_64,
1020 set_const_128,
1021 insert_const_16,
1022 insert_const_32,
1023 insert_const_64,
1024 insert_const_128,
1025 push_const_16,
1026 push_const_32,
1027 push_const_64,
1028 push_const_128,
1029 /// Sets a byte with up to three bits set biased to less bits
1030 set_few_8,
1031 /// Sets a 16-bit value with up to six bits set biased to less bits
1032 set_few_16,
1033 /// Sets a 32-bit value with up to nine bits set biased to less bits
1034 set_few_32,
1035 /// Sets a 64-bit value with up to twelve bits set biased to less bits
1036 set_few_64,
1037 insert_few_8,
1038 insert_few_16,
1039 insert_few_32,
1040 insert_few_64,
1041 push_few_8,
1042 push_few_16,
1043 push_few_32,
1044 push_few_64,
1045 /// Randomizes a random contigous group of bits in a byte
1046 packed_set_rng_8,
1047 packed_set_rng_16le,
1048 packed_set_rng_16be,
1049 packed_set_rng_32le,
1050 packed_set_rng_32be,
1051 packed_set_rng_64le,
1052 packed_set_rng_64be,
1053
1054 fn fewValue(rng: std.Random, T: type, comptime bits: u16) T {
1055 var result: T = 0;
1056 var remaining_bits = rng.intRangeAtMostBiased(u16, 1, bits);
1057 while (remaining_bits > 0) {
1058 result |= @shlExact(@as(T, 1), rng.int(math.Log2Int(T)));
1059 remaining_bits -= 1;
1060 }
1061 return result;
1062 }
1063
1064 /// Returns if the mutation was applicable to the input
1065 pub fn mutate(
1066 mutation: Mutation,
1067 rng: std.Random,
1068 in: []const u8,
1069 out: *MemoryMappedList,
1070 corpus: []const []const u8,
1071 const_vals2: []const u16,
1072 const_vals4: []const u32,
1073 const_vals8: []const u64,
1074 const_vals16: []const u128,
1075 ) bool {
1076 out.clearRetainingCapacity();
1077 const new_capacity = 8 + in.len + @max(
1078 16, // builtin 128 value
1079 Mutation.max_insert_len,
1080 Mutation.max_large_insert_len,
1081 );
1082 out.ensureTotalCapacity(new_capacity) catch |e|
1083 panic("could not resize shared input file: {t}", .{e});
1084 out.items.len = 8; // Length field
1085
1086 const applied = switch (mutation) {
1087 inline else => |m| m.comptimeMutate(
1088 rng,
1089 in,
1090 out,
1091 corpus,
1092 const_vals2,
1093 const_vals4,
1094 const_vals8,
1095 const_vals16,
1096 ),
1097 };
1098 if (!applied)
1099 assert(out.items.len == 8)
1100 else
1101 assert(out.items.len <= new_capacity);
1102 return applied;
1103 }
1104
1105 /// Assumes out has already been cleared
1106 fn comptimeMutate(
1107 comptime mutation: Mutation,
1108 rng: std.Random,
1109 in: []const u8,
1110 out: *MemoryMappedList,
1111 corpus: []const []const u8,
1112 const_vals2: []const u16,
1113 const_vals4: []const u32,
1114 const_vals8: []const u64,
1115 const_vals16: []const u128,
1116 ) bool {
1117 const Class = enum { new, remove, rmw, move_span, replicate_splice_span };
1118 const class: Class, const class_ctx = switch (mutation) {
1119 // zig fmt: off
1120 .move_span => .{ .move_span, null },
1121 .replicate_splice_span => .{ .replicate_splice_span, null },
1122
1123 .delete_byte => .{ .remove, .{ .delete, 1 } },
1124 .delete_span => .{ .remove, .{ .delete, max_delete_len } },
1125
1126 .pop_byte => .{ .remove, .{ .pop, 1 } },
1127 .pop_span => .{ .remove, .{ .pop, max_delete_len } },
1128
1129 .set_rng_byte => .{ .new, .{ .set , 1, .rng , .one } },
1130 .set_zero_byte => .{ .new, .{ .set , 1, .zero , .one } },
1131 .set_rng_span => .{ .new, .{ .set , 1, .rng , .many } },
1132 .set_zero_span => .{ .new, .{ .set , 1, .zero , .many } },
1133 .set_common_span => .{ .new, .{ .set , 1, .common , .many } },
1134 .set_print_span => .{ .new, .{ .set , 1, .print , .many } },
1135 .set_existing_span => .{ .new, .{ .set , 2, .existing, .many } },
1136 .set_splice_span => .{ .new, .{ .set , 1, .splice , .many } },
1137 .set_const_16 => .{ .new, .{ .set , 2, .@"const", const_vals2 } },
1138 .set_const_32 => .{ .new, .{ .set , 4, .@"const", const_vals4 } },
1139 .set_const_64 => .{ .new, .{ .set , 8, .@"const", const_vals8 } },
1140 .set_const_128 => .{ .new, .{ .set , 16, .@"const", const_vals16 } },
1141 .set_small_16le => .{ .new, .{ .set , 2, .small , .{ i16, .little } } },
1142 .set_small_32le => .{ .new, .{ .set , 4, .small , .{ i32, .little } } },
1143 .set_small_64le => .{ .new, .{ .set , 8, .small , .{ i64, .little } } },
1144 .set_small_16be => .{ .new, .{ .set , 2, .small , .{ i16, .big } } },
1145 .set_small_32be => .{ .new, .{ .set , 4, .small , .{ i32, .big } } },
1146 .set_small_64be => .{ .new, .{ .set , 8, .small , .{ i64, .big } } },
1147 .set_few_8 => .{ .new, .{ .set , 1, .few , .{ u8 , 3 } } },
1148 .set_few_16 => .{ .new, .{ .set , 2, .few , .{ u16, 6 } } },
1149 .set_few_32 => .{ .new, .{ .set , 4, .few , .{ u32, 9 } } },
1150 .set_few_64 => .{ .new, .{ .set , 8, .few , .{ u64, 12 } } },
1151
1152 .insert_rng_byte => .{ .new, .{ .insert, 0, .rng , .one } },
1153 .insert_zero_byte => .{ .new, .{ .insert, 0, .zero , .one } },
1154 .insert_rng_span => .{ .new, .{ .insert, 0, .rng , .many } },
1155 .insert_zero_span => .{ .new, .{ .insert, 0, .zero , .many } },
1156 .insert_print_span => .{ .new, .{ .insert, 0, .print , .many } },
1157 .insert_common_span => .{ .new, .{ .insert, 0, .common , .many } },
1158 .insert_integer => .{ .new, .{ .insert, 0, .integer , .many } },
1159 .insert_wtf8_char => .{ .new, .{ .insert, 0, .wtf8 , .one } },
1160 .insert_wtf8_span => .{ .new, .{ .insert, 0, .wtf8 , .many } },
1161 .insert_existing_span => .{ .new, .{ .insert, 1, .existing, .many } },
1162 .insert_splice_span => .{ .new, .{ .insert, 0, .splice , .many } },
1163 .insert_const_16 => .{ .new, .{ .insert, 0, .@"const", const_vals2 } },
1164 .insert_const_32 => .{ .new, .{ .insert, 0, .@"const", const_vals4 } },
1165 .insert_const_64 => .{ .new, .{ .insert, 0, .@"const", const_vals8 } },
1166 .insert_const_128 => .{ .new, .{ .insert, 0, .@"const", const_vals16 } },
1167 .insert_small_16le => .{ .new, .{ .insert, 0, .small , .{ i16, .little } } },
1168 .insert_small_32le => .{ .new, .{ .insert, 0, .small , .{ i32, .little } } },
1169 .insert_small_64le => .{ .new, .{ .insert, 0, .small , .{ i64, .little } } },
1170 .insert_small_16be => .{ .new, .{ .insert, 0, .small , .{ i16, .big } } },
1171 .insert_small_32be => .{ .new, .{ .insert, 0, .small , .{ i32, .big } } },
1172 .insert_small_64be => .{ .new, .{ .insert, 0, .small , .{ i64, .big } } },
1173 .insert_few_8 => .{ .new, .{ .insert, 0, .few , .{ u8 , 3 } } },
1174 .insert_few_16 => .{ .new, .{ .insert, 0, .few , .{ u16, 6 } } },
1175 .insert_few_32 => .{ .new, .{ .insert, 0, .few , .{ u32, 9 } } },
1176 .insert_few_64 => .{ .new, .{ .insert, 0, .few , .{ u64, 12 } } },
1177
1178 .push_rng_byte => .{ .new, .{ .push , 0, .rng , .one } },
1179 .push_zero_byte => .{ .new, .{ .push , 0, .zero , .one } },
1180 .push_rng_span => .{ .new, .{ .push , 0, .rng , .many } },
1181 .push_zero_span => .{ .new, .{ .push , 0, .zero , .many } },
1182 .push_print_span => .{ .new, .{ .push , 0, .print , .many } },
1183 .push_common_span => .{ .new, .{ .push , 0, .common , .many } },
1184 .push_integer => .{ .new, .{ .push , 0, .integer , .many } },
1185 .push_large_zero_span => .{ .new, .{ .push , 0, .zero , .large } },
1186 .push_wtf8_char => .{ .new, .{ .push , 0, .wtf8 , .one } },
1187 .push_wtf8_span => .{ .new, .{ .push , 0, .wtf8 , .many } },
1188 .push_existing_span => .{ .new, .{ .push , 1, .existing, .many } },
1189 .push_splice_span => .{ .new, .{ .push , 0, .splice , .many } },
1190 .push_const_16 => .{ .new, .{ .push , 0, .@"const", const_vals2 } },
1191 .push_const_32 => .{ .new, .{ .push , 0, .@"const", const_vals4 } },
1192 .push_const_64 => .{ .new, .{ .push , 0, .@"const", const_vals8 } },
1193 .push_const_128 => .{ .new, .{ .push , 0, .@"const", const_vals16 } },
1194 .push_small_16le => .{ .new, .{ .push , 0, .small , .{ i16, .little } } },
1195 .push_small_32le => .{ .new, .{ .push , 0, .small , .{ i32, .little } } },
1196 .push_small_64le => .{ .new, .{ .push , 0, .small , .{ i64, .little } } },
1197 .push_small_16be => .{ .new, .{ .push , 0, .small , .{ i16, .big } } },
1198 .push_small_32be => .{ .new, .{ .push , 0, .small , .{ i32, .big } } },
1199 .push_small_64be => .{ .new, .{ .push , 0, .small , .{ i64, .big } } },
1200 .push_few_8 => .{ .new, .{ .push , 0, .few , .{ u8 , 3 } } },
1201 .push_few_16 => .{ .new, .{ .push , 0, .few , .{ u16, 6 } } },
1202 .push_few_32 => .{ .new, .{ .push , 0, .few , .{ u32, 9 } } },
1203 .push_few_64 => .{ .new, .{ .push , 0, .few , .{ u64, 12 } } },
1204
1205 .xor_1 => .{ .rmw, .{ .xor , u8 , native_endian, 1 } },
1206 .xor_few_8 => .{ .rmw, .{ .xor , u8 , native_endian, 3 } },
1207 .xor_few_16 => .{ .rmw, .{ .xor , u16, native_endian, 6 } },
1208 .xor_few_32 => .{ .rmw, .{ .xor , u32, native_endian, 9 } },
1209 .xor_few_64 => .{ .rmw, .{ .xor , u64, native_endian, 12 } },
1210
1211 .truncate_8 => .{ .rmw, .{ .truncate , u8 , native_endian, {} } },
1212 .truncate_16le => .{ .rmw, .{ .truncate , u16, .little , {} } },
1213 .truncate_32le => .{ .rmw, .{ .truncate , u32, .little , {} } },
1214 .truncate_64le => .{ .rmw, .{ .truncate , u64, .little , {} } },
1215 .truncate_16be => .{ .rmw, .{ .truncate , u16, .big , {} } },
1216 .truncate_32be => .{ .rmw, .{ .truncate , u32, .big , {} } },
1217 .truncate_64be => .{ .rmw, .{ .truncate , u64, .big , {} } },
1218
1219 .add_8 => .{ .rmw, .{ .add , i8 , native_endian, {} } },
1220 .add_16le => .{ .rmw, .{ .add , i16, .little , {} } },
1221 .add_32le => .{ .rmw, .{ .add , i32, .little , {} } },
1222 .add_64le => .{ .rmw, .{ .add , i64, .little , {} } },
1223 .add_16be => .{ .rmw, .{ .add , i16, .big , {} } },
1224 .add_32be => .{ .rmw, .{ .add , i32, .big , {} } },
1225 .add_64be => .{ .rmw, .{ .add , i64, .big , {} } },
1226
1227 .packed_set_rng_8 => .{ .rmw, .{ .packed_rng, u8 , native_endian, {} } },
1228 .packed_set_rng_16le => .{ .rmw, .{ .packed_rng, u16, .little , {} } },
1229 .packed_set_rng_32le => .{ .rmw, .{ .packed_rng, u32, .little , {} } },
1230 .packed_set_rng_64le => .{ .rmw, .{ .packed_rng, u64, .little , {} } },
1231 .packed_set_rng_16be => .{ .rmw, .{ .packed_rng, u16, .big , {} } },
1232 .packed_set_rng_32be => .{ .rmw, .{ .packed_rng, u32, .big , {} } },
1233 .packed_set_rng_64be => .{ .rmw, .{ .packed_rng, u64, .big , {} } },
1234 // zig fmt: on
1235 };
1236
1237 switch (class) {
1238 .new => {
1239 const op: enum {
1240 set,
1241 insert,
1242 push,
1243
1244 pub fn maxLen(comptime op: @This(), in_len: usize) usize {
1245 return switch (op) {
1246 .set => @min(in_len, max_set_len),
1247 .insert, .push => max_insert_len,
1248 };
1249 }
1250 }, const min_in_len, const data: enum {
1251 rng,
1252 zero,
1253 common,
1254 print,
1255 integer,
1256 wtf8,
1257 existing,
1258 splice,
1259 @"const",
1260 small,
1261 few,
1262 }, const data_ctx = class_ctx;
1263 const Size = enum { one, many, large };
1264 if (in.len < min_in_len) return false;
1265 if (data == .@"const" and data_ctx.len == 0) return false;
1266
1267 const splice_i = if (data == .splice) blk: {
1268 // Element zero always holds an empty input, so we do not select it
1269 if (corpus.len == 1) return false;
1270 break :blk rng.intRangeLessThanBiased(usize, 1, corpus.len);
1271 } else undefined;
1272
1273 // Only needs to be followed for set
1274 const len = switch (data) {
1275 else => switch (@as(Size, data_ctx)) {
1276 .one => 1,
1277 .many => rng.intRangeAtMostBiased(usize, 1, op.maxLen(in.len)),
1278 .large => rng.intRangeAtMostBiased(usize, 1, max_large_insert_len),
1279 },
1280 .wtf8 => undefined, // varies by size of each code unit
1281 .splice => rng.intRangeAtMostBiased(usize, 1, @min(
1282 corpus[splice_i].len,
1283 op.maxLen(in.len),
1284 )),
1285 .existing => rng.intRangeAtMostBiased(usize, 1, @min(
1286 in.len,
1287 op.maxLen(in.len),
1288 )),
1289 .@"const" => @sizeOf(@typeInfo(@TypeOf(data_ctx)).pointer.child),
1290 .small, .few => @sizeOf(data_ctx[0]),
1291 };
1292
1293 const i = switch (op) {
1294 .set => rng.uintAtMostBiased(usize, in.len - len),
1295 .insert => rng.uintAtMostBiased(usize, in.len),
1296 .push => in.len,
1297 };
1298
1299 out.appendSliceAssumeCapacity(in[0..i]);
1300 switch (data) {
1301 .rng => {
1302 var bytes: [@max(max_insert_len, max_set_len)]u8 = undefined;
1303 rng.bytes(bytes[0..len]);
1304 out.appendSliceAssumeCapacity(bytes[0..len]);
1305 },
1306 .zero => out.appendNTimesAssumeCapacity(0, len),
1307 .common => for (out.addManyAsSliceAssumeCapacity(len)) |*c| {
1308 c.* = switch (rng.int(u6)) {
1309 0 => ' ',
1310 1...10 => |x| '0' + (@as(u8, x) - 1),
1311 11...36 => |x| 'A' + (@as(u8, x) - 11),
1312 37 => '_',
1313 38...63 => |x| 'a' + (@as(u8, x) - 38),
1314 };
1315 },
1316 .print => for (out.addManyAsSliceAssumeCapacity(len)) |*c| {
1317 c.* = rng.intRangeAtMostBiased(u8, 0x20, 0x7E);
1318 },
1319 .integer => {
1320 const negative = len != 0 and rng.boolean();
1321 if (negative) {
1322 out.appendAssumeCapacity('-');
1323 }
1324
1325 for (out.addManyAsSliceAssumeCapacity(len - @intFromBool(negative))) |*c| {
1326 c.* = rng.intRangeAtMostBiased(u8, '0', '9');
1327 }
1328 },
1329 .wtf8 => {
1330 comptime assert(op != .set);
1331 var codepoints: usize = if (data_ctx == .one)
1332 1
1333 else
1334 rng.intRangeAtMostBiased(usize, 1, Mutation.max_insert_len / 4);
1335
1336 while (true) {
1337 const units1 = rng.int(u2);
1338 const value = switch (units1) {
1339 0 => rng.int(u7),
1340 1 => rng.intRangeAtMostBiased(u11, 0x000080, 0x0007FF),
1341 2 => rng.intRangeAtMostBiased(u16, 0x000800, 0x00FFFF),
1342 3 => rng.intRangeAtMostBiased(u21, 0x010000, 0x10FFFF),
1343 };
1344 const units = @as(u3, units1) + 1;
1345
1346 var buf: [4]u8 = undefined;
1347 assert(std.unicode.wtf8Encode(value, &buf) catch unreachable == units);
1348 out.appendSliceAssumeCapacity(buf[0..units]);
1349
1350 codepoints -= 1;
1351 if (codepoints == 0) break;
1352 }
1353 },
1354 .existing => {
1355 const j = rng.uintAtMostBiased(usize, in.len - len);
1356 out.appendSliceAssumeCapacity(in[j..][0..len]);
1357 },
1358 .splice => {
1359 const j = rng.uintAtMostBiased(usize, corpus[splice_i].len - len);
1360 out.appendSliceAssumeCapacity(corpus[splice_i][j..][0..len]);
1361 },
1362 .@"const" => out.appendSliceAssumeCapacity(mem.asBytes(
1363 &data_ctx[rng.uintLessThanBiased(usize, data_ctx.len)],
1364 )),
1365 .small => out.appendSliceAssumeCapacity(mem.asBytes(
1366 &mem.nativeTo(data_ctx[0], rng.int(SmallValue), data_ctx[1]),
1367 )),
1368 .few => out.appendSliceAssumeCapacity(mem.asBytes(
1369 &fewValue(rng, data_ctx[0], data_ctx[1]),
1370 )),
1371 }
1372 switch (op) {
1373 .set => out.appendSliceAssumeCapacity(in[i + len ..]),
1374 .insert => out.appendSliceAssumeCapacity(in[i..]),
1375 .push => {},
1376 }
1377 },
1378 .remove => {
1379 if (in.len == 0) return false;
1380 const Op = enum { delete, pop };
1381 const op: Op, const max_len = class_ctx;
1382 // LessThan is used so we don't delete the entire span (which is unproductive since
1383 // an empty input has always been tried)
1384 const len = if (max_len == 1) 1 else rng.uintLessThanBiased(
1385 usize,
1386 @min(max_len + 1, in.len),
1387 );
1388 switch (op) {
1389 .delete => {
1390 const i = rng.uintAtMostBiased(usize, in.len - len);
1391 out.appendSliceAssumeCapacity(in[0..i]);
1392 out.appendSliceAssumeCapacity(in[i + len ..]);
1393 },
1394 .pop => out.appendSliceAssumeCapacity(in[0 .. in.len - len]),
1395 }
1396 },
1397 .rmw => {
1398 const Op = enum { xor, truncate, add, packed_rng };
1399 const op: Op, const T, const endian, const xor_bits = class_ctx;
1400 if (in.len < @sizeOf(T)) return false;
1401 const Log2T = math.Log2Int(T);
1402
1403 const idx = rng.uintAtMostBiased(usize, in.len - @sizeOf(T));
1404 const old = mem.readInt(T, in[idx..][0..@sizeOf(T)], endian);
1405 const new = switch (op) {
1406 .xor => old ^ fewValue(rng, T, xor_bits),
1407 .truncate => old & (@as(T, math.maxInt(T)) >> rng.int(Log2T)),
1408 .add => old +% addend: {
1409 const val = rng.int(Mutation.AddValue);
1410 break :addend if (val == 0) 1 else val;
1411 },
1412 .packed_rng => blk: {
1413 const bits = rng.int(math.Log2Int(T)) +| 1;
1414 break :blk old ^ (rng.int(T) >> bits << rng.uintAtMostBiased(Log2T, bits));
1415 },
1416 };
1417 out.appendSliceAssumeCapacity(in);
1418 mem.bytesAsValue(T, out.items[8..][idx..][0..@sizeOf(T)]).* =
1419 mem.nativeTo(T, new, endian);
1420 },
1421 .move_span => {
1422 if (in.len < 2) return false;
1423 // One less since moving whole output will never change anything
1424 const len = rng.intRangeAtMostBiased(usize, 1, @min(
1425 in.len - 1,
1426 Mutation.max_set_len,
1427 ));
1428
1429 const src = rng.uintAtMostBiased(usize, in.len - len);
1430 // This indexes into the final input
1431 const dst = blk: {
1432 const res = rng.uintAtMostBiased(usize, in.len - len - 1);
1433 break :blk res + @intFromBool(res >= src);
1434 };
1435
1436 if (src < dst) {
1437 out.appendSliceAssumeCapacity(in[0..src]);
1438 out.appendSliceAssumeCapacity(in[src + len .. dst + len]);
1439 out.appendSliceAssumeCapacity(in[src..][0..len]);
1440 out.appendSliceAssumeCapacity(in[dst + len ..]);
1441 } else {
1442 out.appendSliceAssumeCapacity(in[0..dst]);
1443 out.appendSliceAssumeCapacity(in[src..][0..len]);
1444 out.appendSliceAssumeCapacity(in[dst..src]);
1445 out.appendSliceAssumeCapacity(in[src + len ..]);
1446 }
1447 },
1448 .replicate_splice_span => {
1449 if (in.len == 0) return false;
1450 if (corpus.len == 1) return false;
1451 const from = corpus[rng.intRangeLessThanBiased(usize, 1, corpus.len)];
1452 const len = rng.uintLessThanBiased(usize, @min(in.len, from.len, max_replicate_len));
1453 const i = rng.uintAtMostBiased(usize, @min(in.len, from.len) - len);
1454 out.appendSliceAssumeCapacity(in[0..i]);
1455 out.appendSliceAssumeCapacity(from[i..][0..len]);
1456 out.appendSliceAssumeCapacity(in[i + len ..]);
1457 },
1458 }
1459 return true;
1460 }
1461};
1462
530/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.1463/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.
531pub const MemoryMappedList = struct {1464pub const MemoryMappedList = struct {
532 /// Contents of the list.1465 /// Contents of the list.
...@@ -654,8 +1587,23 @@ pub const MemoryMappedList = struct {...@@ -654,8 +1587,23 @@ pub const MemoryMappedList = struct {
654 fn growCapacity(current: usize, minimum: usize) usize {1587 fn growCapacity(current: usize, minimum: usize) usize {
655 var new = current;1588 var new = current;
656 while (true) {1589 while (true) {
657 new = std.mem.alignForward(usize, new + new / 2, std.heap.page_size_max);1590 new = mem.alignForward(usize, new + new / 2, std.heap.page_size_max);
658 if (new >= minimum) return new;1591 if (new >= minimum) return new;
659 }1592 }
660 }1593 }
1594
1595 pub fn insertAssumeCapacity(l: *MemoryMappedList, i: usize, item: u8) void {
1596 assert(l.items.len + 1 <= l.capacity);
1597 l.items.len += 1;
1598 volatileCopyBackwards(u8, l.items[i + 1 ..], l.items[i .. l.items.len - 1]);
1599 l.items[i] = item;
1600 }
1601
1602 pub fn orderedRemove(l: *MemoryMappedList, i: usize) u8 {
1603 assert(l.items.len + 1 <= l.capacity);
1604 const old = l.items[i];
1605 volatileCopyForwards(u8, l.items[i .. l.items.len - 1], l.items[i + 1 ..]);
1606 l.items.len -= 1;
1607 return old;
1608 }
661};1609};
lib/std/Build/Fuzz.zig+2-3
...@@ -252,9 +252,8 @@ pub fn sendUpdate(...@@ -252,9 +252,8 @@ pub fn sendUpdate(
252 const seen_pcs = cov_header.seenBits();252 const seen_pcs = cov_header.seenBits();
253 const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);253 const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
254 const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);254 const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
255 if (prev.unique_runs != unique_runs) {255 {
256 // There has been an update.256 if (unique_runs != 0 and prev.unique_runs == 0) {
257 if (prev.unique_runs == 0) {
258 // We need to send initial context.257 // We need to send initial context.
259 const header: abi.SourceIndexHeader = .{258 const header: abi.SourceIndexHeader = .{
260 .directories_len = @intCast(coverage_map.coverage.directories.entries.len),259 .directories_len = @intCast(coverage_map.coverage.directories.entries.len),
lib/std/Build/abi.zig+20
...@@ -138,6 +138,26 @@ pub const Rebuild = extern struct {...@@ -138,6 +138,26 @@ pub const Rebuild = extern struct {
138138
139/// ABI bits specifically relating to the fuzzer interface.139/// ABI bits specifically relating to the fuzzer interface.
140pub const fuzz = struct {140pub const fuzz = struct {
141 pub const TestOne = *const fn (Slice) callconv(.c) void;
142 pub extern fn fuzzer_init(cache_dir_path: Slice) void;
143 pub extern fn fuzzer_coverage_id() u64;
144 pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void;
145 pub extern fn fuzzer_new_input(bytes: Slice) void;
146 pub extern fn fuzzer_main() void;
147
148 pub const Slice = extern struct {
149 ptr: [*]const u8,
150 len: usize,
151
152 pub fn toSlice(s: Slice) []const u8 {
153 return s.ptr[0..s.len];
154 }
155
156 pub fn fromSlice(s: []const u8) Slice {
157 return .{ .ptr = s.ptr, .len = s.len };
158 }
159 };
160
141 /// libfuzzer uses this and its usize is the one that counts. To match the ABI,161 /// libfuzzer uses this and its usize is the one that counts. To match the ABI,
142 /// make the ints be the size of the target used with libfuzzer.162 /// make the ints be the size of the target used with libfuzzer.
143 ///163 ///
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 = false,1118 .TraceLoads = options.fuzz,
1119 .TraceStores = false,1119 .TraceStores = false,
1120 .CollectControlFlow = false,1120 .CollectControlFlow = false,
1121 },1121 },
test/standalone/libfuzzer/build.zig+1
...@@ -16,6 +16,7 @@ pub fn build(b: *std.Build) void {...@@ -16,6 +16,7 @@ pub fn build(b: *std.Build) void {
16 .optimize = optimize,16 .optimize = optimize,
17 .fuzz = true,17 .fuzz = true,
18 }),18 }),
19 .use_llvm = true, // #23423
19 });20 });
2021
21 b.installArtifact(exe);22 b.installArtifact(exe);
test/standalone/libfuzzer/main.zig+35-21
...@@ -1,29 +1,43 @@...@@ -1,29 +1,43 @@
1const std = @import("std");1const std = @import("std");
2const abi = std.Build.abi.fuzz;
3const native_endian = @import("builtin").cpu.arch.endian();
24
3const FuzzerSlice = extern struct {5fn testOne(in: abi.Slice) callconv(.c) void {
4 ptr: [*]const u8,6 std.debug.assertReadable(in.toSlice());
5 len: usize,7}
68
7 fn fromSlice(s: []const u8) FuzzerSlice {9pub fn main() !void {
8 return .{ .ptr = s.ptr, .len = s.len };10 var debug_gpa_ctx: std.heap.DebugAllocator(.{}) = .init;
9 }11 defer _ = debug_gpa_ctx.deinit();
10};12 const gpa = debug_gpa_ctx.allocator();
1113
12extern fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void;14 var args = try std.process.argsWithAllocator(gpa);
13extern fn fuzzer_init(cache_dir: FuzzerSlice) void;15 defer args.deinit();
14extern fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void;16 _ = args.skip(); // executable name
15extern fn fuzzer_coverage_id() u64;
1617
17pub fn main() !void {18 const cache_dir_path = args.next() orelse @panic("expected cache directory path argument");
18 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;19 var cache_dir = try std.fs.cwd().openDir(cache_dir_path, .{});
19 defer _ = gpa.deinit();20 defer cache_dir.close();
20 const args = try std.process.argsAlloc(gpa.allocator());21
21 defer std.process.argsFree(gpa.allocator(), args);22 abi.fuzzer_init(.fromSlice(cache_dir_path));
23 abi.fuzzer_init_test(testOne, .fromSlice("test"));
24 abi.fuzzer_new_input(.fromSlice(""));
25 abi.fuzzer_new_input(.fromSlice("hello"));
26
27 const pc_digest = abi.fuzzer_coverage_id();
28 const coverage_file_path = "v/" ++ std.fmt.hex(pc_digest);
29 const coverage_file = try cache_dir.openFile(coverage_file_path, .{});
30 defer coverage_file.close();
2231
23 const cache_dir = args[1];32 var read_buf: [@sizeOf(abi.SeenPcsHeader)]u8 = undefined;
33 var r = coverage_file.reader(&read_buf);
34 const pcs_header = r.interface.takeStruct(abi.SeenPcsHeader, native_endian) catch return r.err.?;
2435
25 fuzzer_init(FuzzerSlice.fromSlice(cache_dir));36 if (pcs_header.pcs_len == 0)
26 fuzzer_init_corpus_elem("hello".ptr, "hello".len);37 return error.ZeroPcs;
27 fuzzer_set_name("test".ptr, "test".len);38 const expected_len = @sizeOf(abi.SeenPcsHeader) +
28 _ = fuzzer_coverage_id();39 try std.math.divCeil(usize, pcs_header.pcs_len, @bitSizeOf(usize)) * @sizeOf(usize) +
40 pcs_header.pcs_len * @sizeOf(usize);
41 if (try coverage_file.getEndPos() != expected_len)
42 return error.WrongEnd;
29}43}