From 0feacc2b81679514c0168a6ba4c0decafeb2e43e Mon Sep 17 00:00:00 2001 From: Loris Cro Date: Wed, 24 Sep 2025 12:10:32 +0200 Subject: [PATCH 1/5] 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. --- lib/compiler/build_runner.zig | 90 ++++++++++++++-- lib/compiler/test_runner.zig | 79 +++++++++++++- lib/fuzzer.zig | 7 +- lib/std/Build/Fuzz.zig | 196 ++++++++++++++++++++++++++-------- lib/std/Build/Step/Run.zig | 53 +++++++-- lib/std/Build/WebServer.zig | 10 +- lib/std/Build/abi.zig | 4 +- lib/std/zig/Client.zig | 12 ++- lib/std/zig/Server.zig | 29 ++++- 9 files changed, 407 insertions(+), 73 deletions(-) diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 5fcb8081b7..4aeffe21a9 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -112,7 +112,7 @@ pub fn main() !void { var steps_menu = false; var output_tmp_nonce: ?[16]u8 = null; var watch = false; - var fuzz = false; + var fuzz: ?std.Build.Fuzz.Mode = null; var debounce_interval_ms: u16 = 50; var webui_listen: ?std.net.Address = null; @@ -274,10 +274,44 @@ pub fn main() !void { webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable; } } else if (mem.eql(u8, arg, "--fuzz")) { - fuzz = true; + fuzz = .{ .forever = undefined }; if (webui_listen == null) { webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable; } + } else if (mem.startsWith(u8, arg, "--fuzz=")) { + const value = arg["--fuzz=".len..]; + if (value.len == 0) fatal("missing argument to --fuzz\n", .{}); + + const unit: u8 = value[value.len - 1]; + const digits = switch (value[value.len - 1]) { + '0'...'9' => value, + 'K', 'M', 'G' => value[0 .. value.len - 1], + else => fatal( + "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]\n", + .{}, + ), + }; + + const amount = std.fmt.parseInt(u64, digits, 10) catch { + fatal( + "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]\n", + .{}, + ); + }; + + const normalized_amount = std.math.mul(u64, amount, switch (unit) { + else => unreachable, + '0'...'9' => 1, + 'K' => 1000, + 'M' => 1_000_000, + 'G' => 1_000_000_000, + }) catch fatal("fuzzing limit amount overflows u64\n", .{}); + + fuzz = .{ + .limit = .{ + .amount = normalized_amount, + }, + }; } else if (mem.eql(u8, arg, "-fincremental")) { graph.incremental = true; } else if (mem.eql(u8, arg, "-fno-incremental")) { @@ -476,6 +510,7 @@ pub fn main() !void { targets.items, main_progress_node, &run, + fuzz, ) catch |err| switch (err) { error.UncleanExit => { assert(!run.watch and run.web_server == null); @@ -485,7 +520,8 @@ pub fn main() !void { }; if (run.web_server) |*web_server| { - web_server.finishBuild(.{ .fuzz = fuzz }); + if (fuzz) |mode| assert(mode == .forever); + web_server.finishBuild(.{ .fuzz = fuzz != null }); } if (!watch and run.web_server == null) { @@ -651,6 +687,7 @@ fn runStepNames( step_names: []const []const u8, parent_prog_node: std.Progress.Node, run: *Run, + fuzz: ?std.Build.Fuzz.Mode, ) !void { const gpa = run.gpa; const step_stack = &run.step_stack; @@ -676,6 +713,7 @@ fn runStepNames( }); } } + assert(run.memory_blocked_steps.items.len == 0); var test_skip_count: usize = 0; @@ -724,6 +762,45 @@ fn runStepNames( } } + const ttyconf = run.ttyconf; + + if (fuzz) |mode| blk: { + switch (builtin.os.tag) { + // Current implementation depends on two things that need to be ported to Windows: + // * Memory-mapping to share data between the fuzzer and build runner. + // * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving + // many addresses to source locations). + .windows => fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}), + else => {}, + } + if (@bitSizeOf(usize) != 64) { + // Current implementation depends on posix.mmap()'s second parameter, `length: usize`, + // being compatible with `std.fs.getEndPos() u64`'s return value. This is not the case + // on 32-bit platforms. + // Affects or affected by issues #5185, #22523, and #22464. + fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)}); + } + + switch (mode) { + .forever => break :blk, + .limit => {}, + } + + assert(mode == .limit); + var f = std.Build.Fuzz.init( + gpa, + thread_pool, + step_stack.keys(), + parent_prog_node, + ttyconf, + mode, + ) catch |err| fatal("failed to start fuzzer: {s}", .{@errorName(err)}); + defer f.deinit(); + + f.start(); + f.waitAndPrintReport(); + } + // A proper command line application defaults to silently succeeding. // The user may request verbose mode if they have a different preference. const failures_only = switch (run.summary) { @@ -737,8 +814,6 @@ fn runStepNames( std.Progress.setStatus(.failure); } - const ttyconf = run.ttyconf; - if (run.summary != .none) { const w = std.debug.lockStderrWriter(&stdio_buffer_allocation); defer std.debug.unlockStderrWriter(); @@ -1366,7 +1441,10 @@ fn printUsage(b: *std.Build, w: *Writer) !void { \\ --watch Continuously rebuild when source files are modified \\ --debounce Delay before rebuilding after changed file detected \\ --webui[=ip] Enable the web interface on the given IP address - \\ --fuzz Continuously search for unit test failures (implies '--webui') + \\ --fuzz[=limit] Continuously search for unit test failures with an optional + \\ limit to the max number of iterations. The argument supports + \\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies + \\ '--webui' when no limit is specified. \\ --time-report Force full rebuild and provide detailed information on \\ compilation time of Zig source code (implies '--webui') \\ -fincremental Enable incremental compilation diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 2a7eeaeb72..5cdeb95d18 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -2,6 +2,7 @@ const builtin = @import("builtin"); const std = @import("std"); +const fatal = std.process.fatal; const testing = std.testing; const assert = std.debug.assert; const fuzz_abi = std.Build.abi.fuzz; @@ -62,13 +63,13 @@ pub fn main() void { } if (listen) { - return mainServer() catch @panic("internal test runner failure"); + return mainServer(opt_cache_dir) catch @panic("internal test runner failure"); } else { return mainTerminal(); } } -fn mainServer() !void { +fn mainServer(opt_cache_dir: ?[]const u8) !void { @disableInstrumentation(); var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer); var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); @@ -78,9 +79,66 @@ fn mainServer() !void { .zig_version = builtin.zig_version_string, }); - if (builtin.fuzz) { + if (builtin.fuzz) blk: { + const cache_dir = opt_cache_dir.?; const coverage_id = fuzz_abi.fuzzer_coverage_id(); - try server.serveU64Message(.coverage_id, coverage_id); + const coverage_file_path: std.Build.Cache.Path = .{ + .root_dir = .{ + .path = cache_dir, + .handle = std.fs.cwd().openDir(cache_dir, .{}) catch |err| { + if (err == error.FileNotFound) { + try server.serveCoverageIdMessage(coverage_id, 0, 0, 0); + break :blk; + } + + fatal("failed to access cache dir '{s}': {s}", .{ + cache_dir, @errorName(err), + }); + }, + }, + .sub_path = "v/" ++ std.fmt.hex(coverage_id), + }; + + var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| { + if (err == error.FileNotFound) { + try server.serveCoverageIdMessage(coverage_id, 0, 0, 0); + break :blk; + } + + fatal("failed to load coverage file '{f}': {s}", .{ + coverage_file_path, @errorName(err), + }); + }; + defer coverage_file.close(); + + var rbuf: [0x1000]u8 = undefined; + var r = coverage_file.reader(&rbuf); + + var header: fuzz_abi.SeenPcsHeader = undefined; + r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { + fatal("failed to read from coverage file '{f}': {s}", .{ + coverage_file_path, @errorName(err), + }); + }; + + if (header.pcs_len == 0) { + fatal("corrupted coverage file '{f}': pcs_len was zero", .{ + coverage_file_path, + }); + } + + var seen_count: usize = 0; + const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len); + for (0..chunk_count) |_| { + const seen = r.interface.takeInt(usize, .little) catch |err| { + fatal("failed to read from coverage file '{f}': {s}", .{ + coverage_file_path, @errorName(err), + }); + }; + seen_count += @popCount(seen); + } + + try server.serveCoverageIdMessage(coverage_id, header.n_runs, header.unique_runs, seen_count); } while (true) { @@ -158,6 +216,9 @@ fn mainServer() !void { if (!builtin.fuzz) unreachable; const index = try server.receiveBody_u32(); + const mode: fuzz_abi.LimitKind = @enumFromInt(try server.receiveBody_u8()); + const amount_or_instance = try server.receiveBody_u64(); + const test_fn = builtin.test_functions[index]; const entry_addr = @intFromPtr(test_fn.func); @@ -165,6 +226,8 @@ fn mainServer() !void { defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1); is_fuzz_test = false; fuzz_test_index = index; + fuzz_mode = mode; + fuzz_amount_or_instance = amount_or_instance; test_fn.func() catch |err| switch (err) { error.SkipZigTest => return, @@ -178,6 +241,8 @@ fn mainServer() !void { }; if (!is_fuzz_test) @panic("missed call to std.testing.fuzz"); if (log_err_count != 0) @panic("error logs detected"); + assert(mode != .forever); + std.process.exit(0); }, else => { @@ -343,6 +408,8 @@ pub fn mainSimple() anyerror!void { var is_fuzz_test: bool = undefined; var fuzz_test_index: u32 = undefined; +var fuzz_mode: fuzz_abi.LimitKind = undefined; +var fuzz_amount_or_instance: u64 = undefined; pub fn fuzz( context: anytype, @@ -401,9 +468,11 @@ pub fn fuzz( global.ctx = context; fuzz_abi.fuzzer_init_test(&global.test_one, .fromSlice(builtin.test_functions[fuzz_test_index].name)); + for (options.corpus) |elem| fuzz_abi.fuzzer_new_input(.fromSlice(elem)); - fuzz_abi.fuzzer_main(); + + fuzz_abi.fuzzer_main(fuzz_mode, fuzz_amount_or_instance); return; } diff --git a/lib/fuzzer.zig b/lib/fuzzer.zig index 9171129427..4db2b4a2cd 100644 --- a/lib/fuzzer.zig +++ b/lib/fuzzer.zig @@ -600,9 +600,10 @@ export fn fuzzer_new_input(bytes: abi.Slice) void { } /// fuzzer_init_test must be called first -export fn fuzzer_main() void { - while (true) { - fuzzer.cycle(); +export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void { + switch (limit_kind) { + .forever => while (true) fuzzer.cycle(), + .iterations => for (0..amount -| 1) |_| fuzzer.cycle(), } } diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig index 6bea7654b2..d6c239f6ed 100644 --- a/lib/std/Build/Fuzz.zig +++ b/lib/std/Build/Fuzz.zig @@ -8,17 +8,22 @@ const Allocator = std.mem.Allocator; const log = std.log; const Coverage = std.debug.Coverage; const abi = Build.abi.fuzz; +const tty = std.Io.tty; const Fuzz = @This(); const build_runner = @import("root"); -ws: *Build.WebServer, +gpa: Allocator, +mode: Mode, -/// Allocated into `ws.gpa`. +/// Allocated into `gpa`. run_steps: []const *Step.Run, wait_group: std.Thread.WaitGroup, +root_prog_node: std.Progress.Node, prog_node: std.Progress.Node, +thread_pool: *std.Thread.Pool, +ttyconf: tty.Config, /// Protects `coverage_files`. coverage_mutex: std.Thread.Mutex, @@ -28,9 +33,23 @@ queue_mutex: std.Thread.Mutex, queue_cond: std.Thread.Condition, msg_queue: std.ArrayListUnmanaged(Msg), +pub const Mode = union(enum) { + forever: struct { ws: *Build.WebServer }, + limit: Limited, + + pub const Limited = struct { + amount: u64, + }; +}; + const Msg = union(enum) { coverage: struct { id: u64, + cumulative: struct { + runs: u64, + unique: u64, + coverage: u64, + }, run: *Step.Run, }, entry_point: struct { @@ -54,23 +73,28 @@ const CoverageMap = struct { } }; -pub fn init(ws: *Build.WebServer) Allocator.Error!Fuzz { - const gpa = ws.gpa; - +pub fn init( + gpa: Allocator, + thread_pool: *std.Thread.Pool, + all_steps: []const *Build.Step, + root_prog_node: std.Progress.Node, + ttyconf: tty.Config, + mode: Mode, +) Allocator.Error!Fuzz { const run_steps: []const *Step.Run = steps: { var steps: std.ArrayListUnmanaged(*Step.Run) = .empty; defer steps.deinit(gpa); - const rebuild_node = ws.root_prog_node.start("Rebuilding Unit Tests", 0); + const rebuild_node = root_prog_node.start("Rebuilding Unit Tests", 0); defer rebuild_node.end(); var rebuild_wg: std.Thread.WaitGroup = .{}; defer rebuild_wg.wait(); - for (ws.all_steps) |step| { + for (all_steps) |step| { const run = step.cast(Step.Run) orelse continue; if (run.producer == null) continue; if (run.fuzz_tests.items.len == 0) continue; try steps.append(gpa, run); - ws.thread_pool.spawnWg(&rebuild_wg, rebuildTestsWorkerRun, .{ run, gpa, ws.ttyconf, rebuild_node }); + thread_pool.spawnWg(&rebuild_wg, rebuildTestsWorkerRun, .{ run, gpa, ttyconf, rebuild_node }); } if (steps.items.len == 0) fatal("no fuzz tests found", .{}); @@ -86,9 +110,13 @@ pub fn init(ws: *Build.WebServer) Allocator.Error!Fuzz { } return .{ - .ws = ws, + .gpa = gpa, + .mode = mode, .run_steps = run_steps, .wait_group = .{}, + .thread_pool = thread_pool, + .ttyconf = ttyconf, + .root_prog_node = root_prog_node, .prog_node = .none, .coverage_files = .empty, .coverage_mutex = .{}, @@ -99,32 +127,31 @@ pub fn init(ws: *Build.WebServer) Allocator.Error!Fuzz { } pub fn start(fuzz: *Fuzz) void { - const ws = fuzz.ws; - fuzz.prog_node = ws.root_prog_node.start("Fuzzing", fuzz.run_steps.len); + fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", fuzz.run_steps.len); - // For polling messages and sending updates to subscribers. - fuzz.wait_group.start(); - _ = std.Thread.spawn(.{}, coverageRun, .{fuzz}) catch |err| { - fuzz.wait_group.finish(); - fatal("unable to spawn coverage thread: {s}", .{@errorName(err)}); - }; + if (fuzz.mode == .forever) { + // For polling messages and sending updates to subscribers. + fuzz.wait_group.start(); + _ = std.Thread.spawn(.{}, coverageRun, .{fuzz}) catch |err| { + fuzz.wait_group.finish(); + fatal("unable to spawn coverage thread: {s}", .{@errorName(err)}); + }; + } for (fuzz.run_steps) |run| { for (run.fuzz_tests.items) |unit_test_index| { assert(run.rebuilt_executable != null); - ws.thread_pool.spawnWg(&fuzz.wait_group, fuzzWorkerRun, .{ + fuzz.thread_pool.spawnWg(&fuzz.wait_group, fuzzWorkerRun, .{ fuzz, run, unit_test_index, }); } } } -pub fn deinit(fuzz: *Fuzz) void { - if (true) @panic("TODO: terminate the fuzzer processes"); - fuzz.wait_group.wait(); - fuzz.prog_node.end(); - const gpa = fuzz.ws.gpa; - gpa.free(fuzz.run_steps); +pub fn deinit(fuzz: *Fuzz) void { + if (!fuzz.wait_group.isDone()) @panic("TODO: terminate the fuzzer processes"); + fuzz.prog_node.end(); + fuzz.gpa.free(fuzz.run_steps); } fn rebuildTestsWorkerRun(run: *Step.Run, gpa: Allocator, ttyconf: std.Io.tty.Config, parent_prog_node: std.Progress.Node) void { @@ -177,7 +204,7 @@ fn fuzzWorkerRun( var buf: [256]u8 = undefined; const w = std.debug.lockStderrWriter(&buf); defer std.debug.unlockStderrWriter(); - build_runner.printErrorMessages(gpa, &run.step, .{ .ttyconf = fuzz.ws.ttyconf }, w, false) catch {}; + build_runner.printErrorMessages(gpa, &run.step, .{ .ttyconf = fuzz.ttyconf }, w, false) catch {}; return; }, else => { @@ -190,20 +217,20 @@ fn fuzzWorkerRun( } pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void { - const gpa = fuzz.ws.gpa; + assert(fuzz.mode == .forever); - var arena_state: std.heap.ArenaAllocator = .init(gpa); + var arena_state: std.heap.ArenaAllocator = .init(fuzz.gpa); defer arena_state.deinit(); const arena = arena_state.allocator(); const DedupTable = std.ArrayHashMapUnmanaged(Build.Cache.Path, void, Build.Cache.Path.TableAdapter, false); var dedup_table: DedupTable = .empty; - defer dedup_table.deinit(gpa); + defer dedup_table.deinit(fuzz.gpa); for (fuzz.run_steps) |run_step| { const compile_inputs = run_step.producer.?.step.inputs.table; for (compile_inputs.keys(), compile_inputs.values()) |dir_path, *file_list| { - try dedup_table.ensureUnusedCapacity(gpa, file_list.items.len); + try dedup_table.ensureUnusedCapacity(fuzz.gpa, file_list.items.len); for (file_list.items) |sub_path| { if (!std.mem.endsWith(u8, sub_path, ".zig")) continue; 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 { } }; std.mem.sortUnstable(Build.Cache.Path, deduped_paths, SortContext{}, SortContext.lessThan); - return fuzz.ws.serveTarFile(req, deduped_paths); + return fuzz.mode.forever.ws.serveTarFile(req, deduped_paths); } pub const Previous = struct { @@ -319,13 +346,13 @@ fn coverageRun(fuzz: *Fuzz) void { } } fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutOfMemory, AlreadyReported }!void { - const ws = fuzz.ws; - const gpa = ws.gpa; + assert(fuzz.mode == .forever); + const ws = fuzz.mode.forever.ws; fuzz.coverage_mutex.lock(); defer fuzz.coverage_mutex.unlock(); - const gop = try fuzz.coverage_files.getOrPut(gpa, coverage_id); + const gop = try fuzz.coverage_files.getOrPut(fuzz.gpa, coverage_id); if (gop.found_existing) { // We are fuzzing the same executable with multiple threads. // 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 .entry_points = .{}, .start_timestamp = ws.now(), }; - errdefer gop.value_ptr.coverage.deinit(gpa); + errdefer gop.value_ptr.coverage.deinit(fuzz.gpa); const rebuilt_exe_path = run_step.rebuilt_executable.?; - var debug_info = std.debug.Info.load(gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| { + var debug_info = std.debug.Info.load(fuzz.gpa, rebuilt_exe_path, &gop.value_ptr.coverage) catch |err| { log.err("step '{s}': failed to load debug information for '{f}': {s}", .{ run_step.step.name, rebuilt_exe_path, @errorName(err), }); return error.AlreadyReported; }; - defer debug_info.deinit(gpa); + defer debug_info.deinit(fuzz.gpa); const coverage_file_path: Build.Cache.Path = .{ .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 const header: *const abi.SeenPcsHeader = @ptrCast(mapped_memory[0..@sizeOf(abi.SeenPcsHeader)]); const pcs = header.pcAddrs(); - const source_locations = try gpa.alloc(Coverage.SourceLocation, pcs.len); - errdefer gpa.free(source_locations); + const source_locations = try fuzz.gpa.alloc(Coverage.SourceLocation, pcs.len); + errdefer fuzz.gpa.free(source_locations); // Unfortunately the PCs array that LLVM gives us from the 8-bit PC // counters feature is not sorted. var sorted_pcs: std.MultiArrayList(struct { pc: u64, index: u32, sl: Coverage.SourceLocation }) = .{}; - defer sorted_pcs.deinit(gpa); - try sorted_pcs.resize(gpa, pcs.len); + defer sorted_pcs.deinit(fuzz.gpa); + try sorted_pcs.resize(fuzz.gpa, pcs.len); @memcpy(sorted_pcs.items(.pc), pcs); for (sorted_pcs.items(.index), 0..) |*v, i| v.* = @intCast(i); sorted_pcs.sortUnstable(struct { @@ -404,7 +431,7 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO } }{ .addrs = sorted_pcs.items(.pc) }); - debug_info.resolveAddresses(gpa, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| { + debug_info.resolveAddresses(fuzz.gpa, sorted_pcs.items(.pc), sorted_pcs.items(.sl)) catch |err| { log.err("failed to resolve addresses to source locations: {s}", .{@errorName(err)}); return error.AlreadyReported; }; @@ -414,6 +441,7 @@ fn prepareTables(fuzz: *Fuzz, run_step: *Step.Run, coverage_id: u64) error{ OutO ws.notifyUpdate(); } + fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReported, OutOfMemory }!void { fuzz.coverage_mutex.lock(); defer fuzz.coverage_mutex.unlock(); @@ -445,5 +473,89 @@ fn addEntryPoint(fuzz: *Fuzz, coverage_id: u64, addr: u64) error{ AlreadyReporte addr, file_name, sl.line, sl.column, index, pcs[index - 1], pcs[index + 1], }); } - try coverage_map.entry_points.append(fuzz.ws.gpa, @intCast(index)); + try coverage_map.entry_points.append(fuzz.gpa, @intCast(index)); +} + +pub fn waitAndPrintReport(fuzz: *Fuzz) void { + assert(fuzz.mode == .limit); + + fuzz.wait_group.wait(); + fuzz.wait_group.reset(); + + std.debug.print("======= FUZZING REPORT =======\n", .{}); + for (fuzz.msg_queue.items) |msg| { + if (msg != .coverage) continue; + + const cov = msg.coverage; + const coverage_file_path: std.Build.Cache.Path = .{ + .root_dir = cov.run.step.owner.cache_root, + .sub_path = "v/" ++ std.fmt.hex(cov.id), + }; + var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| { + fatal("step '{s}': failed to load coverage file '{f}': {s}", .{ + cov.run.step.name, coverage_file_path, @errorName(err), + }); + }; + defer coverage_file.close(); + + const fuzz_abi = std.Build.abi.fuzz; + var rbuf: [0x1000]u8 = undefined; + var r = coverage_file.reader(&rbuf); + + var header: fuzz_abi.SeenPcsHeader = undefined; + r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { + fatal("step '{s}': failed to read from coverage file '{f}': {s}", .{ + cov.run.step.name, coverage_file_path, @errorName(err), + }); + }; + + if (header.pcs_len == 0) { + fatal("step '{s}': corrupted coverage file '{f}': pcs_len was zero", .{ + cov.run.step.name, coverage_file_path, + }); + } + + var seen_count: usize = 0; + const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len); + for (0..chunk_count) |_| { + const seen = r.interface.takeInt(usize, .little) catch |err| { + fatal("step '{s}': failed to read from coverage file '{f}': {s}", .{ + cov.run.step.name, coverage_file_path, @errorName(err), + }); + }; + seen_count += @popCount(seen); + } + + const seen_f: f64 = @floatFromInt(seen_count); + const total_f: f64 = @floatFromInt(header.pcs_len); + const ratio = seen_f / total_f; + std.debug.print( + \\Step: {s} + \\Fuzz test: "{s}" ({x}) + \\Runs: {} -> {} + \\Unique runs: {} -> {} + \\Coverage: {}/{} -> {}/{} ({:.02}%) + \\ + , .{ + cov.run.step.name, + cov.run.cached_test_metadata.?.testName(cov.run.fuzz_tests.items[0]), + cov.id, + cov.cumulative.runs, + header.n_runs, + cov.cumulative.unique, + header.unique_runs, + cov.cumulative.coverage, + header.pcs_len, + seen_count, + header.pcs_len, + ratio * 100, + }); + + std.debug.print("------------------------------\n", .{}); + } + std.debug.print( + \\Values are accumulated across multiple runs when preserving the cache. + \\============================== + \\ + , .{}); } diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index fbcc0217f3..e6ee5ad233 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -1662,12 +1662,24 @@ fn evalZigTest( // If this is `true`, we avoid ever entering the polling loop below, because the stdin pipe has // somehow already closed; instead, we go straight to capturing stderr in case it has anything // useful. - const first_write_failed = if (fuzz_context) |fuzz| failed: { - sendRunTestMessage(child.stdin.?, .start_fuzzing, fuzz.unit_test_index) catch |err| { - try run.step.addError("unable to write stdin: {s}", .{@errorName(err)}); - break :failed true; - }; - break :failed false; + const first_write_failed = if (fuzz_context) |fctx| failed: { + switch (fctx.fuzz.mode) { + .forever => { + const instance_id = 0; // will be used by mutiprocess forever fuzzing + sendRunFuzzTestMessage(child.stdin.?, fctx.unit_test_index, .forever, instance_id) catch |err| { + try run.step.addError("unable to write stdin: {s}", .{@errorName(err)}); + break :failed true; + }; + break :failed false; + }, + .limit => |limit| { + sendRunFuzzTestMessage(child.stdin.?, fctx.unit_test_index, .iterations, limit.amount) catch |err| { + try run.step.addError("unable to write stdin: {s}", .{@errorName(err)}); + break :failed true; + }; + break :failed false; + }, + } } else failed: { run.fuzz_tests.clearRetainingCapacity(); sendMessage(child.stdin.?, .query_test_metadata) catch |err| { @@ -1778,13 +1790,18 @@ fn evalZigTest( }, .coverage_id => { const fuzz = fuzz_context.?.fuzz; - const msg_ptr: *align(1) const u64 = @ptrCast(body); - coverage_id = msg_ptr.*; + const msg_ptr: *align(1) const [4]u64 = @ptrCast(body); + coverage_id = msg_ptr[0]; { fuzz.queue_mutex.lock(); defer fuzz.queue_mutex.unlock(); - try fuzz.msg_queue.append(fuzz.ws.gpa, .{ .coverage = .{ + try fuzz.msg_queue.append(fuzz.gpa, .{ .coverage = .{ .id = coverage_id.?, + .cumulative = .{ + .runs = msg_ptr[1], + .unique = msg_ptr[2], + .coverage = msg_ptr[3], + }, .run = run, } }); fuzz.queue_cond.signal(); @@ -1797,7 +1814,7 @@ fn evalZigTest( { fuzz.queue_mutex.lock(); defer fuzz.queue_mutex.unlock(); - try fuzz.msg_queue.append(fuzz.ws.gpa, .{ .entry_point = .{ + try fuzz.msg_queue.append(fuzz.gpa, .{ .entry_point = .{ .addr = addr, .coverage_id = coverage_id.?, } }); @@ -1900,6 +1917,22 @@ fn sendRunTestMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag, index: try file.writeAll(full_msg); } +fn sendRunFuzzTestMessage( + file: std.fs.File, + index: u32, + kind: std.Build.abi.fuzz.LimitKind, + amount_or_instance: u64, +) !void { + const header: std.zig.Client.Message.Header = .{ + .tag = .start_fuzzing, + .bytes_len = 4 + 1 + 8, + }; + const full_msg = std.mem.asBytes(&header) ++ std.mem.asBytes(&index) ++ + std.mem.asBytes(&kind) ++ std.mem.asBytes(&amount_or_instance); + + try file.writeAll(full_msg); +} + fn evalGeneric(run: *Run, child: *std.process.Child) !StdIoResult { const b = run.step.owner; const arena = b.allocator; diff --git a/lib/std/Build/WebServer.zig b/lib/std/Build/WebServer.zig index 1b3e3bfe82..8f91a8580d 100644 --- a/lib/std/Build/WebServer.zig +++ b/lib/std/Build/WebServer.zig @@ -219,12 +219,20 @@ pub fn finishBuild(ws: *WebServer, opts: struct { // Affects or affected by issues #5185, #22523, and #22464. std.process.fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)}); } + assert(ws.fuzz == null); ws.build_status.store(.fuzz_init, .monotonic); ws.notifyUpdate(); - ws.fuzz = Fuzz.init(ws) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)}); + ws.fuzz = Fuzz.init( + ws.gpa, + ws.thread_pool, + ws.all_steps, + ws.root_prog_node, + ws.ttyconf, + .{ .forever = .{ .ws = ws } }, + ) catch |err| std.process.fatal("failed to start fuzzer: {s}", .{@errorName(err)}); ws.fuzz.?.start(); } diff --git a/lib/std/Build/abi.zig b/lib/std/Build/abi.zig index 2398a8ed8d..e9482b257f 100644 --- a/lib/std/Build/abi.zig +++ b/lib/std/Build/abi.zig @@ -143,7 +143,7 @@ pub const fuzz = struct { pub extern fn fuzzer_coverage_id() u64; pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void; pub extern fn fuzzer_new_input(bytes: Slice) void; - pub extern fn fuzzer_main() void; + pub extern fn fuzzer_main(limit_kind: LimitKind, amount: u64) void; pub const Slice = extern struct { ptr: [*]const u8, @@ -158,6 +158,8 @@ pub const fuzz = struct { } }; + pub const LimitKind = enum(u8) { forever, iterations }; + /// libfuzzer uses this and its usize is the one that counts. To match the ABI, /// make the ints be the size of the target used with libfuzzer. /// diff --git a/lib/std/zig/Client.zig b/lib/std/zig/Client.zig index 345b9f9797..21168a89d7 100644 --- a/lib/std/zig/Client.zig +++ b/lib/std/zig/Client.zig @@ -33,10 +33,18 @@ pub const Message = struct { /// Ask the test runner to run a particular test. /// The message body is a u32 test index. run_test, - /// Ask the test runner to start fuzzing a particular test. - /// The message body is a u32 test index. + /// Ask the test runner to start fuzzing a particular test forever or for a given amount of time/iterations. + /// The message body is: + /// - a u32 test index. + /// - a u8 test limit kind (std.Build.api.fuzz.LimitKind) + /// - a u64 value whose meaning depends on FuzzLimitKind (either a limit amount or an instance id) start_fuzzing, _, }; + + comptime { + const std = @import("std"); + std.debug.assert(@sizeOf(std.Build.abi.fuzz.LimitKind) == 1); + } }; diff --git a/lib/std/zig/Server.zig b/lib/std/zig/Server.zig index ea60354741..c035cbdec2 100644 --- a/lib/std/zig/Server.zig +++ b/lib/std/zig/Server.zig @@ -42,9 +42,13 @@ pub const Message = struct { /// The remaining bytes is the file path relative to that prefix. /// The prefixes are hard-coded in Compilation.create (cwd, zig lib dir, local cache dir) file_system_inputs, - /// Body is a u64le that indicates the file path within the cache used - /// to store coverage information. The integer is a hash of the PCs - /// stored within that file. + /// Body is: + /// - a u64le that indicates the file path within the cache used + /// to store coverage information. The integer is a hash of the PCs + /// stored within that file. + /// - u64le of total runs accumulated + /// - u64le of unique runs accumulated + /// - u64le of coverage accumulated coverage_id, /// Body is a u64le that indicates the function pointer virtual memory /// address of the fuzz unit test. This is used to provide a starting @@ -141,9 +145,15 @@ pub fn receiveMessage(s: *Server) !InMessage.Header { return s.in.takeStruct(InMessage.Header, .little); } +pub fn receiveBody_u8(s: *Server) !u8 { + return s.in.takeInt(u8, .little); +} pub fn receiveBody_u32(s: *Server) !u32 { return s.in.takeInt(u32, .little); } +pub fn receiveBody_u64(s: *Server) !u64 { + return s.in.takeInt(u64, .little); +} pub fn serveStringMessage(s: *Server, tag: OutMessage.Tag, msg: []const u8) !void { try s.serveMessageHeader(.{ @@ -160,6 +170,7 @@ pub fn serveMessageHeader(s: *const Server, header: OutMessage.Header) !void { } pub fn serveU64Message(s: *const Server, tag: OutMessage.Tag, int: u64) !void { + assert(tag != .coverage_id); try serveMessageHeader(s, .{ .tag = tag, .bytes_len = @sizeOf(u64), @@ -168,6 +179,18 @@ pub fn serveU64Message(s: *const Server, tag: OutMessage.Tag, int: u64) !void { try s.out.flush(); } +pub fn serveCoverageIdMessage(s: *const Server, id: u64, runs: u64, unique: u64, cov: u64) !void { + try serveMessageHeader(s, .{ + .tag = .coverage_id, + .bytes_len = @sizeOf(u64) + @sizeOf(u64) + @sizeOf(u64) + @sizeOf(u64), + }); + try s.out.writeInt(u64, id, .little); + try s.out.writeInt(u64, runs, .little); + try s.out.writeInt(u64, unique, .little); + try s.out.writeInt(u64, cov, .little); + try s.out.flush(); +} + pub fn serveEmitDigest( s: *Server, digest: *const [Cache.bin_digest_len]u8, From 9bb0b43ea3ababb715a15bba8c09ba71e9c3ccc2 Mon Sep 17 00:00:00 2001 From: Loris Cro Date: Thu, 25 Sep 2025 17:02:43 +0200 Subject: [PATCH 2/5] implement review suggestions --- lib/compiler/build_runner.zig | 16 +++--- lib/compiler/test_runner.zig | 85 ++++++------------------------ lib/fuzzer.zig | 19 ++++++- lib/std/Build/abi.zig | 12 ++++- test/standalone/libfuzzer/main.zig | 2 +- 5 files changed, 56 insertions(+), 78 deletions(-) diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 4aeffe21a9..9648bdb6b1 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -280,21 +280,21 @@ pub fn main() !void { } } else if (mem.startsWith(u8, arg, "--fuzz=")) { const value = arg["--fuzz=".len..]; - if (value.len == 0) fatal("missing argument to --fuzz\n", .{}); + if (value.len == 0) fatal("missing argument to --fuzz", .{}); const unit: u8 = value[value.len - 1]; - const digits = switch (value[value.len - 1]) { + const digits = switch (unit) { '0'...'9' => value, 'K', 'M', 'G' => value[0 .. value.len - 1], else => fatal( - "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]\n", + "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]", .{}, ), }; const amount = std.fmt.parseInt(u64, digits, 10) catch { fatal( - "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]\n", + "invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]", .{}, ); }; @@ -305,7 +305,7 @@ pub fn main() !void { 'K' => 1000, 'M' => 1_000_000, 'G' => 1_000_000_000, - }) catch fatal("fuzzing limit amount overflows u64\n", .{}); + }) catch fatal("fuzzing limit amount overflows u64", .{}); fuzz = .{ .limit = .{ @@ -520,7 +520,11 @@ pub fn main() !void { }; if (run.web_server) |*web_server| { - if (fuzz) |mode| assert(mode == .forever); + if (fuzz) |mode| if (mode != .forever) fatal( + "error: limited fuzzing is not implemented yet for --webui", + .{}, + ); + web_server.finishBuild(.{ .fuzz = fuzz != null }); } diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig index 5cdeb95d18..5e7bbd294c 100644 --- a/lib/compiler/test_runner.zig +++ b/lib/compiler/test_runner.zig @@ -56,20 +56,21 @@ pub fn main() void { } } - fba.reset(); if (builtin.fuzz) { const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument"); fuzz_abi.fuzzer_init(.fromSlice(cache_dir)); } + fba.reset(); + if (listen) { - return mainServer(opt_cache_dir) catch @panic("internal test runner failure"); + return mainServer() catch @panic("internal test runner failure"); } else { return mainTerminal(); } } -fn mainServer(opt_cache_dir: ?[]const u8) !void { +fn mainServer() !void { @disableInstrumentation(); var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer); var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer); @@ -79,66 +80,14 @@ fn mainServer(opt_cache_dir: ?[]const u8) !void { .zig_version = builtin.zig_version_string, }); - if (builtin.fuzz) blk: { - const cache_dir = opt_cache_dir.?; - const coverage_id = fuzz_abi.fuzzer_coverage_id(); - const coverage_file_path: std.Build.Cache.Path = .{ - .root_dir = .{ - .path = cache_dir, - .handle = std.fs.cwd().openDir(cache_dir, .{}) catch |err| { - if (err == error.FileNotFound) { - try server.serveCoverageIdMessage(coverage_id, 0, 0, 0); - break :blk; - } - - fatal("failed to access cache dir '{s}': {s}", .{ - cache_dir, @errorName(err), - }); - }, - }, - .sub_path = "v/" ++ std.fmt.hex(coverage_id), - }; - - var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| { - if (err == error.FileNotFound) { - try server.serveCoverageIdMessage(coverage_id, 0, 0, 0); - break :blk; - } - - fatal("failed to load coverage file '{f}': {s}", .{ - coverage_file_path, @errorName(err), - }); - }; - defer coverage_file.close(); - - var rbuf: [0x1000]u8 = undefined; - var r = coverage_file.reader(&rbuf); - - var header: fuzz_abi.SeenPcsHeader = undefined; - r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| { - fatal("failed to read from coverage file '{f}': {s}", .{ - coverage_file_path, @errorName(err), - }); - }; - - if (header.pcs_len == 0) { - fatal("corrupted coverage file '{f}': pcs_len was zero", .{ - coverage_file_path, - }); - } - - var seen_count: usize = 0; - const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len); - for (0..chunk_count) |_| { - const seen = r.interface.takeInt(usize, .little) catch |err| { - fatal("failed to read from coverage file '{f}': {s}", .{ - coverage_file_path, @errorName(err), - }); - }; - seen_count += @popCount(seen); - } - - try server.serveCoverageIdMessage(coverage_id, header.n_runs, header.unique_runs, seen_count); + if (builtin.fuzz) { + const coverage = fuzz_abi.fuzzer_coverage(); + try server.serveCoverageIdMessage( + coverage.id, + coverage.runs, + coverage.unique, + coverage.seen, + ); } while (true) { @@ -235,7 +184,7 @@ fn mainServer(opt_cache_dir: ?[]const u8) !void { if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); } - std.debug.print("failed with error.{s}\n", .{@errorName(err)}); + std.debug.print("failed with error.{t}\n", .{err}); std.process.exit(1); }, }; @@ -305,11 +254,11 @@ fn mainTerminal() void { else => { fail_count += 1; if (have_tty) { - std.debug.print("{d}/{d} {s}...FAIL ({s})\n", .{ - i + 1, test_fn_list.len, test_fn.name, @errorName(err), + std.debug.print("{d}/{d} {s}...FAIL ({t})\n", .{ + i + 1, test_fn_list.len, test_fn.name, err, }); } else { - std.debug.print("FAIL ({s})\n", .{@errorName(err)}); + std.debug.print("FAIL ({t})\n", .{err}); } if (@errorReturnTrace()) |trace| { std.debug.dumpStackTrace(trace.*); @@ -450,7 +399,7 @@ pub fn fuzz( else => { std.debug.lockStdErr(); if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace.*); - std.debug.print("failed with error.{s}\n", .{@errorName(err)}); + std.debug.print("failed with error.{t}\n", .{err}); std.process.exit(1); }, }; diff --git a/lib/fuzzer.zig b/lib/fuzzer.zig index 4db2b4a2cd..72815d42cd 100644 --- a/lib/fuzzer.zig +++ b/lib/fuzzer.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); const std = @import("std"); +const fatal = std.process.fatal; const mem = std.mem; const math = std.math; const Allocator = mem.Allocator; @@ -105,6 +106,7 @@ const Executable = struct { const coverage_file_len = @sizeOf(abi.SeenPcsHeader) + pc_bitset_usizes * @sizeOf(usize) + pcs.len * @sizeOf(usize); + if (populate) { defer coverage_file.lock(.shared) catch |e| panic( "failed to demote lock for coverage file '{s}': {t}", @@ -581,8 +583,21 @@ export fn fuzzer_init(cache_dir_path: abi.Slice) void { } /// Invalid until `fuzzer_init` is called. -export fn fuzzer_coverage_id() u64 { - return exec.pc_digest; +export fn fuzzer_coverage() abi.Coverage { + const coverage_id = exec.pc_digest; + const header: *const abi.SeenPcsHeader = @ptrCast(@volatileCast(exec.shared_seen_pcs.items.ptr)); + + var seen_count: usize = 0; + for (header.seenBits()) |chunk| { + seen_count += @popCount(chunk); + } + + return .{ + .id = coverage_id, + .runs = header.n_runs, + .unique = header.unique_runs, + .seen = seen_count, + }; } /// fuzzer_init must be called beforehand diff --git a/lib/std/Build/abi.zig b/lib/std/Build/abi.zig index e9482b257f..020e2ed032 100644 --- a/lib/std/Build/abi.zig +++ b/lib/std/Build/abi.zig @@ -140,7 +140,7 @@ pub const Rebuild = extern struct { pub const fuzz = struct { pub const TestOne = *const fn (Slice) callconv(.c) void; pub extern fn fuzzer_init(cache_dir_path: Slice) void; - pub extern fn fuzzer_coverage_id() u64; + pub extern fn fuzzer_coverage() Coverage; pub extern fn fuzzer_init_test(test_one: TestOne, unit_test_name: Slice) void; pub extern fn fuzzer_new_input(bytes: Slice) void; pub extern fn fuzzer_main(limit_kind: LimitKind, amount: u64) void; @@ -253,6 +253,16 @@ pub const fuzz = struct { return .{ .locs_len_raw = @bitCast(locs_len) }; } }; + + /// Sent by lib/fuzzer to test_runner to obtain information about the + /// active memory mapped input file and cumulative stats about previous + /// fuzzing runs. + pub const Coverage = extern struct { + id: u64, + runs: u64, + unique: u64, + seen: u64, + }; }; /// ABI bits specifically relating to the time report interface. diff --git a/test/standalone/libfuzzer/main.zig b/test/standalone/libfuzzer/main.zig index ae7b9941d5..b21e9be250 100644 --- a/test/standalone/libfuzzer/main.zig +++ b/test/standalone/libfuzzer/main.zig @@ -24,7 +24,7 @@ pub fn main() !void { abi.fuzzer_new_input(.fromSlice("")); abi.fuzzer_new_input(.fromSlice("hello")); - const pc_digest = abi.fuzzer_coverage_id(); + const pc_digest = abi.fuzzer_coverage().id; const coverage_file_path = "v/" ++ std.fmt.hex(pc_digest); const coverage_file = try cache_dir.openFile(coverage_file_path, .{}); defer coverage_file.close(); From 98253bc0eee2bace0ec1689126a8dd853d296877 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 25 Sep 2025 17:14:26 -0700 Subject: [PATCH 3/5] Compilation: --debug-rt always Debug --debug-rt previously would make rt libs match the root module. Now they are always debug when --debug-rt is passed. This includes compiler-rt, fuzzer lib, and others. --- src/Compilation.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Compilation.zig b/src/Compilation.zig index bf7246b8d8..1ea5e2d652 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -8116,7 +8116,7 @@ pub fn addLinkLib(comp: *Compilation, lib_name: []const u8) !void { /// compiler-rt, libcxx, libc, libunwind, etc. pub fn compilerRtOptMode(comp: Compilation) std.builtin.OptimizeMode { if (comp.debug_compiler_runtime_libs) { - return comp.root_mod.optimize_mode; + return .Debug; } const target = &comp.root_mod.resolved_target.result; switch (comp.root_mod.optimize_mode) { From 2da8ec9865b6b086341b0f01334f27a488bc220a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 25 Sep 2025 17:16:10 -0700 Subject: [PATCH 4/5] fuzzing: fix off-by-one in limit count --- lib/fuzzer.zig | 84 +++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/fuzzer.zig b/lib/fuzzer.zig index 72815d42cd..6b7a846e4c 100644 --- a/lib/fuzzer.zig +++ b/lib/fuzzer.zig @@ -512,7 +512,7 @@ const Fuzzer = struct { self.corpus_pos = 0; const rng = self.rng.random(); - while (true) { + const m = while (true) { const m = self.mutations.items[rng.uintLessThanBiased(usize, self.mutations.items.len)]; if (!m.mutate( rng, @@ -524,53 +524,53 @@ const Fuzzer = struct { inst.const_vals8.items, inst.const_vals16.items, )) continue; + break m; + }; - self.run(); - if (inst.isFresh()) { - @branchHint(.unlikely); + self.run(); - const header = mem.bytesAsValue( - abi.SeenPcsHeader, - exec.shared_seen_pcs.items[0..@sizeOf(abi.SeenPcsHeader)], - ); - _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); + if (inst.isFresh()) { + @branchHint(.unlikely); - inst.setFresh(); - self.minimizeInput(); - inst.updateSeen(); + const header = mem.bytesAsValue( + abi.SeenPcsHeader, + exec.shared_seen_pcs.items[0..@sizeOf(abi.SeenPcsHeader)], + ); + _ = @atomicRmw(usize, &header.unique_runs, .Add, 1, .monotonic); - // An empty-input has always been tried, so if an empty input is fresh then the - // test has to be non-deterministic. This has to be checked as duplicate empty - // entries are not allowed. - if (self.input.items.len - 8 == 0) { - std.log.warn("non-deterministic test (empty input produces different hits)", .{}); - _ = @atomicRmw(usize, &header.unique_runs, .Sub, 1, .monotonic); - return; - } + inst.setFresh(); + self.minimizeInput(); + inst.updateSeen(); - const arena = self.arena_ctx.allocator(); - const bytes = arena.dupe(u8, @volatileCast(self.input.items[8..])) catch @panic("OOM"); - - self.corpus.append(gpa, bytes) catch @panic("OOM"); - self.mutations.appendNTimes(gpa, m, 6) catch @panic("OOM"); - - // Write new corpus to cache - var name_buf: [@sizeOf(usize) * 2]u8 = undefined; - self.corpus_dir.writeFile(.{ - .sub_path = std.fmt.bufPrint( - &name_buf, - "{x}", - .{self.corpus_dir_idx}, - ) catch unreachable, - .data = bytes, - }) catch |e| panic( - "failed to write corpus file '{x}': {t}", - .{ self.corpus_dir_idx, e }, - ); - self.corpus_dir_idx += 1; + // An empty-input has always been tried, so if an empty input is fresh then the + // test has to be non-deterministic. This has to be checked as duplicate empty + // entries are not allowed. + if (self.input.items.len - 8 == 0) { + std.log.warn("non-deterministic test (empty input produces different hits)", .{}); + _ = @atomicRmw(usize, &header.unique_runs, .Sub, 1, .monotonic); + return; } - break; + const arena = self.arena_ctx.allocator(); + const bytes = arena.dupe(u8, @volatileCast(self.input.items[8..])) catch @panic("OOM"); + + self.corpus.append(gpa, bytes) catch @panic("OOM"); + self.mutations.appendNTimes(gpa, m, 6) catch @panic("OOM"); + + // Write new corpus to cache + var name_buf: [@sizeOf(usize) * 2]u8 = undefined; + self.corpus_dir.writeFile(.{ + .sub_path = std.fmt.bufPrint( + &name_buf, + "{x}", + .{self.corpus_dir_idx}, + ) catch unreachable, + .data = bytes, + }) catch |e| panic( + "failed to write corpus file '{x}': {t}", + .{ self.corpus_dir_idx, e }, + ); + self.corpus_dir_idx += 1; } } }; @@ -618,7 +618,7 @@ export fn fuzzer_new_input(bytes: abi.Slice) void { export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void { switch (limit_kind) { .forever => while (true) fuzzer.cycle(), - .iterations => for (0..amount -| 1) |_| fuzzer.cycle(), + .iterations => for (0..amount) |_| fuzzer.cycle(), } } From 52a13f6a7fb0933c065348128ee3e9aecd64255b Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 25 Sep 2025 17:16:41 -0700 Subject: [PATCH 5/5] web ui: fix not sending initial context sometimes This would cause the web ui to crash in js or wasm. --- lib/std/Build/Fuzz.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig index d6c239f6ed..27c2f35d72 100644 --- a/lib/std/Build/Fuzz.zig +++ b/lib/std/Build/Fuzz.zig @@ -257,7 +257,12 @@ pub fn serveSourcesTar(fuzz: *Fuzz, req: *std.http.Server.Request) !void { pub const Previous = struct { unique_runs: usize, entry_points: usize, - pub const init: Previous = .{ .unique_runs = 0, .entry_points = 0 }; + sent_source_index: bool, + pub const init: Previous = .{ + .unique_runs = 0, + .entry_points = 0, + .sent_source_index = false, + }; }; pub fn sendUpdate( fuzz: *Fuzz, @@ -280,7 +285,8 @@ pub fn sendUpdate( const n_runs = @atomicLoad(usize, &cov_header.n_runs, .monotonic); const unique_runs = @atomicLoad(usize, &cov_header.unique_runs, .monotonic); { - if (unique_runs != 0 and prev.unique_runs == 0) { + if (!prev.sent_source_index) { + prev.sent_source_index = true; // We need to send initial context. const header: abi.SourceIndexHeader = .{ .directories_len = @intCast(coverage_map.coverage.directories.entries.len),