//! Seeded fuzz-target runner.
//!
//! `zig build fuzz -- <target> [--seed N] [--rounds N]`
//! `zig build fuzz -- --list`
//! One binary; targets are named-tuple fields dispatched through
//! `std.meta.FieldEnum`, so a module registers a new target by adding one
//! field to `targets` below. The seed is explicit (`--seed`, decimal or
//! `0x`-prefixed) or drawn from the process's csprng; either way it is
//! always printed as `reproduce with seed=0x...` before the target runs, so
//! a failure replays exactly. `--rounds` bounds the run explicitly (default
//! 10_000); an out-of-band runner sweeps higher counts.
const std = @import("std");
const delta_fuzz = @import("delta_fuzz.zig");
const artifact_fuzz = @import("artifact_fuzz.zig");
const sync_fuzz = @import("sync_fuzz.zig");
const logstore_fuzz = @import("logstore_fuzz.zig");
const transport_fuzz = @import("transport_fuzz.zig");
const serve_fuzz = @import("serve_fuzz.zig");
const merge3_fuzz = @import("merge3_fuzz.zig");
const crc32_fuzz = @import("crc32_fuzz.zig");
/// One field per fuzz target: `pub fn(allocator, seed: u64, rounds: usize) !void`.
/// The field name is the argv target name. `pub` so the registration pin at
/// the foot of this file (aggregated into `zig build test` through dst.zig)
/// can assert a target is actually reachable from the runner — a target that
/// exists but is unregistered is invisible to every invocation.
pub const targets = .{
.delta_apply_garbage = delta_fuzz.applyGarbage,
.delta_round_trip = delta_fuzz.roundTrip,
.delta_decode_int_bounded = delta_fuzz.decodeIntBounded,
.delta_target_size_garbage = delta_fuzz.targetSizeGarbage,
.delta_create_large_sizes = delta_fuzz.createLargeSizes,
.artifact_parse_garbage = artifact_fuzz.parseGarbage,
.artifact_parse_structured = artifact_fuzz.parseStructured,
.artifact_card_line = artifact_fuzz.cardLine,
.artifact_escapes = artifact_fuzz.escapes,
.artifact_timestamp = artifact_fuzz.timestamp,
.artifact_framing = artifact_fuzz.framing,
.artifact_predicates = artifact_fuzz.predicates,
.artifact_create_round_trip = artifact_fuzz.createRoundTrip,
// draft-fossil-submanifests-00: the S card / SUBMANIFEST surface, and
// the tree walk behind it. No reference implementation exists for either
// (fossil 2.28 has no submanifest support at all), so these two carry
// load the smoke lane cannot.
.artifact_submanifest_cards = artifact_fuzz.submanifestCards,
.artifact_submanifest_tree_walk = artifact_fuzz.submanifestTreeWalk,
.sync_container_round_trip = sync_fuzz.containerRoundTrip,
.sync_signal_cookie_round_trip = sync_fuzz.signalCookieRoundTrip,
.sync_parsers_adversarial = sync_fuzz.parsersAdversarial,
.sync_consume_reply_adversarial = sync_fuzz.consumeReplyAdversarial,
.logstore_open_garbage = logstore_fuzz.openGarbage,
.logstore_zlib_round_trip = logstore_fuzz.zlibRoundTrip,
.logstore_delta_round_trip = logstore_fuzz.deltaRoundTrip,
.logstore_pick_base_round_trip = logstore_fuzz.pickBaseRoundTrip,
.logstore_pack_round_trip = logstore_fuzz.packRoundTrip,
.logstore_pack_same_path_round_trip = logstore_fuzz.packSamePathRoundTrip,
.logstore_parse_config = logstore_fuzz.parseConfig,
// The two CLI-layer wire surfaces: bytes chosen by an untrusted peer.
.transport_frame_garbage = transport_fuzz.frameGarbage,
.serve_request_garbage = serve_fuzz.requestGarbage,
// The merge engine's algebraic laws: no hostile input, but the worst
// failure mode (silent corruption of merged content).
.merge3_properties = merge3_fuzz.properties,
// The checksum four on-disk formats are defined in terms of. Every other
// lane round-trips within zit and so cannot see a numerically-shifted but
// self-consistent implementation; these diff against std.hash.Crc32.
.crc32_one_shot = crc32_fuzz.oneShot,
.crc32_streaming_splits = crc32_fuzz.streamingSplits,
.crc32_scan_chunking = crc32_fuzz.scanChunking,
};
const Target = std.meta.FieldEnum(@TypeOf(targets));
const default_rounds: usize = 10_000;
/// Most targets do microsecond-scale work per round and run at `default_rounds`.
/// The two pack round-trip targets are ~100x heavier: each builds AND repacks a
/// full store every round (dozens of deflate/inflate cycles plus the best-of-N
/// `pickBase` search over the whole store), so `default_rounds` runs a single
/// seed far past the 10-minute execution cap. They get reduced defaults sized to
/// finish a seed with margin under that cap while still covering thousands of
/// distinct stores; an explicit `--rounds` still sweeps higher out of band.
///
/// `logstore_pack_round_trip` measures ~0.073 s/round in Debug (~2.4 min/seed at
/// this count). Its same-path sibling adds a per-round manifest walk (F-card
/// parse + `path → versions` grouping) on top of the same pack, making it ~4.4x
/// heavier at ~0.31 s/round; sharing the 2_000 default pushed it to ~10.3 min/
/// seed — over the cap. It therefore gets its own lower default (~5.2 min/seed,
/// ~1.9x headroom) rather than the shared one.
const pack_round_trip_default_rounds: usize = 2_000;
const pack_same_path_round_trip_default_rounds: usize = 1_000;
/// The per-target default round count applied when `--rounds` is absent.
fn defaultRoundsFor(target: Target) usize {
return switch (target) {
.logstore_pack_round_trip => pack_round_trip_default_rounds,
.logstore_pack_same_path_round_trip => pack_same_path_round_trip_default_rounds,
else => default_rounds,
};
}
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const args = try init.minimal.args.toSlice(init.arena.allocator());
if (args.len == 2 and std.mem.eql(u8, args[1], "--list")) {
var stdout_buffer: [1024]u8 = undefined;
var stdout_writer: std.Io.File.Writer = .init(.stdout(), init.io, &stdout_buffer);
const out = &stdout_writer.interface;
try writeTargets(out, "");
try out.flush();
return;
}
var stderr_buffer: [1024]u8 = undefined;
var stderr_writer: std.Io.File.Writer = .init(.stderr(), init.io, &stderr_buffer);
const err = &stderr_writer.interface;
if (args.len < 2) {
try usage(err);
try err.flush();
std.process.exit(2);
}
const target = std.meta.stringToEnum(Target, args[1]) orelse {
try err.print("fuzz: unknown target '{s}'\n\n", .{args[1]});
try usage(err);
try err.flush();
std.process.exit(2);
};
var seed: u64 = undefined;
init.io.random(std.mem.asBytes(&seed));
var rounds: usize = defaultRoundsFor(target);
try parseFlags(args[2..], &seed, &rounds);
try err.print("reproduce with seed=0x{x}\n", .{seed});
try err.flush();
switch (target) {
inline else => |tag| try @field(targets, @tagName(tag))(gpa, seed, rounds),
}
}
/// Parse `[--seed N] [--rounds N]` in any order, overwriting the defaults
/// already in `seed`/`rounds`. Each flag consumes exactly one value; an
/// unknown flag or a missing value is an explicit error, never ignored.
fn parseFlags(args: []const []const u8, seed: *u64, rounds: *usize) !void {
var i: usize = 0;
while (i < args.len) : (i += 1) {
if (std.mem.eql(u8, args[i], "--seed")) {
i += 1;
if (i >= args.len) return error.MissingSeedValue;
seed.* = try std.fmt.parseInt(u64, args[i], 0);
} else if (std.mem.eql(u8, args[i], "--rounds")) {
i += 1;
if (i >= args.len) return error.MissingRoundsValue;
rounds.* = try std.fmt.parseInt(usize, args[i], 0);
} else {
return error.UnknownArgument;
}
}
}
fn usage(w: *std.Io.Writer) !void {
try w.writeAll("usage: zig build fuzz -- <target> [--seed N] [--rounds N]\n" ++
" zig build fuzz -- --list\n\ntargets:\n");
try writeTargets(w, " ");
}
fn writeTargets(w: *std.Io.Writer, prefix: []const u8) !void {
inline for (@typeInfo(Target).@"enum".fields) |f| {
try w.print("{s}{s}\n", .{ prefix, f.name });
}
}
// ---------------------------------------------------------------------------
// Registration pin — the one test in this file. It runs in `zig build test`
// via dst.zig's aggregation, not in the fuzz binary (which has no test step).
// ---------------------------------------------------------------------------
// Every fuzz target the release bar names must be *reachable*: an unregistered
// target compiles, passes review, and is then never run by any invocation.
// This pin is structural, so the registration cannot silently rot away.
test "fuzz: every named release target is registered in the runner table" {
const required = [_][]const u8{
// P4-F6 — the two surfaces an untrusted remote peer can reach.
"transport_frame_garbage",
"serve_request_garbage",
// P4-F7 — the algebraic laws of the three-way line merge. Silent merge
// corruption is the worst VCS failure mode, and it is invisible to the
// wire-surface targets above: `merge3` never sees a byte from a peer,
// only content the repository already accepted.
"merge3_properties",
};
inline for (required) |name| {
if (!@hasField(@TypeOf(targets), name)) {
std.debug.print("fuzz target '{s}' is not registered in fuzz_tests.zig targets\n", .{name});
return error.TargetNotRegistered;
}
}
}