Commit 0feacc2b81
Changed files (9)
lib
compiler
std
Build
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 <ms> 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
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;
}
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;
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.
///
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);
-
- // 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)});
- };
+ fuzz.prog_node = fuzz.root_prog_node.start("Fuzzing", fuzz.run_steps.len);
+
+ 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();
+ if (!fuzz.wait_group.isDone()) @panic("TODO: terminate the fuzzer processes");
fuzz.prog_node.end();
-
- const gpa = fuzz.ws.gpa;
- gpa.free(fuzz.run_steps);
+ 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.
+ \\==============================
+ \\
+ , .{});
}
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();
}
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);
+ }
};
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,
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(),
}
}