Files
glm-5.3-flash 0ceecf582b docs: remove em dashes from repo prose and forbid them going forward
- Rewrote the remaining em dashes outside docs/reviews/ (preserved as
  historical record, matching the alktty precedent): tests/ (313),
  fuzz/ (34 plus one shell comment), tasks/ (671 plus 12 kept in
  fences), docs/research + sdd_process + plans (525 plus 25 in
  fences), docs/architecture/overview.md fence labels (13), README,
  CHANGELOG, AGENTS.md, fuzz/README.md (176), and the
  .opencode/agents prompts (110)
- Rewrote fenced content too: ASCII tree and module-map label
  separators, Rust snippet comments, and template placeholders; wire
  data, JSON/BAST examples, and transcripts survive verbatim
- Rewrote 6 assert/expect message literals in tests/ and 2 format
  strings in fuzz/shared/src; no test data changed
- Added Project Convention 18: no em dashes in prose (doc comments,
  markdown including docs/ and tasks/, agent prompts, commit
  messages); fence content rule, en-dash clause rule, and the
  no-spaced-hyphen prohibition follow alktty's convention 15

Follows 6b20924 (src doc comments) and cd0afe0 (docs/architecture).

Verification: cargo test (141 pass); cargo clippy --all-targets -D
warnings; cargo fmt --check; cargo doc --no-deps; wasm check and
clippy for wasm32-unknown-unknown with -D warnings; em dash grep over
the repo returns matches only in docs/reviews/
2026-11-03 14:12:46 +00:00

596 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fuzzing alktunnels: Proposal and Plan
**Status:** adopted; wave-1 complete (`datagram_codec` +
`params_parse` implemented, seeds committed, two 10-min campaigns
clean, two harness-model findings fixed; see §7); wave-2
(`session_pumps`) implemented and campaigning (see §3.3/§7).
Campaigns run via the detached runner (§6), **never as a foreground
child of an agent session** (§5).
**Date:** 2026-09-28
**Upstream precedent:** `/workspace/@alkdev/alkcall/docs/research/fuzzing.md`
(0.8.0 to 0.8.1 campaign; one real finding: duplicate adopt/open destroying
the live channel, fixed in 0.8.1). Tooling, corpus policy, runner, and the
no-hosted-CI posture are inherited from that doc wholesale: this doc does
not re-litigate them; it scopes them to this crate.
---
## 1. Should this crate be fuzzed?
**Yes, but scoped smaller than alkcall's.** The decision follows from
three lines, all pointing the same way:
1. **alkcall's fuzz doc already assigned this to us.** Its §5
"non-surfaces (deliberately)" note ends: *the channels layer carries
opaque `Bytes` and the handler owns sub-stream framing (ADR-035);
those downstream parsers are downstream crates' fuzz targets.* Our
`wire.rs` datagram codec IS that named downstream target. Fuzzing it
completes alkcall's fuzz scope rather than duplicating it.
2. **We own one hand-rolled parse surface of exactly the covered class.**
`DatagramReader::feed` (`src/wire.rs:100`) is an incremental
length-prefix state machine reassembling `[len: u16 BE][payload]`
frames across arbitrary chunk boundaries: length arithmetic and
boundary handling, the dominant rust-fuzz trophy-case classes. It is
sync, pure, allocation-light, and infallible **by contract** (the
decode direction cannot fail; oversize is rejected at frame time),
and a fuzz target is how that contract is *demonstrated* rather than
believed.
3. **The stateful DoS class applies to us directly.** One pre-fuzz
code-review candidate, the analog of alkcall §6.2-1 (the
early-arrival key-space): `DatagramHalves.pending`
(`src/consumer.rs:78`) queues every datagram decoded from a chunk
beyond the one returned to the caller. A single 16 MiB chunk of
`[00 00]` pairs = ~8.4 M empty datagrams → ~8.4 M `VecDeque`
entries (≈ 400+ MiB) from ONE peer chunk, a ~25–50× memory
amplification riding a bounded channel buffer. Example-based tests
structurally cannot find this; a byte-sequence target with a
pending-bytes assertion will.
**The honest caveat** (mirroring alkcall §1): the highest-value alkcall
targets (envelope framing, chunk header, demux, manager) are upstream
and now fuzzed (0.8.1). Our params parsing is serde + registry-side
jsonschema (upstream's `spec_parse` target covers attacker-shaped
schemas compiling at registration). Expected yield here: datagram-codec
pathologies, the pending-queue bound (§1.3), serde/untagged-enum edge
cases in params, and lifecycle invariants in the two-pump shape. That
is worthwhile, not five-targets-worth of new machinery on day one,
hence the two-wave scope in §4. If the first campaigns find nothing,
that is the successful-negative-result posture alkcall's §1 codified:
"we think the codec is robust" becomes a demonstrated property, and the
targets become regression protection for a wire-stable format
(ADR-003).
## 2. Toolchain (inherited, not re-decided)
Identical to alkcall §4: **cargo-fuzz 0.13.2 + `arbitrary` 1.4.x**,
nightly confined to the `fuzz/` subtree via `fuzz/rust-toolchain.toml`,
the crate itself stays stable at MSRV 1.88. AFL optional/deferred;
bolero deferred (same async-friction argument); libafl/honggfuzz/OSS-Fuzz
not adopted. Sanitizer: `--sanitizer=address` with debug assertions
(cargo-fuzz defaults); no MSAN (no `unsafe` in this crate).
**Layout (the alkcall pattern, load-bearing):**
```
fuzz/
├── Cargo.toml alktunnels-fuzz (nightly-only bins; own [workspace])
├── rust-toolchain.toml pins nightly for this subtree only
├── fuzz_targets/ thin fuzz_target! wrappers (3-line each)
│ ├── datagram_codec.rs
│ ├── params_parse.rs
│ └── session_pumps.rs
├── shared/ alktunnels-fuzz-shared, the STABLE-toolchain library
│ └── src/… invariant logic + corpus replay (§3)
├── corpus/<target>/ committed hand-made seeds (§3)
├── gen_fuzz_seeds.py deterministic seed generator (quiche pattern)
├── run-detached.sh §6 detached runner (CWD-independent)
└── README.md
```
- **Invariant logic lives in `fuzz/shared/`**, not inline in the target
binaries: the stable-toolchain corpus replay
(`cargo test --manifest-path fuzz/shared/Cargo.toml`) runs the SAME
invariant functions the fuzzer runs, on stable, in the release
verification checklist. This is CI tier-3's deliverable (alkcall
§7.2) minus automation; per alkcall §7.9 this repo has no hosted CI
and will not get one (same gitea posture); corpus replay as plain
`cargo test` is the fuzz gate.
- **Root `Cargo.toml` needs the explicit `[workspace]` table**
(`members = ["."]`, `exclude = ["fuzz"]`) and `"fuzz/"` on the
publish exclude list; without it cargo auto-discovers `fuzz/shared/`
into the main workspace and the MSRV toolchain tries to build the
nightly-consumed dev-deps (the exact failure alkcall hit).
- **Corpus policy:** commit hand-made seeds, gitignore grown corpora
(quinn/h2 model). Seeds per target: valid frames, truncation at every
short prefix length, `len = 0` / `len = MAX` / `len = MAX+1`, empty
datagrams, batching (two frames per chunk), splitting (one frame per
many chunks), and for params: every substrate value, unknown
substrate, unknown/missing fields, `listen: 0`, malformed reply
shapes, untagged cross-shape targets, deep/invalid JSON.
## 3. Invariants by target
### 3.1 `datagram_codec` (wave 1)
Input: raw `&[u8]` (and in the shared logic, chunk-slice parameter
arbitrary). Drives `DatagramReader::feed` + `frame_datagram`
(`src/wire.rs`). Invariants:
- **no-panic on any byte sequence** (the baseline; the one
`unreachable!` in `feed` is a structural invariant of the state
machine; the target is how it stays true).
- **infallibility**: `feed` never errors (contract: oversize is
rejected at frame time; the decode direction is infallible by
design, `src/wire.rs` docs).
- **datagram-size bound**: every yielded datagram is
`<= MAX_DATAGRAM_LEN` (u16 frame field makes this structural; the
target pins it against future refactors).
- **no partial yields**: a datagram completes only when exactly
`declared` payload bytes are buffered (`is_mid_datagram` observable
consistency); `feed(&[])` never yields anything and never clears
mid-datagram state.
- **round-trip**: `feed(frame_datagram(x)) == [x]` for any x; the
reverse direction `write(feed-parse(x))` re-frames to the same
frame for any well-framed input.
- **F-2 sentinel layering**: `len == 0` is a legal empty datagram,
never EOF; the codec never emits zero-length datagrams for non-empty
frames and never treats an empty datagram as stream end.
- **consumption accounting**: re-framing all consumed bytes and
feeding the byte-level remainder yields the same datagram stream
(splitting a feed at any boundary is transparent).
- **oversize frame rejection**: `frame_datagram` errors iff payload >
`MAX_DATAGRAM_LEN`, echoing the sizes.
- **pending-queue bound (the §1.3 finding-class assertion)**: total
bytes retained across `buf` + `current` + `pending` after a feed is
bounded by the fed chunk size plus the one in-flight frame, so
an adversarial peer chunk cannot force unbounded queue growth in
the decoder state. (NOTE: this is the *decoder-state* bound; the
consumer-side `DatagramHalves.pending` backlog between chunk-read
and `recv_datagram` pulls is a session-shape question tracked in
§4/§8 (the wave-2 stateful target, or a code fix if the bound is
judged unacceptable. The fuzzer documents the amplification factor;
the remediation decision is ours. **Wave-2 resolution (§3.3):** the
session-shape backlog is now an asserted invariant: the harness's
per-op drain holds the pending queue to the routed-batch remainder
(≤ one chunk's worth of batched datagrams, drained by
`recv_datagram` pulls), so the amplification question is bounded
by construction + assertion rather than remediated; the
`datagram_backlog()` diagnostic exposes the live count.)
### 3.2 `params_parse` (wave 1)
Input: raw bytes → `serde_json` (with alkcall's bounded
`arbitrary_value` approach, depth/width-capped `Value` generation) →
`TunnelParams` / `TunnelDirectParams` / `TunnelForwardedParams` /
`SubstrateAddr` deserialization + `parse_listen_reply` /
`listen_reply_fields` (`src/params.rs`). Invariants:
- **no-panic on any JSON-shaped input** (untagged-enum serde dispatch,
`deny_unknown_fields`, the `positive_u32` pin).
- **clean failure only**: parse failures are `Err`, never panics;
`parse_listen_reply` returns `TunnelError::MalformedReply` on
malformed shapes.
- **round-trip for valid params**: `from_json(to_json(p)) == p` for
constructed params of every substrate.
- **deny-unknown posture**: unknown top-level fields reject;
untagged-variant leniency (extra fields inside a `SubstrateAddr`
variant) is the documented asymmetry (asserted, not "fixed").
- **the `listen: 0` pin**: zero rejects at serde level.
- **projection agreement**: `listen_reply_fields` yields both-`Some`
iff `parse_listen_reply` is `Ok`; it never panics and never refuses
construction (the lenient view projects malformed to both-`None`).
- **establishment-reason surface**: `establishment_reason` never
panics on arbitrary `ChannelOpenError` shapes.
### 3.3 `session_pumps` (wave 2)
Input: op sequences (`#[derive(Arbitrary)]` `SessionSequence`, the
`manager_routing` pattern) over one live channels stack per exec:
MuxRunner → raw chunk stream → the real `run_demux_loop_for_client`
demux → `ChannelManager` per side (producer Connect / consumer
Accept over one duplex transport, the reverse POC's topology). Ops:
`Route` (producer→consumer framed datagrams / raw stream bytes via
`route_payload`), `AdoptDatagram` / `AdoptStream` (the
`TunnelSession` data-plane mirrors: codec reader + `pending`, and
`pump_bidi` against in-process peer halves), `Pump` (peer→channel;
`len == 0` drops the p2c source, the EOF path), `Teardown`,
`ClearAll`. Invariants:
- **no-panic / no-wedge on any op sequence** (the baseline; the
harness's 5 s reconcile bound turns a wedged pump into a finding,
and §5's `-timeout=25` fork containment is the outer net).
- **two-pump shutdown-on-completion (convention 8)**: after the p2c
source drops, the pump's channel-side shutdown must propagate and
the pump must complete once the teardown EOFs the c2p source,
pinned by the `reconcile` join with the 5 s bound.
- **lossless p2c copy**: the pump's `p2c` count equals exactly what
the harness pushed into the peer half (copy never truncates).
- **EOF-sentinel teardown semantics**: after a sentinel (teardown /
`clear_all`), the reassembled stream drains its queue before
reporting EOF, with every routed datagram consumable and nothing
lost;
the consumer map empties; the codec's `len == 0` sentinel layering
holds at the session layer (a routed empty datagram is data; the
channels EOF is a separate mechanism).
- **FIFO reconciliation**: every complete frame routed through the
real demux path arrives as exactly one datagram of the declared
length, in order (the model walks the framing as the codec does,
the wave-1 F-2 walk reused at the session layer).
- **pending-queue bound (the §1.3 finding-class assertion)**: the
session-shape `DatagramHalves.pending` backlog never exceeds the
unrouted remainder (i.e. the backlog holds only datagrams decoded
from routed chunks beyond the ones consumed; with the harness's
per-op drain the queue returns to zero after every op, so an
amplification factor > 1× chunk size would trip the assert. (The
wave-1 decoder-state bound re-asserted per op alongside.)
- **monotonicity**: the manager's early-arrival / dropped counters
survive `clear_all` (the alkcall §7.7 pattern), and the consumer
map empties on the transport-EOF path.
Bounds: 128 ops, ≤ 512-byte payloads (generator-clamped; the codec's
`MAX_DATAGRAM_LEN` stays the wire bound). Even-id masking (ADR-047
§5: the consumer is the Accept side; the producer allocates even).
## 4. Scope: two waves
**Wave 1 (complete):** the two targets above. Both drive already-
`pub` APIs, with no `#[cfg(fuzzing)]` exposure needed (matching alkcall's
experience where none of five targets needed it).
**Wave 2 (this session):** `session_pumps`, the stateful op-sequence
target over the two-pump tunnel shape in the alkcall `manager_routing`
style (`#[derive(Arbitrary)]` op enum; live mux + demux runners; the
alkcall harness lessons applied: even-id masking, drained-state
asserts). Drives: the `TunnelSession` data-plane shapes (the datagram
codec plane + the `pump_bidi` stream plane) over a REAL channels
stack: the two-pump shutdown-on-completion contract (AGENTS.md
convention 8), the EOF-sentinel teardown semantics, and the §1.3
backlog bound as byte-accounting invariants (§3.3). The
`forwarded_routing` correlation target stays deferred until the
assembly-side correlation registry (ADR-008 §3, assembly
bookkeeping) grows into this crate.
Sequencing mirrors alkcall §7.4: init + wave-1 targets → 10–30 min
campaigns per target via the detached runner → fix anything found →
wave 2 (`session_pumps`) → further targets when justified.
## 5. Operational isolation: campaigns never share fate with the agent session
**This is an environment constraint, not a nice-to-have** (upstream
doc §7.6, adopted verbatim as policy here). Agent sessions (opencode)
run fuzzer builds/campaigns through a bash tool that spawns the fuzzer
as a **child of the session's own process tree and cgroup**. A fuzz
run is precisely the workload shape that kills its own host:
- **OOM-killer shared fate**: the target's worst case IS unbounded
allocation. If libFuzzer's soft RSS guard loses the race with the
kernel, the OOM killer fires, and it picks the largest-RSS process
in the cgroup, which can be the opencode server hosting the session.
That is *recoverable* (sessions are recorded in a DB) but hugely
derailing, since
the session's live environment access dies mid-task.
An OOM in a fuzz target must cost the fuzzer, never the session.
- **Fork bombs / CPU saturation**: `-fork=N` workers; a runaway
campaign starves the session host.
- **Wedged targets**: a deadlock in an async harness would hang a
foreground bash call forever.
Three layers of defense, cheapest first, **all mandatory**:
1. **libFuzzer soft limits (always on)**: `-rss_limit_mb=2048`
`-malloc_limit_mb=2048` make libFuzzer exit cleanly (artifact
recorded) before the kernel gets involved. Note the RSS limit is
*soft* (polls `/proc/self/statm`); a fast huge allocation can beat
it, and that is
what layers 2–3 are for. Never `ulimit -v` with ASAN
(it reserves terabytes of virtual address space); the ASAN-safe
hard-limit recipe is `-fork=1`.
2. **Fork mode as blast-radius containment (default)**: `-fork=1`
(or a small number) runs each input in a short-lived child, so a
crash/timeout/OOM/leak kills only that child; the harness survives,
records the artifact, keeps fuzzing. Also the deadlock answer: the
per-input timeout kills the child, not the session's bash call.
3. **Process detachment from the session tree (MANDATORY for agent-run
campaigns)**: never run fuzzing as a foreground child of the
session. The runner script wraps `cargo fuzz run` in `setsid` +
`nohup` + full redirection to a log file, which (a) removes the
fuzzer
from the session's controlling terminal and signal relation, (b)
survives the session ending, (c) gives the agent a pollable log +
artifact directory instead of a blocking call. Combined with fork
mode, an OOM-class finding costs one child process.
**The runner** (`fuzz/run-detached.sh`, CWD-independent, since it
resolves its
own repo root):
```bash
#!/usr/bin/env bash
# Detached fuzzing runner for agent sessions: the campaign never runs
# as a foreground child of the session (an OOM in a target must cost
# the fuzzer, never the agent host), and survives the session ending.
set -euo pipefail
target="${1:?usage: run-detached.sh <target> [extra libfuzzer args...]}"
shift
runtime="${FUZZ_RUNTIME_SECS:-600}"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
log_dir="$root/fuzz/artifacts"
mkdir -p "$log_dir"
log="$log_dir/${target}-$(date -u +%Y%m%d-%H%M%S).log"
cd "$root"
setsid nohup cargo fuzz run "$target" -- \
-fork=1 -rss_limit_mb=2048 -malloc_limit_mb=2048 -timeout=25 \
-max_total_time="$runtime" "$@" \
>"$log" 2>&1 < /dev/null &
echo "pid=$! log=$log"
```
**Agent polling protocol** (never `wait` on the detached process from a
tool call; poll instead):
- `tail -n 50 fuzz/artifacts/<target>-*.log`: campaign progress.
- `ls fuzz/artifacts/`: `crash-*` / `oom-*` / `timeout-*` / `leak-*`
artifacts are the findings.
- `pgrep -f "cargo fuzz run <target>"`: still running?
- One detached campaign per target at a time; log filenames carry the
UTC timestamp.
**Flags worth pinning beyond the runner defaults**: `-max_len=65536`
on the codec target; `-use_value_profile=1`; a JSON token `-dict` for
the params target (alkcall's `json.dict` transfers as-is).
## 6. No hosted CI
Per alkcall §7.9, the standing policy applies to this repo identically:
no hosted CI will exist; do not add Actions/Gitea-Actions workflow
files. **Corpus replay is the fuzz gate as plain `cargo test`**:
`cargo test --manifest-path fuzz/shared/Cargo.toml` replays every
committed seed through the invariant functions; add it to the release
verification checklist (AGENTS.md). Campaigns run manually via the
detached runner (§6), e.g. before a release, or after touching
`src/wire.rs`, the consumer data planes, or `src/params.rs`.
## 7. Implementation log
- **2026-09-28, proposal adopted** (this doc). Wave-1 targets
`datagram_codec` + `params_parse`; wave-2 deferred (§4).
- **2026-09-28, `alkcall` bumped 0.8.0 → 0.8.1.** Bug-fix release, no
API/wire change; the fix (duplicate adopt/open no longer destroying
the live channel) is directly relevant to this crate's
`TunnelSession::adopt` path: the changelog's "open-op response
replayed / id re-announced" scenario is our reverse path. Verification:
full suite (135 passed), clippy, fmt, wasm check (§8 commands).
- **2026-09-28, wave-1 implemented.** `fuzz/` workspace laid out per
§2 (nightly-pinned subtree, stable `fuzz/shared` library, runner,
dict, generator); `datagram_codec` + `params_parse` targets over
already-`pub` APIs, with no `#[cfg(fuzzing)]` exposure needed. One new
public diagnostic: `DatagramReader::retained_bytes()` (the
decoder-state byte count the §3.1 bound asserts against, in the
`is_mid_datagram` diagnostic style). Seeds committed: 27 +
69 generated (fuzz/gen_fuzz_seeds.py) + 1 campaign-found crash
input promoted to `datagram_codec/seed-900` (see the finding below).
Stable side: corpus replay green, shared crate clippy/fmt clean;
main suite untouched and green; `cargo fuzz build` clean.
- **2026-09-28, first campaign finding: a HARNESS-model bug, not a
crate bug.** The `datagram_codec` campaign crashed in its first
minute on input `00 2b 01 00 00 05` (`fuzz/artifacts/datagram_codec/
crash-e9ba284e…`, exit 77, fork-mode containment worked as
designed: the harness parent survived and persisted the artifact).
Analysis: the codec is CORRECT, since `00 2b` declares a 43-byte
datagram; `01 00 00 05` is its in-flight payload; nothing is
yielded. The invariant that tripped ("an empty datagram must come
only from a [00 00] length header") was mis-modeled: it counted
`00 00` byte pairs anywhere in the RAW stream, but a `00 00` pair
sitting INSIDE a frame's payload region is payload, not a length
header, which is exactly the same class as alkcall §7.7's probe (the
harness claimed `4 + len` when the prefix counts only the body).
Fix: the count now walks the framing as the decoder does (complete
frames from the start; the walk stops where the decoder would be
stuck mid-frame). The crash input is committed as seed-900 and
replays clean; the campaign relaunched clean.
- **2026-09-28, second harness finding (caught by corpus replay,
pre-campaign): `establishment_reason` is a pass-through extractor,
not a validated table.** The params.rs doc names the pinned reason
strings, but the upstream implementation (alkcall
`channels/client.rs` `establishment_reason`) extracts
`details.reason` AS-IS whenever the code is `channel:open_failed`,
so
a made-up reason is surfaced as-is by design, and validation is the
consumer's job. The harness first asserted a closed set of reason
strings and corpus replay tripped it on `made_up_reason` before any
campaign ran. Invariant corrected to assert the extraction shape
(Some iff code == `channel:open_failed` and reason is a string).
- **2026-09-28, second harness finding (same replay pass):
`is_mid_datagram` means a partial DATAGRAM, not buffered header
bytes.** A dangling 1-byte length prefix at input end is buffered
header state (no frame in flight, `is_mid_datagram == false`);
the harness first asserted `true` for that case. Corrected:
mid-datagram iff the framing walk leaves a remainder ≥ 2 bytes
(a parsed-but-unfinished payload), not `!= 0`.
- **2026-09-28, wave-1 campaigns run (10-min detached runs, §6 runner;
`-fork=1 -rss_limit_mb=2048 -malloc_limit_mb=2048 -timeout=25`;
codec with `-max_len=65536`, params with `-dict=json.dict
-use_value_profile=1`). Both exited 0.**
- `params_parse`: 3.14M execs (~5.3k exec/s), 33 fork jobs,
`oom/timeout/crash: 0/0/0` throughout. Coverage 2049 edges /
15.6k features; in-memory corpus 69 committed seeds → 3018
entries; the serde/untagged-enum shape space was still expanding
at budget end, so longer campaigns keep paying here.
- `datagram_codec` (post-harness-fix): 827k execs, 33 fork jobs,
`oom/timeout/crash: 0/0/0` throughout. Coverage saturated at 221
edges (ft 1198); the 2-byte-header state machine is fully
enumerated, matching alkcall §7.7's `chunk_header` saturation
profile. Exec/s reads low (~400) because each exec replays the
input through a ~35-feed 3-byte-sliced sequence alongside the
whole-input feed; the state space is small and exhausted early.
- The pre-fix `datagram_codec` run (the §7 finding above) crashed at
job 4 and exited 77; fork-mode containment behaved as designed,
with the harness surviving, persisting the artifact, and the run
wrapping up. That artifact is the promoted seed-900.
- Grown corpora (3018 params entries) were **not** merged into the
committed seeds, since
per the §2 policy, grown corpora stay
gitignored.
- Operational note for future sessions: `cargo fuzz run` returns
before its last fork-mode children fully exit; a `pkill -f
"cargo fuzz run <target>"` followed immediately by another
`pgrep` of the same pattern can wedge a bash tool call on its own
process match. Prefer `pgrep -af` to LIST, kill by explicit pid
from the runner's echoed `pid=`, and never pattern-kill-and-recheck
in one call.
- **NO crate bugs found by wave 1.** The two findings this session
were both harness-model defects (§7 above), each invisible to
example-based review and caught by fuzzing/replay: the alkcall §7.7
"fuzzer as a harness-checker" pattern, confirmed. The §1.3
decoder-state bound held everywhere: `retained_bytes()` never
exceeded fed + one frame on any adversarial input. The session-shape
`DatagramHalves.pending` amplification remains a documented
§1.3/§4 note (wave-2's motivation), not a campaign finding, since
the
wave-1 targets assert the decoder, not the session pump.
- **2026-09-28, wave-2 implemented: `session_pumps` (the stateful
two-pump/EOF-sentinel target, §3.3).** The harness mirrors
`TunnelSession`'s data-plane shapes over one REAL channels stack
per exec (MuxRunner → raw chunk stream → the real
`run_demux_loop_for_client` demux → `ChannelManager` per side over
one duplex transport; producer Connect / consumer Accept, the
reverse POC's topology). Ops: Route/AdoptDatagram/AdoptStream/
Pump/Teardown/ClearAll; invariants per §3.3. One diagnostic
addition to the crate: `TunnelSession::datagram_backlog()` (the
`DatagramHalves.pending` count the §3.3 bound asserts against, in
the `retained_bytes()` diagnostic style) + a suite test pinning
its rest states. Seeds: 8 committed (deterministic decode-scan
hex patterns, the `manager_routing` precedent, since
arbitrary ships
no encoder) + 2 in-source semantic fixtures (the §1.3 batched-
backlog probe; the W4 half-close shape). Two harness-model
findings caught by corpus replay pre-campaign:
- **the p2c-EOF pump-join wedge is the DESIGNED half-close
shape, not a wedge.** The first `reconcile` draft awaited the
pump after dropping only the p2c source, but the c2p pump's
source (the channel read half) EOFs only via teardown, so the
join legitimately blocked (the 5 s bound tripped). `pump_bidi`
completes when BOTH pumps finish; the half-close contract is
that the p2c completion shuts the channel side down (the
observable), and the pump completes once the c2p source EOFs
too. The harness now models exactly that: drop p2c source →
teardown the entry → join with the 5 s bound. (The alkcall §7.7
harness-checker pattern again: the fuzzer harness's first
campaign-found crash in wave 1 and this replay catch are both
harness-model defects, not crate bugs.)
- **the stream-plane c2p accounting is asynchronous**: the pump
writes channel→peer into the peer duplex while a far-end reader
task counts; the reconcile join (after the pump completes) is
when the count is final. First draft read it early. Fixed by
reading after the reader task's join.
- **2026-09-28, third harness finding (campaign-found, fork-mode
containment as designed): duplicate adopt is a LEGAL manager
outcome, not a finding.** The first campaign crashed at ~49 s on
`AdoptDatagram { channel_id: 0 }` after the id had already been
adopted. `ChannelExists` is the manager's documented
id-collision error, and my adopt arms treated every error as a
finding. Fix: both adopt arms model the documented error table
(`ChannelExists` / `TooManyChannels` are no-ops, exactly the
alkcall `manager_routing` harness's treatment). The crash input
is committed as seed-900 and replays clean; the campaign
relaunched clean.
- **2026-09-28, fourth harness finding (second campaign): the
harness modeled the producer→consumer path through the WRONG
primitive.** The second campaign crashed at ~5 s on `Route`:
raw-id addressing (`even_id` masked arbitrary ids; collisions +
an `UnknownChannel`-class state the model didn't track), `len ==
0` raw routes doubling as EOF sentinels mid-model, and
(the structural one) `route_payload` was used as the producer's
forward write. `route_payload` routes into the LOCAL manager's
channel entry (the demux direction); the producer pump handler
writes the channel's SEND half (the mux pump frames onto the
transport). Bytes never reached the consumer. Rewrite (the real
protocol shape, matching `manager_routing`'s op-vocabulary
lesson): producer `Open` (`open_channel` allocates the id, the
harness holds the send half, the pump handler's write side),
session-INDEX addressing (no masking, no collisions), `Adopt`
consumes the producer-allocated id (the early-arrival drain is
the real adopt-race path), `Route` writes the producer send half
(the real wire path: mux pump → transport → consumer demux →
reassembly), `Pull` (the `recv_datagram` pull pops the backlog +
the model), and a STATEFUL framing model (partial frames persist
across ops, with the wave-1 walk carried across ops). EOF sentinels
now arise only from teardown/sender-drop (the real mechanisms),
never from `len == 0` routes (F-2: a routed empty datagram is
DATA). All committed session_pumps seeds regenerated by the
rewrite (the old hex patterns decode against the new op enum);
the semantic fixtures re-pinned. Verification of the fix: the
campaign's 0-exec crash went away; batched frames now reconcile
FIFO through the real two-manager stack.
- **2026-09-28, fifth + sixth harness findings (third campaign's
first minutes): the two ClearAll modeling defects.** (5) A
double-adopt of a live session panicked the harness's own guard,
but a re-adopt of a LIVE id is the manager's documented
`ChannelExists` outcome (a legal no-op; alkcall 0.8.1's
insert-replace fix leaves the live entry intact). Modeled as a
no-op. (6) A `ClearAll` followed by `Route` on the same session
tripped the backlog reconciliation. The harness retired only the
sessions whose ids appeared in `clear_all`'s RETURN value, but
that value is the OPENER-LEDGER records (ADR-047 §7): adopted
channels carry no ledger entry, so the set is empty while the map
itself empties. The alkcall §7.7 lesson ("`drained.len() ==
open_count()` is not an invariant"), learned again at this
crate's layer: the map-empties assert is the ground truth; the
harness now retires EVERY session on `ClearAll`.
- **2026-09-28, wave-2 campaign run (10-min detached run, §6
runner; `-fork=1 -rss_limit_mb=2048 -malloc_limit_mb=2048
-timeout=25 -max_len=4096 -use_value_profile=1`): exited 0.**
228,676 execs (~380 exec/s; each exec replays up to 128 ops on a
live two-manager stack with real mux/demux tasks), 32 fork jobs,
`oom/timeout/crash: 0/0/0` throughout. Coverage 3427 edges /
17,529 features / 1184 in-memory corpus entries and STILL
EXPANDING at budget end (the op-interleaving space is much larger
than the parser targets'), so longer campaigns keep paying. The
two-pump contract held across every adversarial sequence: no
wedged pump (the 5 s reconcile join), no lost datagram (the
FIFO/backlog reconciliation), no premature EOF (the sentinel
layering). The §1.3 backlog bound held everywhere: the
`DatagramHalves.pending` queue never exceeded the
routed-but-unpulled remainder.
- **NO crate bugs found by wave 2.** All six findings across waves
1–2 were harness-model defects (four this session), each
invisible to example-based review and caught by fuzzing/replay:
the alkcall §7.7 "fuzzer as a harness-checker" pattern,
re-confirmed. The stateful thesis is validated from the harness
side: the harness defects themselves (double-adopt semantics, the
clear_all ledger/map distinction, the route_payload direction)
are exactly the interleaving-shape mistakes the alkcall §7.8
campaigns caught in their own drivers.
- (progress appended as implementation proceeds; see git log)
## 8. Verification commands
```bash
cargo test # main suite (stable)
cargo test --manifest-path fuzz/shared/Cargo.toml # corpus replay (fuzz gate)
cargo fuzz build # in fuzz/ (nightly, ASAN)
fuzz/run-detached.sh <target> # campaigns (never foreground)
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo clippy --manifest-path fuzz/shared/Cargo.toml --all-targets -- -D warnings
cargo fmt --manifest-path fuzz/shared/Cargo.toml --check
cargo check --target wasm32-unknown-unknown # default crate stays wasm-clean
```
Campaign flags per target: `datagram_codec -max_len=65536`;
`params_parse -dict=json.dict -use_value_profile=1`;
`session_pumps -max_len=4096 -use_value_profile=1`.
## 9. References
- Upstream campaign + operational model (authoritative for tooling,
runner, corpus policy, no-CI posture):
`/workspace/@alkdev/alkcall/docs/research/fuzzing.md`
- alkcall 0.8.1 changelog: the finding our adopt path is exposed to
- This crate: ADR-003 (codec framing), ADR-002 (ALPN), ADR-007/008
(direct/forwarded ops), `docs/architecture/wire.md`
- External: RUSTSEC-2026-0037 (quinn-proto, the maintainers'
fuzzing-coverage statement), rust-fuzz trophy case, cargo-fuzz Rust
Fuzz Book (all linked from the upstream doc's §9)