authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-11 11:54:12-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-11 13:39:20-08:00
logd789f1e5cf60b063d5140c27926fd1a6b1654356
treeb9323c389b8bac7930df6e30fa274fdfada1ff10
parent31c132081857b126524ccd261da331c9b1bda856

fuzzer: write inputs to shared memory before running

breaking change to the fuzz testing API; it now passes a type-safe context parameter to the fuzz function. libfuzzer is reworked to select inputs from the entire corpus. I tested that it's roughly as good as it was before in that it can find the panics in the simple examples, as well as achieve decent coverage on the tokenizer fuzz test. however I think the next step here will be figuring out why so many points of interest are missing from the tokenizer in both Debug and ReleaseSafe modes. does not quite close #20803 yet since there are some more important things to be done, such as opening the previous corpus, continuing fuzzing after finding bugs, storing the length of the inputs, etc.

5 files changed, 319 insertions(+), 198 deletions(-)

lib/compiler/test_runner.zig+16-6
...@@ -150,6 +150,7 @@ fn mainServer() !void {...@@ -150,6 +150,7 @@ fn mainServer() !void {
150 try server.serveU64Message(.fuzz_start_addr, entry_addr);150 try server.serveU64Message(.fuzz_start_addr, entry_addr);
151 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);151 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
152 is_fuzz_test = false;152 is_fuzz_test = false;
153 fuzzer_set_name(test_fn.name.ptr, test_fn.name.len);
153 test_fn.func() catch |err| switch (err) {154 test_fn.func() catch |err| switch (err) {
154 error.SkipZigTest => return,155 error.SkipZigTest => return,
155 else => {156 else => {
...@@ -341,12 +342,15 @@ const FuzzerSlice = extern struct {...@@ -341,12 +342,15 @@ const FuzzerSlice = extern struct {
341342
342var is_fuzz_test: bool = undefined;343var is_fuzz_test: bool = undefined;
343344
344extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.C) void) void;345extern fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void;
345extern fn fuzzer_init(cache_dir: FuzzerSlice) void;346extern fn fuzzer_init(cache_dir: FuzzerSlice) void;
347extern fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void;
348extern fn fuzzer_start(testOne: *const fn ([*]const u8, usize) callconv(.C) void) void;
346extern fn fuzzer_coverage_id() u64;349extern fn fuzzer_coverage_id() u64;
347350
348pub fn fuzz(351pub fn fuzz(
349 comptime testOne: fn ([]const u8) anyerror!void,352 context: anytype,
353 comptime testOne: fn (context: @TypeOf(context), []const u8) anyerror!void,
350 options: testing.FuzzInputOptions,354 options: testing.FuzzInputOptions,
351) anyerror!void {355) anyerror!void {
352 // Prevent this function from confusing the fuzzer by omitting its own code356 // Prevent this function from confusing the fuzzer by omitting its own code
...@@ -371,12 +375,14 @@ pub fn fuzz(...@@ -371,12 +375,14 @@ pub fn fuzz(
371 // our standard unit test checks such as memory leaks, and interaction with375 // our standard unit test checks such as memory leaks, and interaction with
372 // error logs.376 // error logs.
373 const global = struct {377 const global = struct {
378 var ctx: @TypeOf(context) = undefined;
379
374 fn fuzzer_one(input_ptr: [*]const u8, input_len: usize) callconv(.C) void {380 fn fuzzer_one(input_ptr: [*]const u8, input_len: usize) callconv(.C) void {
375 @disableInstrumentation();381 @disableInstrumentation();
376 testing.allocator_instance = .{};382 testing.allocator_instance = .{};
377 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);383 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
378 log_err_count = 0;384 log_err_count = 0;
379 testOne(input_ptr[0..input_len]) catch |err| switch (err) {385 testOne(ctx, input_ptr[0..input_len]) catch |err| switch (err) {
380 error.SkipZigTest => return,386 error.SkipZigTest => return,
381 else => {387 else => {
382 std.debug.lockStdErr();388 std.debug.lockStdErr();
...@@ -395,18 +401,22 @@ pub fn fuzz(...@@ -395,18 +401,22 @@ pub fn fuzz(
395 if (builtin.fuzz) {401 if (builtin.fuzz) {
396 const prev_allocator_state = testing.allocator_instance;402 const prev_allocator_state = testing.allocator_instance;
397 testing.allocator_instance = .{};403 testing.allocator_instance = .{};
404 defer testing.allocator_instance = prev_allocator_state;
405
406 for (options.corpus) |elem| fuzzer_init_corpus_elem(elem.ptr, elem.len);
407
408 global.ctx = context;
398 fuzzer_start(&global.fuzzer_one);409 fuzzer_start(&global.fuzzer_one);
399 testing.allocator_instance = prev_allocator_state;
400 return;410 return;
401 }411 }
402412
403 // When the unit test executable is not built in fuzz mode, only run the413 // When the unit test executable is not built in fuzz mode, only run the
404 // provided corpus.414 // provided corpus.
405 for (options.corpus) |input| {415 for (options.corpus) |input| {
406 try testOne(input);416 try testOne(context, input);
407 }417 }
408418
409 // In case there is no provided corpus, also use an empty419 // In case there is no provided corpus, also use an empty
410 // string as a smoke test.420 // string as a smoke test.
411 try testOne("");421 try testOne(context, "");
412}422}
lib/fuzzer.zig+293-185
...@@ -68,8 +68,7 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void {...@@ -68,8 +68,7 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void {
68 const len = cases_ptr[0];68 const len = cases_ptr[0];
69 const val_size_in_bits = cases_ptr[1];69 const val_size_in_bits = cases_ptr[1];
70 const cases = cases_ptr[2..][0..len];70 const cases = cases_ptr[2..][0..len];
71 _ = val;71 fuzzer.traceValue(pc ^ val);
72 fuzzer.visitPc(pc);
73 _ = val_size_in_bits;72 _ = val_size_in_bits;
74 _ = cases;73 _ = cases;
75 //std.log.debug("0x{x}: switch on value {d} ({d} bits) with {d} cases", .{74 //std.log.debug("0x{x}: switch on value {d} ({d} bits) with {d} cases", .{
...@@ -78,28 +77,24 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void {...@@ -78,28 +77,24 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void {
78}77}
7978
80export fn __sanitizer_cov_trace_pc_indir(callee: usize) void {79export fn __sanitizer_cov_trace_pc_indir(callee: usize) void {
81 const pc = @returnAddress();80 // Not valuable because we already have pc tracing via 8bit counters.
82 _ = callee;81 _ = callee;
83 fuzzer.visitPc(pc);82 //const pc = @returnAddress();
83 //fuzzer.traceValue(pc ^ callee);
84 //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee });84 //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee });
85}85}
8686
87fn handleCmp(pc: usize, arg1: u64, arg2: u64) void {87fn handleCmp(pc: usize, arg1: u64, arg2: u64) void {
88 fuzzer.visitPc(pc ^ arg1 ^ arg2);88 fuzzer.traceValue(pc ^ arg1 ^ arg2);
89 //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 });89 //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 });
90}90}
9191
92const Fuzzer = struct {92const Fuzzer = struct {
93 gpa: Allocator,
94 rng: std.Random.DefaultPrng,93 rng: std.Random.DefaultPrng,
95 input: std.ArrayListUnmanaged(u8),
96 pcs: []const usize,94 pcs: []const usize,
97 pc_counters: []u8,95 pc_counters: []u8,
98 n_runs: usize,96 n_runs: usize,
99 recent_cases: RunMap,97 traced_comparisons: std.AutoArrayHashMapUnmanaged(usize, void),
100 /// Data collected from code coverage instrumentation from one execution of
101 /// the test function.
102 coverage: Coverage,
103 /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.98 /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.
104 /// Stored in a memory-mapped file so that it can be shared with other99 /// Stored in a memory-mapped file so that it can be shared with other
105 /// processes and viewed while the fuzzer is running.100 /// processes and viewed while the fuzzer is running.
...@@ -108,42 +103,25 @@ const Fuzzer = struct {...@@ -108,42 +103,25 @@ const Fuzzer = struct {
108 /// Identifies the file name that will be used to store coverage103 /// Identifies the file name that will be used to store coverage
109 /// information, available to other processes.104 /// information, available to other processes.
110 coverage_id: u64,105 coverage_id: u64,
106 unit_test_name: []const u8,
111107
112 const RunMap = std.ArrayHashMapUnmanaged(Run, void, Run.HashContext, false);108 /// The index corresponds to the file name within the f/ subdirectory.
113109 /// The string is the input.
114 const Coverage = struct {110 /// This data is read-only; it caches what is on the filesystem.
115 pc_table: std.AutoArrayHashMapUnmanaged(usize, void),111 corpus: std.ArrayListUnmanaged(Input),
116 run_id_hasher: std.hash.Wyhash,112 corpus_directory: std.Build.Cache.Directory,
117
118 fn reset(cov: *Coverage) void {
119 cov.pc_table.clearRetainingCapacity();
120 cov.run_id_hasher = std.hash.Wyhash.init(0);
121 }
122 };
123
124 const Run = struct {
125 id: Id,
126 input: []const u8,
127 score: usize,
128113
129 const Id = u64;114 /// The next input that will be given to the testOne function. When the
130115 /// current process crashes, this memory-mapped file is used to recover the
131 const HashContext = struct {116 /// input.
132 pub fn eql(ctx: HashContext, a: Run, b: Run, b_index: usize) bool {117 ///
133 _ = b_index;118 /// The file size corresponds to the capacity. The length is not stored
134 _ = ctx;119 /// and that is the next thing to work on!
135 return a.id == b.id;120 input: MemoryMappedList,
136 }
137 pub fn hash(ctx: HashContext, a: Run) u32 {
138 _ = ctx;
139 return @truncate(a.id);
140 }
141 };
142121
143 fn deinit(run: *Run, gpa: Allocator) void {122 const Input = struct {
144 gpa.free(run.input);123 bytes: []u8,
145 run.* = undefined;124 last_traced_comparison: usize,
146 }
147 };125 };
148126
149 const Slice = extern struct {127 const Slice = extern struct {
...@@ -162,11 +140,6 @@ const Fuzzer = struct {...@@ -162,11 +140,6 @@ const Fuzzer = struct {
162 }140 }
163 };141 };
164142
165 const Analysis = struct {
166 score: usize,
167 id: Run.Id,
168 };
169
170 fn init(f: *Fuzzer, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []const usize) !void {143 fn init(f: *Fuzzer, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []const usize) !void {
171 f.cache_dir = cache_dir;144 f.cache_dir = cache_dir;
172 f.pc_counters = pc_counters;145 f.pc_counters = pc_counters;
...@@ -186,7 +159,6 @@ const Fuzzer = struct {...@@ -186,7 +159,6 @@ const Fuzzer = struct {
186 .read = true,159 .read = true,
187 .truncate = false,160 .truncate = false,
188 });161 });
189 defer coverage_file.close();
190 const n_bitset_elems = (pcs.len + @bitSizeOf(usize) - 1) / @bitSizeOf(usize);162 const n_bitset_elems = (pcs.len + @bitSizeOf(usize) - 1) / @bitSizeOf(usize);
191 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);163 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);
192 comptime assert(SeenPcsHeader.trailing[1] == .pc_addr);164 comptime assert(SeenPcsHeader.trailing[1] == .pc_addr);
...@@ -228,156 +200,196 @@ const Fuzzer = struct {...@@ -228,156 +200,196 @@ const Fuzzer = struct {
228 }200 }
229 }201 }
230202
231 fn analyzeLastRun(f: *Fuzzer) Analysis {203 fn initNextInput(f: *Fuzzer) void {
232 return .{204 while (true) {
233 .id = f.coverage.run_id_hasher.final(),205 const i = f.corpus.items.len;
234 .score = f.coverage.pc_table.count(),206 var buf: [30]u8 = undefined;
235 };207 const input_sub_path = std.fmt.bufPrint(&buf, "{d}", .{i}) catch unreachable;
208 const input = f.corpus_directory.handle.readFileAlloc(gpa, input_sub_path, 1 << 31) catch |err| switch (err) {
209 error.FileNotFound => {
210 // Make this one the next input.
211 const input_file = f.corpus_directory.handle.createFile(input_sub_path, .{
212 .exclusive = true,
213 .truncate = false,
214 .read = true,
215 }) catch |e| switch (e) {
216 error.PathAlreadyExists => continue,
217 else => fatal("unable to create '{}{d}: {s}", .{ f.corpus_directory, i, @errorName(err) }),
218 };
219 errdefer input_file.close();
220 // Initialize the mmap for the current input.
221 f.input = MemoryMappedList.create(input_file, 0, std.heap.page_size_max) catch |e| {
222 fatal("unable to init memory map for input at '{}{d}': {s}", .{
223 f.corpus_directory, i, @errorName(e),
224 });
225 };
226 break;
227 },
228 else => fatal("unable to read '{}{d}': {s}", .{ f.corpus_directory, i, @errorName(err) }),
229 };
230 errdefer gpa.free(input);
231 f.corpus.append(gpa, .{
232 .bytes = input,
233 .last_traced_comparison = 0,
234 }) catch |err| oom(err);
235 }
236 }
237
238 fn addCorpusElem(f: *Fuzzer, input: []const u8) !void {
239 try f.corpus.append(gpa, .{
240 .bytes = try gpa.dupe(u8, input),
241 .last_traced_comparison = 0,
242 });
236 }243 }
237244
238 fn start(f: *Fuzzer) !void {245 fn start(f: *Fuzzer) !void {
239 const gpa = f.gpa;
240 const rng = fuzzer.rng.random();246 const rng = fuzzer.rng.random();
241247
242 // Prepare initial input.248 // Grab the corpus which is namespaced based on `unit_test_name`.
243 assert(f.recent_cases.entries.len == 0);249 {
250 if (f.unit_test_name.len == 0) fatal("test runner never set unit test name", .{});
251 const sub_path = try std.fmt.allocPrint(gpa, "f/{s}", .{f.unit_test_name});
252 f.corpus_directory = .{
253 .handle = f.cache_dir.makeOpenPath(sub_path, .{}) catch |err|
254 fatal("unable to open corpus directory 'f/{s}': {s}", .{ sub_path, @errorName(err) }),
255 .path = sub_path,
256 };
257 initNextInput(f);
258 }
259
244 assert(f.n_runs == 0);260 assert(f.n_runs == 0);
245 try f.recent_cases.ensureUnusedCapacity(gpa, 100);
246 const len = rng.uintLessThanBiased(usize, 80);
247 try f.input.resize(gpa, len);
248 rng.bytes(f.input.items);
249 f.recent_cases.putAssumeCapacity(.{
250 .id = 0,
251 .input = try gpa.dupe(u8, f.input.items),
252 .score = 0,
253 }, {});
254261
255 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);262 // If the corpus is empty, synthesize one input.
263 if (f.corpus.items.len == 0) {
264 const len = rng.uintLessThanBiased(usize, 200);
265 const slice = try gpa.alloc(u8, len);
266 rng.bytes(slice);
267 f.input.appendSliceAssumeCapacity(slice);
268 try f.corpus.append(gpa, .{
269 .bytes = slice,
270 .last_traced_comparison = 0,
271 });
272 runOne(f, 0);
273 }
256274
257 while (true) {275 while (true) {
258 const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len);276 const chosen_index = rng.uintLessThanBiased(usize, f.corpus.items.len);
259 const run = &f.recent_cases.keys()[chosen_index];277 const modification = rng.enumValue(Mutation);
260 f.input.clearRetainingCapacity();278 f.mutateAndRunOne(chosen_index, modification);
261 f.input.appendSliceAssumeCapacity(run.input);279 }
262 try f.mutate();280 }
263281
264 @memset(f.pc_counters, 0);282 /// `x` represents a possible branch. It is the PC address of the possible
265 __sancov_lowest_stack = std.math.maxInt(usize);283 /// branch site, hashed together with the value(s) used that determine to
266 f.coverage.reset();284 /// where it branches.
285 fn traceValue(f: *Fuzzer, x: usize) void {
286 errdefer |err| oom(err);
287 try f.traced_comparisons.put(gpa, x, {});
288 }
267289
268 fuzzer_one(f.input.items.ptr, f.input.items.len);290 const Mutation = enum {
291 remove_byte,
292 modify_byte,
293 add_byte,
294 };
269295
270 f.n_runs += 1;296 fn mutateAndRunOne(f: *Fuzzer, corpus_index: usize, mutation: Mutation) void {
271 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);297 const rng = fuzzer.rng.random();
298 f.input.clearRetainingCapacity();
299 const old_input = f.corpus.items[corpus_index].bytes;
300 f.input.ensureTotalCapacity(old_input.len + 1) catch @panic("mmap file resize failed");
301 switch (mutation) {
302 .remove_byte => {
303 const omitted_index = rng.uintLessThanBiased(usize, old_input.len);
304 f.input.appendSliceAssumeCapacity(old_input[0..omitted_index]);
305 f.input.appendSliceAssumeCapacity(old_input[omitted_index + 1 ..]);
306 },
307 .modify_byte => {
308 const modified_index = rng.uintLessThanBiased(usize, old_input.len);
309 f.input.appendSliceAssumeCapacity(old_input);
310 f.input.items[modified_index] = rng.int(u8);
311 },
312 .add_byte => {
313 const modified_index = rng.uintLessThanBiased(usize, old_input.len);
314 f.input.appendSliceAssumeCapacity(old_input[0..modified_index]);
315 f.input.appendAssumeCapacity(rng.int(u8));
316 f.input.appendSliceAssumeCapacity(old_input[modified_index..]);
317 },
318 }
319 runOne(f, corpus_index);
320 }
272321
273 if (f.n_runs % 10000 == 0) f.dumpStats();322 fn runOne(f: *Fuzzer, corpus_index: usize) void {
323 const header: *volatile SeenPcsHeader = @ptrCast(f.seen_pcs.items[0..@sizeOf(SeenPcsHeader)]);
274324
275 const analysis = f.analyzeLastRun();325 f.traced_comparisons.clearRetainingCapacity();
276 const gop = f.recent_cases.getOrPutAssumeCapacity(.{326 @memset(f.pc_counters, 0);
277 .id = analysis.id,327 __sancov_lowest_stack = std.math.maxInt(usize);
278 .input = undefined,
279 .score = undefined,
280 });
281 if (gop.found_existing) {
282 //std.log.info("duplicate analysis: score={d} id={d}", .{ analysis.score, analysis.id });
283 if (f.input.items.len < gop.key_ptr.input.len or gop.key_ptr.score == 0) {
284 gpa.free(gop.key_ptr.input);
285 gop.key_ptr.input = try gpa.dupe(u8, f.input.items);
286 gop.key_ptr.score = analysis.score;
287 }
288 } else {
289 std.log.info("unique analysis: score={d} id={d}", .{ analysis.score, analysis.id });
290 gop.key_ptr.* = .{
291 .id = analysis.id,
292 .input = try gpa.dupe(u8, f.input.items),
293 .score = analysis.score,
294 };
295
296 {
297 // Track code coverage from all runs.
298 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);
299 const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]);
300 const remainder = f.pcs.len % @bitSizeOf(usize);
301 const aligned_len = f.pcs.len - remainder;
302 const seen_pcs = header_end_ptr[0..aligned_len];
303 const pc_counters = std.mem.bytesAsSlice([@bitSizeOf(usize)]u8, f.pc_counters[0..aligned_len]);
304 const V = @Vector(@bitSizeOf(usize), u8);
305 const zero_v: V = @splat(0);
306
307 for (header_end_ptr[0..pc_counters.len], pc_counters) |*elem, *array| {
308 const v: V = array.*;
309 const mask: usize = @bitCast(v != zero_v);
310 _ = @atomicRmw(usize, elem, .Or, mask, .monotonic);
311 }
312 if (remainder > 0) {
313 const i = pc_counters.len;
314 const elem = &seen_pcs[i];
315 var mask: usize = 0;
316 for (f.pc_counters[i * @bitSizeOf(usize) ..][0..remainder], 0..) |byte, bit_index| {
317 mask |= @as(usize, @intFromBool(byte != 0)) << @intCast(bit_index);
318 }
319 _ = @atomicRmw(usize, elem, .Or, mask, .monotonic);
320 }
321 }
322328
323 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);329 fuzzer_one(@volatileCast(f.input.items.ptr), f.input.items.len);
324 }
325330
326 if (f.recent_cases.entries.len >= 100) {331 f.n_runs += 1;
327 const Context = struct {332 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
328 values: []const Run,333
329 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {334 // Track code coverage from all runs.
330 return ctx.values[b_index].score < ctx.values[a_index].score;335 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);
331 }336 const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]);
332 };337 const remainder = f.pcs.len % @bitSizeOf(usize);
333 f.recent_cases.sortUnstable(Context{ .values = f.recent_cases.keys() });338 const aligned_len = f.pcs.len - remainder;
334 const cap = 50;339 const seen_pcs = header_end_ptr[0..aligned_len];
335 // This has to be done before deinitializing the deleted items.340 const pc_counters = std.mem.bytesAsSlice([@bitSizeOf(usize)]u8, f.pc_counters[0..aligned_len]);
336 const doomed_runs = f.recent_cases.keys()[cap..];341 const V = @Vector(@bitSizeOf(usize), u8);
337 f.recent_cases.shrinkRetainingCapacity(cap);342 const zero_v: V = @splat(0);
338 for (doomed_runs) |*doomed_run| {343 var fresh = false;
339 std.log.info("culling score={d} id={d}", .{ doomed_run.score, doomed_run.id });344 var superset = true;
340 doomed_run.deinit(gpa);345
341 }346 for (header_end_ptr[0..pc_counters.len], pc_counters) |*elem, *array| {
347 const v: V = array.*;
348 const mask: usize = @bitCast(v != zero_v);
349 const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic);
350 fresh = fresh or (prev | mask) != prev;
351 superset = superset and (prev | mask) != mask;
352 }
353 if (remainder > 0) {
354 const i = pc_counters.len;
355 const elem = &seen_pcs[i];
356 var mask: usize = 0;
357 for (f.pc_counters[i * @bitSizeOf(usize) ..][0..remainder], 0..) |byte, bit_index| {
358 mask |= @as(usize, @intFromBool(byte != 0)) << @intCast(bit_index);
342 }359 }
360 const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic);
361 fresh = fresh or (prev | mask) != prev;
362 superset = superset and (prev | mask) != mask;
343 }363 }
344 }
345
346 fn visitPc(f: *Fuzzer, pc: usize) void {
347 errdefer |err| oom(err);
348 try f.coverage.pc_table.put(f.gpa, pc, {});
349 f.coverage.run_id_hasher.update(std.mem.asBytes(&pc));
350 }
351364
352 fn dumpStats(f: *Fuzzer) void {365 // First check if this is a better version of an already existing
353 for (f.recent_cases.keys()[0..@min(f.recent_cases.entries.len, 5)], 0..) |run, i| {366 // input, replacing that input.
354 std.log.info("best[{d}] id={x} score={d} input: '{}'", .{367 if (superset or f.traced_comparisons.entries.len >= f.corpus.items[corpus_index].last_traced_comparison) {
355 i, run.id, run.score, std.zig.fmtEscapes(run.input),368 const new_input = gpa.realloc(f.corpus.items[corpus_index].bytes, f.input.items.len) catch |err| oom(err);
356 });369 f.corpus.items[corpus_index] = .{
370 .bytes = new_input,
371 .last_traced_comparison = f.traced_comparisons.count(),
372 };
373 @memcpy(new_input, @volatileCast(f.input.items));
374 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
375 return;
357 }376 }
358 }
359377
360 fn mutate(f: *Fuzzer) !void {378 if (!fresh) return;
361 const gpa = f.gpa;
362 const rng = fuzzer.rng.random();
363379
364 if (f.input.items.len == 0) {380 // Input is already committed to the file system, we just need to open a new file
365 const len = rng.uintLessThanBiased(usize, 80);381 // for the next input.
366 try f.input.resize(gpa, len);382 // Pre-add it to the corpus list so that it does not get redundantly picked up.
367 rng.bytes(f.input.items);383 f.corpus.append(gpa, .{
368 return;384 .bytes = gpa.dupe(u8, @volatileCast(f.input.items)) catch |err| oom(err),
369 }385 .last_traced_comparison = f.traced_comparisons.entries.len,
386 }) catch |err| oom(err);
387 f.input.deinit();
388 initNextInput(f);
370389
371 const index = rng.uintLessThanBiased(usize, f.input.items.len * 3);390 // TODO: also mark input as "hot" so it gets prioritized for checking mutations above others.
372 if (index < f.input.items.len) {391
373 f.input.items[index] = rng.int(u8);392 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
374 } else if (index < f.input.items.len * 2) {
375 _ = f.input.orderedRemove(index - f.input.items.len);
376 } else if (index < f.input.items.len * 3) {
377 try f.input.insert(gpa, index - f.input.items.len * 2, rng.int(u8));
378 } else {
379 unreachable;
380 }
381 }393 }
382};394};
383395
...@@ -402,20 +414,26 @@ fn oom(err: anytype) noreturn {...@@ -402,20 +414,26 @@ fn oom(err: anytype) noreturn {
402 }414 }
403}415}
404416
405var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;417var debug_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;
418
419const gpa = switch (builtin.mode) {
420 .Debug => debug_allocator.allocator(),
421 .ReleaseFast, .ReleaseSmall, .ReleaseSafe => std.heap.smp_allocator,
422};
406423
407var fuzzer: Fuzzer = .{424var fuzzer: Fuzzer = .{
408 .gpa = general_purpose_allocator.allocator(),
409 .rng = std.Random.DefaultPrng.init(0),425 .rng = std.Random.DefaultPrng.init(0),
410 .input = .{},426 .input = undefined,
411 .pcs = undefined,427 .pcs = undefined,
412 .pc_counters = undefined,428 .pc_counters = undefined,
413 .n_runs = 0,429 .n_runs = 0,
414 .recent_cases = .{},
415 .coverage = undefined,
416 .cache_dir = undefined,430 .cache_dir = undefined,
417 .seen_pcs = undefined,431 .seen_pcs = undefined,
418 .coverage_id = undefined,432 .coverage_id = undefined,
433 .unit_test_name = &.{},
434 .corpus = .empty,
435 .corpus_directory = undefined,
436 .traced_comparisons = .empty,
419};437};
420438
421/// Invalid until `fuzzer_init` is called.439/// Invalid until `fuzzer_init` is called.
...@@ -427,9 +445,11 @@ var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C...@@ -427,9 +445,11 @@ var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C
427445
428export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void {446export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void {
429 fuzzer_one = testOne;447 fuzzer_one = testOne;
430 fuzzer.start() catch |err| switch (err) {448 fuzzer.start() catch |err| oom(err);
431 error.OutOfMemory => fatal("out of memory", .{}),449}
432 };450
451export fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void {
452 fuzzer.unit_test_name = name_ptr[0..name_len];
433}453}
434454
435export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {455export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
...@@ -472,6 +492,11 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {...@@ -472,6 +492,11 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
472 fatal("unable to init fuzzer: {s}", .{@errorName(err)});492 fatal("unable to init fuzzer: {s}", .{@errorName(err)});
473}493}
474494
495export fn fuzzer_init_corpus_elem(input_ptr: [*]const u8, input_len: usize) void {
496 fuzzer.addCorpusElem(input_ptr[0..input_len]) catch |err|
497 fatal("failed to add corpus element: {s}", .{@errorName(err)});
498}
499
475/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.500/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.
476pub const MemoryMappedList = struct {501pub const MemoryMappedList = struct {
477 /// Contents of the list.502 /// Contents of the list.
...@@ -483,6 +508,8 @@ pub const MemoryMappedList = struct {...@@ -483,6 +508,8 @@ pub const MemoryMappedList = struct {
483 items: []align(std.heap.page_size_min) volatile u8,508 items: []align(std.heap.page_size_min) volatile u8,
484 /// How many bytes this list can hold without allocating additional memory.509 /// How many bytes this list can hold without allocating additional memory.
485 capacity: usize,510 capacity: usize,
511 /// The file is kept open so that it can be resized.
512 file: std.fs.File,
486513
487 pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList {514 pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList {
488 const ptr = try std.posix.mmap(515 const ptr = try std.posix.mmap(
...@@ -494,11 +521,52 @@ pub const MemoryMappedList = struct {...@@ -494,11 +521,52 @@ pub const MemoryMappedList = struct {
494 0,521 0,
495 );522 );
496 return .{523 return .{
524 .file = file,
497 .items = ptr[0..length],525 .items = ptr[0..length],
498 .capacity = capacity,526 .capacity = capacity,
499 };527 };
500 }528 }
501529
530 pub fn create(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList {
531 try file.setEndPos(capacity);
532 return init(file, length, capacity);
533 }
534
535 pub fn deinit(l: *MemoryMappedList) void {
536 l.file.close();
537 std.posix.munmap(@volatileCast(l.items.ptr[0..l.capacity]));
538 l.* = undefined;
539 }
540
541 /// Modify the array so that it can hold at least `additional_count` **more** items.
542 /// Invalidates element pointers if additional memory is needed.
543 pub fn ensureUnusedCapacity(l: *MemoryMappedList, additional_count: usize) !void {
544 return l.ensureTotalCapacity(l.items.len + additional_count);
545 }
546
547 /// If the current capacity is less than `new_capacity`, this function will
548 /// modify the array so that it can hold at least `new_capacity` items.
549 /// Invalidates element pointers if additional memory is needed.
550 pub fn ensureTotalCapacity(l: *MemoryMappedList, new_capacity: usize) !void {
551 if (l.capacity >= new_capacity) return;
552
553 const better_capacity = growCapacity(l.capacity, new_capacity);
554 return l.ensureTotalCapacityPrecise(better_capacity);
555 }
556
557 pub fn ensureTotalCapacityPrecise(l: *MemoryMappedList, new_capacity: usize) !void {
558 if (l.capacity >= new_capacity) return;
559
560 std.posix.munmap(@volatileCast(l.items.ptr[0..l.capacity]));
561 try l.file.setEndPos(new_capacity);
562 l.* = try init(l.file, l.items.len, new_capacity);
563 }
564
565 /// Invalidates all element pointers.
566 pub fn clearRetainingCapacity(l: *MemoryMappedList) void {
567 l.items.len = 0;
568 }
569
502 /// Append the slice of items to the list.570 /// Append the slice of items to the list.
503 /// Asserts that the list can hold the additional items.571 /// Asserts that the list can hold the additional items.
504 pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void {572 pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void {
...@@ -509,6 +577,24 @@ pub const MemoryMappedList = struct {...@@ -509,6 +577,24 @@ pub const MemoryMappedList = struct {
509 @memcpy(l.items[old_len..][0..items.len], items);577 @memcpy(l.items[old_len..][0..items.len], items);
510 }578 }
511579
580 /// Extends the list by 1 element.
581 /// Never invalidates element pointers.
582 /// Asserts that the list can hold one additional item.
583 pub fn appendAssumeCapacity(l: *MemoryMappedList, item: u8) void {
584 const new_item_ptr = l.addOneAssumeCapacity();
585 new_item_ptr.* = item;
586 }
587
588 /// Increase length by 1, returning pointer to the new item.
589 /// The returned pointer becomes invalid when the list is resized.
590 /// Never invalidates element pointers.
591 /// Asserts that the list can hold one additional item.
592 pub fn addOneAssumeCapacity(l: *MemoryMappedList) *volatile u8 {
593 assert(l.items.len < l.capacity);
594 l.items.len += 1;
595 return &l.items[l.items.len - 1];
596 }
597
512 /// Append a value to the list `n` times.598 /// Append a value to the list `n` times.
513 /// Never invalidates element pointers.599 /// Never invalidates element pointers.
514 /// The function is inline so that a comptime-known `value` parameter will600 /// The function is inline so that a comptime-known `value` parameter will
...@@ -520,4 +606,26 @@ pub const MemoryMappedList = struct {...@@ -520,4 +606,26 @@ pub const MemoryMappedList = struct {
520 @memset(l.items.ptr[l.items.len..new_len], value);606 @memset(l.items.ptr[l.items.len..new_len], value);
521 l.items.len = new_len;607 l.items.len = new_len;
522 }608 }
609
610 /// Resize the array, adding `n` new elements, which have `undefined` values.
611 /// The return value is a slice pointing to the newly allocated elements.
612 /// Never invalidates element pointers.
613 /// The returned pointer becomes invalid when the list is resized.
614 /// Asserts that the list can hold the additional items.
615 pub fn addManyAsSliceAssumeCapacity(l: *MemoryMappedList, n: usize) []volatile u8 {
616 assert(l.items.len + n <= l.capacity);
617 const prev_len = l.items.len;
618 l.items.len += n;
619 return l.items[prev_len..][0..n];
620 }
621
622 /// Called when memory growth is necessary. Returns a capacity larger than
623 /// minimum that grows super-linearly.
624 fn growCapacity(current: usize, minimum: usize) usize {
625 var new = current;
626 while (true) {
627 new = std.mem.alignForward(usize, new + new / 2, std.heap.page_size_max);
628 if (new >= minimum) return new;
629 }
630 }
523};631};
lib/init/src/main.zig+4-3
...@@ -30,13 +30,14 @@ test "use other module" {...@@ -30,13 +30,14 @@ test "use other module" {
30}30}
3131
32test "fuzz example" {32test "fuzz example" {
33 const global = struct {33 const Context = struct {
34 fn testOne(input: []const u8) anyerror!void {34 fn testOne(context: @This(), input: []const u8) anyerror!void {
35 _ = context;
35 // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!36 // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
36 try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));37 try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
37 }38 }
38 };39 };
39 try std.testing.fuzz(global.testOne, .{});40 try std.testing.fuzz(Context{}, Context.testOne, .{});
40}41}
4142
42const std = @import("std");43const std = @import("std");
lib/std/testing.zig+3-2
...@@ -1156,8 +1156,9 @@ pub const FuzzInputOptions = struct {...@@ -1156,8 +1156,9 @@ pub const FuzzInputOptions = struct {
11561156
1157/// Inline to avoid coverage instrumentation.1157/// Inline to avoid coverage instrumentation.
1158pub inline fn fuzz(1158pub inline fn fuzz(
1159 comptime testOne: fn (input: []const u8) anyerror!void,1159 context: anytype,
1160 comptime testOne: fn (context: @TypeOf(context), input: []const u8) anyerror!void,
1160 options: FuzzInputOptions,1161 options: FuzzInputOptions,
1161) anyerror!void {1162) anyerror!void {
1162 return @import("root").fuzz(testOne, options);1163 return @import("root").fuzz(context, testOne, options);
1163}1164}
lib/std/zig/tokenizer.zig+3-2
...@@ -1712,7 +1712,7 @@ test "invalid tabs and carriage returns" {...@@ -1712,7 +1712,7 @@ test "invalid tabs and carriage returns" {
1712}1712}
17131713
1714test "fuzzable properties upheld" {1714test "fuzzable properties upheld" {
1715 return std.testing.fuzz(testPropertiesUpheld, .{});1715 return std.testing.fuzz({}, testPropertiesUpheld, .{});
1716}1716}
17171717
1718fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void {1718fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void {
...@@ -1730,7 +1730,8 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v...@@ -1730,7 +1730,8 @@ fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !v
1730 try std.testing.expectEqual(source.len, last_token.loc.end);1730 try std.testing.expectEqual(source.len, last_token.loc.end);
1731}1731}
17321732
1733fn testPropertiesUpheld(source: []const u8) anyerror!void {1733fn testPropertiesUpheld(context: void, source: []const u8) anyerror!void {
1734 _ = context;
1734 const source0 = try std.testing.allocator.dupeZ(u8, source);1735 const source0 = try std.testing.allocator.dupeZ(u8, source);
1735 defer std.testing.allocator.free(source0);1736 defer std.testing.allocator.free(source0);
1736 var tokenizer = Tokenizer.init(source0);1737 var tokenizer = Tokenizer.init(source0);