fuzz: adopt fuzzing (wave 1) + alkcall 0.8.1 / alktunnels 0.2.1 bumps
Adopt the alk*/fuzzing posture (alkcall 0.8.1 + alktunnels 0.2.1 precedent) before the v0.1 publish — the cheapest moment this crate will ever have for it: - alkcall 0.8.1 + alktunnels 0.2.1: the duplicate-adopt/open fix is directly relevant to this crate's consumer open/adopt paths - fuzz/ workspace (the sibling layout): nightly-pinned subtree, stable fuzz/shared library (corpus replay = the fuzz gate), detached runner, JSON dict, deterministic seed generator; 129 committed seeds - three wave-1 targets over already-pub APIs: datagram_codec (the ASSOCIATE frame codec — the alkcall-fuzz-doc-assigned downstream target), rfc1928_codec (the forked RFC 1928 core's handshake state machines, both protocol directions), params_parse (the wire-stable open-op surface) - one diagnostic: DatagramReader::retained_bytes() (the decoder-state bound's assertion target, the alktunnels retained_bytes() posture) - docs/research/fuzzing.md: the scoped plan + implementation log; AGENTS.md gains the corpus-replay verification line - campaigns: 3 × 10-min detached runs exited 0 (datagram ~1.9M execs, rfc1928 ~2.0M, params ~3.1M; oom/timeout/crash 0/0/0 on all 33 fork jobs per target; coverage saturated on both codec targets). No crate bugs; three harness-model defects found + fixed (2 pre-campaign by corpus replay, 2 campaign-found, crash inputs promoted as seeds 900/ 901): the len==0 well-framed decode partition, the fieldless-struct serde-seq form, the BadAtyp re-frame arm, the short-domain count-walk gate - wave 2 (session_pumps stateful target) deferred to post-0.1 — the byte-level targets carry the attacker-reachable load for the publish Verification: cargo test (15 suites ok, default + all-features), clippy (all-targets + wasm + fuzz/shared), fmt, cargo doc, publish --dry-run, corpus replay 129 seeds green, cargo fuzz build clean. Refs: alkcall docs/research/fuzzing.md (§7.9 posture), alktunnels docs/research/fuzzing.md; docs/research/fuzzing.md (this crate's copy)
This commit is contained in:
1 parent
8af23eeee0
commit
08a3694464
149 files changed
+1822
-5
No files matched your search
@@ -0,0 +1,435 @@
|
||||
# Fuzzing alksocks: Research and Plan
|
||||
|
||||
**Status:** adopted — wave-1 targets (`datagram_codec`, `rfc1928_codec`,
|
||||
`params_parse`) implemented, seeds committed, campaigns run clean (§7).
|
||||
Wave 2 (`session_pumps`) deferred to post-0.1 (§4). 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`
|
||||
(one real finding: the duplicate adopt/open destroying the live channel —
|
||||
fixed in 0.8.1) and
|
||||
`/workspace/@alkdev/alktunnels/docs/research/fuzzing.md` (wave-1/wave-2
|
||||
adopted, six harness-model findings, no crate bugs). Tooling, corpus
|
||||
policy, runner, and the no-hosted-CI posture are inherited from those
|
||||
docs wholesale — this doc does not re-litigate them; it scopes them to
|
||||
this crate.
|
||||
|
||||
---
|
||||
|
||||
## 1. Should this crate be fuzzed?
|
||||
|
||||
**Yes — and alkcall's fuzz doc already assigned this to us.** Three
|
||||
lines, all pointing the same way:
|
||||
|
||||
1. **alkcall's fuzz doc §5 named this crate's surfaces as downstream
|
||||
targets.** Its "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 `src/wire.rs` datagram codec **is** that named
|
||||
downstream target (shaped on alktunnels' codec, which alktunnels
|
||||
fuzzed as its wave-1 first target — the same assignment, landed
|
||||
here). Fuzzing it completes the upstream scope rather than
|
||||
duplicating it.
|
||||
|
||||
2. **We own an entire RFC handshake state machine fed by arbitrary
|
||||
peers — on both sides.** The targeted fork (`src/rfc1928/`, ADR-013)
|
||||
is fully owned code with project conventions applying unconditionally
|
||||
(convention 17). Its parse surfaces — `read_address` (the address
|
||||
codec), `negotiate_auth` (method negotiation), `read_command` (the
|
||||
command encoding), `read_username_password` (RFC 1929), the client's
|
||||
`read_request_reply` — are fed bytes produced by **arbitrary peers**
|
||||
in both directions: the producer drives the server typestate against
|
||||
consumer bytes, and the consumer sessions + front doors drive the
|
||||
client path against producer bytes. An RFC state machine is exactly
|
||||
the shape the quinn-proto CVE class (RUSTSEC-2026-0037 — remote DoS
|
||||
via `unwrap()` in parsing, the upstream doc §1's strongest
|
||||
datapoint) lives in: hand-rolled state transitions, length arithmetic,
|
||||
and boundary handling. One fork-carried correction is already of this
|
||||
class — upstream's `ReplyError::from_u8` ended in `unreachable!()`
|
||||
for unassigned reply codes; the fork made it a typed catch-all (the
|
||||
review's F-1) precisely because the value comes off the wire. A fuzz
|
||||
target is how that posture is *demonstrated* across the whole codec,
|
||||
not believed spotwise.
|
||||
|
||||
3. **The differential suite's blind spot is exactly fuzzing's
|
||||
strength.** `tests/rfc1928-differential.rs` pins our fork's behavior
|
||||
against the reference checkout on *valid* RFC edge cases — curated
|
||||
inputs a human thought of. What no example-based suite provides:
|
||||
random *malformed* input exploration (the panic-on-attacker-bytes
|
||||
class), stateful interleavings, and resource-bound assertions under
|
||||
adversarial sequences. The fork is about to be published (0.1) as
|
||||
wire-stable; after that, targets are regression protection for a
|
||||
frozen surface — cheapest now.
|
||||
|
||||
**The honest caveat** (mirroring both upstream docs): the crate has no
|
||||
`unsafe` (checked at the crate root), the codec's decode direction is
|
||||
infallible by contract (oversize rejected at frame time; the decoder
|
||||
buffers, bounded by the u16 declared length), the RFC 1929 length
|
||||
fields are one byte each (the attacker-controlled byte cannot ask for
|
||||
more than 255), and the fork's error vocabulary is typed end-to-end
|
||||
(the `unreachable!()`/`unwrap` sweep already ran in review 001).
|
||||
Expected yield is therefore not memory-safety bugs; it is (a) the
|
||||
demonstration that the infallibility/no-panic contracts hold on
|
||||
adversarial input, (b) the decoder-state bound assertion (alktunnels
|
||||
§1.3's finding class — a peer chunk amplifying into unbounded state),
|
||||
and (c) regression protection for the published wire surface. If the
|
||||
campaigns find nothing, that is the successful-negative-result posture
|
||||
alkcall's §1 codified.
|
||||
|
||||
## 2. Toolchain (inherited, not re-decided)
|
||||
|
||||
Identical to both upstream docs: **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 (the 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 upstream pattern, load-bearing):**
|
||||
|
||||
```
|
||||
fuzz/
|
||||
├── Cargo.toml alksocks-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
|
||||
│ ├── rfc1928_codec.rs
|
||||
│ └── params_parse.rs
|
||||
├── shared/ alksocks-fuzz-shared — 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 alkcall §7.2 tier-3's deliverable
|
||||
minus automation; per alkcall §7.9 (adopted here identically) this
|
||||
repo has no hosted CI and will not get one — 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 listed in §3.
|
||||
|
||||
## 3. Invariants by target
|
||||
|
||||
### 3.1 `datagram_codec` (the ASSOCIATE frame codec, `src/wire.rs`)
|
||||
|
||||
Input: raw `&[u8]`. Drives `DatagramReader::feed` + `frame` /
|
||||
`frame_inner` / `addr_encoding_len`. This is the alktunnels wave-1
|
||||
`datagram_codec` target's shape, scoped to this crate's codec (the
|
||||
frame carries `addr + data` inner bytes and decodes to
|
||||
`(TargetAddr, payload)`, not opaque datagrams — the invariants follow
|
||||
the inner structure). Invariants:
|
||||
|
||||
- **no-panic on any byte sequence** (the baseline).
|
||||
- **infallible reassembly**: `feed` never errors at the reassembly
|
||||
layer — every frame completes as `Ok` or as one of the four
|
||||
*decode-garbage* `WireError` variants (`EmptyFrame` / `BadAtyp` /
|
||||
`ShortAddress` / `DomainNotUtf8`); `Oversize`/`Address` are
|
||||
unreachable from `feed` (they are frame-time errors — a shape
|
||||
assertion pins the partition).
|
||||
- **empty-payload F-2 layering**: a decoded `(addr, empty)` pair comes
|
||||
only from a real `[len][addr]` frame whose payload region is empty —
|
||||
never invented by the codec; a `len == 0` frame (zero inner bytes)
|
||||
always surfaces as `EmptyFrame`, never as an empty datagram.
|
||||
- **bad-ATYP loudness**: every `BadAtyp` error's byte is the actual
|
||||
first inner byte of its frame, and it is not one of the three
|
||||
RFC ATYPs (the post-reply control-byte semantic — any non-ATYP
|
||||
control byte must fail loud, never guess).
|
||||
- **consumption accounting / splitting transparency**: feeding in
|
||||
3-byte slices yields exactly the datagram sequence the whole-input
|
||||
feed yields (both directions' framing is transparent to splitting).
|
||||
- **round-trip**: every decoded datagram re-frames via `frame` (or
|
||||
`frame_inner` over its inner bytes) and re-decodes to itself;
|
||||
well-framed input re-frames byte-for-byte.
|
||||
- **joint bound**: `frame` rejects iff
|
||||
`addr_encoding_len(addr) + payload.len() > MAX_INNER_LEN`, echoing
|
||||
`(total, max)` exactly — including the max-domain case where a legal
|
||||
RFC payload (65507 to a 255-char domain = 65766) must reject (the
|
||||
joint-bound reason the codec exists); `frame_inner` rejects iff
|
||||
`inner.len() > MAX_INNER_LEN`. The `checked_add` path (payload
|
||||
overflow wrapping the u16) rejects, never wraps.
|
||||
- **decoder-state bound**: `buf` + in-flight `current` stay bounded by
|
||||
the fed chunk size plus one frame (header + `MAX_INNER_LEN`) on any
|
||||
adversarial input — the alktunnels §1.3 finding-class assertion
|
||||
(state must never grow with input). The crate's `DatagramReader`
|
||||
exposes this via `is_mid_frame()` + a new `retained_bytes()`
|
||||
diagnostic (the alktunnels `retained_bytes()` posture).
|
||||
|
||||
Seeds: valid frames (all three ATYPs), truncation at every prefix
|
||||
length, `len = 0` / `len = MAX` / `len = MAX+1`, empty payloads,
|
||||
batched frames, split frames, garbage ATYP, non-UTF-8 domains, the
|
||||
65766 joint-bound shape, deep batch/split mixes.
|
||||
|
||||
### 3.2 `rfc1928_codec` (the forked RFC 1928 core, `src/rfc1928/`)
|
||||
|
||||
Input: raw `&[u8]`. Drives the fork's byte-level parsers over tokio
|
||||
duplex streams (the in-tree test shape — mock bytes, no sockets):
|
||||
|
||||
- **`read_address`** (the address codec, both directions consume it):
|
||||
no-panic on any ATYP/bytes; every failure is a typed `AddrError`
|
||||
(`IncorrectAddressType` for unknown ATYP — the F-1 class, already
|
||||
fixed; never a panic); round-trip for every well-formed address
|
||||
(`to_be_bytes` → parse → equal).
|
||||
- **the server handshake** (`negotiate_auth` → `read_command` over a
|
||||
duplex, `accept_no_auth` + `read_command`): no-panic on any byte
|
||||
sequence; every malformed shape is a typed `SocksServerError`
|
||||
(version mismatch, unacceptable method, unknown command, bad addr);
|
||||
a well-formed negotiation + CONNECT request parses to the exact
|
||||
command + target.
|
||||
- **the client reply path** (`read_request_reply`): no-panic on any
|
||||
server byte sequence; unassigned reply codes map to the
|
||||
`GeneralFailure` catch-all (the F-1 pin — never `unreachable!()`);
|
||||
version mismatches are typed errors.
|
||||
- **`parse_udp_request`** (the RFC UDP header codec, front doors only
|
||||
but still wire-reachable): no-panic; garbage RSV / unknown ATYP /
|
||||
truncation are typed `UdpHeaderError`s; round-trip with
|
||||
`new_udp_header`.
|
||||
- **auth sub-handshakes** (`read_username_password`, RFC 1929):
|
||||
no-panic; the u8 length fields bound allocations structurally
|
||||
(≤ 255 bytes — asserted); empty username/password are typed errors;
|
||||
round-trip accept/reject status bytes.
|
||||
- **`ReplyError::from_u8`** full-domain table: every `u8` maps, no
|
||||
panic, unassigned → catch-all (the direct probe of the F-1 class).
|
||||
|
||||
This is the wave-1 target of highest value: it is the entire RFC 1928
|
||||
byte surface, fed attacker bytes from both protocol directions, and the
|
||||
fork is owned code whose behavior the differential suite pins only on
|
||||
valid inputs.
|
||||
|
||||
Seeds: valid handshakes (no-auth / password), version mismatches,
|
||||
truncations at every prefix length of each message shape, unknown
|
||||
methods/commands/ATYPs, empty and 255-byte domains, non-UTF-8 domains,
|
||||
unassigned reply codes, RSV garbage.
|
||||
|
||||
### 3.3 `params_parse` (the open-op params, `src/params.rs`)
|
||||
|
||||
Input: raw bytes → `serde_json` (alkcall's bounded `arbitrary_value`
|
||||
generator) → `Socks5Params` deserialization + `socks5_open_spec()` +
|
||||
`SocksEstablishmentReason::parse` / `establishment_reason`. Invariants:
|
||||
|
||||
- **no-panic on any JSON-shaped input** (`deny_unknown_fields`
|
||||
dispatch, the registry-spec builder).
|
||||
- **the trivial-params pin**: `{}` parses, `Socks5Params::as_json()` ==
|
||||
`{}`, and unknown fields always reject (the loud-rejection posture —
|
||||
the serde pin and the schema's `additionalProperties: false` agree).
|
||||
- **the spec is a constant**: `socks5_open_spec()`'s wire-visible
|
||||
fields (op id, ALPN, scopes, schemas) are identical on every
|
||||
execution — the open-op surface is wire-stable (ADR-001); the
|
||||
reason-enum table equals the typed parse table (the schema/typed-view
|
||||
drift guard, mirrored from the in-suite test).
|
||||
- **the reason surface is a pass-through extraction, not a validated
|
||||
table**: `establishment_reason` yields `Some(details.reason)` iff the
|
||||
code is `channel:open_failed` and the reason is a string — any string
|
||||
is surfaced as-is (the alktunnels harness-model finding #2, adopted
|
||||
here as the modeled contract); `SocksEstablishmentReason::parse`
|
||||
never panics and is total.
|
||||
|
||||
### 3.4 What is deliberately NOT a target (yet)
|
||||
|
||||
- **The producer pump (`serve_socks5_over`) / consumer sessions as
|
||||
stateful op-sequence targets** — wave 2, deferred (§4). The
|
||||
handshake-level target (§3.2) covers the byte surface; the
|
||||
session-level interleavings (associate stage select-shape, BIND quiet
|
||||
wait, the two-pump CONNECT plane) are stateful shapes over a live
|
||||
channels stack — the alktunnels wave-2 pattern, worth its own session
|
||||
post-0.1.
|
||||
- **The doors (`door.rs`, `local/`, `composed/`)** — feature-gated
|
||||
assembly; their RFC-codec boundary (`parse_udp_request` /
|
||||
`new_udp_header`) is covered by the §3.2 target. The rest is I/O
|
||||
plumbing over the fuzzed cores, not parse surfaces.
|
||||
- **`front_door` RFC 1929 negotiation** — covered by §3.2's auth
|
||||
sub-handshake invariants (the door drives the same forked parsers).
|
||||
|
||||
## 4. Scope: two waves
|
||||
|
||||
**Wave 1 (this session):** the three targets above. All drive
|
||||
already-`pub` APIs — no `#[cfg(fuzzing)]` exposure needed (matching
|
||||
both upstream docs' experience across all eight targets to date). One
|
||||
diagnostic addition to the crate: `DatagramReader::retained_bytes()`
|
||||
(the decoder-state byte count §3.1's bound asserts against, in
|
||||
alktunnels' `retained_bytes()` style).
|
||||
|
||||
**Wave 2 (deferred — post-0.1):** `session_pumps`, the stateful
|
||||
op-sequence target over the producer pump + consumer sessions in the
|
||||
alkcall `manager_routing` / alktunnels `session_pumps` style (live
|
||||
channels stack; ops over CONNECT/ASSOCIATE/BIND interleavings; the
|
||||
two-pump shutdown-on-completion contract, the egress readiness
|
||||
contract, and the datagram-stage exit matrix as invariants). Deferred
|
||||
because wave 1 covers the wire byte surface completely, the session
|
||||
harness is a large build, and the crate is one commit from its first
|
||||
publish — the alktunnels sequencing (wave 1 at 0.2.0's fuzz adoption,
|
||||
wave 2 the same session) was possible there because its session shape
|
||||
was the crate's whole surface; here the byte-level targets carry the
|
||||
attacker-reachable load and the stateful session target is a separate
|
||||
planning unit.
|
||||
|
||||
Sequencing mirrors the upstream docs: init + wave-1 targets → 10–30
|
||||
min campaigns per target via the detached runner → fix anything found →
|
||||
wave 2 when scheduled.
|
||||
|
||||
## 5. Operational isolation: campaigns never share fate with the agent session
|
||||
|
||||
**Adopted verbatim from the upstream docs (alkcall §7.6, alktunnels
|
||||
§5)** — an environment constraint, not a nice-to-have. Agent sessions
|
||||
run fuzzer builds/campaigns through a bash tool that spawns the fuzzer
|
||||
as a child of the session's own process tree and cgroup; an OOM in a
|
||||
fuzz target must cost the fuzzer, never the session. Three layers, all
|
||||
mandatory: (1) libFuzzer soft limits (`-rss_limit_mb=2048`
|
||||
`-malloc_limit_mb=2048`) — never `ulimit -v` under ASAN; (2) fork mode
|
||||
(`-fork=1`) as blast-radius containment (crash/timeout/OOM kills only
|
||||
the short-lived child; the harness survives and persists the artifact;
|
||||
also the deadlock answer); (3) process detachment — the runner script
|
||||
wraps `cargo fuzz run` in `setsid` + `nohup` + full redirection to a
|
||||
log file; poll, never `wait`.
|
||||
|
||||
Also inherited: the operational note that `cargo fuzz run` returns
|
||||
before its last fork-mode children fully exit — prefer `pgrep -af` to
|
||||
LIST, kill by explicit pid from the runner's echoed `pid=`, and never
|
||||
pattern-kill-and-recheck in one call.
|
||||
|
||||
## 6. No hosted CI
|
||||
|
||||
Per alkcall §7.9 / alktunnels §6 — 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`, `src/rfc1928/`, or `src/params.rs`.
|
||||
|
||||
## 7. Implementation log
|
||||
|
||||
- **2026-09-28 — proposal adopted** (this doc). Wave-1 targets
|
||||
`datagram_codec` + `rfc1928_codec` + `params_parse`; wave 2 deferred
|
||||
(§4).
|
||||
- **2026-09-28 — `alkcall` bumped 0.8.0 → 0.8.1,
|
||||
`alktunnels` 0.2.0 → 0.2.1.** Bug-fix releases, no API/wire change;
|
||||
alkcall's fix (the duplicate adopt/open destroying the live channel)
|
||||
is directly relevant to this crate's consumer sessions — every
|
||||
session's teardown reaps via the manager, and the
|
||||
open-op-response-replayed scenario alkcall's changelog names is the
|
||||
consumer open path. Verification: full suite (default + all-features)
|
||||
green, clippy, fmt, wasm check (§8 commands).
|
||||
- **2026-09-28 — wave-1 implemented.** The `fuzz/` workspace laid out
|
||||
per §2 (nightly-pinned subtree, stable `fuzz/shared` library, runner,
|
||||
dict, generator); the three targets over already-`pub` APIs — no
|
||||
`#[cfg(fuzzing)]` exposure needed (the upstream docs' experience,
|
||||
now across eleven targets). One diagnostic addition to the crate:
|
||||
`DatagramReader::retained_bytes()` (§3.1's bound target). Seeds
|
||||
committed: 45 + 65 + 19 generated (fuzz/gen_fuzz_seeds.py) + 2
|
||||
campaign-found crash inputs promoted (`datagram_codec/seed-900`,
|
||||
`seed-901` — the §7 harness findings below).
|
||||
- **2026-09-28 — two harness-model defects caught by corpus replay
|
||||
pre-campaign** (the alkcall §7.7 fuzzer-as-harness-checker pattern):
|
||||
1. **A `len == 0` frame is well-framed but decodes to
|
||||
`Err(EmptyFrame)`** — the re-frame round-trip first expected every
|
||||
well-framed frame to decode `Ok`. The frame count still holds over
|
||||
every frame; the byte-for-byte re-frame holds over the decodable
|
||||
ones; `EmptyFrame` is pinned as the only zero-inner-byte shape.
|
||||
The same arm widened for `ShortAddress`/`DomainNotUtf8`/
|
||||
`BadAtyp`: a well-framed frame can carry any addr-garbage shape —
|
||||
the decode refuses the addr, and the raw frame is the only
|
||||
re-frame possible.
|
||||
2. **A fieldless `deny_unknown_fields` struct deserializes from ANY
|
||||
empty JSON composite** (the serde seq form `[]` included) — not
|
||||
only `{}`. Not wire-reachable (the registry's `type: object`
|
||||
input schema is the gate that keeps non-object forms from ever
|
||||
reaching the type), but the harness must model the serde reality:
|
||||
the trivial-params invariant is now "an empty composite parses,
|
||||
everything non-empty rejects, the canonical round-trip is `{}`."
|
||||
- **2026-09-28 — first campaign finding (fork-mode containment as
|
||||
designed): a third harness-model defect.** The `datagram_codec`
|
||||
campaign crashed at ~35 s on input `0005 0301 1203 03bb 00`
|
||||
(`BadAtyp(0)` shape) — the re-frame walk's `Err` arm (fixed in the
|
||||
replay pass above) still omitted `BadAtyp`. Fixed; the crash input is
|
||||
committed as `seed-900`; the campaign relaunched clean.
|
||||
- **2026-09-28 — second campaign finding (also harness, fork-mode
|
||||
containment): the count walk's `declared >= 7` gate excluded short
|
||||
domains.** Input `0005 0301 1203 03bb 00` — a domain frame with a
|
||||
1-byte domain (`addr_len = 5`) is a legal empty-payload frame, but
|
||||
the walk's `declared >= 7` gate (meant to skip addr-less frames)
|
||||
rejected it; the per-ATYP `addr_len` computation already does that
|
||||
job. The gate lowered to `>= 4` (the minimum ATYP + length-byte
|
||||
prefix the walk reads); the crash input is committed as `seed-901`;
|
||||
the campaign relaunched clean.
|
||||
- **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 `-max_len=65536`, rfc1928
|
||||
`-max_len=4096 -use_value_profile=1`, params
|
||||
`-dict=json.dict -use_value_profile=1). All three exited 0 with
|
||||
empty artifact directories — no crashes, hangs, OOMs, or leaks.**
|
||||
- `datagram_codec` (post-harness-fixes) — ~1.9M execs (~3k exec/s),
|
||||
33 fork jobs, `oom/timeout/crash: 0/0/0` throughout. Coverage
|
||||
saturated at 481 edges (ft 2222, corp 209) — the 2-byte-header
|
||||
state machine is fully enumerated (the alktunnels wave-1
|
||||
saturation profile).
|
||||
- `rfc1928_codec` — ~2.0M execs (~3.6k exec/s), 33 fork jobs,
|
||||
`oom/timeout/crash: 0/0/0` throughout. Coverage saturated at 1429
|
||||
edges (ft 7987, corp 704) — the handshake state machines'
|
||||
byte-level shape space is fully enumerated; both protocol
|
||||
directions held across every adversarial sequence.
|
||||
- `params_parse` — ~3.1M execs (~5.5k exec/s), 33 fork jobs,
|
||||
`oom/timeout/crash: 0/0/0` throughout. Coverage 1512 edges (ft
|
||||
12970, in-memory corpus 2981 from the 19 committed seeds) — the
|
||||
serde/untagged shape space was still expanding at budget end; the
|
||||
spec-constant and pass-through-reason invariants held everywhere.
|
||||
- The decoder-state bound (§3.1) held everywhere:
|
||||
`retained_bytes()` never exceeded fed + one frame on any
|
||||
adversarial input. Grown corpora were **not** merged into the
|
||||
committed seeds (the §2 policy).
|
||||
- **NO crate bugs found by wave 1.** All three findings this session
|
||||
were harness-model defects, each invisible to example-based review
|
||||
and caught by fuzzing/replay — the upstream "fuzzer as a
|
||||
harness-checker" pattern, confirmed a third time (alkcall §7.7,
|
||||
alktunnels §7, here §7). The codec's contracts (infallible
|
||||
reassembly, the F-2 layering, the joint bound, the retained-bytes
|
||||
bound) and the RFC core's no-panic posture held across every
|
||||
adversarial input the campaigns generated.
|
||||
- (progress appended as implementation proceeds — see git log)
|
||||
|
||||
## 8. Verification commands
|
||||
|
||||
```bash
|
||||
cargo test # main suite (stable)
|
||||
cargo test --all-features # the feature-gated suite
|
||||
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`;
|
||||
`rfc1928_codec -max_len=4096 -use_value_profile=1`;
|
||||
`params_parse -dict=json.dict -use_value_profile=1`.
|
||||
|
||||
## 9. References
|
||||
|
||||
- Upstream campaigns + operational model (authoritative for tooling,
|
||||
runner, corpus policy, no-CI posture):
|
||||
`/workspace/@alkdev/alkcall/docs/research/fuzzing.md`,
|
||||
`/workspace/@alkdev/alktunnels/docs/research/fuzzing.md`
|
||||
- alkcall 0.8.1 changelog — the finding our consumer open/adopt paths
|
||||
are exposed to
|
||||
- This crate: ADR-001 (op/params), ADR-003 (the frame codec), ADR-013
|
||||
(the targeted fork), `docs/architecture/wire.md`,
|
||||
`docs/architecture/rfc1928.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)
|
||||
Reference in new issue
Block a user