authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-03-12 20:25:02+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-03-12 20:25:02+01:00
logd5bfa657c48e9d023bb789fbf8dacbcbd42f528d
treeb5934c60fcecd60f3710594c94d0f26cbcf20ab4
parenteff332fd042a4ef556409c7650c6b74da0e30235
parent9d1e47d220a50fc5db4b622bd86e2fe51673c163

Merge pull request 'fix several fuzzing bugs' (#31470) from gooncreeper/zig:fuzzing-fixes into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/31470 Reviewed-by: Andrew Kelley <andrew@ziglang.org>

8 files changed, 103 insertions(+), 50 deletions(-)

lib/build-web/fuzz.zig+1-1
......@@ -255,7 +255,7 @@ fn unpackSourcesInner(tar_bytes: []u8) !void {
255255}
256256
257257fn updateStats() error{OutOfMemory}!void {
258 @setFloatMode(.optimized);
258 // No @setFloatMode(.optimized) since some stats may be at zero and lead to divisions by zero
259259
260260 if (recent_coverage_update.items.len == 0) return;
261261
lib/compiler/test_runner.zig+20-3
......@@ -180,7 +180,23 @@ fn mainServer(init: std.process.Init.Minimal) !void {
180180 // since they are not present.
181181 if (!builtin.fuzz) unreachable;
182182
183 const index = try server.receiveBody_u32();
183 const index: u32 = @intCast(index: {
184 testing.allocator_instance = .{};
185 defer if (testing.allocator_instance.deinit() == .leak) {
186 @panic("internal test runner memory leak");
187 };
188
189 const name_len = try server.receiveBody_u32();
190 const name = try server.in.readAlloc(testing.allocator, @intCast(name_len));
191 defer testing.allocator.free(name);
192 for (0.., builtin.test_functions) |i, test_fn| {
193 if (std.mem.eql(u8, name, test_fn.name)) {
194 break :index i;
195 }
196 } else {
197 std.debug.panic("fuzz test {s} no longer exists", .{name});
198 }
199 });
184200 const mode: fuzz_abi.LimitKind = @enumFromInt(try server.receiveBody_u8());
185201 const amount_or_instance = try server.receiveBody_u64();
186202
......@@ -406,13 +422,13 @@ pub fn fuzz(
406422 const global = struct {
407423 var ctx: @TypeOf(context) = undefined;
408424
409 fn test_one() callconv(.c) void {
425 fn test_one() callconv(.c) bool {
410426 @disableInstrumentation();
411427 testing.allocator_instance = .{};
412428 defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
413429 log_err_count = 0;
414430 testOne(ctx, @constCast(&testing.Smith{ .in = null })) catch |err| switch (err) {
415 error.SkipZigTest => return,
431 error.SkipZigTest => return true,
416432 else => {
417433 const stderr = std.debug.lockStderr(&.{}).terminal();
418434 p: {
......@@ -429,6 +445,7 @@ pub fn fuzz(
429445 stderr.writer.print("error logs detected\n", .{}) catch {};
430446 std.process.exit(1);
431447 }
448 return false;
432449 }
433450 };
434451 if (builtin.fuzz) {
lib/fuzzer.zig+44-18
......@@ -686,7 +686,7 @@ const Fuzzer = struct {
686686 const len = mem.readInt(u32, f.mmap_input.mmap.memory[0..4], .little);
687687 if (len < f.mmap_input.mmap.memory[4..].len) {
688688 f.mmap_input.len = len;
689 f.runBytes(f.mmap_input.inputSlice(), .bytes_dry);
689 _ = f.runBytes(f.mmap_input.inputSlice(), .bytes_dry);
690690 f.mmap_input.clearRetainingCapacity();
691691 }
692692 }
......@@ -703,7 +703,7 @@ const Fuzzer = struct {
703703 else => panic("failed to read corpus file '{s}': {t}", .{ name, e }),
704704 };
705705 defer gpa.free(bytes);
706 f.newInput(bytes, false);
706 f.newInputExternal(bytes);
707707 }
708708 f.corpus_pos = @enumFromInt(0);
709709 }
......@@ -761,12 +761,13 @@ const Fuzzer = struct {
761761 return fresh;
762762 }
763763
764 fn runBytes(f: *Fuzzer, bytes: []const u8, mode: Input.Index) void {
764 /// Returns if `error.SkipZigTest` was indicated
765 fn runBytes(f: *Fuzzer, bytes: []const u8, mode: Input.Index) bool {
765766 assert(mode == .bytes_dry or mode == .bytes_fresh);
766767
767768 f.bytes_input = .{ .in = bytes };
768769 f.corpus_pos = mode;
769 f.run(0); // 0 since `f.uid_data` is unused
770 return f.run(0); // 0 since `f.uid_data` is unused
770771 }
771772
772773 fn updateSeenPcs(f: *Fuzzer) void {
......@@ -861,8 +862,21 @@ const Fuzzer = struct {
861862 }
862863 }
863864
864 pub fn newInput(f: *Fuzzer, bytes: []const u8, modify_fs_corpus: bool) void {
865 f.runBytes(bytes, .bytes_fresh);
865 pub fn newInputExternal(f: *Fuzzer, bytes: []const u8) void {
866 // All inputs including the corpus are required to go through the memory
867 // mapped input in case they cause a crash so they can be identified.
868 f.mmap_input.appendSlice(bytes);
869 f.newInput(false);
870 f.mmap_input.clearRetainingCapacity();
871 }
872
873 fn newInput(f: *Fuzzer, modify_fs_corpus: bool) void {
874 const bytes = f.mmap_input.inputSlice();
875 // `error.SkipZigTest` here can be from one of these causes:
876 // * The test has changed and a previous corpus input is being used
877 // * An input provided by the test results in it
878 // * The test is non-deterministic
879 if (f.runBytes(bytes, .bytes_fresh)) return;
866880 f.req_values = f.input_builder.total_ints + f.input_builder.total_bytes;
867881 f.req_bytes = @intCast(f.input_builder.bytes_table.items.len);
868882 var input = f.input_builder.build();
......@@ -996,15 +1010,17 @@ const Fuzzer = struct {
9961010 panic("failed to write corpus file '{s}': {t}", .{ name, e });
9971011 }
9981012
999 fn run(f: *Fuzzer, input_uids: usize) void {
1013 /// Returns if `error.SkipZigTest` was indicated
1014 fn run(f: *Fuzzer, input_uids: usize) bool {
10001015 @memset(exec.pc_counters, 0);
10011016 f.uid_data_i.items.len = input_uids;
10021017 @memset(f.uid_data_i.items, 0);
10031018 f.req_values = 0;
10041019 f.req_bytes = 0;
10051020
1006 f.test_one();
1021 const skip = f.test_one();
10071022 _ = @atomicRmw(usize, &exec.seenPcsHeader().n_runs, .Add, 1, .monotonic);
1023 return skip;
10081024 }
10091025
10101026 /// Returns a number of mutations to perform from 1-4
......@@ -1076,12 +1092,12 @@ const Fuzzer = struct {
10761092 i.* = data.order[order_i];
10771093 };
10781094
1079 f.run(data.uid_slices.entries.len);
1080 if (f.isFresh()) {
1095 const skip = f.run(data.uid_slices.entries.len);
1096 if (!skip and f.isFresh()) {
10811097 @branchHint(.unlikely);
10821098
10831099 _ = @atomicRmw(usize, &exec.seenPcsHeader().unique_runs, .Add, 1, .monotonic);
1084 f.newInput(f.mmap_input.inputSlice(), true);
1100 f.newInput(true);
10851101 }
10861102 f.mmap_input.clearRetainingCapacity();
10871103
......@@ -1320,13 +1336,23 @@ const Fuzzer = struct {
13201336
13211337 if (opts.copy != 0) {
13221338 if (opts.fresh == 0 or slice_i == data_slice.len) return .fresh;
1323 return .{ .mutate = switch (uid.kind) {
1324 .int => .{ .int = data.ints[data_i] },
1325 .bytes => .{ .bytes = b: {
1339 switch (uid.kind) {
1340 .int => {
1341 const int = data.ints[data_i];
1342 if (weightsContain(int, weights)) {
1343 @branchHint(.likely);
1344 return .{ .mutate = .{ .int = int } };
1345 }
1346 },
1347 .bytes => {
13261348 const entry = data.bytes.entries[data_i];
1327 break :b data.bytes.table[entry.off..][0..entry.len];
1328 } },
1329 } };
1349 const bytes = data.bytes.table[entry.off..][0..entry.len];
1350 if (weightsContainBytes(bytes, weights)) {
1351 @branchHint(.likely);
1352 return .{ .mutate = .{ .bytes = bytes } };
1353 }
1354 },
1355 }
13301356 }
13311357
13321358 if (!opts.splice) {
......@@ -1665,7 +1691,7 @@ export fn fuzzer_set_test(test_one: abi.TestOne, unit_test_name: abi.Slice) void
16651691
16661692export fn fuzzer_new_input(bytes: abi.Slice) void {
16671693 if (bytes.len == 0) return; // An entry of length zero is always present
1668 fuzzer.newInput(bytes.toSlice(), false);
1694 fuzzer.newInputExternal(bytes.toSlice());
16691695}
16701696
16711697export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {
lib/std/Build/Fuzz.zig+7-8
......@@ -145,9 +145,9 @@ pub fn start(fuzz: *Fuzz) void {
145145 }
146146
147147 for (fuzz.run_steps) |run| {
148 for (run.fuzz_tests.items) |unit_test_index| {
148 for (run.fuzz_tests.items) |unit_test_name| {
149149 assert(run.rebuilt_executable != null);
150 fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run, unit_test_index });
150 fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run, unit_test_name });
151151 }
152152 }
153153}
......@@ -193,17 +193,16 @@ fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_nod
193193 run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename);
194194}
195195
196fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run, unit_test_index: u32) void {
196fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run, unit_test_name: []const u8) void {
197197 const owner = run.step.owner;
198198 const gpa = owner.allocator;
199199 const graph = owner.graph;
200200 const io = graph.io;
201 const test_name = run.cached_test_metadata.?.testName(unit_test_index);
202201
203 const prog_node = fuzz.prog_node.start(test_name, 0);
202 const prog_node = fuzz.prog_node.start(unit_test_name, 0);
204203 defer prog_node.end();
205204
206 run.rerunInFuzzMode(fuzz, unit_test_index, prog_node) catch |err| switch (err) {
205 run.rerunInFuzzMode(fuzz, unit_test_name, prog_node) catch |err| switch (err) {
207206 error.MakeFailed => {
208207 var buf: [256]u8 = undefined;
209208 const stderr = io.lockStderr(&buf, graph.stderr_mode) catch |e| switch (e) {
......@@ -214,7 +213,7 @@ fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run, unit_test_index: u32) void {
214213 return;
215214 },
216215 else => {
217 log.err("step '{s}': failed to rerun '{s}' in fuzz mode: {t}", .{ run.step.name, test_name, err });
216 log.err("step '{s}': failed to rerun '{s}' in fuzz mode: {t}", .{ run.step.name, unit_test_name, err });
218217 return;
219218 },
220219 };
......@@ -588,7 +587,7 @@ pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void {
588587 \\
589588 , .{
590589 cov.run.step.name,
591 cov.run.cached_test_metadata.?.testName(cov.run.fuzz_tests.items[0]),
590 cov.run.fuzz_tests.items[0],
592591 cov.id,
593592 cov.cumulative.runs,
594593 header.n_runs,
lib/std/Build/Step/Run.zig+18-14
......@@ -88,9 +88,10 @@ dep_output_file: ?*Output,
8888
8989has_side_effects: bool,
9090
91/// If this is a Zig unit test binary, this tracks the indexes of the unit
92/// tests that are also fuzz tests.
93fuzz_tests: std.ArrayList(u32),
91/// If this is a Zig unit test binary, this tracks the names of the unit
92/// tests that are also fuzz tests. Indexes cannot be used as they may
93/// change between reruns.
94fuzz_tests: std.ArrayList([]const u8),
9495cached_test_metadata: ?CachedTestMetadata = null,
9596
9697/// Populated during the fuzz phase if this run step corresponds to a unit test
......@@ -1067,7 +1068,7 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
10671068pub fn rerunInFuzzMode(
10681069 run: *Run,
10691070 fuzz: *std.Build.Fuzz,
1070 unit_test_index: u32,
1071 unit_test_name: []const u8,
10711072 prog_node: std.Progress.Node,
10721073) !void {
10731074 const step = &run.step;
......@@ -1138,7 +1139,7 @@ pub fn rerunInFuzzMode(
11381139 .unit_test_timeout_ns = null, // don't time out fuzz tests for now
11391140 .gpa = fuzz.gpa,
11401141 }, .{
1141 .unit_test_index = unit_test_index,
1142 .unit_test_name = unit_test_name,
11421143 .fuzz = fuzz,
11431144 });
11441145}
......@@ -1210,7 +1211,7 @@ fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool {
12101211
12111212const FuzzContext = struct {
12121213 fuzz: *std.Build.Fuzz,
1213 unit_test_index: u32,
1214 unit_test_name: []const u8,
12141215};
12151216
12161217fn runCommand(
......@@ -1843,7 +1844,7 @@ fn waitZigTest(
18431844 sendRunFuzzTestMessage(
18441845 io,
18451846 child.stdin.?,
1846 ctx.unit_test_index,
1847 ctx.unit_test_name,
18471848 .forever,
18481849 0, // instance ID; will be used by multiprocess forever fuzzing in the future
18491850 ) catch |err| return .{ .write_failed = err };
......@@ -1852,7 +1853,7 @@ fn waitZigTest(
18521853 sendRunFuzzTestMessage(
18531854 io,
18541855 child.stdin.?,
1855 ctx.unit_test_index,
1856 ctx.unit_test_name,
18561857 .iterations,
18571858 limit.amount,
18581859 ) catch |err| return .{ .write_failed = err };
......@@ -2001,10 +2002,10 @@ fn waitZigTest(
20012002 results.leak_count +|= leak_count;
20022003 results.log_err_count +|= log_err_count;
20032004
2004 if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, tr_hdr.index);
2005 if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index));
20052006
20062007 if (tr_hdr.flags.status == .fail) {
2007 const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
2008 const name = md.testName(tr_hdr.index);
20082009 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
20092010 stderr.tossBuffered();
20102011 if (stderr_bytes.len == 0) {
......@@ -2013,12 +2014,12 @@ fn waitZigTest(
20132014 try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes });
20142015 }
20152016 } else if (leak_count > 0) {
2016 const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
2017 const name = md.testName(tr_hdr.index);
20172018 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
20182019 stderr.tossBuffered();
20192020 try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
20202021 } else if (log_err_count > 0) {
2021 const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
2022 const name = md.testName(tr_hdr.index);
20222023 const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
20232024 stderr.tossBuffered();
20242025 try run.step.addError("'{s}' logged {d} errors:\n{s}", .{ name, log_err_count, stderr_bytes });
......@@ -2148,7 +2149,7 @@ fn sendRunTestMessage(io: Io, file: Io.File, tag: std.zig.Client.Message.Tag, in
21482149fn sendRunFuzzTestMessage(
21492150 io: Io,
21502151 file: Io.File,
2151 index: u32,
2152 test_name: []const u8,
21522153 kind: std.Build.abi.fuzz.LimitKind,
21532154 amount_or_instance: u64,
21542155) !void {
......@@ -2160,7 +2161,10 @@ fn sendRunFuzzTestMessage(
21602161 w.interface.writeStruct(header, .little) catch |err| switch (err) {
21612162 error.WriteFailed => return w.err.?,
21622163 };
2163 w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
2164 w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) {
2165 error.WriteFailed => return w.err.?,
2166 };
2167 w.interface.writeAll(test_name) catch |err| switch (err) {
21642168 error.WriteFailed => return w.err.?,
21652169 };
21662170 w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) {
lib/std/Build/abi.zig+8-4
......@@ -139,7 +139,8 @@ pub const Rebuild = extern struct {
139139
140140/// ABI bits specifically relating to the fuzzer interface.
141141pub const fuzz = struct {
142 pub const TestOne = *const fn () callconv(.c) void;
142 /// Returns if `error.SkipZigTest` was indicated
143 pub const TestOne = *const fn () callconv(.c) bool;
143144
144145 /// A unique value to identify the related requests across runs
145146 pub const Uid = packed struct(u32) {
......@@ -249,9 +250,12 @@ pub const fuzz = struct {
249250 }
250251 // Reject types that don't have a fixed bitsize (esp. usize)
251252 // since they are not gauraunteed to fit in a u64 across targets.
252 if (std.mem.indexOfScalar(type, &.{
253 usize, c_char, c_ushort, c_uint, c_ulong, c_ulonglong,
254 }, T) != null) {
253 //
254 // std.mem.indexOfScalar is not used to avoid backward branches
255 // and preserve the eval branch quota.
256 if (T == usize or T == c_char or T == c_ushort or
257 T == c_uint or T == c_ulong or T == c_ulonglong)
258 {
255259 @compileError("type does not have a fixed bitsize: " ++ @typeName(T));
256260 }
257261 }
lib/std/zig/Client.zig+2-1
......@@ -35,7 +35,8 @@ pub const Message = struct {
3535 run_test,
3636 /// Ask the test runner to start fuzzing a particular test forever or for a given amount of time/iterations.
3737 /// The message body is:
38 /// - a u32 test index.
38 /// - a u32 test name len.
39 /// - a test name with the above length
3940 /// - a u8 test limit kind (std.Build.api.fuzz.LimitKind)
4041 /// - a u64 value whose meaning depends on FuzzLimitKind (either a limit amount or an instance id)
4142 start_fuzzing,
test/standalone/libfuzzer/main.zig+3-1
......@@ -2,7 +2,9 @@ const std = @import("std");
22const abi = std.Build.abi.fuzz;
33const native_endian = @import("builtin").cpu.arch.endian();
44
5fn testOne() callconv(.c) void {}
5fn testOne() callconv(.c) bool {
6 return false;
7}
68
79pub fn main(init: std.process.Init) !void {
810 const gpa = init.gpa;