Skip to main content

fuzzing-an-api-surface

Use when a surface parses untrusted or model-generated input — a wire protocol, tool-call JSON, upstream SSE, an HTTP body — or after a parser/dispatch bug, to fuzz that surface and turn any deterministic failure into a permanent, flake-guarded regression test. Covers picking a seam, choosing oracles beyond "no panic", the portable promoter/schema-generator core, and the safety bounds that keep a fuzzer from executing dangerous handlers.

来源信息

仓库
prime-radiant-inc/evener
最近来源活动
2026年8月19日 00:39
检测到的 SKILL.md 语言
英语
星标
170
分支
15

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
fuzzing-an-api-surface
description
Use when a surface parses untrusted or model-generated input — a wire protocol, tool-call JSON, upstream SSE, an HTTP body — or after a parser/dispatch bug, to fuzz that surface and turn any deterministic failure into a permanent, flake-guarded regression test. Covers picking a seam, choosing oracles beyond "no panic", the portable promoter/schema-generator core, and the safety bounds that keep a fuzzer from executing dangerous handlers.
# Fuzzing an API surface Use this skill when you want to systematically hammer a surface that decodes input you do not control, and have every real failure become a named regression test rather than a transient console reproduction. Evener is the proving ground; the portable core (`fuzz/promoter`, `fuzz/schemagen`) carries unchanged to any project, so the methodology below is project-agnostic and the evener targets are worked examples. Read `docs/research/api-fuzzing-toolkit.md` (§3 oracle soft spots, §5 the promote discipline, §8 the portable split) and `docs/design/fuzzing-toolkit-design.md` before extending the toolkit. ## When to reach for this A surface is fuzz-ready when it has: - **A single dispatch choke point** — one function every input flows through (`tool.Registry.ExecuteCall`, `Router.Dispatch`, `WebServer.Handler()`). One harness then covers the whole surface by varying which operation it drives. - **A machine-readable schema** — per-operation JSON Schema, reflected param types, OpenAPI, protobuf. Schema-driven generation reaches deep validator paths raw bytes never hit (evener hands us tool `Definition.Parameters` and the `appwire.Methods` catalog for free). - **A decode / parse seam** — a byte-level frame/stream parser (`llm.ParseSSE`, `appwire.Message.UnmarshalJSON`). These are the cheapest, highest-value targets: a Go `testing.F` byte fuzzer auto-promotes its own crashers. - **A metamorphic relationship** — two ways to feed the same logical input that must agree (re-chunked reads, reordered/whitespace-padded frames, blocking vs timeout code paths). The relationship *is* the oracle. Do NOT reach for this when the seam's only effect is to **execute** something dangerous (see Safety bounds), when no determinism is achievable offline (the real behavior needs a live LLM/network/subprocess), or when there is no oracle richer than "it returned" — a fuzzer with only a floor oracle on a trivial surface buys little. ## Core rules (non-negotiable) - **Safety bound: never let a fuzzer EXECUTE a dangerous handler.** Shell (`bash -c`), network fetch, file read/write under attacker-controlled paths, subprocess/agent spawn — a temp dir does NOT sandbox these. Fuzz the **decode + validate boundary** instead, or restrict execution to a vetted allowlist of genuinely sandboxable operations. Evener's `FuzzToolArgsValidate` drives decode+`Schema.Validate` and never calls a tool handler; `FuzzWebHandler` allowlists GET-only, non-mutating, non-networked routes and excludes spawn, shell, and provider-probe endpoints. - **Determinism is non-negotiable.** Seed the generator; run offline (no real LLM, network, or subprocess); a flaky target — or a promoter that *could* promote a flake — is a defect, not a nuisance. - **"No panic" is the floor, never the only oracle.** Invest in semantic oracles; the real bugs live there. - **A thin-but-real model beats a broad fake one.** Model only what you can drive deterministically against the real seam, and report the boundary you could not reach. Never fake the unreachable part and assert on the mock — that tests mocked behavior. `TestRouterSeqFuzz` models exactly the transport initialize-gate it can reach offline and states in its doc comment that turn/job *status* lifecycle needs a live agent and is out of scope. ## Procedure: pick a surface and seam (research §8, 7 steps) 1. **Pick a surface and its seam.** Find the single choke point all input flows through. That is what the harness drives. 2. **Find or derive the schema.** Point generation at whatever machine-readable structure exists (`Definition.Parameters`, reflected `appwire.Methods`, OpenAPI). No schema → fall back to byte fuzzing. 3. **Write the thinnest `testing.F` first** and harvest the free auto-promotion: a single `[]byte` or typed-tuple target whose crashers Go saves to `testdata/fuzz/` and re-runs forever. This is ~60–200 LoC and finds the crash-class bugs before you build anything else. 4. **Add oracles beyond "no panic"** (next section). This is where valid-but-wrong bugs surface. 5. **For stateful surfaces, model the state machine** and drive it with property-based testing (`rapid`). Keep the model declarative — evener's `liveOpTable` is a transition table, the reusable artifact; the `rapid` machine just draws op sequences. 6. **Wire the promoter with flake-guard before promote** so non-`testing.F` failures (`rapid` sequences, HTTP op-sequences) become permanent. 7. **Gate it in CI:** seed corpus + saved crashers under `-short` on every run (`make fuzz` / `make test`); the unbounded coverage-guided search runs nightly (`make fuzz-nightly`, 60s/target). ## The oracle taxonomy The portable tags are `promoter.OracleTag`: `Panic`, `Invariant`, `ErrorShape`, `Wedge`, `HTTP5xx`, `PathEscape`. How to spot which fits, each tied to a real evener target: - **Floor — never panic** (`Panic`). The seam crashes on some input. Always present; never sufficient alone. Live panic-hunt wherever validation is *not* `recover()`-wrapped at runtime — e.g. `Schema.Validate` in `FuzzToolArgsValidate`, and `Router.Dispatch` (which propagates handler panics) in `TestRouterSeqFuzz`. - **Metamorphic** (`Invariant`). Two equivalent inputs must produce the same output. Spot it when one logical input has more than one delivery shape: re-chunked reads or blocking-vs-timeout paths (`FuzzParseSSE`), one-byte-at-a-time SSE accumulation (`FuzzOpenAIResponsesMetamorphic`), decode→encode→decode fixed point (`FuzzMessageDecode`, `FuzzMethodParams`). - **Divergence** (`ErrorShape`). A validator says clean but the handler disagrees, or vice versa. Spot it when validation and use are separate steps: `TestToolArgsSchemaFuzz` flags a schema-VALID generated value the real validator rejects — generator and schema disagree. - **Invariant / monotonicity** (`Invariant`). A modeled property must hold across a sequence. Spot it on stateful surfaces with an ordering contract: `TestRouterSeqFuzz` asserts the connection's initialize-gate is monotonic (fresh → initialized, never reverts). - **Never 5xx** (`HTTP5xx`). Bad input may legitimately 4xx, but a 500 from your own handler is a fault. Spot it on HTTP surfaces: `FuzzWebHandler` (it excludes the bare stdlib `http.FileServer`, whose `fs.ErrInvalid`→500 is documented net/http behavior, not handler logic). - **Path escape** (`PathEscape`). Attacker-controlled path/query escapes its intended root. Spot it on any file/path resolver: `FuzzWebHandler` plants a secret one level above the session cwd and fails if any response body contains it. - **Never wedge** (`Wedge`). A call that should return makes no progress. Spot it on synchronous seams that could hang: `TestRouterSeqFuzz` runs each dispatch under a bounded context in a goroutine and reports a wedge on timeout. - **Internal invariant** (`Panic`, under `-tags evenerfuzz`). A load-bearing assumption asserted *inside* production code with `invariant.Hold()` so a logic bug trips at its origin, not at a distant surface. Zero-cost in a normal build; panics under `-tags evenerfuzz` (the fuzz build), so it surfaces via the Panic oracle. Spot it wherever a subsystem relies on a property a reader assumes — e.g. a folded job status never leaves a terminal state; an emitted item carries its turn id. Conditions must be side-effect-free; verify the invariant is TRUE and reached before trusting it. - **Differential** (`Invariant`/`ErrorShape`). Two code paths that must compute the same thing, driven from one input and compared modulo an allow-list. The strongest class — it found both real decoder bugs this codebase has caught. Spot it wherever a value is produced two ways: per-provider streaming vs non-streaming decode (`FuzzStreamVsNonStreamDifferential`), one logical response across all providers (`FuzzCrossProviderDifferential`), a decoder vs a committed snapshot of its own output (`appwire/golden_test.go`), or independent readers of one format (`FuzzTranscriptReadersAgree`). A divergence outside the allow-list is a real bug — never widen the allow-list to silence it. **Generation technique — structure-aware inputs.** Independent of which oracle you pick: if the surface has a rich grammar, most raw-byte execs die at the first parse. Drive the target from a deterministic byte-`Source` generator (`fuzz/schemagen`) that emits *valid-but-adversarial* inputs (the `Fuzz…Structured` targets), so coverage-guided search reaches deep logic. It typically lifts input acceptance from ~0% to ~90%+. ## The portable core Two packages in the `primeradiant.com/evener/fuzz` module are the travelling tooling. **Nothing in them imports the project under test** — `fuzz/go.mod` declares no evener dependency, so the module will not build if that boundary is violated. That structural guarantee *is* the portability test. **`schemagen.FromJSONSchema(schema map[string]any) *rapid.Generator[any]`** walks the JSON Schema subset a surface actually uses (`type`/`properties`/`required`/ `enum`/`additionalProperties`/`items`) and yields both schema-`Valid` and schema-`Adjacent` (wrong-type, missing-required, out-of-enum, extra-when-closed) values. Use `schemagen.Generator(schema, mode)` when an oracle needs known-valid input. **`promoter.Adapter`** is the four-hook seam a project implements; everything else (flake-guard loop, bucket store, emitter, commit) is generic: ```go type Adapter interface { Minimize(Failure) Failure // usually a passthrough Signature(Failure) Signature // dedup key Replay(context.Context, Failure) (failed bool, sameSignature bool) Emit(Failure) (path string, err error) } ``` A project implements it by carrying enough state to deterministically reproduce a failure. `toolArgsAdapter` holds each tool's compiled validator; `seqAdapter` holds the transition table + handler registrar. `Replay` re-runs the *same* oracle function that found the failure (`toolArgsFailure` / `seqOracleRun`), so the live property and the replay classify identically. `Emit` calls `promoter.WriteGoTest(dir, promoter.GoTest{...})` with an adapter-rendered `ReplayBody` that decodes the minimized artifact and asserts the bug is fixed. `promoter.New(adapter, store, quarantiner, K)` then runs `Promote`. ## The flake-guard / dedup rubric (the load-bearing discipline) `Promoter.Promote` is the whole point. In order: 1. **Dedup first.** `store.Has(sig)` against the committed `BucketStore` (a JSON file mapping `Signature` → test path). A known bug returns `AlreadyKnown` and is never re-promoted. 2. **Flake-guard.** Replay the minimized artifact **K times** (default 5). Promote ONLY if it fails all K with the **same** signature. Any non-reproduction or signature shift → `Quarantined`: logged via the `Quarantiner`, **never written as a test**. This is the rule that keeps the gate trustworthy; budget it generously on timing-sensitive seams. 3. **Emit-only by default.** A surviving failure writes a `TestRegression_*` file with a provenance header (`// Code generated by fuzz/promoter ... DO NOT EDIT`) and records its bucket. Promotion *into the tree* is the human/opt-in step — review the diff before committing. Signatures must distinguish distinct bugs: `Signature{Oracle, Key}` where for `Panic` the Key is the top-N normalized stack frames, and for semantic oracles (`Invariant`/`ErrorShape`) the adapter folds the named invariant / `Detail` into Key. Collapse them and distinct bugs silently dedup as already-known. ## Hard-won lessons - **rapid owns its shrink loop.** `pgregory.net/rapid` reports failures through `testing.T` and Goexits during shrinking; it does **not** hand you the minimized artifact as a return value. Capture the failing artifact into a closure variable inside `rapid.Check`, and promote it in `t.Cleanup` (which runs during the Goexit unwind). `Adapter.Minimize` is then a passthrough — rapid already minimized. Both `TestToolArgsSchemaFuzz` and `TestRouterSeqFuzz` use this `captured *Failure` + cleanup pattern. - **Liberal-in / conservative-out — the fix location matters.** `FuzzMessageDecode` found a real codec asymmetry: the decoder accepted an id-less response/error frame the encoder then re-rendered unreadable, breaking the round-trip fixed point. The fix (commit `6c7f4eac`) was on the **producer** side — `Response.MarshalJSON`/`ErrorResponse.MarshalJSON` omit the empty id — keeping the decoder liberal. On a transport, tightening a decoder turns a tolerable frame into a connection-fatal error; tighten the producer instead. - **Model only what you can drive for real.** When the real status lifecycle needs a live LLM, model the transport/dispatch layer you *can* reach offline and report the boundary in the target's doc comment. Do not fabricate the unreachable part. ## Per-language portability The loop, the triage/promotion discipline, schema-driven generation, and the oracle taxonomy generalize. Only the engine, corpus format, and emitter syntax are per-ecosystem, all behind the same `Adapter`: - **Go:** `testing.F` (coverage-guided, free crasher auto-promotion) for byte/tuple targets; `pgregory.net/rapid` for stateful/structured PBT. Emitter writes a `*_test.go`; corpus is `f.Add` seeds + `testdata/fuzz/`. - **JS:** `fast-check` (property/model-based) for a renderer/protocol surface; its emitter writes a `test-*.js`. - **Python:** `hypothesis` (+ `schemathesis` for HTTP); its emitter writes a `test_*.py`. ## Cross-module wiring (Go specifics) A local `v0.0.0` workspace module is not `go get`-able. To consume `fuzz/promoter` and `fuzz/schemagen` from another module in the same `go.work`: 1. Add `require primeradiant.com/evener/fuzz v0.0.0` (and `pgregory.net/rapid`) to the consumer's `go.mod`. 2. Add `replace primeradiant.com/evener/fuzz v0.0.0 => ./fuzz` in `go.work`, then `go work sync`. 3. Seed third-party checksums the workspace replace hides: `GOFLAGS=-mod=mod GOWORK=off go mod download pgregory.net/rapid`. Add `fuzz` to `GO_MODULES` so its tests are gated. `make fuzz` runs every `Fuzz*` target's seed corpus + saved crashers per module as deterministic tests (no `-fuzz` flag = no random search); `make fuzz-nightly` runs the bounded search.
在 GitHub 查看