feat(fuzz): target 3 spec_import — import-surface fuzz harness + 42 seeds
- fuzz/shared/src/spec_import.rs: invariants over the import-parse surface — the three OpenAPISpec constructors (str/json/yaml, ADR-051 JSON-first), the YAML→JSON normalization boundary (reject-loudly partition: non-finite floats, pointer keys), the compile/harden/validate lane (hardening partition, validate determinism, closed-by-default unknown-key probe), the FromMCP build_spec tool-shape lane, and the $ref-bounded resolver lane (cycles/hop chains/deep nesting fail cleanly, no hang, no stack blowout). - fuzz/fuzz_targets/spec_import.rs: thin fuzz_target! wrapper. - 42 committed corpus seeds via gen_fuzz_seeds.py (valid JSON/YAML docs, structural failures, YAML normalization edges, $ref cycles/ chains/unresolvable/external, schema-validation edges, MCP tool-shape edges); corpus-replay gate count floor 12. - fuzzing exposure hubs next to the wave-1 one: openapi_spec::fuzzing, input_validation::fuzzing (opaque FuzzCompiledValidator newtype — the pub(crate) validator never leaks), from_mcp::fuzzing (mcp feature). - fuzz/shared gains openapi/mcp features (default on); json.dict extended with the OpenAPI token set (75 unique tokens). - clippy follow-up in wave-1's ws_chunk_framing replay test (unnecessary u32 casts against the newer lint). - docs/plans/fuzzing.md §3/§6 target 3 → implemented; fuzz/README target table + campaign-advice lines updated. Verification: corpus replay 11/11 green (default and --no-default-features), cargo fuzz build clean, main crate 457 default / 590 all-features tests green, clippy+fmt clean both crates. Campaign: detached 10-min smoke run — see docs/plans/fuzzing.md §5.
This commit is contained in:
1 parent
84c4b48407
commit
e6491e31c7
56 files changed
+1098
-12
No files matched your search
+38
-7
@@ -9,9 +9,9 @@ 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:** targets 1–2 implemented and verified this session
|
||||
(2026-09-29); targets 3–4 are planned and can be handed to subsequent
|
||||
sessions.
|
||||
**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.
|
||||
|
||||
Progress log:
|
||||
|
||||
@@ -38,6 +38,10 @@ Progress log:
|
||||
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).
|
||||
- **2026-09-29 — target 3 (`spec_import`) implemented (wave 2).**
|
||||
Harness + 42 seeds + the three `fuzzing` hubs; no crate findings in
|
||||
the initial corpus replay (11 shared tests green, main crate 457
|
||||
default / 590 all-features). Smoke campaign results in §5.
|
||||
|
||||
---
|
||||
|
||||
@@ -133,7 +137,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 | planned |
|
||||
| 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) |
|
||||
|
||||
### Target 1 — `sse_parser` (the chunk_header-class target)
|
||||
@@ -224,7 +228,7 @@ the chunk-walk loop and the pending accumulator.
|
||||
`#[cfg(fuzzing)]` re-export and a current-thread runtime with a
|
||||
mock sink (the alkcall `envelope_frame` Cursor pattern).
|
||||
|
||||
### Target 3 — `spec_import` (planned)
|
||||
### Target 3 — `spec_import` (implemented 2026-09-29)
|
||||
|
||||
- **Drives:** raw bytes (and `json.dict`) → `serde_json`/`yaml_serde`
|
||||
→ `OpenAPISpec::from_str` (JSON-first, YAML-fallback, ADR-051) and
|
||||
@@ -243,10 +247,31 @@ the chunk-walk loop and the pending accumulator.
|
||||
`additionalProperties: false`; registry-compile shape parity with
|
||||
alkcall's `spec_parse` invariants (bounded compile, always
|
||||
`Result`).
|
||||
- **Implemented shape:** the harness drives one attacker string
|
||||
through all four lanes — the three spec constructors (str/json,
|
||||
yaml under `openapi`), the direct YAML→JSON normalization boundary
|
||||
(merge keys applied; a value the boundary accepted can't contain
|
||||
non-finite numbers — serde_json-invariant, asserted recursively),
|
||||
the compile/harden/validate lane against the whole doc (hardening
|
||||
partition: injected iff absent and the input is an object;
|
||||
validate determinism probe with an unknown-key input; deterministic
|
||||
two-call agreement), the MCP tool-shape lane (the doc and a
|
||||
`{name, inputSchema}` wrapper through `serde_json`→`Tool`→
|
||||
`build_spec`; success pins the namespace prefix), and the `$ref`
|
||||
lane (`resolve_refs_recursive` over the raw doc + declared
|
||||
component schemas when the doc parses). Exposure via
|
||||
`fuzzing` hubs alongside the wave-1 one:
|
||||
`openapi_spec::fuzzing`, `input_validation::fuzzing`
|
||||
(an opaque `FuzzCompiledValidator` newtype so the `pub(crate)`
|
||||
validator never leaks), `from_mcp::fuzzing` (`mcp` feature).
|
||||
The shared crate gates `openapi`/`mcp` (both on by default, riding
|
||||
the main crate's default build); corpus replay passes both with and
|
||||
without.
|
||||
- **Note:** yaml_serde (ADR-051: YAML 1.2 + merge keys) is the
|
||||
deepest third-party parse we own — the campaign should also encode
|
||||
the already-documented boundaries (`apply_merge` error path,
|
||||
duplicate-key rejection with line/column) as corpus expectations.
|
||||
42 committed seeds cover the menu below.
|
||||
|
||||
### Target 4 — stateful wave (planned)
|
||||
|
||||
@@ -330,14 +355,20 @@ exited 0, artifact dirs empty — no crash/hang/OOM/leak):
|
||||
- Toolchain notes live in `fuzz/README.md`; nightly stays confined to
|
||||
`fuzz/`.
|
||||
|
||||
Wave-2 (`spec_import`) campaign results: filled in below after the
|
||||
smoke run completes.
|
||||
|
||||
## 6. Sequencing
|
||||
|
||||
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
|
||||
2. ✅ **Done (2026-09-29)** — target 3 (`spec_import`): harness +
|
||||
invariants (`fuzz/shared/src/spec_import.rs`), 42 seeds, `json.dict`
|
||||
extended with the OpenAPI-token set, fuzzing hubs in the three
|
||||
driven modules. Smoke campaign: see §5.
|
||||
3. **Next** — target 4 stateful wave + longer campaigns; revisit
|
||||
corpus merges if any campaign yields inputs worth curating.
|
||||
|
||||
Status updates land in §3's table and §6's list as work completes.
|
||||
Reference in new issue
Block a user