feat(fuzz): cargo-fuzz workspace, 3 wire-surface targets; bump alkcall 0.8.1

- Bump alkcall 0.8.0 -> 0.8.1 (duplicate adopt/open channel-state
  destruction fix from alkcall's fuzz campaign; AGENTS.md stale-pin fix).
- Adopt fuzzing per docs/plans/fuzzing.md (mirrors alkcall's
  docs/research/fuzzing.md as-built layout): fuzz/ workspace with own
  [workspace] table, nightly pinned for the subtree only, invariant
  logic in a stable-toolchain shared crate.
- Targets: chunk_frame (5-byte chunk codec), negotiation_frame
  (negotiation framing + NegotiateRequest + error_response_bytes +
  the cross-codec peek-disambiguation seam), control_json
  (ControlMessage JSON + signal_from_name). src/local/ out of scope
  (not wire-attacker-shaped).
- 328 committed seeds (deterministic generator); grown corpora
  gitignored. Corpus replay on stable is the standing fuzz gate
  (cargo test --manifest-path fuzz/shared/Cargo.toml).
- Detached-runner rule (fuzz/run-detached.sh): campaigns never run in
  the foreground of an agent session - OOM in a target must cost the
  fuzzer, never the session host.
- Root Cargo.toml gained [workspace] members/exclude (MSRV vs fuzz
  nightly dev-deps footgun) and fuzz/ in the publish exclude.

Verification: cargo test (113), cargo test --all-features (156),
clippy stable + wasm32-unknown-unknown, fmt, doc, publish dry-run,
corpus replay 328 seeds - all green. 10-min detached campaigns on all
three targets: 0 crashes/hangs/OOMs/leaks (chunk_frame cov-saturated
at 710 edges; negotiation_frame 3.69M execs cov 3157; control_json
8.6M execs cov 2035). Corpus replay caught three harness-model
mismatches pre-campaign (payload-slice shape; the negotiation framing
layer is length-prefix-only - Json unreachable from read_frame;
zero-length frames admitted by the reader, rejected by the adapter's
parse). Campaigns recorded in docs/plans/fuzzing.md section 8.
This commit is contained in:
glm-5.3-flash committed 2026-09-28 08:07:30 +00:00
1 parent e95459c425
commit 49f70d9efb
1809 files changed
+9380 -12

No files matched your search

+14 -8
View File
@@ -238,6 +238,7 @@ cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo doc --no-deps
cargo test --all-features
cargo test --manifest-path fuzz/shared/Cargo.toml # fuzz corpus replay — the standing fuzz gate
cargo check --target wasm32-unknown-unknown # the default crate stays wasm-clean
cargo clippy --target wasm32-unknown-unknown -- -D warnings
cargo publish --dry-run --allow-dirty # before a release
@@ -248,6 +249,13 @@ wasm-clean" invariant — run it whenever a non-`local` module changes.
`cargo test --all-features` exercises the `local` backend (PTY mode is
`#[cfg(unix)]`; pipe mode is cross-platform).
The fuzz corpus replay (`docs/plans/fuzzing.md`) runs every committed
seed through the same invariant functions the cargo-fuzz targets run —
on stable, no nightly needed. Run detached campaigns
(`fuzz/run-detached.sh <target>` — never foreground fuzzing) before a
release and after touching `src/wire.rs`, `src/negotiation.rs`,
`src/control.rs`, the adapter, or the session pump.
## Architecture Context
- `docs/plans/project-setup.md` — the current plan (phases 0–5). All
@@ -312,11 +320,9 @@ wasm-clean" invariant — run it whenever a non-`local` module changes.
`BiStream`, `BidiStreamSource`, `AuthContext`, `Identity`,
`IdentityProvider`, `AccessControl`, `OwnershipProvider`,
`OwnershipStore`, `InMemoryOwnershipStore`, `HandlerError`,
`StreamError`) come from `alkcall::core`. alkcall is v0.4.x —
breaking changes are expected at this major-zero stage; this is the
first real consumer, so we find and fix issues upstream rather than
working around them. Pin `alkcall = "0.6.0"` (the
establishment-plan payload — ADR-049 amendment 2, review 007 R-01;
adopted by alktty ADR-010 §2A — allocation runs in the channels
establisher, the `TtyHandle` crosses via the plan) and bump
deliberately.
`StreamError`) come from `alkcall::core`. alkcall is v0.8.x —
breaking changes are expected at this major-zero stage; this is the
first real consumer, so we find and fix issues upstream rather than
working around them. Pin `alkcall = "0.8.1"` (0.8.1 fixes the
duplicate adopt/open channel-state destruction found by alkcall's
fuzz campaign) and bump deliberately.
Generated
+2 -2
View File
@@ -27,9 +27,9 @@ dependencies = [
[[package]]
name = "alkcall"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8695d47cee5b5a5fe68471f04a505bc257a7e7ed9e64d827de33ff83adf4db12"
checksum = "bf6af1e4af1460480849adf959c972d65deeafc36f4101064ced1ce6d1adfd5b"
dependencies = [
"async-trait",
"bytes",
+6 -2
View File
@@ -9,7 +9,11 @@ repository = "https://git.alk.dev/alkdev/alktty"
readme = "README.md"
keywords = ["tty", "terminal", "pty", "channels", "alkcall"]
categories = ["network-programming", "asynchronous"]
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/sdd_process.md"]
exclude = [".opencode/", "AGENTS.md", "docs/reviews/", "docs/plans/", "docs/sdd_process.md", "fuzz/"]
[workspace]
members = ["."]
exclude = ["fuzz"]
[lib]
name = "alktty"
@@ -25,7 +29,7 @@ default = []
local = ["dep:portable-pty", "dep:tokio-util", "tokio/process", "tokio/rt-multi-thread"]
[dependencies]
alkcall = "0.8.0"
alkcall = "0.8.1"
# Minimal, wasm-clean tokio features. `local` adds `process` +
# `rt-multi-thread` (non-wasm). Do NOT use `features = ["full"]` — it
# pulls in `signal`/`fs`/`net` which break `wasm32-unknown-unknown`.
+296
View File
@@ -0,0 +1,296 @@
# Fuzzing alktty: Plan
**Status:** adopted — implementation in progress (see §8 progress log)
**Date:** 2026-09-28
**Research input:** `/workspace/@alkdev/alkcall/docs/research/fuzzing.md` (the
alkcall fuzzing evaluation; the toolchain decision, corpus policy, detached
runner, and no-CI policy below are adopted from it wholesale rather than
re-derived).
---
## 1. Decision: fuzz this crate
**Yes.** Alktty is the *named follow-up* in the alkcall research: §5
"Non-surfaces" explicitly defers the downstream parsers to "downstream
crates' fuzz targets" — the channels layer carries opaque `Bytes` and the
handler owns sub-stream framing (ADR-035). Alktty is that handler, and it
owns four parse surfaces fed by arbitrary peers on both transports (direct
ALPN and channels):
| # | Surface | Location | Shape |
|---|---|---|---|
| 1 | 5-byte chunk codec | `src/wire.rs` (`ChunkReader`/`ChunkWriter`, `validate_header`) | async, `Cursor`-drivable; validate-then-alloc at `wire.rs:244` |
| 2 | Negotiation framing | `src/negotiation.rs:185` (`NegotiationReader::read_frame`) | 4-byte BE prefix, bounds-check then `vec![0u8; length]` at `:199` — the same bounded-but-allocation-rate pattern alkcall flagged (§6.2-3) |
| 3 | `NegotiateRequest` JSON | `src/negotiation.rs` (serde parse of up to 16 MiB bodies) | attacker-shaped JSON, typed struct |
| 4 | `ControlMessage` JSON | `src/control.rs:105` (`from_slice`), `signal_from_name` | sync pure |
Reasons this pays off here, beyond the generic alkcall argument:
1. **Semantic invariants no example test explores adversarially.** The
peek/disambiguation machinery (`peek_stream_type` vs
`read_chunk_after_peek` — both call orders must yield exactly one
chunk) and the cross-codec invariant that makes it sound: an
error-response frame's length-prefix high byte must be `0x00` so a
`ChunkReader` peek distinguishes it from a chunk whose first byte is a
server stream type ∈ {1, 2, 4} (`wire.rs:189-213`, ADR-052 §5). If
`MAX_CHUNK_LEN` ever changes or a stream type is added, that invariant
breaks subtly. A fuzz target spanning both codecs is the cheapest
enforcement that exists.
2. **The alkcall bug class has a plausible analog here.** The duplicate
adopt/open finding (alkcall §7.8) was *stateful*: an interleaving no
unit test replays destroyed the live channel. Alktty's adapter has the
same shape — take-once `Mutex<Option<tx>>` slots (`adapter.rs:777-792`),
session re-registration, handle replacement. A stateful op-sequence
target against the session pump + mock backend is where a real finding
would live.
3. **Cost is low and the playbook is proven.** Mirroring alkcall's
as-built layout is a port, not a design exercise (§5 below).
Zero parser-bug yield is a successful negative result — it converts "we
think the codecs are robust" into a demonstrated property (the quinn
lesson cuts both ways).
**Scope excludes `src/local/`:** PTY/pipe output is local-OS-shaped, not
wire-attacker-shaped. The wire-facing side is entirely the
shared/producer/consumer modules (`wire`, `negotiation`, `control`,
`backend`, `adapter`, `channels`, `session`).
**The wasm invariant is unaffected:** `fuzz/` is host-only (nightly
toolchain, `path` dep on the crate), excluded from publishing, and never
compiled for `wasm32-unknown-unknown`.
## 2. Toolchain decision (adopted from alkcall §4/§7.7)
- `cargo-fuzz` + `arbitrary`, standard `fuzz/` workspace with its own
`[workspace]` table (nightly-only bins).
- **The invariant logic lives in `fuzz/shared/` — a stable-toolchain
library crate** — not inline in the target binaries. The `fuzz_target!`
binaries are thin wrappers. This makes corpus replay a plain
`cargo test --manifest-path fuzz/shared/Cargo.toml` on stable: it
replays every committed seed through the identical invariant functions
the fuzzer runs, and joins the release verification checklist.
- `fuzz/rust-toolchain.toml` pins `nightly` for the subtree only; the
crate stays stable at MSRV 1.88.
- Committed hand-made seeds, gitignored grown corpora (quinn/h2 pattern);
deterministic seed generator script committed next to them.
- **No hosted CI** — this repo has the same no-CI policy as alkcall
(§7.9 there): the gitea host serves git only; verification and
campaigns are manual. Corpus replay is the standing fuzz gate;
campaigns run via the detached runner before releases and after
touching `src/wire.rs`, `src/negotiation.rs`, `src/control.rs`, the
adapter, or the session pump. Do not add workflow files or "when CI
exists" language.
- AFL/bolero/libafl/OSS-Fuzz: not adopted, same reasoning as alkcall §4.
## 3. Operational isolation: campaigns must be detached from the agent session
**This is mandatory, not a nice-to-have.** Agent sessions (opencode) run
commands through a bash tool that spawns processes as children of the
session's own process tree and cgroup. A fuzz campaign is exactly the
workload shape that can kill its own host:
- **OOM-killer shared fate.** The target's worst case *is* unbounded
allocation. libFuzzer's RSS guard is a *soft* poll — a fast single huge
allocation can beat it, and then the kernel OOM killer fires. It picks
the largest-RSS process in the cgroup, which can be the opencode server
hosting the session. Losing that server is recoverable (sessions are
recorded in a db) but hugely derailing and entirely avoidable. An OOM
in a fuzz target must cost the fuzzer, never the session.
- **Fork bombs / CPU saturation:** `-fork=N` workers and unbounded task
spawning in a buggy target can starve the session host.
- **Deadlock hangs:** a wedged target must not hang the session's bash
call.
Three layers, all standard:
1. **libFuzzer soft limits (always on):** `-rss_limit_mb=2048`
`-malloc_limit_mb=2048` make libFuzzer exit cleanly (OOM-class
artifact) before the kernel gets involved. Do **not** use `ulimit -v`
with ASAN (the sanitizer reserves terabytes of virtual address space;
the classic failure is instant `MmapAlloc` death) — the ASAN-safe
equivalent is `malloc_limit_mb`.
2. **Fork mode as blast-radius containment (default):** `-fork=1` runs
each input in a short-lived child; a crash/timeout/OOM/leak kills only
that child, the parent records the artifact and keeps going. Also
turns deadlocks into per-input timeouts instead of hung sessions.
3. **Process detachment (mandatory for agent-run campaigns):** never run
fuzzing as a foreground child of the session. `fuzz/run-detached.sh`
wraps `cargo fuzz run` in `setsid` + `nohup` + redirection to a log
file: removed from the session's controlling terminal and signal
relation, survives the session ending, and gives a pollable log
instead of a blocking call.
Polling protocol (never `wait` on the detached process):
```bash
tail -n 50 fuzz/artifacts/<target>-*.log # campaign progress
ls fuzz/artifacts/ # crash-* / oom-* / timeout-* artifacts
pgrep -f "cargo fuzz run <target>" # still running?
```
One session holds one detached campaign per target; log filenames carry
UTC timestamps. `FUZZ_RUNTIME_SECS` overrides the per-campaign budget
(default 600 s).
## 4. Targets
In priority order. Targets 1–3 are the initial scope; target 4 is second
wave, decided after campaigns on 1–3 come back clean (same sequencing as
alkcall §7.4).
| # | Target | Input style | Drives | Invariants |
|---|---|---|---|---|
| 1 | `chunk_frame` | raw `&[u8]` | `ChunkReader`/`ChunkWriter` over `Cursor` + `block_on` | no-panic; error-shape partition (`ConnectionClosed` only on truncation; `InvalidStreamType` iff `stream_type > 4`; `ChunkTooLarge` iff `length > MAX_CHUNK_LEN`; `Io` impossible on a Cursor); Ok iff header complete + payload complete; round-trip (`write_chunk` then `read_chunk` reproduces stream_type + bytes); 5-byte consumption accounting; peek interleave (`peek_stream_type` → `read_chunk_after_peek` == `read_chunk`, idempotent peek, peek state resets per chunk); never allocates for out-of-range headers |
| 2 | `negotiation_frame` | raw `&[u8]` | `NegotiationReader`/`NegotiationWriter` + `NegotiateRequest` serde parse + `error_response_bytes` | no-panic; error-shape partition (`ConnectionClosed` only on truncation; `FrameTooLarge` iff `length > MAX_CHUNK_LEN`; `Json` only when the full body was present; `Io` impossible); consumption accounting (4 + length); negotiation JSON round-trip (`NegotiateRequest` → `to_json` → frame → `from_slice` → equal struct); **cross-codec disambiguation** (see §4.1) |
| 3 | `control_json` | raw `&[u8]` | `ControlMessage::from_slice` / `to_json`, `signal_from_name` | no-panic on any bytes; clean serde errors (never silent); `to_json` → `from_slice` structural round-trip; `signal_from_name` is total (returns `Option`, never panics) |
| 4 | `session_opseq` (second wave) | `#[derive(Arbitrary)]` op sequence | the producer session pump + mock backend (`src/testing.rs`, `drive_session`) | no-panic; take-once slot semantics; duplicate/replayed session ops leave the live session intact — the alkcall-bug-class target |
### 4.1 Cross-codec disambiguation invariant (target 2, the subtle one)
The consumer disambiguates an error-response frame from a raw chunk by
peeking one byte (`wire.rs:189-213`): server stream types are {1, 2, 4};
a 4-byte BE length prefix ≤ 16 MiB − 1 starts with `0x00`, which is
neither. Encoded as fuzz assertions:
- For every `error_response_bytes` output and every negotiation frame of
length < `MAX_CHUNK_LEN`, the first byte is `0x00` — a `ChunkReader`
peek on `[frame bytes…]` never mistakes it for a chunk.
- **Known boundary collision, encoded deliberately:** a frame of length
exactly `MAX_CHUNK_LEN` (= `0x01000000`) has prefix `[0x01, …]` —
first byte `0x01`, colliding with `STREAM_STDOUT`. The read paths
admit it (`length > MAX_CHUNK_LEN` rejects, `==` passes). This is an
inherent edge of the one-byte-peek design; producers only ever send
tiny error frames, so it is accepted — but the assertion pins the
boundary so any future `MAX_CHUNK_LEN` change that widens the
collision gets caught. (`validate_header`'s doc comment at
`wire.rs:262-270` claims the ≤ cap high byte is `0x00`; strictly the
claim holds only for `<` the cap. Worth a one-word fix in the doc
comment if we touch `wire.rs` for any other reason.)
Also encoded (mirroring alkcall §7.7's trailing-byte probe): a valid
negotiation frame with one extra JSON byte appended *and counted in the
length prefix* must fail with a `Json` error, so a future switch to a
tolerant streaming reader is caught.
## 5. Layout (mirrors alkcall §7.7 as-built)
```
fuzz/
├── Cargo.toml alktty-fuzz (nightly-only bins; own [workspace])
├── rust-toolchain.toml pins nightly for this subtree only
├── fuzz_targets/
│ ├── chunk_frame.rs thin fuzz_target! wrapper
│ ├── negotiation_frame.rs thin wrapper
│ └── control_json.rs thin wrapper
├── shared/ alktty-fuzz-shared — STABLE-toolchain library
│ └── src/{chunk_frame,negotiation_frame,control_json}.rs
├── corpus/<target>/ committed seeds
├── gen_fuzz_seeds.py deterministic seed generator
├── run-detached.sh §3 detached runner (CWD-independent)
└── README.md
```
Root `Cargo.toml` changes (the alkcall §7.7 footguns, both required):
- `[workspace]` table: `members = ["."]`, `exclude = ["fuzz"]` — without
it cargo auto-discovers `fuzz/shared/` into the main workspace and the
MSRV toolchain tries to build nightly-consumed dev-deps.
- `"fuzz/"` added to the publish `exclude` list.
No `#[cfg(fuzzing)]` exposure expected — `ChunkReader`/`ChunkWriter`,
`NegotiationReader`/`NegotiationWriter`, `NegotiateRequest`,
`ControlMessage`, and `error_response_bytes` are all already `pub`
(alkcall needed none of its five either). The one private constant,
`MAX_CHUNK_LEN`, is wire-stable (ADR-001); the shared crate carries its
own copy, and the shape invariants assert the rejection boundary so a
drift is caught.
## 6. Corpus policy
Commit hand-made seeds; gitignore grown corpora and artifacts
(`fuzz/.gitignore` per alkcall's). Seeds per target: valid frames/headers
for every stream type / every control variant, truncation at every prefix
length, `length = 0`, `length = MAX`, `length = MAX + 1`, `length =
u32::MAX`, invalid UTF-8 in JSON, deep-ish nesting, peek-interleave
fixtures, cross-codec error-frame fixtures. The generator script is
deterministic (no randomness) so seeds are reproducible.
## 7. Sequencing
1. **Bump alkcall 0.8.0 → 0.8.1** (the duplicate adopt/open fix lands on
alktty's own path — `TtySession::open_via_channels` and the channels
establisher run on the fixed `ChannelManager`). Full verification set
including wasm checks. Also fix AGENTS.md's stale "pin alkcall 0.6.0"
line while touching it. **Status: see §8.**
2. Scaffold `fuzz/` + root workspace table + publish exclude.
3. Targets 1–3 invariant logic in `fuzz/shared/` + thin target bins +
seeds + detached runner + README.
4. Corpus replay green on stable (`cargo test --manifest-path
fuzz/shared/Cargo.toml`); add it to the AGENTS.md verification
checklist.
5. Detached campaigns (10 min per target via `run-detached.sh`), triage
anything found, record results in §8.
6. Decide on target 4 (`session_opseq`) after 1–3 are clean.
## 8. Progress log (append-only across sessions)
- **2026-09-28** — Proposal discussed and adopted; this plan written.
Alkcall dependency bump + fuzz scaffold started.
- **2026-09-28 (later)** — **All of §7 steps 1–5 landed.**
- alkcall 0.8.0 → 0.8.1 verified across the full checklist (113 +
all-features 156 tests, clippy stable/wasm, fmt, doc).
- AGENTS.md: stale "pin alkcall 0.6.0" corrected to 0.8.1; corpus
replay added to the verification checklist; campaign guidance
added.
- `fuzz/` scaffolded exactly as §5: root `[workspace]` table + publish
exclude, `fuzz/rust-toolchain.toml` nightly pin, thin target bins,
invariant logic in `fuzz/shared/` (stable toolchain), 328 committed
seeds via `gen_fuzz_seeds.py`, `run-detached.sh`, README,
`.gitignore`.
- Corpus replay green on stable
(`cargo test --manifest-path fuzz/shared/Cargo.toml`).
- Harness-model corrections the corpus replay caught *before* any
campaign (the alkcall §7.7 pattern repeating): (1) a valid first
chunk with trailing stream bytes is correct reader behavior — the
payload slice is `5..5+len`, not `5..`; (2) the negotiation framing
layer is length-prefix-only — `read_frame` returns raw bytes and the
adapter does the serde parse (`adapter.rs:411`), so
`NegotiationError::Json` is unreachable from this reader (unlike
alkcall's serde-backed envelope reader; the `Json` arm now panics as
"unreachable" and the trailing-byte probe asserts exact-body
consumption instead of a Json error); (3) zero-length negotiation
frames are admitted by this reader (the adapter's parse rejects
them) — the shape assertion was relaxed accordingly.
- **10-min detached campaigns, all three targets: every run exited 0
with an empty artifact directory — no crashes, hangs, OOMs, or
leaks** (per-target `oom/timeout/crash: 0/0/0` on all ~28 fork
jobs): `chunk_frame` 380k execs (cov saturated at 710 edges within
seconds — the 5-byte-parser ceiling is fully enumerated);
`negotiation_frame` 3.69M execs (cov 3157, still finding coverage at
budget end — JSON structure exploration); `control_json` 8.6M execs
(cov 2035, still growing). Grown corpus entries were deleted per the
§6 policy; committed seeds unchanged.
- §7 step 6 (target 4 `session_opseq`) remains open for a follow-up
session.
## 9. Relationship to existing tests
The existing suite covers valid-input round-trips and documented error
paths (`wire.rs` codec tests, `negotiation.rs` and `tests/negotiation.rs`
frame tests, `control.rs` message tests). Fuzzing adds: random malformed
input exploration (the quinn CVE class), stateful interleavings (target
4), and adversarial exploration of the peek/disambiguation seam that
example-based tests structurally cannot enumerate.
Verification additions once landed:
```bash
cargo test --manifest-path fuzz/shared/Cargo.toml # corpus replay — the standing fuzz gate
```
…added to the AGENTS.md checklist alongside the existing commands. The
wasm checks (`cargo check --target wasm32-unknown-unknown`,
`cargo clippy --target wasm32-unknown-unknown -- -D warnings`) remain
untouched by fuzz work but must still pass after the root `Cargo.toml`
gains its `[workspace]` table.
+7
View File
@@ -0,0 +1,7 @@
target
corpus/*
!corpus/chunk_frame
!corpus/negotiation_frame
!corpus/control_json
artifacts
coverage
+1312
View File
File diff suppressed because it is too large. Load diff
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "alktty-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
alktty-fuzz-shared = { path = "shared" }
[dependencies.alktty]
path = ".."
[[bin]]
name = "chunk_frame"
path = "fuzz_targets/chunk_frame.rs"
test = false
doc = false
bench = false
[[bin]]
name = "negotiation_frame"
path = "fuzz_targets/negotiation_frame.rs"
test = false
doc = false
bench = false
[[bin]]
name = "control_json"
path = "fuzz_targets/control_json.rs"
test = false
doc = false
bench = false
[workspace]
+60
View File
@@ -0,0 +1,60 @@
# alktty fuzzing
cargo-fuzz targets for the wire-facing parse surfaces. The design and
operating rules live in `docs/plans/fuzzing.md` (adopted from alkcall's
`docs/research/fuzzing.md`); this README is the operational cheat-sheet.
## Layout
- `fuzz_targets/` — nightly-only `fuzz_target!` binaries (thin wrappers).
- `shared/` — stable-toolchain library holding the invariant logic; the
corpus replay tests run here on plain `cargo test`.
- `corpus/<target>/` — committed seeds (regenerate with
`python3 fuzz/gen_fuzz_seeds.py`).
- `artifacts/` — gitignored crash/oom/timeout artifacts + campaign logs.
## Targets
| Target | Drives |
|---|---|
| `chunk_frame` | `ChunkReader`/`ChunkWriter` (5-byte chunk codec, ADR-001) |
| `negotiation_frame` | `NegotiationReader`/`NegotiationWriter` + `NegotiateRequest` + `error_response_bytes` + the cross-codec peek-disambiguation seam |
| `control_json` | `ControlMessage::from_slice`/`to_json` + `signal_from_name` |
## Running a campaign — always detached
Agent sessions must never run fuzzing in the foreground (an OOM in a
target can take down the session host; see docs/plans/fuzzing.md §3).
Use the detached runner:
```bash
fuzz/run-detached.sh chunk_frame
# poll:
tail -n 50 fuzz/artifacts/chunk_frame-*.log
ls fuzz/artifacts/chunk_frame/
pgrep -f "cargo fuzz run chunk_frame"
```
`FUZZ_RUNTIME_SECS=1800 fuzz/run-detached.sh negotiation_frame` for a
longer campaign. The runner pins `-fork=1 -rss_limit_mb=2048
-malloc_limit_mb=2048 -timeout=25` and detaches via `setsid` + `nohup`.
## Corpus replay (the standing fuzz gate)
```bash
cargo test --manifest-path fuzz/shared/Cargo.toml
```
Replays every committed seed through the same invariant functions the
fuzzer runs, on stable, no nightly needed. Part of the release
verification checklist (AGENTS.md).
## Toolchain notes
`rust-toolchain.toml` pins nightly for this subtree only (llvm-tools
required by cargo-fuzz); the crate itself stays on stable at MSRV 1.88.
`cargo fuzz build` must be run with the fuzz dir as CWD or trust the
toolchain file (rustup resolves per directory).
Run campaigns before releases and after touching `src/wire.rs`,
`src/negotiation.rs`, `src/control.rs`, the adapter, or the session pump.
View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 1809 files, more files were not shown because too many files have changed in this diff. Show more