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 {
150150 try server.serveU64Message(.fuzz_start_addr, entry_addr);
151151 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
152152 is_fuzz_test = false;
153 fuzzer_set_name(test_fn.name.ptr, test_fn.name.len);
153154 test_fn.func() catch |err| switch (err) {
154155 error.SkipZigTest => return,
155156 else => {
......@@ -341,12 +342,15 @@ const FuzzerSlice = extern struct {
341342
342343var 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;
345346extern 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;
346349extern fn fuzzer_coverage_id() u64;
347350
348351pub fn fuzz(
349 comptime testOne: fn ([]const u8) anyerror!void,
352 context: anytype,
353 comptime testOne: fn (context: @TypeOf(context), []const u8) anyerror!void,
350354 options: testing.FuzzInputOptions,
351355) anyerror!void {
352356 // Prevent this function from confusing the fuzzer by omitting its own code
......@@ -371,12 +375,14 @@ pub fn fuzz(
371375 // our standard unit test checks such as memory leaks, and interaction with
372376 // error logs.
373377 const global = struct {
378 var ctx: @TypeOf(context) = undefined;
379
374380 fn fuzzer_one(input_ptr: [*]const u8, input_len: usize) callconv(.C) void {
375381 @disableInstrumentation();
376382 testing.allocator_instance = .{};
377383 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
378384 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) {
380386 error.SkipZigTest => return,
381387 else => {
382388 std.debug.lockStdErr();
......@@ -395,18 +401,22 @@ pub fn fuzz(
395401 if (builtin.fuzz) {
396402 const prev_allocator_state = testing.allocator_instance;
397403 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;
398409 fuzzer_start(&global.fuzzer_one);
399 testing.allocator_instance = prev_allocator_state;
400410 return;
401411 }
402412
403413 // When the unit test executable is not built in fuzz mode, only run the
404414 // provided corpus.
405415 for (options.corpus) |input| {
406 try testOne(input);
416 try testOne(context, input);
407417 }
408418
409419 // In case there is no provided corpus, also use an empty
410420 // string as a smoke test.
411 try testOne("");
421 try testOne(context, "");
412422}
lib/fuzzer.zig+293-185
......@@ -68,8 +68,7 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void {
6868 const len = cases_ptr[0];
6969 const val_size_in_bits = cases_ptr[1];
7070 const cases = cases_ptr[2..][0..len];
71 _ = val;
72 fuzzer.visitPc(pc);
71 fuzzer.traceValue(pc ^ val);
7372 _ = val_size_in_bits;
7473 _ = cases;
7574 //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 {
7877}
7978
8079export 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.
8281 _ = callee;
83 fuzzer.visitPc(pc);
82 //const pc = @returnAddress();
83 //fuzzer.traceValue(pc ^ callee);
8484 //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee });
8585}
8686
8787fn handleCmp(pc: usize, arg1: u64, arg2: u64) void {
88 fuzzer.visitPc(pc ^ arg1 ^ arg2);
88 fuzzer.traceValue(pc ^ arg1 ^ arg2);
8989 //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 });
9090}
9191
9292const Fuzzer = struct {
93 gpa: Allocator,
9493 rng: std.Random.DefaultPrng,
95 input: std.ArrayListUnmanaged(u8),
9694 pcs: []const usize,
9795 pc_counters: []u8,
9896 n_runs: usize,
99 recent_cases: RunMap,
100 /// Data collected from code coverage instrumentation from one execution of
101 /// the test function.
102 coverage: Coverage,
97 traced_comparisons: std.AutoArrayHashMapUnmanaged(usize, void),
10398 /// Tracks which PCs have been seen across all runs that do not crash the fuzzer process.
10499 /// Stored in a memory-mapped file so that it can be shared with other
105100 /// processes and viewed while the fuzzer is running.
......@@ -108,42 +103,25 @@ const Fuzzer = struct {
108103 /// Identifies the file name that will be used to store coverage
109104 /// information, available to other processes.
110105 coverage_id: u64,
106 unit_test_name: []const u8,
111107
112 const RunMap = std.ArrayHashMapUnmanaged(Run, void, Run.HashContext, false);
113
114 const Coverage = struct {
115 pc_table: std.AutoArrayHashMapUnmanaged(usize, void),
116 run_id_hasher: std.hash.Wyhash,
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,
108 /// The index corresponds to the file name within the f/ subdirectory.
109 /// The string is the input.
110 /// This data is read-only; it caches what is on the filesystem.
111 corpus: std.ArrayListUnmanaged(Input),
112 corpus_directory: std.Build.Cache.Directory,
128113
129 const Id = u64;
130
131 const HashContext = struct {
132 pub fn eql(ctx: HashContext, a: Run, b: Run, b_index: usize) bool {
133 _ = b_index;
134 _ = ctx;
135 return a.id == b.id;
136 }
137 pub fn hash(ctx: HashContext, a: Run) u32 {
138 _ = ctx;
139 return @truncate(a.id);
140 }
141 };
114 /// The next input that will be given to the testOne function. When the
115 /// current process crashes, this memory-mapped file is used to recover the
116 /// input.
117 ///
118 /// The file size corresponds to the capacity. The length is not stored
119 /// and that is the next thing to work on!
120 input: MemoryMappedList,
142121
143 fn deinit(run: *Run, gpa: Allocator) void {
144 gpa.free(run.input);
145 run.* = undefined;
146 }
122 const Input = struct {
123 bytes: []u8,
124 last_traced_comparison: usize,
147125 };
148126
149127 const Slice = extern struct {
......@@ -162,11 +140,6 @@ const Fuzzer = struct {
162140 }
163141 };
164142
165 const Analysis = struct {
166 score: usize,
167 id: Run.Id,
168 };
169
170143 fn init(f: *Fuzzer, cache_dir: std.fs.Dir, pc_counters: []u8, pcs: []const usize) !void {
171144 f.cache_dir = cache_dir;
172145 f.pc_counters = pc_counters;
......@@ -186,7 +159,6 @@ const Fuzzer = struct {
186159 .read = true,
187160 .truncate = false,
188161 });
189 defer coverage_file.close();
190162 const n_bitset_elems = (pcs.len + @bitSizeOf(usize) - 1) / @bitSizeOf(usize);
191163 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);
192164 comptime assert(SeenPcsHeader.trailing[1] == .pc_addr);
......@@ -228,156 +200,196 @@ const Fuzzer = struct {
228200 }
229201 }
230202
231 fn analyzeLastRun(f: *Fuzzer) Analysis {
232 return .{
233 .id = f.coverage.run_id_hasher.final(),
234 .score = f.coverage.pc_table.count(),
235 };
203 fn initNextInput(f: *Fuzzer) void {
204 while (true) {
205 const i = f.corpus.items.len;
206 var buf: [30]u8 = undefined;
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 });
236243 }
237244
238245 fn start(f: *Fuzzer) !void {
239 const gpa = f.gpa;
240246 const rng = fuzzer.rng.random();
241247
242 // Prepare initial input.
243 assert(f.recent_cases.entries.len == 0);
248 // Grab the corpus which is namespaced based on `unit_test_name`.
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
244260 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
257275 while (true) {
258 const chosen_index = rng.uintLessThanBiased(usize, f.recent_cases.entries.len);
259 const run = &f.recent_cases.keys()[chosen_index];
260 f.input.clearRetainingCapacity();
261 f.input.appendSliceAssumeCapacity(run.input);
262 try f.mutate();
276 const chosen_index = rng.uintLessThanBiased(usize, f.corpus.items.len);
277 const modification = rng.enumValue(Mutation);
278 f.mutateAndRunOne(chosen_index, modification);
279 }
280 }
263281
264 @memset(f.pc_counters, 0);
265 __sancov_lowest_stack = std.math.maxInt(usize);
266 f.coverage.reset();
282 /// `x` represents a possible branch. It is the PC address of the possible
283 /// branch site, hashed together with the value(s) used that determine to
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;
271 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
296 fn mutateAndRunOne(f: *Fuzzer, corpus_index: usize, mutation: Mutation) void {
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();
276 const gop = f.recent_cases.getOrPutAssumeCapacity(.{
277 .id = analysis.id,
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 }
325 f.traced_comparisons.clearRetainingCapacity();
326 @memset(f.pc_counters, 0);
327 __sancov_lowest_stack = std.math.maxInt(usize);
322328
323 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
324 }
329 fuzzer_one(@volatileCast(f.input.items.ptr), f.input.items.len);
325330
326 if (f.recent_cases.entries.len >= 100) {
327 const Context = struct {
328 values: []const Run,
329 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
330 return ctx.values[b_index].score < ctx.values[a_index].score;
331 }
332 };
333 f.recent_cases.sortUnstable(Context{ .values = f.recent_cases.keys() });
334 const cap = 50;
335 // This has to be done before deinitializing the deleted items.
336 const doomed_runs = f.recent_cases.keys()[cap..];
337 f.recent_cases.shrinkRetainingCapacity(cap);
338 for (doomed_runs) |*doomed_run| {
339 std.log.info("culling score={d} id={d}", .{ doomed_run.score, doomed_run.id });
340 doomed_run.deinit(gpa);
341 }
331 f.n_runs += 1;
332 _ = @atomicRmw(usize, &header.n_runs, .Add, 1, .monotonic);
333
334 // Track code coverage from all runs.
335 comptime assert(SeenPcsHeader.trailing[0] == .pc_bits_usize);
336 const header_end_ptr: [*]volatile usize = @ptrCast(f.seen_pcs.items[@sizeOf(SeenPcsHeader)..]);
337 const remainder = f.pcs.len % @bitSizeOf(usize);
338 const aligned_len = f.pcs.len - remainder;
339 const seen_pcs = header_end_ptr[0..aligned_len];
340 const pc_counters = std.mem.bytesAsSlice([@bitSizeOf(usize)]u8, f.pc_counters[0..aligned_len]);
341 const V = @Vector(@bitSizeOf(usize), u8);
342 const zero_v: V = @splat(0);
343 var fresh = false;
344 var superset = true;
345
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);
342359 }
360 const prev = @atomicRmw(usize, elem, .Or, mask, .monotonic);
361 fresh = fresh or (prev | mask) != prev;
362 superset = superset and (prev | mask) != mask;
343363 }
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 {
353 for (f.recent_cases.keys()[0..@min(f.recent_cases.entries.len, 5)], 0..) |run, i| {
354 std.log.info("best[{d}] id={x} score={d} input: '{}'", .{
355 i, run.id, run.score, std.zig.fmtEscapes(run.input),
356 });
365 // First check if this is a better version of an already existing
366 // input, replacing that input.
367 if (superset or f.traced_comparisons.entries.len >= f.corpus.items[corpus_index].last_traced_comparison) {
368 const new_input = gpa.realloc(f.corpus.items[corpus_index].bytes, f.input.items.len) catch |err| oom(err);
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;
357376 }
358 }
359377
360 fn mutate(f: *Fuzzer) !void {
361 const gpa = f.gpa;
362 const rng = fuzzer.rng.random();
378 if (!fresh) return;
363379
364 if (f.input.items.len == 0) {
365 const len = rng.uintLessThanBiased(usize, 80);
366 try f.input.resize(gpa, len);
367 rng.bytes(f.input.items);
368 return;
369 }
380 // Input is already committed to the file system, we just need to open a new file
381 // for the next input.
382 // Pre-add it to the corpus list so that it does not get redundantly picked up.
383 f.corpus.append(gpa, .{
384 .bytes = gpa.dupe(u8, @volatileCast(f.input.items)) catch |err| oom(err),
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);
372 if (index < f.input.items.len) {
373 f.input.items[index] = rng.int(u8);
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 }
390 // TODO: also mark input as "hot" so it gets prioritized for checking mutations above others.
391
392 _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic);
381393 }
382394};
383395
......@@ -402,20 +414,26 @@ fn oom(err: anytype) noreturn {
402414 }
403415}
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
407424var fuzzer: Fuzzer = .{
408 .gpa = general_purpose_allocator.allocator(),
409425 .rng = std.Random.DefaultPrng.init(0),
410 .input = .{},
426 .input = undefined,
411427 .pcs = undefined,
412428 .pc_counters = undefined,
413429 .n_runs = 0,
414 .recent_cases = .{},
415 .coverage = undefined,
416430 .cache_dir = undefined,
417431 .seen_pcs = undefined,
418432 .coverage_id = undefined,
433 .unit_test_name = &.{},
434 .corpus = .empty,
435 .corpus_directory = undefined,
436 .traced_comparisons = .empty,
419437};
420438
421439/// Invalid until `fuzzer_init` is called.
......@@ -427,9 +445,11 @@ var fuzzer_one: *const fn (input_ptr: [*]const u8, input_len: usize) callconv(.C
427445
428446export fn fuzzer_start(testOne: @TypeOf(fuzzer_one)) void {
429447 fuzzer_one = testOne;
430 fuzzer.start() catch |err| switch (err) {
431 error.OutOfMemory => fatal("out of memory", .{}),
432 };
448 fuzzer.start() catch |err| oom(err);
449}
450
451export fn fuzzer_set_name(name_ptr: [*]const u8, name_len: usize) void {
452 fuzzer.unit_test_name = name_ptr[0..name_len];
433453}
434454
435455export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
......@@ -472,6 +492,11 @@ export fn fuzzer_init(cache_dir_struct: Fuzzer.Slice) void {
472492 fatal("unable to init fuzzer: {s}", .{@errorName(err)});
473493}
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
475500/// Like `std.ArrayListUnmanaged(u8)` but backed by memory mapping.
476501pub const MemoryMappedList = struct {
477502 /// Contents of the list.
......@@ -483,6 +508,8 @@ pub const MemoryMappedList = struct {
483508 items: []align(std.heap.page_size_min) volatile u8,
484509 /// How many bytes this list can hold without allocating additional memory.
485510 capacity: usize,
511 /// The file is kept open so that it can be resized.
512 file: std.fs.File,
486513
487514 pub fn init(file: std.fs.File, length: usize, capacity: usize) !MemoryMappedList {
488515 const ptr = try std.posix.mmap(
......@@ -494,11 +521,52 @@ pub const MemoryMappedList = struct {
494521 0,
495522 );
496523 return .{
524 .file = file,
497525 .items = ptr[0..length],
498526 .capacity = capacity,
499527 };
500528 }
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
502570 /// Append the slice of items to the list.
503571 /// Asserts that the list can hold the additional items.
504572 pub fn appendSliceAssumeCapacity(l: *MemoryMappedList, items: []const u8) void {
......@@ -509,6 +577,24 @@ pub const MemoryMappedList = struct {
509577 @memcpy(l.items[old_len..][0..items.len], items);
510578 }
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
512598 /// Append a value to the list `n` times.
513599 /// Never invalidates element pointers.
514600 /// The function is inline so that a comptime-known `value` parameter will
......@@ -520,4 +606,26 @@ pub const MemoryMappedList = struct {
520606 @memset(l.items.ptr[l.items.len..new_len], value);
521607 l.items.len = new_len;
522608 }
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 }
523631};
lib/init/src/main.zig+4-3
......@@ -30,13 +30,14 @@ test "use other module" {
3030}
3131
3232test "fuzz example" {
33 const global = struct {
34 fn testOne(input: []const u8) anyerror!void {
33 const Context = struct {
34 fn testOne(context: @This(), input: []const u8) anyerror!void {
35 _ = context;
3536 // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
3637 try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input));
3738 }
3839 };
39 try std.testing.fuzz(global.testOne, .{});
40 try std.testing.fuzz(Context{}, Context.testOne, .{});
4041}
4142
4243const std = @import("std");
lib/std/testing.zig+3-2
......@@ -1156,8 +1156,9 @@ pub const FuzzInputOptions = struct {
11561156
11571157/// Inline to avoid coverage instrumentation.
11581158pub 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,
11601161 options: FuzzInputOptions,
11611162) anyerror!void {
1162 return @import("root").fuzz(testOne, options);
1163 return @import("root").fuzz(context, testOne, options);
11631164}
lib/std/zig/tokenizer.zig+3-2
......@@ -1712,7 +1712,7 @@ test "invalid tabs and carriage returns" {
17121712}
17131713
17141714test "fuzzable properties upheld" {
1715 return std.testing.fuzz(testPropertiesUpheld, .{});
1715 return std.testing.fuzz({}, testPropertiesUpheld, .{});
17161716}
17171717
17181718fn 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
17301730 try std.testing.expectEqual(source.len, last_token.loc.end);
17311731}
17321732
1733fn testPropertiesUpheld(source: []const u8) anyerror!void {
1733fn testPropertiesUpheld(context: void, source: []const u8) anyerror!void {
1734 _ = context;
17341735 const source0 = try std.testing.allocator.dupeZ(u8, source);
17351736 defer std.testing.allocator.free(source0);
17361737 var tokenizer = Tokenizer.init(source0);