diff --git a/.gitignore b/.gitignore index 76e38a5..3a4b638 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,8 @@ node_modules/ fuzz/target/ fuzz/artifacts/ fuzz/coverage/ -# Grown corpora (the committed hand-made seeds are force-added via -# fuzz/corpus//.gitignore un-ignores, the sibling pattern). -fuzz/corpus/*/ -!fuzz/corpus/sse_parser/ -!fuzz/corpus/ws_chunk_framing/ +# Grown corpora: the hash-named files the campaigns drop into +# fuzz/corpus// are gitignored (the quinn/h2 policy); the +# committed seeds are the seed-* files, kept via the per-dir +# .gitignore un-ignores. +fuzz/corpus/*/[0-9a-f][0-9a-f]* diff --git a/docs/plans/fuzzing.md b/docs/plans/fuzzing.md index b18385f..f8d9481 100644 --- a/docs/plans/fuzzing.md +++ b/docs/plans/fuzzing.md @@ -9,9 +9,35 @@ plan for alkhttp; the sibling docs are the reference for rationale survey, CI tiers) — this plan stays focused on what alkhttp fuzzes and in what order. -**Status:** in progress. Targets 1–2 (`sse_parser`, -`ws_chunk_framing`) + infra are this session's scope; targets 3–4 are -planned and can be handed to subsequent sessions. +**Status:** targets 1–2 implemented and verified this session +(2026-09-29); targets 3–4 are planned and can be handed to subsequent +sessions. + +Progress log: + +- **2026-09-29 — targets 1–2 + infra landed (commit 5971c0a).** The + `fuzz/` workspace, 80 committed seeds (52 + 28), the detached + runner, the corpus-replay gate (AGENTS.md verification checklist + gains the line), nightly pinned subtree, explicit root `[workspace]` + exclusion. One crate finding on the way (below). `cargo fuzz build` + clean; corpus replay 80/80 green; campaigns pending at commit time. +- **2026-09-29 — FWD-20, the SSE dribble-drain quadratic (fixed in + the same commit).** Found by driving the new `sse_parser` harness's + split-feed invariants: `SseParser::feed` re-walked the whole + reassembly buffer per feed — a subscription dribble-fed one giant + unterminated line (the attacker can hold `data: ` open + forever without sending `\n`) paid O(n²) in the dribbled bytes: + 64 KiB dribble ≈ 12.5 s CPU, 1 MiB would run to minutes per + connection, with no per-stream total-CPU bound (the alkcall + research §6.2-3 shape, in this crate). The fix: line assembly walks + only the arriving chunk — the reassembly buffer holds just the + current line, so the scan never revisits drained bytes; measured + 1 MiB dribble now 21 ms (linear, 4× data = ~4× time). Regression + test `sse_parser_dribble_feeds_do_not_rescan_the_buffer`. Also + fixed by corpus replay pre-campaign: the harness's own re-frame + round-trip was wrong for data ending in `\r` (un-re-encodable under + WHATWG CR-stripping — the serde_json key-order class; the harness + skips that documented-lossy shape). --- @@ -105,9 +131,9 @@ checklist, as the siblings did. | # | Target | Drives | Input style | Status | |---|---|---|---|---| -| 1 | `sse_parser` | `SseParser::feed` + `sse_event_envelope` mapping (`src/adapters/forward.rs`) | chunk-split sequencess × eof flag | implemented this session | -| 2 | `ws_chunk_framing` | the WS↔byte-adapter chunk parsers (`run_write_pump` parse loop, inbound header path) (`src/websocket/byte_adapter.rs`) | byte sequences + message-split boundaries | implemented this session | -| 3 | `spec_import` | `OpenAPISpec::from_str`/`from_yaml`/`from_json`, `FromJsonSchemaImporter`/`CompiledInputSchema`/`harden_closed_by_default`, `FromMCP::build_spec` (`src/adapters/openapi_spec.rs`, `from_jsonschema.rs`, `from_mcp/mod.rs`) | attacker-shaped OpenAPI/JSON/YAML docs + schemas | planned | +| 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 | planned | | 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) | ### Target 1 — `sse_parser` (the chunk_header-class target) @@ -284,6 +310,21 @@ repo): foreground child of an agent session); budget 10 min per target for a smoke campaign, 30+ min before a release or after touching the SSE parser, the WS adapter, or the import adapters. +- **Grown corpora stay gitignored**: the hash-named files a campaign + drops into `fuzz/corpus//` after run-detached runs are + ignored via a hash-pattern (the committed `seed-*` files stay); + merge worthy entries into seeds only deliberately. + +Wave-1 smoke campaign results (2026-09-29, 10 min per target, both +exited 0, artifact dirs empty — no crash/hang/OOM/leak): + +- `sse_parser` — 18.9k execs at ~30/s (each exec replays the + whole-feed + 3-byte-split + cap-probe + envelope-mapping + + round-trip chain, so per-exec work is heavy), coverage 1100 edges / + 9510 features / 807 in-memory corpus entries, still discovering when + the budget ended — longer campaigns keep paying. +- `ws_chunk_framing` — 4.8M execs at ~9k/s, coverage saturated at 245 + edges / 1071 features / 136 corpus entries. - **Seeds regenerate deterministically:** `python3 fuzz/gen_fuzz_seeds.py`. - Toolchain notes live in `fuzz/README.md`; nightly stays confined to @@ -291,9 +332,10 @@ repo): ## 6. Sequencing -1. **This session** — infra (workspace exclude, toolchain pin, runner, - seed generator, README) + targets 1–2 + corpora + corpus replay + - AGENTS.md gate + smoke campaigns. +1. ✅ **Done (2026-09-29, commit 5971c0a)** — infra (workspace + exclude, toolchain pin, runner, seed generator, README) + targets + 1–2 + corpora + corpus replay + AGENTS.md gate + smoke campaigns + (clean, see §5). Includes the FWD-20 SSE dribble quadratic fix. 2. **Next session(s)** — target 3 (`spec_import`) + campaigns. 3. **Then** — target 4 stateful wave + longer campaigns; revisit corpus merges if any campaign yields inputs worth curating.