Skip to content

Commit e0dc2e4

Browse files
authored
Merge pull request #25342 from ziglang/fuzz-limit
fuzzing: implement limited fuzzing
2 parents 3b365a1 + 52a13f6 commit e0dc2e4

File tree

11 files changed

+445
-127
lines changed

11 files changed

+445
-127
lines changed

lib/compiler/build_runner.zig

Lines changed: 88 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", .{});
284+
285+
const unit: u8 = value[value.len - 1];
286+
const digits = switch (unit) {
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]",
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]",
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", .{});
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,12 @@ pub fn main() !void {
485520
};
486521

487522
if (run.web_server) |*web_server| {
488-
web_server.finishBuild(.{ .fuzz = fuzz });
523+
if (fuzz) |mode| if (mode != .forever) fatal(
524+
"error: limited fuzzing is not implemented yet for --webui",
525+
.{},
526+
);
527+
528+
web_server.finishBuild(.{ .fuzz = fuzz != null });
489529
}
490530

491531
if (!watch and run.web_server == null) {
@@ -651,6 +691,7 @@ fn runStepNames(
651691
step_names: []const []const u8,
652692
parent_prog_node: std.Progress.Node,
653693
run: *Run,
694+
fuzz: ?std.Build.Fuzz.Mode,
654695
) !void {
655696
const gpa = run.gpa;
656697
const step_stack = &run.step_stack;
@@ -676,6 +717,7 @@ fn runStepNames(
676717
});
677718
}
678719
}
720+
679721
assert(run.memory_blocked_steps.items.len == 0);
680722

681723
var test_skip_count: usize = 0;
@@ -724,6 +766,45 @@ fn runStepNames(
724766
}
725767
}
726768

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

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

lib/compiler/test_runner.zig

Lines changed: 27 additions & 9 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;
@@ -55,12 +56,13 @@ pub fn main() void {
5556
}
5657
}
5758

58-
fba.reset();
5959
if (builtin.fuzz) {
6060
const cache_dir = opt_cache_dir orelse @panic("missing --cache-dir=[path] argument");
6161
fuzz_abi.fuzzer_init(.fromSlice(cache_dir));
6262
}
6363

64+
fba.reset();
65+
6466
if (listen) {
6567
return mainServer() catch @panic("internal test runner failure");
6668
} else {
@@ -79,8 +81,13 @@ fn mainServer() !void {
7981
});
8082

8183
if (builtin.fuzz) {
82-
const coverage_id = fuzz_abi.fuzzer_coverage_id();
83-
try server.serveU64Message(.coverage_id, coverage_id);
84+
const coverage = fuzz_abi.fuzzer_coverage();
85+
try server.serveCoverageIdMessage(
86+
coverage.id,
87+
coverage.runs,
88+
coverage.unique,
89+
coverage.seen,
90+
);
8491
}
8592

8693
while (true) {
@@ -158,26 +165,33 @@ fn mainServer() !void {
158165
if (!builtin.fuzz) unreachable;
159166

160167
const index = try server.receiveBody_u32();
168+
const mode: fuzz_abi.LimitKind = @enumFromInt(try server.receiveBody_u8());
169+
const amount_or_instance = try server.receiveBody_u64();
170+
161171
const test_fn = builtin.test_functions[index];
162172
const entry_addr = @intFromPtr(test_fn.func);
163173

164174
try server.serveU64Message(.fuzz_start_addr, entry_addr);
165175
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
166176
is_fuzz_test = false;
167177
fuzz_test_index = index;
178+
fuzz_mode = mode;
179+
fuzz_amount_or_instance = amount_or_instance;
168180

169181
test_fn.func() catch |err| switch (err) {
170182
error.SkipZigTest => return,
171183
else => {
172184
if (@errorReturnTrace()) |trace| {
173185
std.debug.dumpStackTrace(trace.*);
174186
}
175-
std.debug.print("failed with error.{s}\n", .{@errorName(err)});
187+
std.debug.print("failed with error.{t}\n", .{err});
176188
std.process.exit(1);
177189
},
178190
};
179191
if (!is_fuzz_test) @panic("missed call to std.testing.fuzz");
180192
if (log_err_count != 0) @panic("error logs detected");
193+
assert(mode != .forever);
194+
std.process.exit(0);
181195
},
182196

183197
else => {
@@ -240,11 +254,11 @@ fn mainTerminal() void {
240254
else => {
241255
fail_count += 1;
242256
if (have_tty) {
243-
std.debug.print("{d}/{d} {s}...FAIL ({s})\n", .{
244-
i + 1, test_fn_list.len, test_fn.name, @errorName(err),
257+
std.debug.print("{d}/{d} {s}...FAIL ({t})\n", .{
258+
i + 1, test_fn_list.len, test_fn.name, err,
245259
});
246260
} else {
247-
std.debug.print("FAIL ({s})\n", .{@errorName(err)});
261+
std.debug.print("FAIL ({t})\n", .{err});
248262
}
249263
if (@errorReturnTrace()) |trace| {
250264
std.debug.dumpStackTrace(trace.*);
@@ -343,6 +357,8 @@ pub fn mainSimple() anyerror!void {
343357

344358
var is_fuzz_test: bool = undefined;
345359
var fuzz_test_index: u32 = undefined;
360+
var fuzz_mode: fuzz_abi.LimitKind = undefined;
361+
var fuzz_amount_or_instance: u64 = undefined;
346362

347363
pub fn fuzz(
348364
context: anytype,
@@ -383,7 +399,7 @@ pub fn fuzz(
383399
else => {
384400
std.debug.lockStdErr();
385401
if (@errorReturnTrace()) |trace| std.debug.dumpStackTrace(trace.*);
386-
std.debug.print("failed with error.{s}\n", .{@errorName(err)});
402+
std.debug.print("failed with error.{t}\n", .{err});
387403
std.process.exit(1);
388404
},
389405
};
@@ -401,9 +417,11 @@ pub fn fuzz(
401417

402418
global.ctx = context;
403419
fuzz_abi.fuzzer_init_test(&global.test_one, .fromSlice(builtin.test_functions[fuzz_test_index].name));
420+
404421
for (options.corpus) |elem|
405422
fuzz_abi.fuzzer_new_input(.fromSlice(elem));
406-
fuzz_abi.fuzzer_main();
423+
424+
fuzz_abi.fuzzer_main(fuzz_mode, fuzz_amount_or_instance);
407425
return;
408426
}
409427

0 commit comments

Comments
 (0)