Skip to content

feat(fuzz): add a cargo-fuzz crate for the P2P wire parsers and req/resp codec - #768

Draft
pablodeymo wants to merge 1 commit into
mainfrom
feat/fuzzing-p2p-layer0
Draft

pablodeymo wants to merge 1 commit into
mainfrom
feat/fuzzing-p2p-layer0

Conversation

@pablodeymo

Copy link
Copy Markdown
Collaborator

🗒️ Description / Motivation

ethlambda had no fuzzing. Every byte a remote peer sends goes through a
parser (snappy, varint, chunked response framing, SSZ containers, topic
strings, ENRs) and then the req/resp Codec, and none of that was
exercised beyond hand-written unit tests and the spec fixtures. A panic or
an unbounded allocation anywhere on that path is a remote crash of the
node.

This adds a cargo-fuzz (libFuzzer) crate at fuzz/, with sixteen
coverage-guided targets over the P2P input path, bottom-up: the wire
parsers first (layer 0), then the framing and the real Codec as libp2p
drives it, over an in-memory cursor with no runtime (layer 1). It copies
the layout and workflow shape of lambdaclass/libssz's fuzz crate so the
tooling is familiar. Generic SSZ decoding is not fuzzed here: libssz already
fuzzes it, differentially against lighthouse's ethereum_ssz.

Deliberately not in this PR: a CI job (the targets are run by hand for
now; a smoke/nightly workflow is a follow-up), the validation rules with a
store (layer 2) and a hostile-peer driver against the live actor (layer 3).

What Changed

  • fuzz/: a separate cargo workspace on the pinned nightly
    (fuzz/rust-toolchain.toml, the same date the cooldown check installs),
    so libfuzzer-sys and nightly never enter the production lockfile or
    build. Its lockfile is committed and covered by make cooldown-check.
    • src/lib.rs: two helpers, a selector-byte split and a once-built
      Config::mainnet().
    • fuzz_targets/: sixteen targets, each documenting its property at the
      top. Layer 0: snappy_raw, topic_strings, enr_parse,
      lean_gossip_ssz, lean_req_resp_ssz, beacon_gossip_decode,
      beacon_slot_readers, beacon_status_metadata, beacon_container_ssz,
      xmss_decode. Layer 1: varint, snappy_framed, chunked_response,
      codec_lean, codec_beacon, message_id.
  • crates/net/p2p: a fuzzing cargo feature, off by default, that
    re-exports the crate-private gossip codec helpers, the lean topic parser,
    the req/resp framing functions, the codec (as ReqRespCodec, since
    Codec is already a name at the crate root) and a wrapper over the
    private message-id function. The production API is unchanged.
  • Makefile: fuzz-list, fuzz FUZZ_TARGET=<name>, fuzz-smoke FUZZ_SECONDS=<n>; fuzz/Cargo.toml added to the cooldown check's
    manifest list.
  • docs/fuzzing.md (linked from the mdbook index): how to run, reproduce
    and minimise, the target table, the design and the crash policy.

Correctness / Behavior Guarantees

  • No runtime behavior changes. The only edits to production crates are a
    feature-gated set of pub use lines and one feature-gated wrapper
    function in ethlambda-p2p.
  • fuzz/Cargo.toml pins libssz and the libp2p fork to the same revs the
    workspace does, so the targets cannot fuzz a different decoder or build a
    different StreamProtocol type than the node ships.
  • Properties checked beyond "no panic": the snappy and payload caps hold
    whatever the header claims; compress/decompress and write/read round trips
    are the identity; the slot readers agree with the full decode; chunked
    responses never exceed their limit; a request or response the codec read
    writes and reads back the same.

Tests Added / Run

  • Every target built (cargo fuzz build) and ran 20 s under
    -timeout=10 -rss_limit_mb=2048. Fifteen are clean.
  • First finding: varint trips on ff ff ff ff ff 0a.
    decode_varint (req_resp/encoding.rs:320) shifts by 35 bits on a sixth
    continuation byte, a panic with overflow checks and a silent wrap without.
    Not reachable from the wire: its only production caller, read_varint,
    stops after five bytes. The fix (bound the loop, regression test) is left
    for its own PR per the crash policy in docs/fuzzing.md; until then this
    one target fails immediately.
  • cargo clippy -p ethlambda-p2p --features fuzzing -- -D warnings: clean.
  • cargo test -p ethlambda-p2p --profile release-fast: passing.
  • make cooldown-check with the fuzz lockfile included: clean.

Related Issues / PRs

  • Follow-ups: fix decode_varint; a fuzz CI workflow (smoke per PR, nightly
    with a cached corpus); layer 2 and 3 targets.

✅ Verification Checklist

  • Ran make fmt — clean
  • Ran make lint (clippy with -D warnings) — clean
  • Ran make test (test-consensus plus test-node, at release-fast) — only ethlambda-p2p's tests run locally; no other crate's code changed

input path, so every parser a peer's bytes reach is fuzzed and not only
unit-tested: snappy, varint and chunked-response framing, topic strings,
ENRs, the lean and beacon gossip and req/resp containers, XMSS material,
the gossipsub message id, and the req/resp Codec itself driven over an
in-memory cursor for every lean and beacon protocol. The crate is its own
workspace on a pinned nightly so libfuzzer and nightly stay out of the
production build, pins libssz and the libp2p fork to the workspace's revs,
and its lockfile joins the cooldown check. ethlambda-p2p gains a `fuzzing`
feature, off by default, that re-exports the crate-private helpers the
targets need. Makefile targets and docs/fuzzing.md cover running,
reproducing and minimising; there is no CI job yet. The varint target
already finds a latent shift overflow in decode_varint on a sixth
continuation byte, unreachable from the wire because read_varint stops at
five; the fix is left for its own PR.

This branch has not been deployed

No deployments
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