Skip to content
zit Check-in [21ca364fb3]
Login

Check-in [21ca364fb3]

Overview
Comment:Add merge3_properties: algebraic-property fuzzing over the three-way merge

Silent merge corruption is the worst failure a version-control system can have — it produces a tree that looks right — and merge3 had no fuzz target. This adds one, driving the merger with generated base/current/other triples and asserting the properties that must hold for any input.

Oracle 4 (no invented lines) needs a carve-out, because the merger is supposed to fabricate lines inside a conflict envelope. That carve-out is only sound if the recorded spans really are envelopes, so expectNoInventedLines validates the claim before subtracting anything: each span must be in-bounds, open at a line boundary with conflict_sentinel, close with conflict_terminator, start at or after the previous span's end, and carry no second terminator inside it.

The ordering and interior-terminator checks are load-bearing, not belt-and- braces. inAnySpan matches on overlap, so an over-long span exempts every output line up to its end. End-pinning alone admits a single span stretched from the first envelope's start to the last one's end — it opens and closes correctly and overlaps nothing — which would exempt the clean lines between two envelopes from the oracle entirely. closes_once catches that case on the first envelope's terminator. merge3 asserts ordering itself, but only under runtime_safety, which a ReleaseFast sweep — the build a fuzz run uses — erases; this loop holds in every optimize mode.

merge3.zig gains one export: conflict_terminator, the closing counterpart of the existing conflict_sentinel, with a comptime tripwire pinning its shape. Additive only, no behavior change — it lets the oracle pin both envelope ends against the module that produces them instead of re-typing the literal, so the two cannot drift apart.

Battery green in the worktree: zig fmt --check ., zig build, zig build test, zig build e2e, zig build dst (6 seeds x 1000 steps). The new target runs clean at seed 1 over 2000 rounds; no bug was found, so no regression seed is pinned.

Timelines: family | ancestors | descendants | both | release-0.1
Files: files | file ages | folders
SHA3-256: 21ca364fb326dc7e17640f159248d4b11ddd5b61b9d62681f7dc51b68402979b
User & Date: dmestas on 2026-07-19 01:54:32.397
Other Links: branch diff | manifest | tags
Context
2026-07-19
01:58
Merge release-0.1: merge3_properties algebraic-property fuzz (P4-F7)

Lands the last of the seven 0.1.0 release blockers. merge3 — the three-way merge — had no fuzz target, and silent merge corruption is the worst failure a version-control system can have, because it produces a tree that looks right.

The merged tree was re-verified on trunk after the merge, not inherited from the branch: zig fmt --check ., zig build, and zig build test all exit 0. The merge reported no conflicts and touched four files; fossil extras is empty, so nothing referenced went unadded. The conflict-marker sequences a repo-wide grep reports in src/merge3.zig and src/e2e.zig are that module's own sentinel constants and the tests asserting on them, not merge residue.

Landed by hand rather than by the release workflow: its TigerStyle gate spent its three-round budget on this diff and halted without a confirming pass. The final round's two blockers were real — a zig fmt failure and an oracle hole where conflict spans were validated individually but never as ordered and disjoint, which would have let one widened span exempt every line from oracle 4 — and both were fixed by the round that followed. Verified here before landing: fmt clean, the ordered/closes_once checks present, and merge3_properties running clean at seed 1 over 2000 rounds. check-in: 08e7c6d6f1 user: dmestas tags: trunk

01:54
Add merge3_properties: algebraic-property fuzzing over the three-way merge

Silent merge corruption is the worst failure a version-control system can have — it produces a tree that looks right — and merge3 had no fuzz target. This adds one, driving the merger with generated base/current/other triples and asserting the properties that must hold for any input.

Oracle 4 (no invented lines) needs a carve-out, because the merger is supposed to fabricate lines inside a conflict envelope. That carve-out is only sound if the recorded spans really are envelopes, so expectNoInventedLines validates the claim before subtracting anything: each span must be in-bounds, open at a line boundary with conflict_sentinel, close with conflict_terminator, start at or after the previous span's end, and carry no second terminator inside it.

The ordering and interior-terminator checks are load-bearing, not belt-and- braces. inAnySpan matches on overlap, so an over-long span exempts every output line up to its end. End-pinning alone admits a single span stretched from the first envelope's start to the last one's end — it opens and closes correctly and overlaps nothing — which would exempt the clean lines between two envelopes from the oracle entirely. closes_once catches that case on the first envelope's terminator. merge3 asserts ordering itself, but only under runtime_safety, which a ReleaseFast sweep — the build a fuzz run uses — erases; this loop holds in every optimize mode.

merge3.zig gains one export: conflict_terminator, the closing counterpart of the existing conflict_sentinel, with a comptime tripwire pinning its shape. Additive only, no behavior change — it lets the oracle pin both envelope ends against the module that produces them instead of re-typing the literal, so the two cannot drift apart.

Battery green in the worktree: zig fmt --check ., zig build, zig build test, zig build e2e, zig build dst (6 seeds x 1000 steps). The new target runs clean at seed 1 over 2000 rounds; no bug was found, so no regression seed is pinned. closed check-in: 21ca364fb3 user: dmestas tags: release-0.1

00:10
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. check-in: cec4afaf89 user: dmestas tags: release-0.1

Changes