authorgravatar for kappaloris@gmail.comLoris Cro <kappaloris@gmail.com> 2025-09-24 12:10:32+02:00
committergravatar for kappaloris@gmail.comLoris Cro <kappaloris@gmail.com> 2025-09-24 12:46:48+02:00
log0feacc2b81679514c0168a6ba4c0decafeb2e43e
treeecd0b572273020af3b27f3d2e1ae65e07c39f12d
parent26825e95066c104585d248787c0e56ce4e8413e0

fuzzing: implement limited fuzzing

Adds the limit option to `--fuzz=[limit]`. the limit expresses a number of iterations that *each fuzz test* will perform at maximum before exiting. The limit argument supports also 'K', 'M', and 'G' suffixeds (e.g. '10K'). Does not imply `--web-ui` (like unlimited fuzzing does) and prints a fuzzing report at the end. Closes #22900 but does not implement the time based limit, as after internal discussions we concluded to be problematic to both implement and use correctly.

9 files changed, 407 insertions(+), 73 deletions(-)

lib/compiler/build_runner.zig+84-6
......@@ -112,7 +112,7 @@ pub fn main() !void {
112112 var steps_menu = false;
113113 var output_tmp_nonce: ?[16]u8 = null;
114114 var watch = false;
115 var fuzz = false;
115 var fuzz: ?std.Build.Fuzz.Mode = null;
116116 var debounce_interval_ms: u16 = 50;
117117 var webui_listen: ?std.net.Address = null;
118118
......@@ -274,10 +274,44 @@ pub fn main() !void {
274274 webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable;
275275 }
276276 } else if (mem.eql(u8, arg, "--fuzz")) {
277 fuzz = true;
277 fuzz = .{ .forever = undefined };
278278 if (webui_listen == null) {
279279 webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable;
280280 }
281 } else if (mem.startsWith(u8, arg, "--fuzz=")) {
282 const value = arg["--fuzz=".len..];
283 if (value.len == 0) fatal("missing argument to --fuzz\n", .{});
284
285 const unit: u8 = value[value.len - 1];
286 const digits = switch (value[value.len - 1]) {
287 '0'...'9' => value,
288 'K', 'M', 'G' => value[0 .. value.len - 1],
289 else => fatal(
290 "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]\n",
291 .{},
292 ),
293 };
294
295 const amount = std.fmt.parseInt(u64, digits, 10) catch {
296 fatal(
297 "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]\n",
298 .{},
299 );
300 };
301
302 const normalized_amount = std.math.mul(u64, amount, switch (unit) {
303 else => unreachable,
304 '0'...'9' => 1,
305 'K' => 1000,
306 'M' => 1_000_000,
307 'G' => 1_000_000_000,
308 }) catch fatal("fuzzing limit amount overflows u64\n", .{});
309
310 fuzz = .{
311 .limit = .{
312 .amount = normalized_amount,
313 },
314 };
281315 } else if (mem.eql(u8, arg, "-fincremental")) {
282316 graph.incremental = true;
283317 } else if (mem.eql(u8, arg, "-fno-incremental")) {
......@@ -476,6 +510,7 @@ pub fn main() !void {
476510 targets.items,
477511 main_progress_node,
478512 &run,
513 fuzz,
479514 ) catch |err| switch (err) {
480515 error.UncleanExit => {
481516 assert(!run.watch and run.web_server == null);
......@@ -485,7 +520,8 @@ pub fn main() !void {
485520 };
486521
487522 if (run.web_server) |*web_server| {
488 web_server.finishBuild(.{ .fuzz = fuzz });
523 if (fuzz) |mode| assert(mode == .forever);
524 web_server.finishBuild(.{ .fuzz = fuzz != null });
489525 }
490526
491527 if (!watch and run.web_server == null) {
......@@ -651,6 +687,7 @@ fn runStepNames(
651687 step_names: []const []const u8,
652688 parent_prog_node: std.Progress.Node,
653689 run: *Run,
690 fuzz: ?std.Build.Fuzz.Mode,
654691) !void {
655692 const gpa = run.gpa;
656693 const step_stack = &run.step_stack;
......@@ -676,6 +713,7 @@ fn runStepNames(
676713 });
677714 }
678715 }
716
679717 assert(run.memory_blocked_steps.items.len == 0);
680718
681719 var test_skip_count: usize = 0;
......@@ -724,6 +762,45 @@ fn runStepNames(
724762 }
725763 }
726764
765 const ttyconf = run.ttyconf;
766
767 if (fuzz) |mode| blk: {
768 switch (builtin.os.tag) {
769 // Current implementation depends on two things that need to be ported to Windows:
770 // * Memory-mapping to share data between the fuzzer and build runner.
771 // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
772 // many addresses to source locations).
773 .windows => fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
774 else => {},
775 }
776 if (@bitSizeOf(usize) != 64) {
777 // Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
778 // being compatible with `std.fs.getEndPos() u64`'s return value. This is not the case
779 // on 32-bit platforms.
780 // Affects or affected by issues #5185, #22523, and #22464.
781 fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
782 }
783
784 switch (mode) {
785 .forever => break :blk,
786 .limit => {},
787 }
788
789 assert(mode == .limit);
790 var f = std.Build.Fuzz.init(
791 gpa,
792 thread_pool,
793 step_stack.keys(),
794 parent_prog_node,
795 ttyconf,
796 mode,
797 ) catch |err| fatal("failed to start fuzzer: {s}", .{@errorName(err)});
798 defer f.deinit();
799
800 f.start();
801 f.waitAndPrintReport();
802 }
803
727804 // A proper command line application defaults to silently succeeding.
728805 // The user may request verbose mode if they have a different preference.
729806 const failures_only = switch (run.summary) {
......@@ -737,8 +814,6 @@ fn runStepNames(
737814 std.Progress.setStatus(.failure);
738815 }
739816
740 const ttyconf = run.ttyconf;
741
742817 if (run.summary != .none) {
743818 const w = std.debug.lockStderrWriter(&stdio_buffer_allocation);
744819 defer std.debug.unlockStderrWriter();
......@@ -1366,7 +1441,10 @@ fn printUsage(b: *std.Build, w: *Writer) !void {
13661441 \\ --watch Continuously rebuild when source files are modified
13671442 \\ --debounce <ms> Delay before rebuilding after changed file detected
13681443 \\ --webui[=ip] Enable the web interface on the given IP address
1369 \\ --fuzz Continuously search for unit test failures (implies '--webui')
1444 \\ --fuzz[=limit] Continuously search for unit test failures with an optional
1445 \\ limit to the max number of iterations. The argument supports
1446 \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
1447 \\ '--webui' when no limit is specified.
13701448 \\ --time-report Force full rebuild and provide detailed information on
13711449 \\ compilation time of Zig source code (implies '--webui')
13721450 \\ -fincremental Enable incremental compilation
lib/compiler/test_runner.zig+74-5
......@@ -2,6 +2,7 @@
22const builtin = @import("builtin");
33
44const std = @import("std");
5const fatal = std.process.fatal;
56const testing = std.testing;
67const assert = std.debug.assert;
78const fuzz_abi = std.Build.abi.fuzz;
......@@ -62,13 +63,13 @@ pub fn main() void {
6263 }
6364
6465 if (listen) {
65 return mainServer() catch @panic("internal test runner failure");
66 return mainServer(opt_cache_dir) catch @panic("internal test runner failure");
6667 } else {
6768 return mainTerminal();
6869 }
6970}
7071
71fn mainServer() !void {
72fn mainServer(opt_cache_dir: ?[]const u8) !void {
7273 @disableInstrumentation();
7374 var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer);
7475 var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer);
......@@ -78,9 +79,66 @@ fn mainServer() !void {
7879 .zig_version = builtin.zig_version_string,
7980 });
8081
81 if (builtin.fuzz) {
82 if (builtin.fuzz) blk: {
83 const cache_dir = opt_cache_dir.?;
8284 const coverage_id = fuzz_abi.fuzzer_coverage_id();
83 try server.serveU64Message(.coverage_id, coverage_id);
85 const coverage_file_path: std.Build.Cache.Path = .{
86 .root_dir = .{
87 .path = cache_dir,
88 .handle = std.fs.cwd().openDir(cache_dir, .{}) catch |err| {
89 if (err == error.FileNotFound) {
90 try server.serveCoverageIdMessage(coverage_id, 0, 0, 0);
91 break :blk;
92 }
93
94 fatal("failed to access cache dir '{s}': {s}", .{
95 cache_dir, @errorName(err),
96 });
97 },
98 },
99 .sub_path = "v/" ++ std.fmt.hex(coverage_id),
100 };
101
102 var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| {
103 if (err == error.FileNotFound) {
104 try server.serveCoverageIdMessage(coverage_id, 0, 0, 0);
105 break :blk;
106 }
107
108 fatal("failed to load coverage file '{f}': {s}", .{
109 coverage_file_path, @errorName(err),
110 });
111 };
112 defer coverage_file.close();
113
114 var rbuf: [0x1000]u8 = undefined;
115 var r = coverage_file.reader(&rbuf);
116
117 var header: fuzz_abi.SeenPcsHeader = undefined;
118 r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| {
119 fatal("failed to read from coverage file '{f}': {s}", .{
120 coverage_file_path, @errorName(err),
121 });
122 };
123
124 if (header.pcs_len == 0) {
125 fatal("corrupted coverage file '{f}': pcs_len was zero", .{
126 coverage_file_path,
127 });
128 }
129
130 var seen_count: usize = 0;
131 const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len);
132 for (0..chunk_count) |_| {
133 const seen = r.interface.takeInt(usize, .little) catch |err| {
134 fatal("failed to read from coverage file '{f}': {s}", .{
135 coverage_file_path, @errorName(err),
136 });
137 };
138 seen_count += @popCount(seen);
139 }
140
141 try server.serveCoverageIdMessage(coverage_id, header.n_runs, header.unique_runs, seen_count);
84142 }
85143
86144 while (true) {
......@@ -158,6 +216,9 @@ fn mainServer() !void {
158216 if (!builtin.fuzz) unreachable;
159217
160218 const index = try server.receiveBody_u32();
219 const mode: fuzz_abi.LimitKind = @enumFromInt(try server.receiveBody_u8());
220 const amount_or_instance = try server.receiveBody_u64();
221
161222 const test_fn = builtin.test_functions[index];
162223 const entry_addr = @intFromPtr(test_fn.func);
163224
......@@ -165,6 +226,8 @@ fn mainServer() !void {
165226 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
166227 is_fuzz_test = false;
167228 fuzz_test_index = index;
229 fuzz_mode = mode;
230 fuzz_amount_or_instance = amount_or_instance;
168231
169232 test_fn.func() catch |err| switch (err) {
170233 error.SkipZigTest => return,
......@@ -178,6 +241,8 @@ fn mainServer() !void {
178241 };
179242 if (!is_fuzz_test) @panic("missed call to std.testing.fuzz");
180243 if (log_err_count != 0) @panic("error logs detected");
244 assert(mode != .forever);
245 std.process.exit(0);
181246 },
182247
183248 else => {
......@@ -343,6 +408,8 @@ pub fn mainSimple() anyerror!void {
343408
344409var is_fuzz_test: bool = undefined;
345410var fuzz_test_index: u32 = undefined;
411var fuzz_mode: fuzz_abi.LimitKind = undefined;
412var fuzz_amount_or_instance: u64 = undefined;
346413
347414pub fn fuzz(
348415 context: anytype,
......@@ -401,9 +468,11 @@ pub fn fuzz(
401468
402469 global.ctx = context;
403470 fuzz_abi.fuzzer_init_test(&global.test_one, .fromSlice(builtin.test_functions[fuzz_test_index].name));
471
404472 for (options.corpus) |elem|
405473 fuzz_abi.fuzzer_new_input(.fromSlice(elem));
406 fuzz_abi.fuzzer_main();
474
475 fuzz_abi.fuzzer_main(fuzz_mode, fuzz_amount_or_instance);
407476 return;
408477 }
409478
lib/fuzzer.zig+4-3
......@@ -600,9 +600,10 @@ export fn fuzzer_new_input(bytes: abi.Slice) void {
600600}
601601
602602/// fuzzer_init_test must be called first
603export fn fuzzer_main() void {
604 while (true) {
605 fuzzer.cycle();
603export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {
604 switch (limit_kind) {
605 .forever => while (true) fuzzer.cycle(),
606 .iterations => for (0..amount -| 1) |_| fuzzer.cycle(),
606607 }
607608}
608609
lib/std/Build/Fuzz.zig+154-42
......@@ -8,17 +8,22 @@ const Allocator = std.mem.Allocator;
88const log = std.log;
99const Coverage = std.debug.Coverage;
1010const abi = Build.abi.fuzz;
11const tty = std.Io.tty;
1112
1213const Fuzz = @This();
1314const build_runner = @import("root");
1415
15ws: *Build.WebServer,
16gpa: Allocator,
17mode: Mode,
1618
17/// Allocated into `ws.gpa`.
19/// Allocated into `gpa`.
1820run_steps: []const *Step.Run,
1921
2022wait_group: std.Thread.WaitGroup,
23root_prog_node: std.Progress.Node,
2124prog_node: std.Progress.Node,
25thread_pool: *std.Thread.Pool,
26ttyconf: tty.Config,
2227
2328/// Protects `coverage_files`.
2429coverage_mutex: std.Thread.Mutex,
......@@ -28,9 +33,23 @@ queue_mutex: std.Thread.Mutex,
2833queue_cond: std.Thread.Condition,
2934msg_queue: std.ArrayListUnmanaged(Msg),
3035
36pub const Mode = union(enum) {
37 forever: struct { ws: *Build.WebServer },
38 limit: Limited,
39
40 pub const Limited = struct {
41 amount: u64,
42 };
43};
44
3145const Msg = union(enum) {
3246 coverage: struct {
3347 id: u64,
48 cumulative: struct {
49 runs: u64,
50 unique: u64,
51 coverage: u64,
52 },
3453 run: *Step.Run,
3554 },
3655 entry_point: struct {
......@@ -54,23 +73,28 @@ const CoverageMap = struct {
5473 }
5574};
5675
57pub fn init(ws: *Build.WebServer) Allocator.Error!Fuzz {
58 const gpa = ws.gpa;
59
76pub fn init(
77 gpa: Allocator,
78 thread_pool: *std.Thread.Pool,
79 all_steps: []const *Build.Step,
80 root_prog_node: std.Progress.Node,
81 ttyconf: tty.Config,
82 mode: Mode,
83) Allocator.Error!Fuzz {
6084 const run_steps: []const *Step.Run = steps: {
6185 var steps: std.ArrayListUnmanaged(*Step.Run) = .empty;
6286 defer steps.deinit(gpa);
63 const rebuild_node = ws.root_prog_node.start("Rebuilding Unit Tests", 0);
87 const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0);
6488 defer rebuild_node.end();
6589 var rebuild_wg: std.Thread.WaitGroup = .{};
6690 defer rebuild_wg.wait();
6791
68 for (ws.all_steps) |step| {
92 for (all_steps) |step| {
6993 const run = step.cast(Step.Run) orelse continue;
7094 if (run.producer == null) continue;
7195 if (run.fuzz_tests.items.len == 0) continue;
7296 try steps.append(gpa, run);
73 ws.thread_pool.spawnWg(&rebuild_wg, rebuildTestsWorkerRun, .{ run, gpa, ws.ttyconf, rebuild_node });
97 thread_pool.spawnWg(&rebuild_wg, rebuildTestsWorkerRun, .{ run, gpa, ttyconf, rebuild_node });
7498 }
7599
76100 if (steps.items.len == 0) fatal("no fuzz tests found", .{});
......@@ -86,9 +110,13 @@ pub fn init(ws: *Build.WebServer) Allocator.Error!Fuzz {
86110 }
87111
88112 return .{
89 .ws = ws,
113 .gpa = gpa,
114 .mode = mode,
90115 .run_steps = run_steps,
91116 .wait_group = .{},
117 .thread_pool = thread_pool,
118 .ttyconf = ttyconf,
119 .root_prog_node = root_prog_node,
92120 .prog_node = .none,
93121 .coverage_files = .empty,
94122 .coverage_mutex = .{},
......@@ -99,32 +127,31 @@ pub fn init(ws: *Build.WebServer) Allocator.Error!Fuzz {
99127}
100128
101129pub fn start(fuzz: *Fuzz) void {
102 const ws = fuzz.ws;
103 fuzz.prog_node = ws.root_prog_node.start("Fuzzing", fuzz.run_steps.len);
104
105 // For polling messages and sending updates to subscribers.
106 fuzz.wait_group.start();
107 _ = std.Thread.spawn(.{}, coverageRun, .{fuzz}) catch |err| {
108 fuzz.wait_group.finish();
109 fatal("unable to spawn coverage thread: {s}", .{@errorName(err)});
110 };
130 fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", fuzz.run_steps.len);
131
132 if (fuzz.mode == .forever) {
133 // For polling messages and sending updates to subscribers.
134 fuzz.wait_group.start();
135 _ = std.Thread.spawn(.{}, coverageRun, .{fuzz}) catch |err| {
136 fuzz.wait_group.finish();
137 fatal("unable to spawn coverage thread: {s}", .{@errorName(err)});
138 };
139 }
111140
112141 for (fuzz.run_steps) |run| {
113142 for (run.fuzz_tests.items) |unit_test_index| {
114143 assert(run.rebuilt_executable != null);
115 ws.thread_pool.spawnWg(&fuzz.wait_group, fuzzWorkerRun, .{
144 fuzz.thread_pool.spawnWg(&fuzz.wait_group, fuzzWorkerRun, .{
116145 fuzz, run, unit_test_index,
117146 });
118147 }
119148 }
120149}
150
121151pub fn deinit(fuzz: *Fuzz) void {
122 if (true) @panic("TODO: terminate the fuzzer processes");
123 fuzz.wait_group.wait();
152 if (!fuzz.wait_group.isDone()) @panic("TODO: terminate the fuzzer processes");
124153 fuzz.prog_node.end();
125
126 const gpa = fuzz.ws.gpa;
127 gpa.free(fuzz.run_steps);
154 fuzz.gpa.free(fuzz.run_steps);
128155}
129156
130157fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, ttyconf: std.Io.tty.Config, parent_prog_node: std.Progress.Node) void {
......@@ -177,7 +204,7 @@ fn fuzzWorkerRun(
177204 var buf: [256]u8 = undefined;
178205 const w = std.debug.lockStderrWriter(&buf);
179206 defer std.debug.unlockStderrWriter();
180 build_runner.printErrorMessages(gpa, &run.step, .{ .ttyconf = fuzz.ws.ttyconf }, w, false) catch {};
207 build_runner.printErrorMessages(gpa, &run.step, .{ .ttyconf = fuzz.ttyconf }, w, false) catch {};
181208 return;
182209 },
183210 else => {
......@@ -190,20 +217,20 @@ fn fuzzWorkerRun(
190217}
191218
192219pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void {
193 const gpa = fuzz.ws.gpa;
220 assert(fuzz.mode == .forever);
194221
195 var arena_state: std.heap.ArenaAllocator = .init(gpa);
222 var arena_state: std.heap.ArenaAllocator = .init(fuzz.gpa);
196223 defer arena_state.deinit();
197224 const arena = arena_state.allocator();
198225
199226 const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
200227 var dedup_table: DedupTable = .empty;
201 defer dedup_table.deinit(gpa);
228 defer dedup_table.deinit(fuzz.gpa);
202229
203230 for (fuzz.run_steps) |run_step| {
204231 const compile_inputs = run_step.producer.?.step.inputs.table;
205232 for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| {
206 try dedup_table.ensureUnusedCapacity(gpa, file_list.items.len);
233 try dedup_table.ensureUnusedCapacity(fuzz.gpa, file_list.items.len);
207234 for (file_list.items) |sub_path| {
208235 if (!std.mem.endsWith(u8, sub_path, ".zig")) continue;
209236 const joined_path = try dir_path.join(arena, sub_path);
......@@ -224,7 +251,7 @@ pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void {
224251 }
225252 };
226253 std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
227 return fuzz.ws.serveTarFile(req, deduped_paths);
254 return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths);
228255}
229256
230257pub const Previous = struct {
......@@ -319,13 +346,13 @@ fn coverageRun(fuzz: *Fuzz) void {
319346 }
320347}
321348fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutOfMemory, AlreadyReported }!void {
322 const ws = fuzz.ws;
323 const gpa = ws.gpa;
349 assert(fuzz.mode == .forever);
350 const ws = fuzz.mode.forever.ws;
324351
325352 fuzz.coverage_mutex.lock();
326353 defer fuzz.coverage_mutex.unlock();
327354
328 const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id);
355 const gop = try fuzz.coverage_files.getOrPut(fuzz.gpa, coverage_id);
329356 if (gop.found_existing) {
330357 // We are fuzzing the same executable with multiple threads.
331358 // Perhaps the same unit test; perhaps a different one. In any
......@@ -343,16 +370,16 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO
343370 .entry_points = .{},
344371 .start_timestamp = ws.now(),
345372 };
346 errdefer gop.value_ptr.coverage.deinit(gpa);
373 errdefer gop.value_ptr.coverage.deinit(fuzz.gpa);
347374
348375 const rebuilt_exe_path = run_step.rebuilt_executable.?;
349 var debug_info = std.debug.Info.load(gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| {
376 var debug_info = std.debug.Info.load(fuzz.gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| {
350377 log.err("step '{s}': failed to load debug information for '{f}': {s}", .{
351378 run_step.step.name, rebuilt_exe_path, @errorName(err),
352379 });
353380 return error.AlreadyReported;
354381 };
355 defer debug_info.deinit(gpa);
382 defer debug_info.deinit(fuzz.gpa);
356383
357384 const coverage_file_path: Build.Cache.Path = .{
358385 .root_dir = run_step.step.owner.cache_root,
......@@ -386,14 +413,14 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO
386413
387414 const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
388415 const pcs = header.pcAddrs();
389 const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
390 errdefer gpa.free(source_locations);
416 const source_locations = try fuzz.gpa.alloc(Coverage.SourceLocation, pcs.len);
417 errdefer fuzz.gpa.free(source_locations);
391418
392419 // Unfortunately the PCs array that LLVM gives us from the 8-bit PC
393420 // counters feature is not sorted.
394421 var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .{};
395 defer sorted_pcs.deinit(gpa);
396 try sorted_pcs.resize(gpa, pcs.len);
422 defer sorted_pcs.deinit(fuzz.gpa);
423 try sorted_pcs.resize(fuzz.gpa, pcs.len);
397424 @memcpy(sorted_pcs.items(.pc), pcs);
398425 for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i);
399426 sorted_pcs.sortUnstable(struct {
......@@ -404,7 +431,7 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO
404431 }
405432 }{ .addrs = sorted_pcs.items(.pc) });
406433
407 debug_info.resolveAddresses(gpa, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| {
434 debug_info.resolveAddresses(fuzz.gpa, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| {
408435 log.err("failed to resolve addresses to source locations: {s}", .{@errorName(err)});
409436 return error.AlreadyReported;
410437 };
......@@ -414,6 +441,7 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO
414441
415442 ws.notifyUpdate();
416443}
444
417445fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory }!void {
418446 fuzz.coverage_mutex.lock();
419447 defer fuzz.coverage_mutex.unlock();
......@@ -445,5 +473,89 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte
445473 addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
446474 });
447475 }
448 try coverage_map.entry_points.append(fuzz.ws.gpa, @intCast(index));
476 try coverage_map.entry_points.append(fuzz.gpa, @intCast(index));
477}
478
479pub fn waitAndPrintReport(fuzz: *Fuzz) void {
480 assert(fuzz.mode == .limit);
481
482 fuzz.wait_group.wait();
483 fuzz.wait_group.reset();
484
485 std.debug.print("======= FUZZING REPORT =======\n", .{});
486 for (fuzz.msg_queue.items) |msg| {
487 if (msg != .coverage) continue;
488
489 const cov = msg.coverage;
490 const coverage_file_path: std.Build.Cache.Path = .{
491 .root_dir = cov.run.step.owner.cache_root,
492 .sub_path = "v/" ++ std.fmt.hex(cov.id),
493 };
494 var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| {
495 fatal("step '{s}': failed to load coverage file '{f}': {s}", .{
496 cov.run.step.name, coverage_file_path, @errorName(err),
497 });
498 };
499 defer coverage_file.close();
500
501 const fuzz_abi = std.Build.abi.fuzz;
502 var rbuf: [0x1000]u8 = undefined;
503 var r = coverage_file.reader(&rbuf);
504
505 var header: fuzz_abi.SeenPcsHeader = undefined;
506 r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| {
507 fatal("step '{s}': failed to read from coverage file '{f}': {s}", .{
508 cov.run.step.name, coverage_file_path, @errorName(err),
509 });
510 };
511
512 if (header.pcs_len == 0) {
513 fatal("step '{s}': corrupted coverage file '{f}': pcs_len was zero", .{
514 cov.run.step.name, coverage_file_path,
515 });
516 }
517
518 var seen_count: usize = 0;
519 const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len);
520 for (0..chunk_count) |_| {
521 const seen = r.interface.takeInt(usize, .little) catch |err| {
522 fatal("step '{s}': failed to read from coverage file '{f}': {s}", .{
523 cov.run.step.name, coverage_file_path, @errorName(err),
524 });
525 };
526 seen_count += @popCount(seen);
527 }
528
529 const seen_f: f64 = @floatFromInt(seen_count);
530 const total_f: f64 = @floatFromInt(header.pcs_len);
531 const ratio = seen_f / total_f;
532 std.debug.print(
533 \\Step: {s}
534 \\Fuzz test: "{s}" ({x})
535 \\Runs: {} -> {}
536 \\Unique runs: {} -> {}
537 \\Coverage: {}/{} -> {}/{} ({:.02}%)
538 \\
539 , .{
540 cov.run.step.name,
541 cov.run.cached_test_metadata.?.testName(cov.run.fuzz_tests.items[0]),
542 cov.id,
543 cov.cumulative.runs,
544 header.n_runs,
545 cov.cumulative.unique,
546 header.unique_runs,
547 cov.cumulative.coverage,
548 header.pcs_len,
549 seen_count,
550 header.pcs_len,
551 ratio * 100,
552 });
553
554 std.debug.print("------------------------------\n", .{});
555 }
556 std.debug.print(
557 \\Values are accumulated across multiple runs when preserving the cache.
558 \\==============================
559 \\
560 , .{});
449561}
lib/std/Build/Step/Run.zig+43-10
......@@ -1662,12 +1662,24 @@ fn evalZigTest(
16621662 // If this is `true`, we avoid ever entering the polling loop below, because the stdin pipe has
16631663 // somehow already closed; instead, we go straight to capturing stderr in case it has anything
16641664 // useful.
1665 const first_write_failed = if (fuzz_context) |fuzz| failed: {
1666 sendRunTestMessage(child.stdin.?, .start_fuzzing, fuzz.unit_test_index) catch |err| {
1667 try run.step.addError("unable to write stdin: {s}", .{@errorName(err)});
1668 break :failed true;
1669 };
1670 break :failed false;
1665 const first_write_failed = if (fuzz_context) |fctx| failed: {
1666 switch (fctx.fuzz.mode) {
1667 .forever => {
1668 const instance_id = 0; // will be used by mutiprocess forever fuzzing
1669 sendRunFuzzTestMessage(child.stdin.?, fctx.unit_test_index, .forever, instance_id) catch |err| {
1670 try run.step.addError("unable to write stdin: {s}", .{@errorName(err)});
1671 break :failed true;
1672 };
1673 break :failed false;
1674 },
1675 .limit => |limit| {
1676 sendRunFuzzTestMessage(child.stdin.?, fctx.unit_test_index, .iterations, limit.amount) catch |err| {
1677 try run.step.addError("unable to write stdin: {s}", .{@errorName(err)});
1678 break :failed true;
1679 };
1680 break :failed false;
1681 },
1682 }
16711683 } else failed: {
16721684 run.fuzz_tests.clearRetainingCapacity();
16731685 sendMessage(child.stdin.?, .query_test_metadata) catch |err| {
......@@ -1778,13 +1790,18 @@ fn evalZigTest(
17781790 },
17791791 .coverage_id => {
17801792 const fuzz = fuzz_context.?.fuzz;
1781 const msg_ptr: *align(1) const u64 = @ptrCast(body);
1782 coverage_id = msg_ptr.*;
1793 const msg_ptr: *align(1) const [4]u64 = @ptrCast(body);
1794 coverage_id = msg_ptr[0];
17831795 {
17841796 fuzz.queue_mutex.lock();
17851797 defer fuzz.queue_mutex.unlock();
1786 try fuzz.msg_queue.append(fuzz.ws.gpa, .{ .coverage = .{
1798 try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{
17871799 .id = coverage_id.?,
1800 .cumulative = .{
1801 .runs = msg_ptr[1],
1802 .unique = msg_ptr[2],
1803 .coverage = msg_ptr[3],
1804 },
17881805 .run = run,
17891806 } });
17901807 fuzz.queue_cond.signal();
......@@ -1797,7 +1814,7 @@ fn evalZigTest(
17971814 {
17981815 fuzz.queue_mutex.lock();
17991816 defer fuzz.queue_mutex.unlock();
1800 try fuzz.msg_queue.append(fuzz.ws.gpa, .{ .entry_point = .{
1817 try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{
18011818 .addr = addr,
18021819 .coverage_id = coverage_id.?,
18031820 } });
......@@ -1900,6 +1917,22 @@ fn sendRunTestMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag, index:
19001917 try file.writeAll(full_msg);
19011918}
19021919
1920fn sendRunFuzzTestMessage(
1921 file: std.fs.File,
1922 index: u32,
1923 kind: std.Build.abi.fuzz.LimitKind,
1924 amount_or_instance: u64,
1925) !void {
1926 const header: std.zig.Client.Message.Header = .{
1927 .tag = .start_fuzzing,
1928 .bytes_len = 4 + 1 + 8,
1929 };
1930 const full_msg = std.mem.asBytes(&header) ++ std.mem.asBytes(&index) ++
1931 std.mem.asBytes(&kind) ++ std.mem.asBytes(&amount_or_instance);
1932
1933 try file.writeAll(full_msg);
1934}
1935
19031936fn evalGeneric(run: *Run, child: *std.process.Child) !StdIoResult {
19041937 const b = run.step.owner;
19051938 const arena = b.allocator;
lib/std/Build/WebServer.zig+9-1
......@@ -219,12 +219,20 @@ pub fn finishBuild(ws: *WebServer, opts: struct {
219219 // Affects or affected by issues #5185, #22523, and #22464.
220220 std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
221221 }
222
222223 assert(ws.fuzz == null);
223224
224225 ws.build_status.store(.fuzz_init, .monotonic);
225226 ws.notifyUpdate();
226227
227 ws.fuzz = Fuzz.init(ws) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)});
228 ws.fuzz = Fuzz.init(
229 ws.gpa,
230 ws.thread_pool,
231 ws.all_steps,
232 ws.root_prog_node,
233 ws.ttyconf,
234 .{ .forever = .{ .ws = ws } },
235 ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)});
228236 ws.fuzz.?.start();
229237 }
230238
lib/std/Build/abi.zig+3-1
......@@ -143,7 +143,7 @@ pub const fuzz = struct {
143143 pub extern fn fuzzer_coverage_id() u64;
144144 pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void;
145145 pub extern fn fuzzer_new_input(bytes: Slice) void;
146 pub extern fn fuzzer_main() void;
146 pub extern fn fuzzer_main(limit_kind: LimitKind, amount: u64) void;
147147
148148 pub const Slice = extern struct {
149149 ptr: [*]const u8,
......@@ -158,6 +158,8 @@ pub const fuzz = struct {
158158 }
159159 };
160160
161 pub const LimitKind = enum(u8) { forever, iterations };
162
161163 /// libfuzzer uses this and its usize is the one that counts. To match the ABI,
162164 /// make the ints be the size of the target used with libfuzzer.
163165 ///
lib/std/zig/Client.zig+10-2
......@@ -33,10 +33,18 @@ pub const Message = struct {
3333 /// Ask the test runner to run a particular test.
3434 /// The message body is a u32 test index.
3535 run_test,
36 /// Ask the test runner to start fuzzing a particular test.
37 /// The message body is a u32 test index.
36 /// Ask the test runner to start fuzzing a particular test forever or for a given amount of time/iterations.
37 /// The message body is:
38 /// - a u32 test index.
39 /// - a u8 test limit kind (std.Build.api.fuzz.LimitKind)
40 /// - a u64 value whose meaning depends on FuzzLimitKind (either a limit amount or an instance id)
3841 start_fuzzing,
3942
4043 _,
4144 };
45
46 comptime {
47 const std = @import("std");
48 std.debug.assert(@sizeOf(std.Build.abi.fuzz.LimitKind) == 1);
49 }
4250};
lib/std/zig/Server.zig+26-3
......@@ -42,9 +42,13 @@ pub const Message = struct {
4242 /// The remaining bytes is the file path relative to that prefix.
4343 /// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir)
4444 file_system_inputs,
45 /// Body is a u64le that indicates the file path within the cache used
46 /// to store coverage information. The integer is a hash of the PCs
47 /// stored within that file.
45 /// Body is:
46 /// - a u64le that indicates the file path within the cache used
47 /// to store coverage information. The integer is a hash of the PCs
48 /// stored within that file.
49 /// - u64le of total runs accumulated
50 /// - u64le of unique runs accumulated
51 /// - u64le of coverage accumulated
4852 coverage_id,
4953 /// Body is a u64le that indicates the function pointer virtual memory
5054 /// address of the fuzz unit test. This is used to provide a starting
......@@ -141,9 +145,15 @@ pub fn receiveMessage(s: *Server) !InMessage.Header {
141145 return s.in.takeStruct(InMessage.Header, .little);
142146}
143147
148pub fn receiveBody_u8(s: *Server) !u8 {
149 return s.in.takeInt(u8, .little);
150}
144151pub fn receiveBody_u32(s: *Server) !u32 {
145152 return s.in.takeInt(u32, .little);
146153}
154pub fn receiveBody_u64(s: *Server) !u64 {
155 return s.in.takeInt(u64, .little);
156}
147157
148158pub fn serveStringMessage(s: *Server, tag: OutMessage.Tag, msg: []const u8) !void {
149159 try s.serveMessageHeader(.{
......@@ -160,6 +170,7 @@ pub fn serveMessageHeader(s: *const Server, header: OutMessage.Header) !void {
160170}
161171
162172pub fn serveU64Message(s: *const Server, tag: OutMessage.Tag, int: u64) !void {
173 assert(tag != .coverage_id);
163174 try serveMessageHeader(s, .{
164175 .tag = tag,
165176 .bytes_len = @sizeOf(u64),
......@@ -168,6 +179,18 @@ pub fn serveU64Message(s: *const Server, tag: OutMessage.Tag, int: u64) !void {
168179 try s.out.flush();
169180}
170181
182pub fn serveCoverageIdMessage(s: *const Server, id: u64, runs: u64, unique: u64, cov: u64) !void {
183 try serveMessageHeader(s, .{
184 .tag = .coverage_id,
185 .bytes_len = @sizeOf(u64) + @sizeOf(u64) + @sizeOf(u64) + @sizeOf(u64),
186 });
187 try s.out.writeInt(u64, id, .little);
188 try s.out.writeInt(u64, runs, .little);
189 try s.out.writeInt(u64, unique, .little);
190 try s.out.writeInt(u64, cov, .little);
191 try s.out.flush();
192}
193
171194pub fn serveEmitDigest(
172195 s: *Server,
173196 digest: *const [Cache.bin_digest_len]u8,