Skip to content

Property tests and fuzz targets against the reference - #37

Merged
Kinflou merged 7 commits into
mainfrom
feat/fuzz-and-property-tests
Oct 25, 2026
Merged

Kinflou merged 7 commits into
mainfrom
feat/fuzz-and-property-tests

Conversation

@Kinflou

@Kinflou Kinflou commented Oct 25, 2026

Copy link
Copy Markdown
Member

Stage 1 step R4 of runtime#29: property tests and fuzz targets against the reference. Formerly stacked on #32, which is now merged.

What's in it

  • Property tests (runtime/conformance/tests/properties.rs, proptest, stable, part of cargo test):

    • random Types and values of them;
    • a round trip in every format;
    • packed@1 is canonical;
    • every decoded value passes check;
    • the seed against the derive mirrors, on mutated and random bytes.

    The mirrors move to tests/common/mod.rs. A run is 64 cases by default; PROPTEST_CASES raises it.

  • cargo-fuzz targets in runtime/fuzz, excluded from the workspace, nightly only: decode_no_panic, packed_canonical, framing_decode and seed_vs_derive.

    • Seed corpora come from the vectors via seed-corpus.sh.
    • The type grammar is one byte per choice, so any vector's type can be written back as a seed.
  • .github/workflows/fuzz.yml:

    • on a PR, each target runs for 60s, seeded from the vectors;
    • nightly, each target runs for 10 min on a cached corpus, plus proptest with 100000 cases;
    • a nightly failure uploads the crash and opens an issue, unless one with that title is already open.
  • Docs: runtime/fuzz/README.md covers running, the targets, and how a finding becomes a found-* vector. runtime/conformance/README.md links to it.

Findings

  • JSON f64 precision (fixed). serde_json's default float parser read 4.520618856987477e-279 back one ulp off.
    • The fix enables serde_json's float_roundtrip feature in comline-runtime.
    • The cost: float parsing in json@1 gets slower (serde_json's docs say about 2x).
    • A unit test and the vector format-json/found-f64-exact-digits cover it.
  • Arrays of data-less elements (decided, not a runtime change). packed@1 couldn't read back T[] when T takes no bytes; [Empty {}] encodes as 01.
  • None from the fuzz targets in 2 minutes per target (about 540k–1.1M runs each).

Not canonical, as in comline-typescript: Envelope::decode ignores bytes after a Fault tag, and HandshakeFrame::decode reads only the first 31 bytes. framing_decode checks that the re-encoding of those two is a prefix of the input. Say if either should be strict; that would be a vector change for both runtimes.

Kinflou added 7 commits October 25, 2026 14:40
The mirrors move to tests/common/mod.rs, each with its Type, samples and
hand-written inputs, so property tests and the fuzz targets can hold the
seed to the same derives.
serde_json's default float parser is best-effort and can land one ulp
off: 4.520618856987477e-279 written by json@1 read back as
4.5206188569874766e-279. Enable float_roundtrip, add a unit test, and
record the input as format-json/found-f64-exact-digits, the first
found-* vector. Found by the property tests.
proptest draws a Type (depth 4, up to 6 fields, 8 elements, fixed
lengths up to 40) and a value of it, per format, and checks the round
trip, that packed@1 is canonical, that whatever decodes passes check,
and the seed against the derive mirrors on mutated and random bytes.
64 cases by default; PROPTEST_CASES raises it.

packed_round_trips fails: packed@1 can't read back an array whose
elements encode to no bytes (an empty struct, unit, T[0]), since a count
may not exceed the bytes left.
Core refuses T[] when T carries no data (unit, T[0], a struct of only
such fields), so the generator gives such an element a bool beside it.
packed@1 stays strict: format-packed/reject-count-past-input pins that a
count past the bytes left is refused even for elements that take none.
The seed that found it is dropped; the generator can't reach it now.
runtime/fuzz, outside the workspace, nightly only:

- decode_no_panic: a format, a type and bytes; decoding never panics,
  stays within -rss_limit_mb/-malloc_limit_mb, and what it accepts
  passes check.
- packed_canonical: bytes that decode as a type in packed@1 re-encode
  to themselves.
- framing_decode: bytes through every frame decoder; binary frames
  re-encode exactly, except the two that read a prefix (a fault
  envelope, the handshake), JSON-RPC responses re-encode and decode the
  same.
- seed_vs_derive: the conformance mirrors, seed against derive.

Types are read one byte per choice, so examples/seed_corpus.rs (and
seed-corpus.sh) can write every vector as a seed input. Arrays of
data-less elements are never generated, as in the property tests.
.github/workflows/fuzz.yml runs each target for 60s on a pull request,
seeded from the vectors, and nightly for 10 minutes on a cached corpus
plus proptest at 100000 cases; a nightly failure uploads the crash and
opens an issue unless one with the same title is open. The fuzz README
covers running, the targets, and how a finding becomes a found-* vector;
the conformance README points to it. The data-less array exclusion now
cites ComlineProject/core#141.
@Kinflou
Kinflou merged commit 7efb684 into main Oct 25, 2026
11 checks passed
@Kinflou
Kinflou deleted the feat/fuzz-and-property-tests branch October 25, 2026 07:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant