The family standing rule (anything worth fuzzing gets fuzzed) on the
crate's ingress surfaces, per the alkgit convention:
- fuzz_targets/{exec_parse,fingerprint,key_decode,terminal_params} —
thin fuzz_target! wrappers over four ingress surfaces:
- exec_parse: the production parse path (from_utf8-fallback ingress
shape), differentially paired against a from-spec grammar oracle;
typed routing classes + determinism.
- fingerprint: blob parse + the single fingerprint path; the
SHA256:/unpadded-base64 contract shape via a from-spec
base64 round-trip oracle; the committed ssh-keygen -lf vectors
replayed inside the gate.
- key_decode: the production decode arm (the #[doc(hidden)]
decode_key_arm exposure, routed through itself); typed-class +
bounded-fixed-message invariants; passphrase-shape robustness
drive on the underlying decode.
- terminal_params: the pty_request conversion via the #[doc(hidden)]
terminal_params exposure; exact clamp, verbatim term, the
reserved-modes {} JSON shape, serialized-shape determinism.
- shared/ — the invariant logic as a stable-clean library; the
corpus gate (cargo test --manifest-path fuzz/shared/Cargo.toml)
replays every committed seed through the batteries and joins
AGENTS.md's verification list.
- corpus/<target>/ committed seeds + the deterministic
gen_fuzz_seeds.py (no-op-diff verified).
- run-detached.sh + nightly pin; the sidecar excluded from the root
workspace (workspace + package excludes; publish dry-run clean).
Exposure shape decided + recorded per target (README + lib.rs doc
comments): fingerprint needs none (fully public ingress); the rest
are #[doc(hidden)] re-exports of the production surfaces, never
forks. Detached campaigns ran per target (600 s default budget, fork
mode): all four clean; the two findings of the first round are
recorded in the task file (the harness oracle's double-/-strip bug;
the PPK echo fixed in the preceding commit).
Verification: corpus gate green (11 tests); scripts/matrix.sh green;
taskgraph validate clean.
10 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| w5-fuzz-harness | Wave 5 — the fuzz sidecar (the crate's ingress surfaces) | completed |
|
moderate | medium | phase | implementation |
Description
The family standing rule — anything worth fuzzing gets fuzzed — on
the crate's ingress surfaces (docs/plans/implementation.md §Wave 5,
the fuzzing bullet): the fuzz/ sidecar per the alkgit convention
(fuzz/README.md there — the layout, the detached runner, the corpus
gate). Four targets over the plan's ingress inventory:
exec_parse— the git-parseable exec grammar + the dispatch parse path (src/git_map.rs's parse +src/dispatch.rs's parse-only routing): no-panic on any command string; the typed decline classes (git-parseable → the git row, non-git → the session pipe path, service-only → decline-with-reason) stable per input; the per-length caps enforced before any allocation (the decline-first-alloc-never invariant under hostile lengths).fingerprint— theSHA256:+ unpadded base64 format path (src/fingerprint.rs): no-panic on arbitrary key blobs; the format invariant holds for every accepted input; determinism; the committedssh-keygen -lfparity vectors still agree.key_decode— thedecode_secret_keyingress (src/key_source.rs's decrypt-at-use seam): no-panic on arbitrary plaintext bytes and passphrase shapes; every failure a typedKeySourceErrorclass (decode failure — never a catch-all, never a panic); no secret material retained past the one-time decode (theZeroizingposture's shape asserted where assertable).terminal_params— thepty_requestargs →TerminalParamsWireconversion (src/tty_map.rs, the SSH modes → reserved-modes JSON): no-panic on arbitrary term strings, sizes, pixel dims, and mode blobs; oversize/overflow fail-closed (typed, not clamped-silent); the reserved-modes JSON shape stable.
Shape (the alkgit convention, adapted):
fuzz_targets/thinfuzz_target!wrappers;shared/the invariant logic as a plain stable-clean library socargo test --manifest-path fuzz/shared/Cargo.tomlreplays the committed corpora through the same invariants — the corpus gate joins the verification list; the sidecar consumes the crate's public API only (plus#[doc(hidden)]fuzz-visible exposures where the ingress is private — the exposure shape decided + recorded, the alkgit pattern; never a dependency of the root crate).corpus/<target>/committed hand-made seeds + the deterministic generator (gen_fuzz_seeds.py, no-op-diff check); grown corpora/artifacts/coverage gitignored, never merged back.run-detached.sh— the mandatory detached runner (agent sessions never foreground a campaign);rust-toolchain.tomlnightly pin insidefuzz/; the rootCargo.tomlgainsexclude = ["fuzz"](the root is a single-member workspace — the sidecar is its own).- Campaigns run detached per target (the runner's default budget); any finding fixed in-task or booked for the gate — never absorbed silently.
Acceptance Criteria
- The
fuzz/sidecar lands with all four targets; the sidecar is excluded from the root workspace and never a root dep - The exposure shape decided + recorded (public API vs
#[doc(hidden)]per target) - Committed seeds + the deterministic generator (no-op diff);
the corpus gate (
cargo test --manifest-path fuzz/shared/Cargo.toml) green and joining the verification list - Detached campaigns run per target; findings fixed or booked (recorded)
fuzz/README.mddocuments the layout + commands (the alkgit shape); the root crate's verification set stays green (the sidecar changes no root behavior)
References
- docs/plans/implementation.md §Wave 5 (the fuzzing bullet — the four-surface inventory)
- /workspace/@alkdev/alkgit/fuzz/ (the convention's reference implementation: README, run-detached.sh, gen_fuzz_seeds.py, shared/), docs/plans/implementation.md (alkgit) §Wave 5 (the fuzz-harness task's shape)
- src/git_map.rs, src/dispatch.rs, src/fingerprint.rs, src/key_source.rs, src/tty_map.rs (the ingress surfaces)
- docs/architecture/mapping.md §Session-channel dispatch (the decline-first-alloc-never invariant the exec_parse target pins)
Notes
The sidecar as landed: fuzz/ (its own workspace — [workspace] members = [] + nightly pin in fuzz/rust-toolchain.toml; the root
Cargo.toml gains exclude = ["fuzz"] at both levels — the
[workspace] exclude and the [package] exclude list (the alkgit
published-crate policy: the sidecar never ships in a publish; verified
with the dry-run)). fuzz/Cargo.lock is committed (the alkgit
convention); artifacts/, coverage/, target/, and grown corpora
are gitignored.
The exposure-shape decision (per target, recorded) — the sidecar consumes alkssh's public API wherever the ingress is already public, and the production surface itself (never a fork) where it is private:
fingerprint— no exposure needed: the ingress is fully public ([alkssh::fingerprint] + the russh 0.65 blob parseparse_public_key_base64, the same re-export surface the crate rides).exec_parse—#[doc(hidden)]re-export of the dispatch parse path:parse_exec,ExecRoute,ExecDecline,GitServiceRoute(madepubin the privatedispatchmodule; the lib re-export gate is the only reachability — the grammar stays a dispatch seam, not a public-API promise).key_decode—#[doc(hidden)]exposuredecode_key_arm, the productiondecrypt_and_decode's decode arm extracted and routed through itself (the fuzz target drives the exact production arm, and the arm takes&Zeroizing<String>so the drive exercises the adapter seam's wrap posture).terminal_params—#[doc(hidden)]re-export (undersession) oftty_map::terminal_params— the exact functionhandler.rs'spty_requestrow constructs the stored params with.
The harness shape: the deterministic framed feed for
terminal_params (term_len u32-LE + term bytes + four u32-LE dims,
zero-defaulted, trailing mode blob ignored — the modes are never
parsed, OQ-44); the production ingress shape for exec_parse
(byte string → from_utf8(..).unwrap_or("") → parse — the handler's
exact shape; the pre-parse cap and the git row's repo-id gate are the
root contract rows' territory, pinned by tests/dispatch_core.rs);
a from-spec base64 round-trip oracle for fingerprint; the
exec_parse verdict is differentially paired against a from-spec
grammar oracle (fuzz/shared/src/exec_parse.rs), fingerprint
parity replays the committed ssh-keygen -lf vectors inside the
corpus gate.
Campaigns run (detached via fuzz/run-detached.sh, fork mode, the
default 600 s budget each; all four final campaigns exited 0 — zero
crashes/OOMs/timeouts):
exec_parse— 2 campaigns: the first crashed on a harness-side oracle bug (the oracle stripped two leading/where the grammar strips exactly one —git-upload-pack //.git); fixed, re-ran clean (40M+ execs).fingerprint— 1 campaign, clean (12M+ execs).key_decode— 2 campaigns: the first found a real crate finding (below); fixed, re-ran clean (3.5M+ execs).terminal_params— 1 campaign, clean (8.8M+ execs).
The real finding (fixed in-task, recorded): russh 0.65's
ssh-key PPK parse path (PpkParseError::Header) interpolates the
raw header line — the plaintext's own bytes — into its Display,
and the decode arm previously passed that message through
KeySourceError::Decode's payload: a malformed or
PPK-header-shaped vault record would echo decrypted-record material
into a stored/logged error string (the ADR-005 decision-7 posture's
textual leak). Fix: decode_key_arm (and so every Decode payload
the crate ever produces) now carries crate-owned fixed text naming
the refusal — the machinery's message never rides the typed payload;
error.rs's Decode doc records the semantics. The upstream ask
is booked (not absorbed): ssh-key's PPK parse errors should not
echo the untrusted header; filed to russh/ssh-key with the owners at
the wave-5 gate. Related harness-side finding (also fixed in-task):
the no-echo invariant's first shape (a plaintext-contains-message
check) was itself unsound — an attacker-chosen plaintext can be a
substring of the machinery's fixed text (the campaign's own first
crash: plaintext "Could not read key"); the assertable form is the
bounded-message pair (the text is a bounded fixed message, ≤ 256
bytes) + the ≥ 256-byte plaintext-run check that cannot false-fire.
Verification list: the corpus gate
(cargo test --manifest-path fuzz/shared/Cargo.toml) is green (11
tests — every committed seed replays the invariants on the stable
toolchain, plus the fixture parity rows) and has joined AGENTS.md's
verification commands. scripts/matrix.sh green after all changes
(the sidecar changes no root behavior: 6 builds × the full suite +
clippy ×2 -D warnings + fmt + doc, exit 0).
Summary
Completed 2026-10-30. The fuzz/ sidecar landed per the alkgit
convention with all four targets over the plan's ingress inventory
(exec_parse, fingerprint, key_decode, terminal_params), each a
thin fuzz_target! wrapper over the stable-clean
fuzz/shared/ invariant library whose corpus replay
(cargo test --manifest-path fuzz/shared/Cargo.toml) joined the
verification list (AGENTS.md); committed hand-made seeds + the
deterministic gen_fuzz_seeds.py (no-op-diff verified); the detached
runner + nightly pin inside fuzz/; the root workspace excludes the
sidecar (workspace + package excludes). The exposure-shape decision is
recorded per target (one no-exposure — fingerprint's fully-public
ingress; three #[doc(hidden)] production-surface re-exports).
Detached campaigns ran per target at the runner's default budget —
all clean after findings: one harness-oracle bug (the exec oracle's
double-/ strip) and one real crate fix — russh's ssh-key PPP/PPK
parse echo (raw header bytes) riding the typed Decode payload;
decode_key_arm now carries a crate-owned fixed message and the
upstream echo ask is booked, not absorbed. Verification: the sidecar
gate green, scripts/matrix.sh green end to end, cargo publish --dry-run clean (the sidecar ships nothing).