quinn is a popular QUIC implementation in Rust. I reported two denial-of-service bugs to the maintainers, both are fixed. One lives in the UDP layer below QUIC, the other in how quinn handles a peer's transport parameters.

1. recv_single() spins after a truncated datagram

GHSA-6pp4-4cxf-xf88 · High · quinn-udp, fixed in 0.5.16 and 0.6.3

recv_single is the Unix fallback that receives one datagram per recvmsg call. It is used on OpenBSD and some other Unix and Apple configurations. The normal Linux batched path and Windows use different code.

Two details combined into a loop:

  • The code checked the MSG_TRUNC flag before checking whether recvmsg had failed.
  • The msghdr was initialized only once, so its flags survived from one call to the next.

So after a datagram that was too big for the buffer, the next call returned EAGAIN, but the old MSG_TRUNC flag was still set. The code treated that as "another truncated datagram, discard and try again" instead of returning WouldBlock. The endpoint's runtime thread kept calling recvmsg until a normal datagram happened to arrive. All of this happens before any QUIC packet authentication.

The fix I proposed returns errors before looking at the flags and re-initializes the header before every syscall.

I reproduced it with a localhost regression test that calls recv_single directly with a 16-byte datagram and an 8-byte buffer. The test fails before the fix and passes after it. I checked the OpenBSD kernel source for the relevant semantics but did not run it on native OpenBSD.

Takeaway: check the return value before you read output parameters. And if a struct is both input and output for a syscall, reset it before each call.

2. A peer can make clamp() panic

GHSA-3g6h-r5qx-f6hq / RUSTSEC-2026-0340 · High (CVSS 7.5) · quinn-proto, fixed in 0.11.18

Duration::clamp(min, max) panics if min > max. quinn uses it to compute the ACK delay when sending ACK_FREQUENCY frames, with a lower bound that comes from the peer's min_ack_delay transport parameter.

A peer could send min_ack_delay = 100 ms together with max_ack_delay = 100 ms. Both pass validation. At an RTT of 5 ms, the upper bound is max(5 ms, 25 ms) = 25 ms, which is lower than the 100 ms lower bound. A minimal illustration:

use std::time::Duration; let lower = Duration::from_millis(100); // from the peer's min_ack_delay let upper = Duration::from_millis(25); // max(rtt, 25 ms) at a 5 ms RTT let _ = Duration::from_millis(50).clamp(lower, upper); // panics: min > max 

This only affects connections where sending ACK_FREQUENCY is enabled with TransportConfig::ack_frequency_config(Some(...)), which is off by default. Whether the panic takes down the whole process depends on how the application handles panics, I did not verify a process-wide crash.

Takeaway: clamp has a precondition. If either bound comes from the network, check the relation between the bounds, not just each value on its own.

Updating

  • quinn-udp: 0.5.16 or 0.6.3 and later
  • quinn-proto: 0.11.18 and later

Thanks to the quinn maintainers for the fixes and the credit in the advisories.