Artifact 94b07393229dc6aa681ac0f90bb0bb2c4d024bc6446fbefb361346942a7630eb:
- File
src/fuzz_tests.zig
— part of check-in
[cec4afaf89]
at
2026-07-19 00:10:09
on branch release-0.1
— Add transport_frame_garbage and serve_request_garbage seeded fuzz targets
The 24 existing fuzz targets cover the delta, artifact, sync-parser and logstore layers, but not the two CLI-layer modules that consume bytes an untrusted remote peer chose. `sync_parsers_adversarial` covers the layer above; the HTTP framing itself was unfuzzed. These are the last two wire surfaces on the release bar, and they are what a hostile peer reaches first.
What landed:
- `src/transport_fuzz.zig` — `frameGarbage`, registered as `transport_frame_garbage`. Client-side §4/§4.1 framing and redirect policy: the round-trip law (whatever `frame` produces, `unframe` returns exactly those plaintext bytes), mutation of framed containers (bit damage never survives as *different* plaintext — it either fails the container's Adler-32/zlib checks or, in the length-field range the §4.1 compression-length erratum makes a capacity bound, decodes back to the same bytes), and garbage `Location` headers through `resolveRedirect`. Totality is the asserted contract: every input yields a value whose postconditions hold or a declared member of `TransportError`.
- `src/serve_fuzz.zig` — `requestGarbage`, registered as `serve_request_garbage`. Server-side HTTP request path: raw bytes into `std.http.Server.receiveHead`, then `handleHttpRequest`'s dispatch (`classifyXfer` -> body read -> responder -> reply framing). It does not re-feed `handleXfer` in isolation — that decoding is already fuzzed twice — it reaches it only as the tail of a full-path dispatch, which is the point. Asserted contract: any `receiveHead` error is a clean rejection, `handleHttpRequest` returns void, and any produced response begins with an HTTP/1.1 status line.
- `src/serve.zig`: `driveHttp` becomes `pub`. It is the request-path seam two callers need and neither should reimplement — the §4 smoke tests and this fuzz target. In-memory reader/writer on both sides, so the target binds no port and cannot flake in CI.
- `src/fuzz_tests.zig`: both targets registered in the `targets` table, and `targets` becomes `pub` for a new structural registration pin. An unregistered target compiles, passes review, and is then never run by any invocation; the pin asserts every target the release bar names is actually reachable from the runner, so the registration cannot rot.
- `src/dst.zig`: aggregates both new files plus `fuzz_tests.zig` into `zig build test`. This root is the one that reaches serve.zig and transport.zig relatively while carrying the `zit` dep those two import; `zig build fuzz` has no test step, so it is also the only place the registration pin can run.
No bug found, so no fix and no pinned-seed regression in this commit — both surfaces held over the seeded runs below. The targets are the standing check, not a one-time sweep.
Test evidence (this worktree, after `fossil update` absorbed the sibling `zit open` commit 5ba418801d and its 8 modified files):
- `zig build fuzz -- transport_frame_garbage --seed 1 --rounds 2000` — clean, no crash/leak/UB. - `zig build fuzz -- serve_request_garbage --seed 1 --rounds 2000` — clean, no crash/leak/UB. - `zig build test` — green, including the new registration pin and the fixed-seed smoke tests at the foot of both new files. - `zig build` and `zig fmt --check .` — green.
No borrowed technique in this change. (user: dmestas, size: 7676) [annotate] [blame] [check-ins using] [more...]
//! Seeded fuzz-target runner. //! //! `zig build fuzz -- <target> [--seed N] [--rounds N]` //! //! 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"); /// 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, }; 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()); 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\ntargets:\n"); inline for (@typeInfo(Target).@"enum".fields) |f| { try w.print(" {s}\n", .{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", }; 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; } } }