Fuzz the three parsers a peer or a relay can reach, and fix what it found #81

Merged
extant2000git merged 1 commits from test/fuzz-the-parsers into main 2026-10-15 01:41:05 +00:00
Owner

docs/BEST-PRACTICES.md said this repository had no fuzzing. It has one now, aimed at the only functions that take bytes somebody else chose: payloadToEnvelope parses the payload transport field, openSealed parses a sealed blob before anything has authenticated it, and bundleFromWire parses a key bundle the relay hands over. isSealed is in there too, because a predicate that throws crashes a caller.

It found a defect on the first run. payloadToEnvelope accepted base64 of valid JSON that is not an object. JSON.parse('null') returns null and a quoted string returns a string, and both went straight back to the caller:

payloadToEnvelope: accepted and returned a string on "ImEgc3RyaW5nLCBub3QgYW4gb2JqZWN0Ig=="
payloadToEnvelope: accepted and returned a object on "bnVsbA=="

e2e-client.js:2255 then calls isSealed on that value and the ratchet reads env.header, so the failure surfaced later as a TypeError about a property of null, or as undefined flowing on quietly. It now throws notAnEnvelope for null, for a primitive and for an array.

A thrown Error counts as a pass here, which is the distinction the suite exists to make: a parser refusing malformed input is doing its job. It fails on a RangeError from unbounded recursion or allocation, a non-Error throw, a case that takes longer than two seconds, and a malformed input that parses into something a caller would treat as valid. Prototype pollution gets its own check, because a successful one leaves no exception behind and the loops would score it as a pass.

The generator is seeded and prints its seed, so a failure is reproducible from the line.

Gate. 32,001 cases at seed 20261015 and 16,001 at seed 7, both 0 problems. The four existing suites are unchanged by the stricter parser: conformance 304 passed with 2 skipped, client 289, ratchet 35, vault 26, all 0 failures. Secret sweep clean across 90 files.

Not in npm test yet. Adding it there, or as a CI job, is a follow-up once the runtime is agreed: 8,000 cases per target takes about 45 seconds.

🤖 Generated with Claude Code

`docs/BEST-PRACTICES.md` said this repository had no fuzzing. It has one now, aimed at the only functions that take bytes somebody else chose: `payloadToEnvelope` parses the `payload` transport field, `openSealed` parses a sealed blob before anything has authenticated it, and `bundleFromWire` parses a key bundle the relay hands over. `isSealed` is in there too, because a predicate that throws crashes a caller. **It found a defect on the first run.** `payloadToEnvelope` accepted base64 of valid JSON that is not an object. `JSON.parse('null')` returns null and a quoted string returns a string, and both went straight back to the caller: ``` payloadToEnvelope: accepted and returned a string on "ImEgc3RyaW5nLCBub3QgYW4gb2JqZWN0Ig==" payloadToEnvelope: accepted and returned a object on "bnVsbA==" ``` `e2e-client.js:2255` then calls `isSealed` on that value and the ratchet reads `env.header`, so the failure surfaced later as a TypeError about a property of null, or as `undefined` flowing on quietly. It now throws `notAnEnvelope` for null, for a primitive and for an array. **A thrown Error counts as a pass here**, which is the distinction the suite exists to make: a parser refusing malformed input is doing its job. It fails on a `RangeError` from unbounded recursion or allocation, a non-Error throw, a case that takes longer than two seconds, and a malformed input that parses into something a caller would treat as valid. Prototype pollution gets its own check, because a successful one leaves no exception behind and the loops would score it as a pass. The generator is seeded and prints its seed, so a failure is reproducible from the line. **Gate.** 32,001 cases at seed 20261015 and 16,001 at seed 7, both 0 problems. The four existing suites are unchanged by the stricter parser: conformance 304 passed with 2 skipped, client 289, ratchet 35, vault 26, all 0 failures. Secret sweep clean across 90 files. Not in `npm test` yet. Adding it there, or as a CI job, is a follow-up once the runtime is agreed: 8,000 cases per target takes about 45 seconds. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
extant2000git added 1 commit 2026-10-15 01:38:19 +00:00
Fuzz the three parsers a peer or a relay can reach, and fix what it found
ci / secret-sweep (pull_request) Successful in 5s
ci / ratchet (pull_request) Successful in 5s
ci / vault (pull_request) Successful in 6s
ci / conformance (pull_request) Successful in 9s
ci / doc-references (pull_request) Successful in 4s
ci / complete (pull_request) Successful in 4s
ci / client (pull_request) Successful in 30s
ci / spec-counts (pull_request) Successful in 26s
3441ac2e51
docs/BEST-PRACTICES.md said this repository had no fuzzing. It has one
now, aimed at the only functions that take bytes somebody else chose:
payloadToEnvelope parses the `payload` transport field, openSealed parses
a sealed blob before anything has authenticated it, and bundleFromWire
parses a key bundle the relay hands over. isSealed is in there too,
because a predicate that throws is a predicate that crashes a caller.

🔴 It found a defect on the first run. payloadToEnvelope accepted base64
of valid JSON that is not an object: `JSON.parse('null')` returns null
and a quoted string returns a string, and both went straight back to the
caller. e2e-client.js:2255 then calls isSealed on it and the ratchet
reads env.header, so the failure surfaced later as a TypeError about a
property of null, or as undefined flowing on quietly. It now throws
`notAnEnvelope` for null, for a primitive and for an array.

A thrown Error counts as a PASS in this suite, which is the distinction
it exists to make: a parser refusing malformed input is doing its job.
What it fails on is a RangeError from unbounded recursion or allocation,
a non-Error throw, a case that takes longer than two seconds, and a
malformed input that parses into something a caller would treat as
valid. Prototype pollution gets its own check, because a successful one
leaves no exception behind and the loops would score it as a pass.

The generator is seeded and prints its seed, so a failure is
reproducible from the line rather than from luck.

Gate: 32,001 cases at seed 20261015 and 16,001 at seed 7, both with 0
problems. The four existing suites are unchanged by the stricter parser:
conformance 304 passed with 2 skipped, client 289, ratchet 35, vault 26,
all 0 failures. Secret sweep clean across 90 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
extant2000git merged commit eda9ad72e2 into main 2026-10-15 01:41:05 +00:00
extant2000git deleted branch test/fuzz-the-parsers 2026-10-15 01:41:05 +00:00
Sign in to join this conversation.