1const Fuzz = @This();
2
3const std = @import("std");
4const Allocator = std.mem.Allocator;
5const Build = std.Build;
6const Cache = std.Build.Cache;
7const Coverage = std.debug.Coverage;
8const Configuration = std.Build.Configuration;
9const Io = std.Io;
10const abi = std.Build.abi.fuzz;
11const assert = std.debug.assert;
12const fatal = std.process.fatal;
13const log = std.log;
14
15const Maker = @import("../Maker.zig");
16const WebServer = @import("WebServer.zig");
17
18maker: *Maker,
19mode: Mode,
20
21/// Allocated into `gpa`.
22run_steps: []const Configuration.Step.Index,
23
24group: Io.Group,
25root_prog_node: std.Progress.Node,
26prog_node: std.Progress.Node,
27
28/// Protects `coverage_files`.
29coverage_mutex: Io.Mutex,
30coverage_files: std.array_hash_map.Auto(u64, CoverageMap),
31
32queue_mutex: Io.Mutex,
33queue_cond: Io.Condition,
34msg_queue: std.ArrayList(Msg),
35
36pub const Mode = union(enum) {
37 forever: struct { ws: *WebServer },
38 limit: Limited,
39
40 pub const Limited = struct {
41 amount: u64,
42 };
43};
44
45const Msg = union(enum) {
46 coverage: struct {
47 id: u64,
48 cumulative: struct {
49 runs: u64,
50 unique: u64,
51 coverage: u64,
52 },
53 run: Configuration.Step.Index,
54 },
55 entry_point: struct {
56 coverage_id: u64,
57 addr: u64,
58 },
59};
60
61const CoverageMap = struct {
62 mapped_memory: []align(std.heap.page_size_min) const u8,
63 coverage: Coverage,
64 source_locations: []Coverage.SourceLocation,
65 /// Elements are indexes into `source_locations` pointing to the unit tests that are being fuzz tested.
66 entry_points: std.ArrayList(u32),
67 start_timestamp: i64,
68 start_n_runs: u64,
69
70 fn deinit(cm: *CoverageMap, gpa: Allocator) void {
71 std.posix.munmap(cm.mapped_memory);
72 cm.coverage.deinit(gpa);
73 cm.* = undefined;
74 }
75};
76
77pub fn init(
78 maker: *Maker,
79 all_steps: []const Configuration.Step.Index,
80 root_prog_node: std.Progress.Node,
81 mode: Mode,
82) error{ OutOfMemory, Canceled }!Fuzz {
83 const graph = maker.graph;
84 const gpa = graph.cache.gpa;
85 const io = graph.io;
86 const conf = &maker.scanned_config.configuration;
87
88 const run_steps: []const Configuration.Step.Index = steps: {
89 var steps: std.ArrayList(Configuration.Step.Index) = .empty;
90 defer steps.deinit(gpa);
91 const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0);
92 defer rebuild_node.end();
93 var rebuild_group: Io.Group = .init;
94 defer rebuild_group.cancel(io);
95
96 for (all_steps) |step_index| {
97 const conf_run = step_index.ptr(conf).extended.cast(conf, Configuration.Step.Run) orelse continue;
98 if (conf_run.producer.value == null) continue;
99 const run = &maker.stepByIndex(step_index).extended.run;
100 if (run.fuzz_tests.items.len == 0) continue;
101 try steps.append(gpa, step_index);
102 rebuild_group.async(io, rebuildTestsWorkerRun, .{ maker, step_index, rebuild_node });
103 }
104
105 if (steps.items.len == 0) fatal("no fuzz tests found", .{});
106 rebuild_node.setEstimatedTotalItems(steps.items.len);
107 const run_steps = try gpa.dupe(Configuration.Step.Index, steps.items);
108 try rebuild_group.await(io);
109 break :steps run_steps;
110 };
111 errdefer gpa.free(run_steps);
112
113 for (run_steps) |run_index| {
114 const run = &maker.stepByIndex(run_index).extended.run;
115 assert(run.fuzz_tests.items.len > 0);
116 if (run.rebuilt_executable == null)
117 fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{});
118 }
119
120 return .{
121 .maker = maker,
122 .mode = mode,
123 .run_steps = run_steps,
124 .group = .init,
125 .root_prog_node = root_prog_node,
126 .prog_node = .none,
127 .coverage_files = .empty,
128 .coverage_mutex = .init,
129 .queue_mutex = .init,
130 .queue_cond = .init,
131 .msg_queue = .empty,
132 };
133}
134
135pub fn start(fuzz: *Fuzz) void {
136 const maker = fuzz.maker;
137 const graph = maker.graph;
138 const io = graph.io;
139
140 fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", 0);
141
142 if (fuzz.mode == .forever) {
143 // For polling messages and sending updates to subscribers.
144 fuzz.group.concurrent(io, coverageRun, .{fuzz}) catch |err|
145 fatal("unable to spawn coverage task: {t}", .{err});
146 }
147
148 for (fuzz.run_steps) |run_index| {
149 const run = &maker.stepByIndex(run_index).extended.run;
150 assert(run.rebuilt_executable != null);
151 fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run_index });
152 }
153}
154
155pub fn deinit(fuzz: *Fuzz) void {
156 const maker = fuzz.maker;
157 const graph = maker.graph;
158 const io = graph.io;
159 const gpa = maker.gpa;
160
161 fuzz.group.cancel(io);
162 fuzz.prog_node.end();
163 gpa.free(fuzz.run_steps);
164}
165
166fn rebuildTestsWorkerRun(
167 maker: *Maker,
168 run_index: Configuration.Step.Index,
169 parent_progress_node: std.Progress.Node,
170) void {
171 rebuildTestsWorkerRunFallible(maker, run_index, parent_progress_node) catch |err| {
172 const conf = &maker.scanned_config.configuration;
173 const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run).?;
174 const comp_index = conf_run.producer.value.?;
175 const step_name = comp_index.ptr(conf).name.slice(conf);
176 log.err("step {s}: failed to rebuild in fuzz mode: {t}", .{ step_name, err });
177 };
178}
179
180fn rebuildTestsWorkerRunFallible(
181 maker: *Maker,
182 run_index: Configuration.Step.Index,
183 parent_progress_node: std.Progress.Node,
184) !void {
185 const graph = maker.graph;
186 const io = graph.io;
187 const gpa = maker.gpa;
188 const conf = &maker.scanned_config.configuration;
189 const run = &maker.stepByIndex(run_index).extended.run;
190 const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run).?;
191 const comp_index = conf_run.producer.value.?;
192 const comp_step = maker.stepByIndex(comp_index);
193 const comp = &comp_step.extended.compile;
194 const conf_comp_step = comp_index.ptr(conf);
195 const conf_comp = conf_comp_step.extended.cast(conf, Configuration.Step.Compile).?;
196 const root_module = conf_comp.root_module.get(conf);
197 const target = root_module.resolved_target.get(conf).?.result.get(conf);
198
199 const prog_node = parent_progress_node.start(conf_comp_step.name.slice(conf), 0);
200 defer prog_node.end();
201
202 const result = comp.rebuildInFuzzMode(maker, comp_index, prog_node);
203
204 const show_compile_errors = comp_step.result_error_bundle.errorMessageCount() > 0;
205 const show_error_msgs = comp_step.result_error_msgs.items.len > 0;
206 const show_stderr = comp_step.result_stderr.len > 0;
207
208 if (show_error_msgs or show_compile_errors or show_stderr) {
209 var buf: [256]u8 = undefined;
210 const stderr = try io.lockStderr(&buf, graph.stderr_mode);
211 defer io.unlockStderr();
212 maker.printErrorMessages(comp_index, .{}, stderr.terminal(), .verbose, .indent) catch {};
213 }
214
215 const rebuilt_bin_path = result catch |err| switch (err) {
216 error.MakeFailed => return,
217 else => |other| return other,
218 };
219 const compile_filename = try std.zig.binNameAlloc(gpa, .{
220 .root_name = conf_comp.root_name.slice(conf),
221 .cpu_arch = target.flags.cpu_arch.unwrap().?,
222 .os_tag = target.flags.os_tag.unwrap().?,
223 .ofmt = target.flags.object_format.unwrap().?,
224 .abi = target.flags.abi.unwrap().?,
225 .output_mode = switch (conf_comp.flags3.kind) {
226 .lib => .Lib,
227 .obj, .test_obj => .Obj,
228 .exe, .@"test" => .Exe,
229 },
230 .link_mode = conf_comp.flags2.linkage.unwrap(),
231 .version = if (conf_comp.version.value) |v|
232 std.SemanticVersion.parse(v.slice(conf)) catch unreachable
233 else
234 null,
235 });
236 defer gpa.free(compile_filename);
237
238 run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile_filename);
239}
240
241fn fuzzWorkerRun(fuzz: *Fuzz, run_index: Configuration.Step.Index) void {
242 const maker = fuzz.maker;
243 const graph = maker.graph;
244 const io = graph.io;
245 const conf = &maker.scanned_config.configuration;
246 const run = &maker.stepByIndex(run_index).extended.run;
247
248 run.rerunInFuzzMode(run_index, fuzz, fuzz.prog_node) catch |err| switch (err) {
249 error.MakeFailed => {
250 var buf: [256]u8 = undefined;
251 const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) {
252 error.Canceled => return,
253 };
254 defer io.unlockStderr();
255 maker.printErrorMessages(run_index, .{}, stderr.terminal(), .verbose, .indent) catch {};
256 return;
257 },
258 else => {
259 const step_name = run_index.ptr(conf).name.slice(conf);
260 log.err("step {s}: failed to rerun in fuzz mode: {t}", .{ step_name, err });
261 return;
262 },
263 };
264}
265
266pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void {
267 assert(fuzz.mode == .forever);
268 const maker = fuzz.maker;
269 const gpa = maker.gpa;
270 const conf = &maker.scanned_config.configuration;
271
272 var arena_state: std.heap.ArenaAllocator = .init(gpa);
273 defer arena_state.deinit();
274 const arena = arena_state.allocator();
275
276 const DedupTable = std.array_hash_map.Custom(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false);
277 var dedup_table: DedupTable = .empty;
278 defer dedup_table.deinit(gpa);
279
280 for (fuzz.run_steps) |run_index| {
281 const conf_run = run_index.ptr(conf).extended.cast(conf, Configuration.Step.Run) orelse continue;
282 const comp_index = conf_run.producer.value.?;
283 const comp_step = maker.stepByIndex(comp_index);
284 const compile_inputs = comp_step.inputs.table;
285 for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| {
286 try dedup_table.ensureUnusedCapacity(gpa, file_list.items.len);
287 for (file_list.items) |sub_path| {
288 if (!std.mem.endsWith(u8, sub_path, ".zig")) continue;
289 const joined_path = try dir_path.join(arena, sub_path);
290 dedup_table.putAssumeCapacity(joined_path, {});
291 }
292 }
293 }
294
295 const deduped_paths = dedup_table.keys();
296 const SortContext = struct {
297 pub fn lessThan(this: @This(), lhs: Build.Cache.Path, rhs: Build.Cache.Path) bool {
298 _ = this;
299 return switch (std.mem.order(u8, lhs.root_dir.path orelse ".", rhs.root_dir.path orelse ".")) {
300 .lt => true,
301 .gt => false,
302 .eq => std.mem.lessThan(u8, lhs.sub_path, rhs.sub_path),
303 };
304 }
305 };
306 std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan);
307 return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths);
308}
309
310pub const Previous = struct {
311 unique_runs: usize,
312 entry_points: usize,
313 sent_source_index: bool,
314 pub const init: Previous = .{
315 .unique_runs = 0,
316 .entry_points = 0,
317 .sent_source_index = false,
318 };
319};
320pub fn sendUpdate(
321 fuzz: *Fuzz,
322 socket: *std.http.Server.WebSocket,
323 prev: *Previous,
324) !void {
325 const maker = fuzz.maker;
326 const graph = maker.graph;
327 const io = graph.io;
328
329 try fuzz.coverage_mutex.lock(io);
330 defer fuzz.coverage_mutex.unlock(io);
331
332 const coverage_maps = fuzz.coverage_files.values();
333 if (coverage_maps.len == 0) return;
334 // TODO: handle multiple fuzz steps in the WebSocket packets
335 const coverage_map = &coverage_maps[0];
336 const cov_header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
337 // TODO: this isn't sound! We need to do volatile reads of these bits rather than handing the
338 // buffer off to the kernel, because we might race with the fuzzer process[es]. This brings the
339 // whole mmap strategy into question. Incidentally, I wonder if post-writergate we could pass
340 // this data straight to the socket with sendfile...
341 const seen_pcs = cov_header.seenBits();
342 const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic);
343 const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic);
344 {
345 if (!prev.sent_source_index) {
346 prev.sent_source_index = true;
347 // We need to send initial context.
348 const header: abi.SourceIndexHeader = .{
349 .directories_len = @intCast(coverage_map.coverage.directories.entries.len),
350 .files_len = @intCast(coverage_map.coverage.files.entries.len),
351 .source_locations_len = @intCast(coverage_map.source_locations.len),
352 .string_bytes_len = @intCast(coverage_map.coverage.string_bytes.items.len),
353 .start_timestamp = coverage_map.start_timestamp,
354 .start_n_runs = coverage_map.start_n_runs,
355 };
356 var iovecs: [5][]const u8 = .{
357 @ptrCast(&header),
358 @ptrCast(coverage_map.coverage.directories.keys()),
359 @ptrCast(coverage_map.coverage.files.keys()),
360 @ptrCast(coverage_map.source_locations),
361 coverage_map.coverage.string_bytes.items,
362 };
363 try socket.writeMessageVec(&iovecs, .binary);
364 }
365
366 const header: abi.CoverageUpdateHeader = .{
367 .n_runs = n_runs,
368 .unique_runs = unique_runs,
369 };
370 var iovecs: [2][]const u8 = .{
371 @ptrCast(&header),
372 @ptrCast(seen_pcs),
373 };
374 try socket.writeMessageVec(&iovecs, .binary);
375
376 prev.unique_runs = unique_runs;
377 }
378
379 if (prev.entry_points != coverage_map.entry_points.items.len) {
380 const header: abi.EntryPointHeader = .init(@intCast(coverage_map.entry_points.items.len));
381 var iovecs: [2][]const u8 = .{
382 @ptrCast(&header),
383 @ptrCast(coverage_map.entry_points.items),
384 };
385 try socket.writeMessageVec(&iovecs, .binary);
386
387 prev.entry_points = coverage_map.entry_points.items.len;
388 }
389}
390
391fn coverageRun(fuzz: *Fuzz) void {
392 coverageRunCancelable(fuzz) catch |err| switch (err) {
393 error.Canceled => return,
394 };
395}
396
397fn coverageRunCancelable(fuzz: *Fuzz) Io.Cancelable!void {
398 const maker = fuzz.maker;
399 const graph = maker.graph;
400 const io = graph.io;
401
402 try fuzz.queue_mutex.lock(io);
403 defer fuzz.queue_mutex.unlock(io);
404
405 while (true) {
406 try fuzz.queue_cond.wait(io, &fuzz.queue_mutex);
407 for (fuzz.msg_queue.items) |msg| switch (msg) {
408 .coverage => |coverage| prepareTables(fuzz, coverage.run, coverage.id) catch |err| switch (err) {
409 error.AlreadyReported => continue,
410 error.Canceled => return,
411 else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
412 },
413 .entry_point => |entry_point| addEntryPoint(fuzz, entry_point.coverage_id, entry_point.addr) catch |err| switch (err) {
414 error.AlreadyReported => continue,
415 error.Canceled => return,
416 else => |e| log.err("failed to prepare code coverage tables: {t}", .{e}),
417 },
418 };
419 fuzz.msg_queue.clearRetainingCapacity();
420 }
421}
422fn prepareTables(fuzz: *Fuzz, run_index: Configuration.Step.Index, coverage_id: u64) error{ OutOfMemory, AlreadyReported, Canceled }!void {
423 assert(fuzz.mode == .forever);
424 const ws = fuzz.mode.forever.ws;
425 const maker = fuzz.maker;
426 const graph = maker.graph;
427 const io = graph.io;
428 const gpa = maker.gpa;
429 const conf = &maker.scanned_config.configuration;
430 const cache_root = graph.local_cache_root;
431
432 try fuzz.coverage_mutex.lock(io);
433 defer fuzz.coverage_mutex.unlock(io);
434
435 const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id);
436 if (gop.found_existing) {
437 // We are fuzzing the same executable with multiple threads.
438 // Perhaps the same unit test; perhaps a different one. In any
439 // case, since the coverage file is the same, we only have to
440 // notice changes to that one file in order to learn coverage for
441 // this particular executable.
442 return;
443 }
444 errdefer _ = fuzz.coverage_files.pop();
445
446 gop.value_ptr.* = .{
447 .coverage = std.debug.Coverage.init,
448 .mapped_memory = undefined, // populated below
449 .source_locations = undefined, // populated below
450 .entry_points = .empty,
451 .start_timestamp = ws.now(),
452 .start_n_runs = undefined, // populated below
453 };
454 errdefer gop.value_ptr.coverage.deinit(gpa);
455
456 const run_step = maker.stepByIndex(run_index);
457 const conf_run_step = run_index.ptr(conf);
458 const conf_run = conf_run_step.extended.cast(conf, Configuration.Step.Run).?;
459 const comp_index = conf_run.producer.value.?;
460 const conf_comp_step = comp_index.ptr(conf);
461 const conf_comp = conf_comp_step.extended.cast(conf, Configuration.Step.Compile).?;
462 const rebuilt_exe_path = run_step.extended.run.rebuilt_executable.?;
463 const root_module = conf_comp.root_module.get(conf);
464 const target = root_module.resolved_target.get(conf).?.result.get(conf);
465
466 var debug_info = std.debug.Info.load(
467 gpa,
468 io,
469 rebuilt_exe_path,
470 &gop.value_ptr.coverage,
471 target.flags.object_format.unwrap().?,
472 target.flags.cpu_arch.unwrap().?,
473 ) catch |err| {
474 log.err("step {s}: failed to load debug information for {f}: {t}", .{
475 conf_run_step.name.slice(conf), rebuilt_exe_path, err,
476 });
477 return error.AlreadyReported;
478 };
479 defer debug_info.deinit(gpa);
480
481 const coverage_file_path: Build.Cache.Path = .{
482 .root_dir = cache_root,
483 .sub_path = "v/" ++ std.fmt.hex(coverage_id),
484 };
485 var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
486 log.err("step {s}: failed to load coverage file {f}: {t}", .{
487 conf_run_step.name.slice(conf), coverage_file_path, err,
488 });
489 return error.AlreadyReported;
490 };
491 defer coverage_file.close(io);
492
493 const file_size = coverage_file.length(io) catch |err| {
494 log.err("unable to check len of coverage file {f}: {t}", .{ coverage_file_path, err });
495 return error.AlreadyReported;
496 };
497
498 const mapped_memory = std.posix.mmap(
499 null,
500 file_size,
501 .{ .READ = true },
502 .{ .TYPE = .SHARED },
503 coverage_file.handle,
504 0,
505 ) catch |err| {
506 log.err("failed to map coverage file {f}: {t}", .{ coverage_file_path, err });
507 return error.AlreadyReported;
508 };
509 gop.value_ptr.mapped_memory = mapped_memory;
510
511 const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
512 const pcs = header.pcAddrs();
513 const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len);
514 errdefer gpa.free(source_locations);
515
516 // Unfortunately the PCs array that LLVM gives us from the 8-bit PC
517 // counters feature is not sorted.
518 var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .empty;
519 defer sorted_pcs.deinit(gpa);
520 try sorted_pcs.resize(gpa, pcs.len);
521 @memcpy(sorted_pcs.items(.pc), pcs);
522 for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i);
523 sorted_pcs.sortUnstable(struct {
524 addrs: []const u64,
525
526 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
527 return ctx.addrs[a_index] < ctx.addrs[b_index];
528 }
529 }{ .addrs = sorted_pcs.items(.pc) });
530
531 debug_info.resolveAddresses(gpa, io, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| {
532 log.err("failed to resolve addresses to source locations: {t}", .{err});
533 return error.AlreadyReported;
534 };
535
536 for (sorted_pcs.items(.index), sorted_pcs.items(.sl)) |i, sl| source_locations[i] = sl;
537 gop.value_ptr.source_locations = source_locations;
538 gop.value_ptr.start_n_runs = header.n_runs;
539
540 ws.notifyUpdate();
541}
542
543fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory, Canceled }!void {
544 const maker = fuzz.maker;
545 const graph = maker.graph;
546 const io = graph.io;
547 const gpa = maker.gpa;
548
549 try fuzz.coverage_mutex.lock(io);
550 defer fuzz.coverage_mutex.unlock(io);
551
552 const coverage_map = fuzz.coverage_files.getPtr(coverage_id).?;
553 const header: *const abi.SeenPcsHeader = @ptrCast(coverage_map.mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]);
554 const pcs = header.pcAddrs();
555
556 // Since this pcs list is unsorted, we must linear scan for the best index.
557 const index = i: {
558 var best: usize = 0;
559 for (pcs[1..], 1..) |elem_addr, i| {
560 if (elem_addr == addr) break :i i;
561 if (elem_addr > addr) continue;
562 if (elem_addr > pcs[best]) best = i;
563 }
564 break :i best;
565 };
566 if (index >= pcs.len) {
567 log.err("unable to find unit test entry address 0x{x} in source locations (range: 0x{x} to 0x{x})", .{
568 addr, pcs[0], pcs[pcs.len - 1],
569 });
570 return error.AlreadyReported;
571 }
572 if (false) {
573 const sl = coverage_map.source_locations[index];
574 const file_name = coverage_map.coverage.stringAt(coverage_map.coverage.fileAt(sl.file).basename);
575 if (pcs.len == 1) {
576 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 (final)", .{
577 addr, file_name, sl.line, sl.column,
578 });
579 } else if (index == 0) {
580 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index 0 before {x}", .{
581 addr, file_name, sl.line, sl.column, pcs[index + 1],
582 });
583 } else if (index == pcs.len - 1) {
584 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} (final) after {x}", .{
585 addr, file_name, sl.line, sl.column, index, pcs[index - 1],
586 });
587 } else {
588 log.debug("server found entry point for 0x{x} at {s}:{d}:{d} - index {d} between {x} and {x}", .{
589 addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1],
590 });
591 }
592 }
593 try coverage_map.entry_points.append(gpa, @intCast(index));
594}
595
596pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void {
597 assert(fuzz.mode == .limit);
598 const maker = fuzz.maker;
599 const graph = maker.graph;
600 const io = graph.io;
601 const cache_root = graph.local_cache_root;
602 const conf = &maker.scanned_config.configuration;
603
604 try fuzz.group.await(io);
605 fuzz.group = .init;
606
607 std.debug.print("======= FUZZING REPORT =======\n", .{});
608 for (fuzz.msg_queue.items) |msg| {
609 if (msg != .coverage) continue;
610
611 const cov = msg.coverage;
612 const run_step_name = cov.run.ptr(conf).name.slice(conf);
613 const run = &maker.stepByIndex(cov.run).extended.run;
614 const coverage_file_path: std.Build.Cache.Path = .{
615 .root_dir = cache_root,
616 .sub_path = "v/" ++ std.fmt.hex(cov.id),
617 };
618 var coverage_file = coverage_file_path.root_dir.handle.openFile(io, coverage_file_path.sub_path, .{}) catch |err| {
619 fatal("step {s}: failed to load coverage file {f}: {t}", .{
620 run_step_name, coverage_file_path, err,
621 });
622 };
623 defer coverage_file.close(io);
624
625 const fuzz_abi = std.Build.abi.fuzz;
626 var rbuf: [0x1000]u8 = undefined;
627 var r = coverage_file.reader(io, &rbuf);
628
629 var header: fuzz_abi.SeenPcsHeader = undefined;
630 r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| {
631 fatal("step {s}: failed to read from coverage file {f}: {t}", .{
632 run_step_name, coverage_file_path, err,
633 });
634 };
635
636 if (header.pcs_len == 0) {
637 fatal("step {s}: corrupted coverage file {f}: pcs_len was zero", .{
638 run_step_name, coverage_file_path,
639 });
640 }
641
642 var seen_count: usize = 0;
643 const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len);
644 for (0..chunk_count) |_| {
645 const seen = r.interface.takeInt(usize, .little) catch |err| {
646 fatal("step {s}: failed to read from coverage file {f}: {t}", .{
647 run_step_name, coverage_file_path, err,
648 });
649 };
650 seen_count += @popCount(seen);
651 }
652
653 const seen_f: f64 = @floatFromInt(seen_count);
654 const total_f: f64 = @floatFromInt(header.pcs_len);
655 const ratio = seen_f / total_f;
656 std.debug.print(
657 \\Step: {s}
658 \\Fuzz test: "{s}" ({x})
659 \\Runs: {} -> {}
660 \\Unique runs: {} -> {}
661 \\Coverage: {}/{} -> {}/{} ({:.02}%)
662 \\
663 , .{
664 run_step_name,
665 run.fuzz_tests.items[0],
666 cov.id,
667 cov.cumulative.runs,
668 header.n_runs,
669 cov.cumulative.unique,
670 header.unique_runs,
671 cov.cumulative.coverage,
672 header.pcs_len,
673 seen_count,
674 header.pcs_len,
675 ratio * 100,
676 });
677
678 std.debug.print("------------------------------\n", .{});
679 }
680 std.debug.print(
681 \\Values are accumulated across multiple runs when preserving the cache.
682 \\==============================
683 \\
684 , .{});
685}