Files
alkssh/tasks/implementation/w5-fuzz-harness.md
T
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
Renames the written-usage rule into practice across the contract surface:
docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files,
taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the
.opencode/agents/ prompts. Every replacement is a sense-preserving
rewrite (colons, semicolons, commas, parentheses, sentence splits, varied),
never a mechanical swap; no spaced hyphens were substituted. Enum fields,
frontmatter, tables, wire data, and cited identifiers survive verbatim;
en dashes inside numeric ranges keep their meaning.

Verification: taskgraph validate (43 tasks), repo grep clean (single
exempt hit: the verification command's own search pattern in AGENTS.md),
cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
2026-11-04 20:28:00 +00:00

10 KiB
Raw Blame History

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
review-wave-4
moderate medium phase implementation

Description

The family standing rule, "anything worth fuzzing gets fuzzed", applied to 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, covering 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, the SHA256: + unpadded base64 format path (src/fingerprint.rs): no-panic on arbitrary key blobs; the format invariant holds for every accepted input; determinism; the committed ssh-keygen -lf parity vectors still agree.
  • key_decode, the decode_secret_key ingress (src/key_source.rs's decrypt-at-use seam): no-panic on arbitrary plaintext bytes and passphrase shapes; every failure a typed KeySourceError class (decode failure, never a catch-all, never a panic); no secret material retained past the one-time decode (the Zeroizing posture's shape asserted where assertable).
  • terminal_params, the pty_request args → TerminalParamsWire conversion (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/ thin fuzz_target! wrappers; shared/ the invariant logic as a plain stable-clean library so cargo test --manifest-path fuzz/shared/Cargo.toml replays the committed corpora through the same invariants, and 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, with 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.toml nightly pin inside fuzz/; the root Cargo.toml gains exclude = ["fuzz"] (the root is a single-member workspace, with the sidecar 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.md documents 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, giving 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: members = []+ nightly pin infuzz/rust-toolchain.toml; the root Cargo.tomlgainsexclude = ["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.lockis committed (the alkgit convention);artifacts/, coverage/, target/`, and grown corpora are gitignored.

The exposure-shape decision (per target and 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, with no exposure needed: the ingress is fully public ([alkssh::fingerprint] + the russh 0.65 blob parse parse_public_key_base64, the same re-export surface the crate rides).
  • exec_parse, a #[doc(hidden)] re-export of the dispatch parse path: parse_exec, ExecRoute, ExecDecline, GitServiceRoute (made pub in the private dispatch module; the lib re-export gate is the only reachability, as the grammar stays a dispatch seam, not a public-API promise).
  • key_decode, a #[doc(hidden)] exposure decode_key_arm, the production decrypt_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, a #[doc(hidden)] re-export (under session) of tty_map::terminal_params, the exact function handler.rs's pty_request row 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, since 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, with 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, so 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, as 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, as 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, and all are 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).