Artifact 3d55d3b16d26b7960bda7625d0a38b07acd1ba2c1d9c4e1fe11375224e8d7106:
- File
src/fuzz_tests.zig
— part of check-in
[72bba34ada]
at
2026-07-28 03:31:28
on branch sub-manifest
— Sub-manifest: complete the conformance corpus
Sub-manifests have no reference implementation to check against, so the corpus is the oracle and had to be built rather than assumed. Adds the eleven vectors Appendix A.5 requires that were missing, among them the three-level tree whose absence is why the step-5 cursor contradiction survived into an implementation: no published artifact carried an interior node, so nothing exercised that path.
Every authored vector is generated by the implementation and then rechecked along a path that shares no code with it, and each derived name is compared against the constant the test pins, so no hash in the corpus is hand-written. Two fuzz targets cover S-card classification and the tree walk.
One A.5 bullet is not satisfiable as written: the re-tune vector asks for two parameters over the A.1 name list that yield trees sharing no sub-manifest, but that list's single qualifying boundary falls in the same place for the first three values and is suppressed above them. No vector was invented for it; an erratum is drafted and a test pins the finding so it stays visible. (user: dmestas, size: 9150) [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, // 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, }; 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; } } }