high · 7.5Jul 24, 2026

CVE-2026-25800: quinn-proto Unbounded Stream Reassembly Memory Exhaustion

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Any remote peer can crash a Quinn-based server or client by sending deliberately fragmented QUIC stream data with many gaps, forcing the receiver to buffer an unbounded number of out-of-order…

Packagequinn-proto
Ecosystemrust
Affected>= 0.1.0, < 0.11.15
Fixed in0.11.15
CVE-2026-25800: quinn-proto Unbounded Stream Reassembly Memory Exhaustion

The problem

The Assembler struct in quinn-proto accumulates out-of-order QUIC STREAM frames into a gap list so it can reassemble them in order. There was no cap on how many non-contiguous fragments that list could hold.

A malicious peer exploits this by opening a stream, skipping offset 0 entirely, and sending tiny STREAM frames at ever-increasing offsets. Each new frame adds another entry to the gap list. Because the receiver waits for the missing head of the stream before yielding data to the application, none of these frames can be freed, and heap usage grows without bound until the process is killed by the OS.

Proof of concept

A working proof-of-concept for this issue in quinn-proto, with the exact payload below.

python
# Attacker sends QUIC STREAM frames that deliberately omit offset 0.
# Each frame lands at a new, higher offset, adding one gap entry to the
# Assembler's internal BTreeMap with no limit on how many are allowed.
#
# Pseudocode representing the wire-level attack loop:

import socket, struct

# Establish a QUIC connection to the target (e.g., via an existing QUIC library
# or raw UDP with crafted packets). Then, on stream ID 0, send N frames like:
#
#   STREAM frame: stream_id=0, offset=CHUNK*i, length=1, data=b'\x00', fin=False
#
# Critically, offset 0 is NEVER sent, so the Assembler can never drain.
# The receiver buffers all N fragments indefinitely.

GAP_SIZE   = 1024          # bytes between each fragment
NUM_FRAMES = 100_000       # tune upward until target OOM

for i in range(1, NUM_FRAMES + 1):
    offset = GAP_SIZE * i  # start at 1024, never send offset 0
    send_stream_frame(stream_id=0, offset=offset, data=b'\x41', fin=False)

The root cause is CWE-770: the Assembler used an internal ordered collection (a BTreeMap-like structure keyed by byte offset) to track non-contiguous fragments, and that collection had no maximum size. Flow-control windows cap the total bytes in flight, but they do not cap the *number of gaps*, so a sender can create thousands of single-byte fragments within the allowed window, each adding metadata overhead with no data ever becoming readable.

PR #2694 (commit fed0321) fixes this by enforcing a hard limit on the number of gap entries the Assembler will accept. When a new incoming STREAM frame would exceed that limit, the connection is closed with a transport error, preventing unbounded allocation.

The fix

Upgrade quinn-proto to 0.11.15 or later. The fix is in PR #2694 (commit fed0321a9a672819662caab37f5662f1ad91308e). No configuration workaround exists in earlier versions; the only mitigation is upgrading.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research