fuzz: wave-1 smoke campaigns clean + grow-corpus gitignore fix

Both 10-minute detached campaigns (§5 budget) ran to completion:
sse_parser 18.9k execs / 1100 edges (still discovering at budget end),
ws_chunk_framing 4.8M execs / 245 edges (saturated) — zero crashes,
hangs, OOMs, or leaks on all fork jobs; artifact dirs empty.

One policy gap the first campaigns exposed: the grown corpus entries
(hash-named files in fuzz/corpus/<target>/) leaked as git noise under
the un-ignored corpus dirs. The hash-pattern gitignore now separates
the committed seed-* files from campaign output, per the quinn/h2
model.

docs/plans/fuzzing.md: §3 table marks targets 1-2 implemented; §5
gains the grow-corpus policy + campaign results; §6 sequencing marks
step 1 done. FWD-20 already carried the fix log from 5971c0a.

Verification: corpus replay 80/80 green, cargo test 441 passed, fmt
--check clean.
This commit is contained in:
glm-5.3-flash committed 2026-09-29 09:18:08 +00:00
1 parent 5971c0a0d6
commit 84c4b48407
2 files changed
+56 -14

No files matched your search

+5 -5
View File
@@ -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/<target>/.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/<target>/ 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]*
+51 -9
View File
@@ -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: <n MiB>` 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/<target>/` 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.