commit d5bfa657c48e9d023bb789fbf8dacbcbd42f528d (tree)
parent eff332fd042a4ef556409c7650c6b74da0e30235
Author: Andrew Kelley <andrew@ziglang.org>
Date: Thu, 12 Mar 2026 20:25:02 +0100
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>
Diffstat:
8 files changed, 103 insertions(+), 50 deletions(-)
diff --git a/lib/build-web/fuzz.zig b/lib/build-web/fuzz.zig
@@ -255,7 +255,7 @@ fn unpackSourcesInner(tar_bytes: []u8) !void {
}
fn updateStats() error{OutOfMemory}!void {
- @setFloatMode(.optimized);
+ // No @setFloatMode(.optimized) since some stats may be at zero and lead to divisions by zero
if (recent_coverage_update.items.len == 0) return;
diff --git a/lib/compiler/test_runner.zig b/lib/compiler/test_runner.zig
@@ -180,7 +180,23 @@ fn mainServer(init: std.process.Init.Minimal) !void {
// since they are not present.
if (!builtin.fuzz) unreachable;
- const index = try server.receiveBody_u32();
+ const index: u32 = @intCast(index: {
+ testing.allocator_instance = .{};
+ defer if (testing.allocator_instance.deinit() == .leak) {
+ @panic("internal test runner memory leak");
+ };
+
+ const name_len = try server.receiveBody_u32();
+ const name = try server.in.readAlloc(testing.allocator, @intCast(name_len));
+ defer testing.allocator.free(name);
+ for (0.., builtin.test_functions) |i, test_fn| {
+ if (std.mem.eql(u8, name, test_fn.name)) {
+ break :index i;
+ }
+ } else {
+ std.debug.panic("fuzz test {s} no longer exists", .{name});
+ }
+ });
const mode: fuzz_abi.LimitKind = @enumFromInt(try server.receiveBody_u8());
const amount_or_instance = try server.receiveBody_u64();
@@ -406,13 +422,13 @@ pub fn fuzz(
const global = struct {
var ctx: @TypeOf(context) = undefined;
- fn test_one() callconv(.c) void {
+ fn test_one() callconv(.c) bool {
@disableInstrumentation();
testing.allocator_instance = .{};
defer if (testing.allocator_instance.deinit() == .leak) std.process.exit(1);
log_err_count = 0;
testOne(ctx, @constCast(&testing.Smith{ .in = null })) catch |err| switch (err) {
- error.SkipZigTest => return,
+ error.SkipZigTest => return true,
else => {
const stderr = std.debug.lockStderr(&.{}).terminal();
p: {
@@ -429,6 +445,7 @@ pub fn fuzz(
stderr.writer.print("error logs detected\n", .{}) catch {};
std.process.exit(1);
}
+ return false;
}
};
if (builtin.fuzz) {
diff --git a/lib/fuzzer.zig b/lib/fuzzer.zig
@@ -686,7 +686,7 @@ const Fuzzer = struct {
const len = mem.readInt(u32, f.mmap_input.mmap.memory[0..4], .little);
if (len < f.mmap_input.mmap.memory[4..].len) {
f.mmap_input.len = len;
- f.runBytes(f.mmap_input.inputSlice(), .bytes_dry);
+ _ = f.runBytes(f.mmap_input.inputSlice(), .bytes_dry);
f.mmap_input.clearRetainingCapacity();
}
}
@@ -703,7 +703,7 @@ const Fuzzer = struct {
else => panic("failed to read corpus file '{s}': {t}", .{ name, e }),
};
defer gpa.free(bytes);
- f.newInput(bytes, false);
+ f.newInputExternal(bytes);
}
f.corpus_pos = @enumFromInt(0);
}
@@ -761,12 +761,13 @@ const Fuzzer = struct {
return fresh;
}
- fn runBytes(f: *Fuzzer, bytes: []const u8, mode: Input.Index) void {
+ /// Returns if `error.SkipZigTest` was indicated
+ fn runBytes(f: *Fuzzer, bytes: []const u8, mode: Input.Index) bool {
assert(mode == .bytes_dry or mode == .bytes_fresh);
f.bytes_input = .{ .in = bytes };
f.corpus_pos = mode;
- f.run(0); // 0 since `f.uid_data` is unused
+ return f.run(0); // 0 since `f.uid_data` is unused
}
fn updateSeenPcs(f: *Fuzzer) void {
@@ -861,8 +862,21 @@ const Fuzzer = struct {
}
}
- pub fn newInput(f: *Fuzzer, bytes: []const u8, modify_fs_corpus: bool) void {
- f.runBytes(bytes, .bytes_fresh);
+ pub fn newInputExternal(f: *Fuzzer, bytes: []const u8) void {
+ // All inputs including the corpus are required to go through the memory
+ // mapped input in case they cause a crash so they can be identified.
+ f.mmap_input.appendSlice(bytes);
+ f.newInput(false);
+ f.mmap_input.clearRetainingCapacity();
+ }
+
+ fn newInput(f: *Fuzzer, modify_fs_corpus: bool) void {
+ const bytes = f.mmap_input.inputSlice();
+ // `error.SkipZigTest` here can be from one of these causes:
+ // * The test has changed and a previous corpus input is being used
+ // * An input provided by the test results in it
+ // * The test is non-deterministic
+ if (f.runBytes(bytes, .bytes_fresh)) return;
f.req_values = f.input_builder.total_ints + f.input_builder.total_bytes;
f.req_bytes = @intCast(f.input_builder.bytes_table.items.len);
var input = f.input_builder.build();
@@ -996,15 +1010,17 @@ const Fuzzer = struct {
panic("failed to write corpus file '{s}': {t}", .{ name, e });
}
- fn run(f: *Fuzzer, input_uids: usize) void {
+ /// Returns if `error.SkipZigTest` was indicated
+ fn run(f: *Fuzzer, input_uids: usize) bool {
@memset(exec.pc_counters, 0);
f.uid_data_i.items.len = input_uids;
@memset(f.uid_data_i.items, 0);
f.req_values = 0;
f.req_bytes = 0;
- f.test_one();
+ const skip = f.test_one();
_ = @atomicRmw(usize, &exec.seenPcsHeader().n_runs, .Add, 1, .monotonic);
+ return skip;
}
/// Returns a number of mutations to perform from 1-4
@@ -1076,12 +1092,12 @@ const Fuzzer = struct {
i.* = data.order[order_i];
};
- f.run(data.uid_slices.entries.len);
- if (f.isFresh()) {
+ const skip = f.run(data.uid_slices.entries.len);
+ if (!skip and f.isFresh()) {
@branchHint(.unlikely);
_ = @atomicRmw(usize, &exec.seenPcsHeader().unique_runs, .Add, 1, .monotonic);
- f.newInput(f.mmap_input.inputSlice(), true);
+ f.newInput(true);
}
f.mmap_input.clearRetainingCapacity();
@@ -1320,13 +1336,23 @@ const Fuzzer = struct {
if (opts.copy != 0) {
if (opts.fresh == 0 or slice_i == data_slice.len) return .fresh;
- return .{ .mutate = switch (uid.kind) {
- .int => .{ .int = data.ints[data_i] },
- .bytes => .{ .bytes = b: {
+ switch (uid.kind) {
+ .int => {
+ const int = data.ints[data_i];
+ if (weightsContain(int, weights)) {
+ @branchHint(.likely);
+ return .{ .mutate = .{ .int = int } };
+ }
+ },
+ .bytes => {
const entry = data.bytes.entries[data_i];
- break :b data.bytes.table[entry.off..][0..entry.len];
- } },
- } };
+ const bytes = data.bytes.table[entry.off..][0..entry.len];
+ if (weightsContainBytes(bytes, weights)) {
+ @branchHint(.likely);
+ return .{ .mutate = .{ .bytes = bytes } };
+ }
+ },
+ }
}
if (!opts.splice) {
@@ -1665,7 +1691,7 @@ export fn fuzzer_set_test(test_one: abi.TestOne, unit_test_name: abi.Slice) void
export fn fuzzer_new_input(bytes: abi.Slice) void {
if (bytes.len == 0) return; // An entry of length zero is always present
- fuzzer.newInput(bytes.toSlice(), false);
+ fuzzer.newInputExternal(bytes.toSlice());
}
export fn fuzzer_main(limit_kind: abi.LimitKind, amount: u64) void {
diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig
@@ -145,9 +145,9 @@ pub fn start(fuzz: *Fuzz) void {
}
for (fuzz.run_steps) |run| {
- for (run.fuzz_tests.items) |unit_test_index| {
+ for (run.fuzz_tests.items) |unit_test_name| {
assert(run.rebuilt_executable != null);
- fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run, unit_test_index });
+ fuzz.group.async(io, fuzzWorkerRun, .{ fuzz, run, unit_test_name });
}
}
}
@@ -193,17 +193,16 @@ fn rebuildTestsWorkerRunFallible(run: *Step.Run, gpa: Allocator, parent_prog_nod
run.rebuilt_executable = try rebuilt_bin_path.join(gpa, compile.out_filename);
}
-fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run, unit_test_index: u32) void {
+fn fuzzWorkerRun(fuzz: *Fuzz, run: *Step.Run, unit_test_name: []const u8) void {
const owner = run.step.owner;
const gpa = owner.allocator;
const graph = owner.graph;
const io = graph.io;
- const test_name = run.cached_test_metadata.?.testName(unit_test_index);
- const prog_node = fuzz.prog_node.start(test_name, 0);
+ const prog_node = fuzz.prog_node.start(unit_test_name, 0);
defer prog_node.end();
- run.rerunInFuzzMode(fuzz, unit_test_index, prog_node) catch |err| switch (err) {
+ run.rerunInFuzzMode(fuzz, unit_test_name, prog_node) catch |err| switch (err) {
error.MakeFailed => {
var buf: [256]u8 = undefined;
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 {
return;
},
else => {
- log.err("step '{s}': failed to rerun '{s}' in fuzz mode: {t}", .{ run.step.name, test_name, err });
+ log.err("step '{s}': failed to rerun '{s}' in fuzz mode: {t}", .{ run.step.name, unit_test_name, err });
return;
},
};
@@ -588,7 +587,7 @@ pub fn waitAndPrintReport(fuzz: *Fuzz) Io.Cancelable!void {
\\
, .{
cov.run.step.name,
- cov.run.cached_test_metadata.?.testName(cov.run.fuzz_tests.items[0]),
+ cov.run.fuzz_tests.items[0],
cov.id,
cov.cumulative.runs,
header.n_runs,
diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig
@@ -88,9 +88,10 @@ dep_output_file: ?*Output,
has_side_effects: bool,
-/// If this is a Zig unit test binary, this tracks the indexes of the unit
-/// tests that are also fuzz tests.
-fuzz_tests: std.ArrayList(u32),
+/// If this is a Zig unit test binary, this tracks the names of the unit
+/// tests that are also fuzz tests. Indexes cannot be used as they may
+/// change between reruns.
+fuzz_tests: std.ArrayList([]const u8),
cached_test_metadata: ?CachedTestMetadata = null,
/// 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 {
pub fn rerunInFuzzMode(
run: *Run,
fuzz: *std.Build.Fuzz,
- unit_test_index: u32,
+ unit_test_name: []const u8,
prog_node: std.Progress.Node,
) !void {
const step = &run.step;
@@ -1138,7 +1139,7 @@ pub fn rerunInFuzzMode(
.unit_test_timeout_ns = null, // don't time out fuzz tests for now
.gpa = fuzz.gpa,
}, .{
- .unit_test_index = unit_test_index,
+ .unit_test_name = unit_test_name,
.fuzz = fuzz,
});
}
@@ -1210,7 +1211,7 @@ fn termMatches(expected: ?process.Child.Term, actual: process.Child.Term) bool {
const FuzzContext = struct {
fuzz: *std.Build.Fuzz,
- unit_test_index: u32,
+ unit_test_name: []const u8,
};
fn runCommand(
@@ -1843,7 +1844,7 @@ fn waitZigTest(
sendRunFuzzTestMessage(
io,
child.stdin.?,
- ctx.unit_test_index,
+ ctx.unit_test_name,
.forever,
0, // instance ID; will be used by multiprocess forever fuzzing in the future
) catch |err| return .{ .write_failed = err };
@@ -1852,7 +1853,7 @@ fn waitZigTest(
sendRunFuzzTestMessage(
io,
child.stdin.?,
- ctx.unit_test_index,
+ ctx.unit_test_name,
.iterations,
limit.amount,
) catch |err| return .{ .write_failed = err };
@@ -2001,10 +2002,10 @@ fn waitZigTest(
results.leak_count +|= leak_count;
results.log_err_count +|= log_err_count;
- if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, tr_hdr.index);
+ if (tr_hdr.flags.fuzz) try run.fuzz_tests.append(gpa, md.testName(tr_hdr.index));
if (tr_hdr.flags.status == .fail) {
- const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
+ const name = md.testName(tr_hdr.index);
const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
stderr.tossBuffered();
if (stderr_bytes.len == 0) {
@@ -2013,12 +2014,12 @@ fn waitZigTest(
try run.step.addError("'{s}' failed:\n{s}", .{ name, stderr_bytes });
}
} else if (leak_count > 0) {
- const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
+ const name = md.testName(tr_hdr.index);
const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
stderr.tossBuffered();
try run.step.addError("'{s}' leaked {d} allocations:\n{s}", .{ name, leak_count, stderr_bytes });
} else if (log_err_count > 0) {
- const name = std.mem.sliceTo(md.testName(tr_hdr.index), 0);
+ const name = md.testName(tr_hdr.index);
const stderr_bytes = std.mem.trim(u8, stderr.buffered(), "\n");
stderr.tossBuffered();
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
fn sendRunFuzzTestMessage(
io: Io,
file: Io.File,
- index: u32,
+ test_name: []const u8,
kind: std.Build.abi.fuzz.LimitKind,
amount_or_instance: u64,
) !void {
@@ -2160,7 +2161,10 @@ fn sendRunFuzzTestMessage(
w.interface.writeStruct(header, .little) catch |err| switch (err) {
error.WriteFailed => return w.err.?,
};
- w.interface.writeInt(u32, index, .little) catch |err| switch (err) {
+ w.interface.writeInt(u32, @intCast(test_name.len), .little) catch |err| switch (err) {
+ error.WriteFailed => return w.err.?,
+ };
+ w.interface.writeAll(test_name) catch |err| switch (err) {
error.WriteFailed => return w.err.?,
};
w.interface.writeByte(@intFromEnum(kind)) catch |err| switch (err) {
diff --git a/lib/std/Build/abi.zig b/lib/std/Build/abi.zig
@@ -139,7 +139,8 @@ pub const Rebuild = extern struct {
/// ABI bits specifically relating to the fuzzer interface.
pub const fuzz = struct {
- pub const TestOne = *const fn () callconv(.c) void;
+ /// Returns if `error.SkipZigTest` was indicated
+ pub const TestOne = *const fn () callconv(.c) bool;
/// A unique value to identify the related requests across runs
pub const Uid = packed struct(u32) {
@@ -249,9 +250,12 @@ pub const fuzz = struct {
}
// Reject types that don't have a fixed bitsize (esp. usize)
// since they are not gauraunteed to fit in a u64 across targets.
- if (std.mem.indexOfScalar(type, &.{
- usize, c_char, c_ushort, c_uint, c_ulong, c_ulonglong,
- }, T) != null) {
+ //
+ // std.mem.indexOfScalar is not used to avoid backward branches
+ // and preserve the eval branch quota.
+ if (T == usize or T == c_char or T == c_ushort or
+ T == c_uint or T == c_ulong or T == c_ulonglong)
+ {
@compileError("type does not have a fixed bitsize: " ++ @typeName(T));
}
}
diff --git a/lib/std/zig/Client.zig b/lib/std/zig/Client.zig
@@ -35,7 +35,8 @@ pub const Message = struct {
run_test,
/// 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 u32 test name len.
+ /// - a test name with the above length
/// - 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,
diff --git a/test/standalone/libfuzzer/main.zig b/test/standalone/libfuzzer/main.zig
@@ -2,7 +2,9 @@ const std = @import("std");
const abi = std.Build.abi.fuzz;
const native_endian = @import("builtin").cpu.arch.endian();
-fn testOne() callconv(.c) void {}
+fn testOne() callconv(.c) bool {
+ return false;
+}
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;