zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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:
Mlib/build-web/fuzz.zig | 2+-
Mlib/compiler/test_runner.zig | 23++++++++++++++++++++---
Mlib/fuzzer.zig | 62++++++++++++++++++++++++++++++++++++++++++++------------------
Mlib/std/Build/Fuzz.zig | 15+++++++--------
Mlib/std/Build/Step/Run.zig | 32++++++++++++++++++--------------
Mlib/std/Build/abi.zig | 12++++++++----
Mlib/std/zig/Client.zig | 3++-
Mtest/standalone/libfuzzer/main.zig | 4+++-
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;