Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,50 @@ jobs:
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body \
"The nightly proptest run (PROPTEST_CASES=100000) failed: $RUN. The minimal failing input is in the job log; the seed is the run's \`proptest-regressions\` artifact."

# `sizes_vs_core`'s proptest twin, and the tiers too large for a vector
# (runtime/fuzz/tests/sizes.rs): core's byte ranges held to the real
# encoders. In release: the large values are megabytes. A pull request runs
# proptest's default count; the nightly run 20000 cases, and opens an issue
# on a failure.
sizes:
name: sizes (proptest)
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
contents: read
issues: write
env:
NIGHTLY: ${{ github.event_name != 'pull_request' }}
steps:
- uses: actions/checkout@v4
- run: rustup toolchain install nightly --profile minimal
- name: Size tests
env:
PROPTEST_CASES: ${{ github.event_name == 'pull_request' && '256' || '20000' }}
run: cargo test --release --manifest-path runtime/fuzz/Cargo.toml --test sizes
- name: Upload the failing seeds
if: failure()
uses: actions/upload-artifact@v4
with:
name: sizes-proptest-regressions
path: runtime/fuzz/tests/sizes.proptest-regressions
if-no-files-found: ignore
- name: Open an issue
if: failure() && env.NIGHTLY == 'true'
env:
GH_TOKEN: ${{ github.token }}
TITLE: "Nightly size tests failed"
RUN: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
open=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --limit 200 \
--json title --jq "map(select(.title == env.TITLE)) | length")
if [ "$open" != "0" ]; then
echo "An issue titled '$TITLE' is already open."
exit 0
fi
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body \
"The nightly size tests (runtime/fuzz/tests/sizes.rs, PROPTEST_CASES=20000) failed: $RUN. The minimal failing input is in the job log; the seed is the run's \`sizes-proptest-regressions\` artifact."

report:
name: fuzz report
needs: [targets, fuzz, properties]
Expand Down
16 changes: 15 additions & 1 deletion runtime/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ comline-runtime-ffi = { path = "../ffi" }
serde = { version = "1.0.190", features = ["derive"] }
# The seed-vs-derive mirrors (`conformance/tests/common`) need it.
serde-big-array = "0.5"
serde_json = "1.0.108"
# The compiler's byte ranges and witnesses, which `sizes_vs_core` holds to the
# real encoders: the IR alone (no parser), and only here. The runtime library
# never depends on it.
comline-core = { git = "https://github.com/ComlineProject/core", rev = "d1b678a983b90453c8c9288ef44a7c00d26711a5", default-features = false }

# The targets: these [[bin]]s are the one list of them. fuzz.yml's matrix and
# its report's `expect` read it (.github/scripts/fuzz-targets.py), which fails
Expand Down Expand Up @@ -83,8 +88,17 @@ test = false
doc = false
bench = false

[[bin]]
name = "sizes_vs_core"
path = "fuzz_targets/sizes_vs_core.rs"
test = false
doc = false
bench = false

[dev-dependencies]
serde_json = "1.0.108"
# The proptest twin of `sizes_vs_core` (`tests/sizes.rs`); `PROPTEST_CASES`
# raises the count.
proptest = "1.5"

# Writes the seed corpora from the conformance vectors; see seed-corpus.sh.
[[example]]
Expand Down
57 changes: 57 additions & 0 deletions runtime/fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ cargo +nightly fuzz run --fuzz-dir runtime/fuzz decode_no_panic -- \
| `ffi_feed` | a config, then operations (see below) | the C ABI on its own rules: no panic crosses and no handle is poisoned, every status is one the function documents, every span lies inside the frame and the frame inside that `feed`'s input, `NEED_MORE` and `BUFFER_TOO_SMALL` report sizes a retry succeeds with, refusals spend no request id, and nothing allocates after `comline_connection_new` |
| `ffi_vs_core` | `ffi_feed`'s | `ffi_feed`'s checks, and each call made again through `State`, the connection framings and `HandshakeFrame` directly, which must give the same status, bytes, request id, `*consumed` and event (spans at the same offsets) |
| `ffi_comline2` | options, then frames | a `comline@2` connection under any side and parameters, fed any frames: no panic crosses and no handle is poisoned, every status is one the function documents, `COMLINE_EVENT_SEND` means `start` has a message, and a finished or refused handshake's readers answer |
| `sizes_vs_core` | a type graph, then values | `comline-core`'s byte range for the type in `packed@1`, `msgpack@1` and `json@1` holds every value's real encoded length (and `encoded_len` agrees), and core's least and most witnesses encode to exactly `min` and `max`; see below |

**What `framing_decode` checks.**
- The binary frames are canonical, so what decodes re-encodes to exactly the
Expand All @@ -56,6 +57,54 @@ cargo +nightly fuzz run --fuzz-dir runtime/fuzz decode_no_panic -- \
JSON-RPC requests are only decoded, since encoding needs a `'static` method
name.

**What `sizes_vs_core` checks.** The compiler's size rules
([ComlineProject/core#242](https://github.com/ComlineProject/core/pull/242),
the specification's `sizes`), held to the encoders that write the bytes. The
input is its own grammar, [`src/sizes.rs`](src/sizes.rs): a bounded type graph
of up to six declarations, then values.
- **The types** are what the compiler freezes (`TypeRef`s, with a `Lookup`
over the declarations), so core sizes them as it sizes a package's:
every primitive including the 128-bit integers and `char`; `char[max N]`,
`char[N]`, `T[]`, `T[max N]` and `T[N]` with bounds at the tiers where a
header grows (15/16, 31/32, 127/128, 255/256, 16383/16384, 65535/65536,
2^21) and past what any header says; structs of up to 20 fields, some
`optional`; unit enums of up to 200 variants, with names up to 300 bytes;
aliases; and recursion under `@max_depth` 1 to 4, or none, through an
`optional` field or an array, as a schema with a value of every type has it.
- **The values** are built by `Gen`, in three styles: the simplest (which
exists exactly when core sizes the type), a random one, and one at the
bounds: full-length strings of the worst characters (four-byte UTF-8,
control characters JSON writes as `\u00XX`), the longest counts, the widest
numbers (`i128::MIN`, floats with the longest text), present options, the
last and the longest-named variant. Each is checked to be of the type, then
encoded with the real format through the conformance crate's `Wire`: its
length lies in `[min, max]`, and `encoded_len` says the same.
- **The witnesses** are core's least and most values of each type, the root's
and each declaration's. The least must encode to exactly `min` bytes and the
most to exactly `max` (unless the range is marked an upper bound), which is
what makes a range tight, not only sound. How much a witness may take to
build is bounded, but by what it holds: one of mostly characters may be
`2^23`, which reaches a string of 2^21 characters (a four-byte varint); one
of nodes, `2^18`, which reaches a MessagePack array of 65536 items (a
five-byte header).
- **The tiers the vectors can't store** (a value of megabytes) are the
deterministic tests of [`tests/sizes.rs`](tests/sizes.rs), which is also the
proptest twin of the target: random graphs over the same bytes, every
scalar over its range, a MessagePack array of 65536 items, strings and
arrays whose length is a three- and a four-byte varint, an enum of 200
variants, recursion to its limit, and the longest `f32` and `f64` in JSON
(a scan finds none longer than core's maximum, and reaches it). Run it in
release: `cargo test --release --manifest-path runtime/fuzz/Cargo.toml --test sizes`.
`PROPTEST_CASES` raises its count.

A disagreement is one of three things, and which decides what changes: the
sizer (core), a rule in the specification's catalog, or an encoder. A sizer
bug is fixed in core, a rule's in the specification, and an encoder's here,
each with the minimized input as a vector or a unit test.

This crate depends on `comline-core` by git rev, with default features off
(the IR alone, no parser); the runtime library never does.

**What the C ABI targets run.** A `comline_config`, any value allowed (the
supported framings, other names, any handshake, mode, ordering, limit and
room), then operations: this side's `start`, `encode_request`,
Expand Down Expand Up @@ -98,6 +147,10 @@ refuses a count past the bytes left even for such an element, and the vector
exchange under every framing, mode, handshake and ordering, so the fuzzer
starts past the handshake.

`sizes_vs_core` has none: its input is a type graph and values in its own
grammar, which no vector is written in, and the vectors' sizes are what
`tests/sizes.rs` and the conformance crate already run.

The corpora are generated, so they aren't committed (`.gitignore`), and neither
are `artifacts/`.

Expand All @@ -118,6 +171,10 @@ are `artifacts/`.
unless one with that title is already open. It doesn't block anyone's pull
request.

- **Both:** the `sizes` job runs `tests/sizes.rs`, the proptest twin of
`sizes_vs_core` and the tiers too large for a vector: on a pull request at
proptest's default count, nightly with `PROPTEST_CASES=20000`.

Every run writes [`rust.fuzz.json`](#the-report) as an artifact and to its
summary. The nightly run, or a manual one from `main`, also commits it to the
[`reports`](https://github.com/ComlineProject/runtime/tree/reports) branch,
Expand Down
22 changes: 22 additions & 0 deletions runtime/fuzz/fuzz_targets/sizes_vs_core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! A random bounded type graph (a root type and up to six declarations:
//! structs, unit enums, aliases, recursion under `@max_depth`), and values of
//! it, from the bytes: `comline-core` sizes the type for `packed@1`,
//! `msgpack@1` and `json@1`, and each real encoder, through the conformance
//! crate's `Wire`, must write a length inside core's `[min, max]` (and
//! `encoded_len` must say it), while core's least and most witnesses must
//! encode to exactly `min` and `max`. See `comline_runtime_fuzz::sizes`.

#![no_main]

use comline_runtime_fuzz::sizes::check_input;
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
if let Err(why) = check_input(data) {
panic!("{why}");
}
});
1 change: 1 addition & 0 deletions runtime/fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
//! own, in [`ffi`].

pub mod ffi;
pub mod sizes;

use arbitrary::Unstructured;
use comline_runtime::contract::{FORMAT_JSON, FORMAT_MSGPACK, FORMAT_PACKED};
Expand Down
Loading
Loading