authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-11 23:36:53-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-02-11 23:36:53-08:00
log58f928814d026fbf8fe7fef778a9fc890f9283bb
treec871c0cfd3ad18f748f51c0181b99bf058db991d
parentdb0d9c21267dd6e482c96be1fc532a5f4eef9219
parentd789f1e5cf60b063d5140c27926fd1a6b1654356
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #22862 from ziglang/fuzz

fuzzer: write inputs to shared memory before running

6 files changed, 346 insertions(+), 209 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);
src/zig_llvm.cpp+27-11
...@@ -311,19 +311,35 @@ ZIG_EXTERN_C bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machi...@@ -311,19 +311,35 @@ ZIG_EXTERN_C bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machi
311 }311 }
312 });312 });
313313
314 //pass_builder.registerOptimizerEarlyEPCallback([&](ModulePassManager &module_pm, OptimizationLevel OL) {314 const bool early_san = options->is_debug;
315 //});315
316316 pass_builder.registerOptimizerEarlyEPCallback([&](ModulePassManager &module_pm, OptimizationLevel OL) {
317 pass_builder.registerOptimizerLastEPCallback([&](ModulePassManager &module_pm, OptimizationLevel level) {317 if (early_san) {
318 // Code coverage instrumentation.318 // Code coverage instrumentation.
319 if (options->sancov) {319 if (options->sancov) {
320 module_pm.addPass(SanitizerCoveragePass(getSanCovOptions(options->coverage)));320 module_pm.addPass(SanitizerCoveragePass(getSanCovOptions(options->coverage)));
321 }
322
323 // Thread sanitizer
324 if (options->tsan) {
325 module_pm.addPass(ModuleThreadSanitizerPass());
326 module_pm.addPass(createModuleToFunctionPassAdaptor(ThreadSanitizerPass()));
327 }
321 }328 }
329 });
322330
323 // Thread sanitizer331 pass_builder.registerOptimizerLastEPCallback([&](ModulePassManager &module_pm, OptimizationLevel level) {
324 if (options->tsan) {332 if (!early_san) {
325 module_pm.addPass(ModuleThreadSanitizerPass());333 // Code coverage instrumentation.
326 module_pm.addPass(createModuleToFunctionPassAdaptor(ThreadSanitizerPass()));334 if (options->sancov) {
335 module_pm.addPass(SanitizerCoveragePass(getSanCovOptions(options->coverage)));
336 }
337
338 // Thread sanitizer
339 if (options->tsan) {
340 module_pm.addPass(ModuleThreadSanitizerPass());
341 module_pm.addPass(createModuleToFunctionPassAdaptor(ThreadSanitizerPass()));
342 }
327 }343 }
328344
329 // Verify the output345 // Verify the output