From 4e8a2cfddde6689018a001409525a546a87f10e7 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Tue, 29 Sep 2026 17:28:23 +0000 Subject: [PATCH] docs(fuzz): record wave-3 campaign results + FWD-21 --- docs/plans/fuzzing.md | 109 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 20 deletions(-) diff --git a/docs/plans/fuzzing.md b/docs/plans/fuzzing.md index 4b2cd87..52c268a 100644 --- a/docs/plans/fuzzing.md +++ b/docs/plans/fuzzing.md @@ -10,8 +10,11 @@ survey, CI tiers) — this plan stays focused on what alkhttp fuzzes and in what order. **Status:** targets 1–3 implemented and verified (1–2 on 2026-09-29, -wave 1; 3 on 2026-09-29, wave 2); target 4 is planned and can be -handed to subsequent sessions. +wave 1; 3 on 2026-09-29, wave 2); target 4 implemented (2026-09-29, +wave 3: `ws_session_opseq` + `retry_throttle`; campaign results in +§5); the only crash found so far (FWD-21, the `WriteChunkWalk` +failure-carry drop) was a crate contract fix, not a harness +misassert. Progress log: @@ -45,6 +48,29 @@ Progress log: 10 min, zero crash/OOM/timeout, coverage still growing at budget end (see §5). One harness-assert fix on the way (the quoted-`""` YAML key noted above). +- **2026-09-29 — target 4 (`ws_session_opseq` + `retry_throttle`) + implemented (wave 3).** Two stateful harnesses + 16 seeds + the + `byte_adapter::fuzzing` drive hub (generic pump bodies over a mock + socket flavor) + the `client::fuzzing` throttle hub. One crate + finding on the way: FWD-21 (above) — fixed same commit. +- **2026-09-29 — FWD-21, the `WriteChunkWalk` failure-carry drop.** + Found minutes into the first `ws_session_opseq` campaign: one feed + that drains a complete chunk and then hits an oversize header (or + the pending cap) in the same call returned + `Walk::Oversize`/`PendingOverCap` and **discarded the already + drained pieces** — the walk's own consumption-accounting contract + ("every parsed byte is either emitted or retained", wave-1 + invariant 6) was violated by the crate, not the harness. Production + could not reach the shape (the mux's per-write invariant caps + `pending` at one chunk + header), so impact was nil, but + `WriteChunkWalk` is public API and the wave-1 `split_walk` equality + check was silently skipped on exactly this path. Fix (user-approved + semver-relevant API change): `Walk::Oversize(Vec>)` / + `Walk::PendingOverCap(Vec>)` carry the pre-failure pieces; + the write pump emits them before `fail_write_pump`, so a 1011 + close never swallows a legal chunk the mux already counted as + written. Regression seeds cover the drain-then-fail shape in both + chunk harnesses. --- @@ -141,7 +167,7 @@ checklist, as the siblings did. | 1 | `sse_parser` | `SseParser::feed` + `sse_event_envelope` mapping (`src/adapters/forward.rs`) | chunk-split sequence × eof flag | implemented 2026-09-29 | | 2 | `ws_chunk_framing` | the WS↔byte-adapter chunk parsers (`WriteChunkWalk`, the write pump's extracted chunk walk) (`src/websocket/byte_adapter.rs`) | byte sequences + message-split boundaries | implemented 2026-09-29 | | 3 | `spec_import` | `OpenAPISpec::from_str`/`from_yaml`/`from_json`, `CompiledInputSchema`/`harden_closed_by_default`/`validate`, `FromMCP::build_spec` (`src/adapters/openapi_spec.rs`, `from_jsonschema.rs`, `from_mcp/mod.rs`) | attacker-shaped OpenAPI/JSON/YAML docs + schemas | implemented 2026-09-29 | -| 4 | `ws_session_opseq` / `retry_throttle` | stateful op sequences over the WS byte-adapter pumps (write/read pump interleavings, close timing, oversize frames); `RetryAfterMiddleware` throttle-map growth | `#[derive(Arbitrary)]` op enums | planned (second wave) | +| 4 | `ws_session_opseq` / `retry_throttle` | stateful op sequences over the WS byte-adapter pumps (write/read pump interleavings, close timing, oversize frames); `RetryAfterMiddleware` throttle-map growth | `#[derive(Arbitrary)]` op enums | implemented 2026-09-29 | ### Target 1 — `sse_parser` (the chunk_header-class target) @@ -276,24 +302,39 @@ the chunk-walk loop and the pending accumulator. duplicate-key rejection with line/column) as corpus expectations. 42 committed seeds cover the menu below. -### Target 4 — stateful wave (planned) +### Target 4 — stateful wave (implemented 2026-09-29) - **`ws_session_opseq`:** `#[derive(Arbitrary)]` op sequences over the WS byte adapter driven end-to-end (like alktty's `session_opseq` / alkcall's `manager_routing`): write-side ops - (`write(bytes)` ≤ cap, `shutdown`, oversized-write), - read-side ops (feed messages, close, text message, oversize - message), close timing both sides. Asserts: backpressure - accounting (`READ_SLOTS`/`WRITE_SLOTS`), idle-timeout resets only - on chunk progress (WS-01 semantics), close frames only at legal - points, EOF sentinel semantics preserved, the alkcall 0.8.1 bug - class (a rejected duplicate must leave live state intact) never - reachable through this adapter. -- **`retry_throttle`:** bounded-map growth of - `RetryAfterMiddleware` (the inlined middleware): capacity bound, - Farthest-future drop policy, ceiling clamp, expired-first - eviction, jitter bounds; op sequences of responses with arbitrary - `Retry-After` values (seconds, HTTP-date, garbage). + (well-formed chunks, oversize headers, the pending shape), + read-side ops (binary deliveries, dribble, text, inbound read + error, peer close), a lane-healthy requested close, shutdown vs + close ordering, backpressure scaled 1..=8 read slots. Asserts: + lossless inbound forwarding (forwarded == delivered exactly after + the teardown joins, prefix mid-sequence), the WS-01 walk's + completed-chunk count vs the declared frame count, the loud-gate + close mapping (1002/1011/verbatim-requested + a per-cause reason, + WS-15), lossless write framing (outbound concat == the declared + chunk stream incl. the FWD-21 failure-carry), lane-failure + isolation (the alkcall 0.8.1 class), and pump termination within + the harness bound. Exposure: `byte_adapter::fuzzing` + (`FuzzMockSocket` flavor, `FuzzChannels`/`FuzzWriteSide` channel + bundle, pump spawn funnels, `FuzzInboundChunkProgress`). +- **`retry_throttle`:** bounded-map growth of the inlined + `RetryAfterMiddleware`: capacity bound (exact, distinct-URL + observable partition), expired-first + Farthest-future eviction + policy, ceiling clamp (deadline never grows past the ceiling of + the record moment), refresh contraction (a higher `Retry-After` + never extends the first-seen deadline; the missing-header refresh + is a documented no-op), `Retry-After` parse partition + (non-negative seconds ∪ future HTTP dates, post-clamp), and the + jitter bounds (≤ 25% of a short wait, capped at 2 s). Op sequences + of records/refreshes/queries/rebuilds with seconds, huge-seconds, + HTTP-date, past-date, garbage, and no-header shapes. Exposure: + `client::fuzzing` (constructor, synthetic throttled response, + record/deadline/parse/jitter funnels — the middleware map never + leaks). - Sequenced after 1–3 stabilize, per the sibling pattern. ## 4. Corpus policy @@ -323,8 +364,16 @@ grown corpora and artifacts gitignored. Seed menus: `$ref`; `additionalProperties` absent (hardening probe) and present both ways. - Stateful seeds (target 4): the semantic fixtures the campaign must - always replay — e.g. an adopted-then-teardown interleaving, a - close-during-stream, a throttle-map cycle at capacity. + always replay — a write/echo flow, the oversize loud-gate after a + clean chunk, the text-rejection + surviving write lane, an inbound + read error, a peer close mid-flight + shutdown drain, a requested + close, the throttle-map fill/evict cycle at capacity, the + missing-header refresh no-op, hostile-seconds clamping, refresh + contraction, and a mid-sequence rebuild (16 total; the + `ws_session_opseq`/`retry_throttle` blocks in + `fuzz/gen_fuzz_seeds.py`, hand-encoded against the `arbitrary` + 1.4.x derive layout with ceiling-division tags and per-element + keep-going bytes, pinned by seed-decode tests). ## 5. Campaign + gate policy @@ -376,7 +425,21 @@ artifact dirs empty): - Toolchain notes live in `fuzz/README.md`; nightly stays confined to `fuzz/`. -## 6. Sequencing +Wave-3 stateful smoke campaigns (2026-09-29, 10 min per target, both +exited 0, artifact dirs empty — no crash/hang/OOM/leak; the first +`ws_session_opseq` launch aborted at 17 s on FWD-21, fixed before +the counted run): + +- `ws_session_opseq` — 206k execs at ~340/s (each exec interleaves up + to 64 two-lane ops with yield quiesce points per exec, so per-exec + work is heavy), coverage 2251 edges / 5831 features / 232 in-memory + corpus entries — coverage still growing at budget end. +- `retry_throttle` — 897k execs at ~1.4k/s, coverage saturated at + 1154 edges / 6925 features / 324 corpus entries. +- **Seeds regenerate deterministically:** `python3 + fuzz/gen_fuzz_seeds.py`. +- Toolchain notes live in `fuzz/README.md`; nightly stays confined to + `fuzz/`. 1. ✅ **Done (2026-09-29, commit 5971c0a)** — infra (workspace exclude, toolchain pin, runner, seed generator, README) + targets @@ -389,5 +452,11 @@ artifact dirs empty): modules. Smoke campaign clean (see §5). 3. **Next** — target 4 stateful wave + longer campaigns; revisit corpus merges if any campaign yields inputs worth curating. +4. ✅ **Done (2026-09-29, wave 3)** — target 4 stateful + (`ws_session_opseq` + `retry_throttle`): two harnesses + 16 seeds + + the `byte_adapter::fuzzing` / `client::fuzzing` hubs + FWD-21 + (the `WriteChunkWalk` failure-carry drop — a crate contract fix; + user-approved semver-relevant API change). Smoke campaigns clean + (see §5). Longer (30+ min) campaigns before the next release. Status updates land in §3's table and §6's list as work completes. \ No newline at end of file