Skip to content

Commit 0feacc2

Browse files
committed
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.
1 parent 26825e9 commit 0feacc2

File tree

9 files changed

+407
-73
lines changed

9 files changed

+407
-73
lines changed

lib/compiler/build_runner.zig

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ pub fn main() !void {
112112
var steps_menu = false;
113113
var output_tmp_nonce: ?[16]u8 = null;
114114
var watch = false;
115-
var fuzz = false;
115+
var fuzz: ?std.Build.Fuzz.Mode = null;
116116
var debounce_interval_ms: u16 = 50;
117117
var webui_listen: ?std.net.Address = null;
118118

@@ -274,10 +274,44 @@ pub fn main() !void {
274274
webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable;
275275
}
276276
} else if (mem.eql(u8, arg, "--fuzz")) {
277-
fuzz = true;
277+
fuzz = .{ .forever = undefined };
278278
if (webui_listen == null) {
279279
webui_listen = std.net.Address.parseIp("::1", 0) catch unreachable;
280280
}
281+
} else if (mem.startsWith(u8, arg, "--fuzz=")) {
282+
const value = arg["--fuzz=".len..];
283+
if (value.len == 0) fatal("missing argument to --fuzz\n", .{});
284+
285+
const unit: u8 = value[value.len - 1];
286+
const digits = switch (value[value.len - 1]) {
287+
'0'...'9' => value,
288+
'K', 'M', 'G' => value[0 .. value.len - 1],
289+
else => fatal(
290+
"invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]\n",
291+
.{},
292+
),
293+
};
294+
295+
const amount = std.fmt.parseInt(u64, digits, 10) catch {
296+
fatal(
297+
"invalid argument to --fuzz, expected a positive number optionally suffixed by one of: [KMG]\n",
298+
.{},
299+
);
300+
};
301+
302+
const normalized_amount = std.math.mul(u64, amount, switch (unit) {
303+
else => unreachable,
304+
'0'...'9' => 1,
305+
'K' => 1000,
306+
'M' => 1_000_000,
307+
'G' => 1_000_000_000,
308+
}) catch fatal("fuzzing limit amount overflows u64\n", .{});
309+
310+
fuzz = .{
311+
.limit = .{
312+
.amount = normalized_amount,
313+
},
314+
};
281315
} else if (mem.eql(u8, arg, "-fincremental")) {
282316
graph.incremental = true;
283317
} else if (mem.eql(u8, arg, "-fno-incremental")) {
@@ -476,6 +510,7 @@ pub fn main() !void {
476510
targets.items,
477511
main_progress_node,
478512
&run,
513+
fuzz,
479514
) catch |err| switch (err) {
480515
error.UncleanExit => {
481516
assert(!run.watch and run.web_server == null);
@@ -485,7 +520,8 @@ pub fn main() !void {
485520
};
486521

487522
if (run.web_server) |*web_server| {
488-
web_server.finishBuild(.{ .fuzz = fuzz });
523+
if (fuzz) |mode| assert(mode == .forever);
524+
web_server.finishBuild(.{ .fuzz = fuzz != null });
489525
}
490526

491527
if (!watch and run.web_server == null) {
@@ -651,6 +687,7 @@ fn runStepNames(
651687
step_names: []const []const u8,
652688
parent_prog_node: std.Progress.Node,
653689
run: *Run,
690+
fuzz: ?std.Build.Fuzz.Mode,
654691
) !void {
655692
const gpa = run.gpa;
656693
const step_stack = &run.step_stack;
@@ -676,6 +713,7 @@ fn runStepNames(
676713
});
677714
}
678715
}
716+
679717
assert(run.memory_blocked_steps.items.len == 0);
680718

681719
var test_skip_count: usize = 0;
@@ -724,6 +762,45 @@ fn runStepNames(
724762
}
725763
}
726764

765+
const ttyconf = run.ttyconf;
766+
767+
if (fuzz) |mode| blk: {
768+
switch (builtin.os.tag) {
769+
// Current implementation depends on two things that need to be ported to Windows:
770+
// * Memory-mapping to share data between the fuzzer and build runner.
771+
// * COFF/PE support added to `std.debug.Info` (it needs a batching API for resolving
772+
// many addresses to source locations).
773+
.windows => fatal("--fuzz not yet implemented for {s}", .{@tagName(builtin.os.tag)}),
774+
else => {},
775+
}
776+
if (@bitSizeOf(usize) != 64) {
777+
// Current implementation depends on posix.mmap()'s second parameter, `length: usize`,
778+
// being compatible with `std.fs.getEndPos() u64`'s return value. This is not the case
779+
// on 32-bit platforms.
780+
// Affects or affected by issues #5185, #22523, and #22464.
781+
fatal("--fuzz not yet implemented on {d}-bit platforms", .{@bitSizeOf(usize)});
782+
}
783+
784+
switch (mode) {
785+
.forever => break :blk,
786+
.limit => {},
787+
}
788+
789+
assert(mode == .limit);
790+
var f = std.Build.Fuzz.init(
791+
gpa,
792+
thread_pool,
793+
step_stack.keys(),
794+
parent_prog_node,
795+
ttyconf,
796+
mode,
797+
) catch |err| fatal("failed to start fuzzer: {s}", .{@errorName(err)});
798+
defer f.deinit();
799+
800+
f.start();
801+
f.waitAndPrintReport();
802+
}
803+
727804
// A proper command line application defaults to silently succeeding.
728805
// The user may request verbose mode if they have a different preference.
729806
const failures_only = switch (run.summary) {
@@ -737,8 +814,6 @@ fn runStepNames(
737814
std.Progress.setStatus(.failure);
738815
}
739816

740-
const ttyconf = run.ttyconf;
741-
742817
if (run.summary != .none) {
743818
const w = std.debug.lockStderrWriter(&stdio_buffer_allocation);
744819
defer std.debug.unlockStderrWriter();
@@ -1366,7 +1441,10 @@ fn printUsage(b: *std.Build, w: *Writer) !void {
13661441
\\ --watch Continuously rebuild when source files are modified
13671442
\\ --debounce <ms> Delay before rebuilding after changed file detected
13681443
\\ --webui[=ip] Enable the web interface on the given IP address
1369-
\\ --fuzz Continuously search for unit test failures (implies '--webui')
1444+
\\ --fuzz[=limit] Continuously search for unit test failures with an optional
1445+
\\ limit to the max number of iterations. The argument supports
1446+
\\ an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
1447+
\\ '--webui' when no limit is specified.
13701448
\\ --time-report Force full rebuild and provide detailed information on
13711449
\\ compilation time of Zig source code (implies '--webui')
13721450
\\ -fincremental Enable incremental compilation

lib/compiler/test_runner.zig

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const builtin = @import("builtin");
33

44
const std = @import("std");
5+
const fatal = std.process.fatal;
56
const testing = std.testing;
67
const assert = std.debug.assert;
78
const fuzz_abi = std.Build.abi.fuzz;
@@ -62,13 +63,13 @@ pub fn main() void {
6263
}
6364

6465
if (listen) {
65-
return mainServer() catch @panic("internal test runner failure");
66+
return mainServer(opt_cache_dir) catch @panic("internal test runner failure");
6667
} else {
6768
return mainTerminal();
6869
}
6970
}
7071

71-
fn mainServer() !void {
72+
fn mainServer(opt_cache_dir: ?[]const u8) !void {
7273
@disableInstrumentation();
7374
var stdin_reader = std.fs.File.stdin().readerStreaming(&stdin_buffer);
7475
var stdout_writer = std.fs.File.stdout().writerStreaming(&stdout_buffer);
@@ -78,9 +79,66 @@ fn mainServer() !void {
7879
.zig_version = builtin.zig_version_string,
7980
});
8081

81-
if (builtin.fuzz) {
82+
if (builtin.fuzz) blk: {
83+
const cache_dir = opt_cache_dir.?;
8284
const coverage_id = fuzz_abi.fuzzer_coverage_id();
83-
try server.serveU64Message(.coverage_id, coverage_id);
85+
const coverage_file_path: std.Build.Cache.Path = .{
86+
.root_dir = .{
87+
.path = cache_dir,
88+
.handle = std.fs.cwd().openDir(cache_dir, .{}) catch |err| {
89+
if (err == error.FileNotFound) {
90+
try server.serveCoverageIdMessage(coverage_id, 0, 0, 0);
91+
break :blk;
92+
}
93+
94+
fatal("failed to access cache dir '{s}': {s}", .{
95+
cache_dir, @errorName(err),
96+
});
97+
},
98+
},
99+
.sub_path = "v/" ++ std.fmt.hex(coverage_id),
100+
};
101+
102+
var coverage_file = coverage_file_path.root_dir.handle.openFile(coverage_file_path.sub_path, .{}) catch |err| {
103+
if (err == error.FileNotFound) {
104+
try server.serveCoverageIdMessage(coverage_id, 0, 0, 0);
105+
break :blk;
106+
}
107+
108+
fatal("failed to load coverage file '{f}': {s}", .{
109+
coverage_file_path, @errorName(err),
110+
});
111+
};
112+
defer coverage_file.close();
113+
114+
var rbuf: [0x1000]u8 = undefined;
115+
var r = coverage_file.reader(&rbuf);
116+
117+
var header: fuzz_abi.SeenPcsHeader = undefined;
118+
r.interface.readSliceAll(std.mem.asBytes(&header)) catch |err| {
119+
fatal("failed to read from coverage file '{f}': {s}", .{
120+
coverage_file_path, @errorName(err),
121+
});
122+
};
123+
124+
if (header.pcs_len == 0) {
125+
fatal("corrupted coverage file '{f}': pcs_len was zero", .{
126+
coverage_file_path,
127+
});
128+
}
129+
130+
var seen_count: usize = 0;
131+
const chunk_count = fuzz_abi.SeenPcsHeader.seenElemsLen(header.pcs_len);
132+
for (0..chunk_count) |_| {
133+
const seen = r.interface.takeInt(usize, .little) catch |err| {
134+
fatal("failed to read from coverage file '{f}': {s}", .{
135+
coverage_file_path, @errorName(err),
136+
});
137+
};
138+
seen_count += @popCount(seen);
139+
}
140+
141+
try server.serveCoverageIdMessage(coverage_id, header.n_runs, header.unique_runs, seen_count);
84142
}
85143

86144
while (true) {
@@ -158,13 +216,18 @@ fn mainServer() !void {
158216
if (!builtin.fuzz) unreachable;
159217

160218
const index = try server.receiveBody_u32();
219+
const mode: fuzz_abi.LimitKind = @enumFromInt(try server.receiveBody_u8());
220+
const amount_or_instance = try server.receiveBody_u64();
221+
161222
const test_fn = builtin.test_functions[index];
162223
const entry_addr = @intFromPtr(test_fn.func);
163224

164225
try server.serveU64Message(.fuzz_start_addr, entry_addr);
165226
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
166227
is_fuzz_test = false;
167228
fuzz_test_index = index;
229+
fuzz_mode = mode;
230+
fuzz_amount_or_instance = amount_or_instance;
168231

169232
test_fn.func() catch |err| switch (err) {
170233
error.SkipZigTest => return,
@@ -178,6 +241,8 @@ fn mainServer() !void {
178241
};
179242
if (!is_fuzz_test) @panic("missed call to std.testing.fuzz");
180243
if (log_err_count != 0) @panic("error logs detected");
244+
assert(mode != .forever);
245+
std.process.exit(0);
181246
},
182247

183248
else => {
@@ -343,6 +408,8 @@ pub fn mainSimple() anyerror!void {
343408

344409
var is_fuzz_test: bool = undefined;
345410
var fuzz_test_index: u32 = undefined;
411+
var fuzz_mode: fuzz_abi.LimitKind = undefined;
412+
var fuzz_amount_or_instance: u64 = undefined;
346413

347414
pub fn fuzz(
348415
context: anytype,
@@ -401,9 +468,11 @@ pub fn fuzz(
401468

402469
global.ctx = context;
403470
fuzz_abi.fuzzer_init_test(&global.test_one, .fromSlice(builtin.test_functions[fuzz_test_index].name));
471+
404472
for (options.corpus) |elem|
405473
fuzz_abi.fuzzer_new_input(.fromSlice(elem));
406-
fuzz_abi.fuzzer_main();
474+
475+
fuzz_abi.fuzzer_main(fuzz_mode, fuzz_amount_or_instance);
407476
return;
408477
}
409478

lib/fuzzer.zig

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -600,9 +600,10 @@ export fn fuzzer_new_input(bytes: abi.Slice) void {
600600
}
601601

602602
/// fuzzer_init_test must be called first
603-
export fn fuzzer_main() void {
604-
while (true) {
605-
fuzzer.cycle();
603+
export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {
604+
switch (limit_kind) {
605+
.forever => while (true) fuzzer.cycle(),
606+
.iterations => for (0..amount -| 1) |_| fuzzer.cycle(),
606607
}
607608
}
608609

0 commit comments

Comments
 (0)