Skip to content

recvfuzz: add QUIC version 2 support - #6355

Open
Matteo Panzeri (matte1782) wants to merge 1 commit into
microsoft:mainfrom
matte1782:fuzz/recvfuzz-v2
Open

Matteo Panzeri (matte1782) wants to merge 1 commit into
microsoft:mainfrom
matte1782:fuzz/recvfuzz-v2

Conversation

@matte1782

Copy link
Copy Markdown

What this does

src/tools/recvfuzz/recvfuzz.cpp has declared this gap in its own header comment since it was
written:

Future:
    Add fuzzing for version 2.

This implements it behind a -version:1|2 flag, defaulting to 1 so existing behaviour and
existing CI invocations are unchanged.

Why only the send side changed

The receive side was already version-aware — ParseLongHeaderPacket handles both — and
QuicPacketEncodeLongHeaderV1 already takes Version and branches on it (the V1 in that name is
the wire format of the long header, not the QUIC version). What was hardcoded to v1 was packet
generation, in four places:

Initial salt and HKDF labels v2 values taken from QuicSupportedVersionList in src/core/packet.c, not transcribed from RFC 9369
Initial packet type three PacketParams initialisers, two comparisons, one assignment
Handshake packet type three assignments
Version selection -version:1|2, with any other value rejected explicitly rather than falling back silently

Access goes through five inline accessors over the existing Version global, which is set once at
startup and does not change during a run.

The handshake packet type is worth calling out: without it, a v2 run would send Initial packets with
the correct type and Handshake packets with the v1 code — which in v2 means 0-RTT. No error, no
coverage, and nothing in the output would look wrong.

Verification

Built on Windows x64 Debug, schannel, with AddressSanitizer (-DQUIC_ENABLE_ASAN=on).

v1 v2 v2, one byte of the v2 salt flipped
Recv datagrams 8,868 8,235 0
Recv Initial packets 1,605 1,484 0
Recv Handshake packets 3,789 3,561 0
Send datagrams 20,294 19,605 77

(20-second runs, same seed.)

The right-hand column is the control that makes the rest meaningful. These counters are incremented
in the fuzzer's own receive path after the CID match but before decryption, so on their own
"the counters went up" could just mean datagrams arrived. With one byte wrong in the salt they
collapse to zero and the send side stalls at 77 datagrams because the handshake never advances —
so they do track packets the server accepted and understood.

Longer soak, same build: 69 runs of 5 minutes each in v2 — 10,201,375 datagrams received and
1,789,884 Initial packets parsed, no crashes, no assertion failures, no ASan reports.
Seven
interleaved v1 runs acted as a live control; mean throughput in v2 was within 3.2% of v1, which
is what says the v2 path is being exercised as hard as the v1 path rather than quietly doing less.

Notes for reviewers

  • Tests. recvfuzz has no unit tests, so there is nothing here to extend. Writing this did
    surface a defect in the v2 constants themselves, which is in Fix the QUIC v2 Retry integrity secret, and test it against the RFC Retry vectors #6354 — that one fixes the v2 Retry
    integrity secret (left at the draft-ietf-quic-v2-03 value) and replaces
    CryptTest.WellKnownClientInitialv2, which had been left on the same draft's vectors. The two
    are independent and can be reviewed in either order, though Fix the QUIC v2 Retry integrity secret, and test it against the RFC Retry vectors #6354 is the one that matters.
  • CI. scripts/recvfuzz.ps1 does not pass -version, so nothing changes for the existing
    pipelines. Whether the stress and code-coverage jobs should spend part of their time budget on v2,
    and how much, seemed like your call rather than something to decide in this PR.
  • Per CONTRIBUTING I am treating this as small because it implements a TODO already in the file, but
    if you would rather discuss it first I am glad to move this to an issue or a discussion.

🤖 Generated with Claude Code

The tool's own header has declared this gap since it was written:

    Future:
        Add fuzzing for version 2.

The receive side was already version-aware (ParseLongHeaderPacket handles
both) and QuicPacketEncodeLongHeaderV1 already takes Version and branches
on it, so this changes only the send side, which was hardcoded to v1 in
four places:

- Initial salt and HKDF labels. The v2 values are copied from
  QuicSupportedVersionList in src/core/packet.c rather than transcribed
  from RFC 9369.
- Initial packet type, in three PacketParams initialisers and in the
  handshake comparisons.
- Handshake packet type, in three assignments. Without this a v2 run
  would send Initial with the correct type and Handshake with the v1
  code, which in v2 means 0-RTT: no error, and no coverage.
- Version selection: -version:1|2, rejecting any other value explicitly
  instead of falling back silently.

Verified on Windows x64 Debug schannel with AddressSanitizer. With
-version:2 the server completes handshakes at the same rate as with v1:
10.2M datagrams received and 1.79M Initial packets parsed over 69 runs,
with mean throughput within 3.2% of the v1 control runs. As a negative
control, flipping a single byte of the v2 salt drops every receive
counter to zero and stalls the send side at 77 datagrams, which confirms
those counters measure packets the server actually accepted rather than
packets that merely arrived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@matte1782
Matteo Panzeri (matte1782) requested a review from a team as a code owner October 3, 2026 13:06
main(int argc, char **argv) {
TryGetValue(argc, argv, "timeout", &RunTimeMs);
uint32_t VersionArg = 1;
if (TryGetValue(argc, argv, "version", &VersionArg)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be a fuzzer argument? Is there reason V1 and V2 cannot be tested in a single run, selecting the version randomly?

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.

2 participants