Artifact 82ab1b4a0125e3171217ca0b84ffb6276b364aa16d08120aea0bf6a9f1183f44:
- File
src/fuzz_tests.zig
— part of check-in
[be97df0398]
at
2026-07-20 21:26:25
on branch trunk
— Release zit 0.1.1
Harden native Linux filesystem behavior, deterministic repository iteration, update and undo transitions, Fossil smoke-process containment, and the three-platform CI matrix. (user: dmestas, size: 8748) [annotate] [blame] [check-ins using]
//! 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"); /// 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, .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, }; 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; } } }