Skip to content

tests: add fuzzing support with a packet parser target - #502

Merged
keepsimple1 merged 4 commits into
mainfrom
add-fuzzing
Sep 5, 2026
Merged

keepsimple1 merged 4 commits into
mainfrom
add-fuzzing

Conversation

@keepsimple1

@keepsimple1 keepsimple1 commented Sep 5, 2026

Copy link
Copy Markdown
Owner

What

Adds [cargo-fuzz] scaffolding under fuzz/ and a first target,parse_packet, covering DnsIncoming::new.

That function is the crate's main attack surface: ServiceDaemon::handle_read passes it the bytes it reads off a UDP socket from potentially any host over the network.

Reaching crate internals

dns_parser is a private module, so a fuzz target — a separate crate — cannot call it. Rather than making DnsIncoming public, this adds an unstable-fuzz-api feature that exposes src/fuzz_api.rs, a #[doc(hidden)] module of thin wrappers.

cargo package still excludes fuzz/ entirely, so publishing is unaffected.

The target parse_packet checks invariants, not just panics

A packet that parses must agree with its own header: the question count is exact, the authority and additional counts are upper bounds (a record with malformed RDATA is skipped, so those can legitimately come up short), and a message is exactly one of a query or a response. It also renders the error of a rejected packet and the Debug of an accepted one, since both format slices of the raw packet.

No corpus files are committed

CI is updated to include a simple fuzz build, no actual fuzzing run

Adds cargo-fuzz scaffolding under fuzz/ and a first target, parse_packet,
covering DnsIncoming::new. ServiceDaemon hands that function the bytes it
reads off a UDP socket without inspecting them first, so every byte it
touches is reachable by any host on the link.

dns_parser is a private module, so a fuzz target -- a separate crate --
cannot call it. The new unstable-fuzz-api feature exposes src/fuzz_api.rs,
a #[doc(hidden)] module of thin wrappers, rather than widening the
visibility of dns_parser itself. It is not public API and carries no
stability guarantee.

The target checks invariants, not just the absence of panics: a parsed
message must agree with its own header counts, and must be exactly one of
a query or a response. It also renders the error of a rejected packet and
the Debug of an accepted one, since both format slices of the raw packet.

Running it needs no arguments:

    cargo +nightly fuzz run parse_packet

cargo-fuzz supplies the corpus directory and artifact prefix itself. A
dictionary and an explicit -max_len were both tried and dropped: over
30-second cold starts neither moved coverage outside noise. DnsIncoming::new
has no size-dependent branch -- MAX_PKT_ABSOLUTE_IPV4 bounds the encoder,
not the parser -- so libFuzzer's default 4096-byte cap costs nothing here.
A later target that exercises to_packets would want -max_len=8972.

CI gains a fuzz job that builds the targets on nightly without running a
campaign. clippy --all-features already covers src/fuzz_api.rs, but nothing
else built fuzz/Cargo.toml or the targets, so they could break unnoticed.

The first run of this target found a remotely-triggerable panic, an HINFO
record with RDLENGTH 0 at the end of a message, fixed in #499 and covered
there by a regression test that runs on stable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@keepsimple1
keepsimple1 merged commit 3656343 into main Sep 5, 2026
4 checks passed
@keepsimple1
keepsimple1 deleted the add-fuzzing branch September 5, 2026 04:46
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