Summary
quinn-proto bounds its receive-side datagram queue by charging each queued frame's payload length against TransportConfig::datagram_receive_buffer_size. A DATAGRAM frame with a zero-length payload is charged zero, so the eviction loop never runs and the frame is appended to a VecDeque that has no other bound.
A peer that completes a handshake can send a stream of empty DATAGRAM frames and grow the
receiver's heap until the process is OOM-killed (unless application actively fetches DATAGRAM frames all the time).
DATAGRAM frames are not flow-controlled, so with datagram_receive_buffer_size bypassed there is nothing holding the queue down. So even if the application does read the incoming datagrams but is slower than sender, the OOM is still possible.
Details
DatagramState::received, quinn-proto/src/connection/datagrams.rs:128:
let was_empty = self.recv_buffered == 0;
while datagram.data.len() + self.recv_buffered > window {
debug!("dropping stale datagram");
self.recv();
}
self.recv_buffered += datagram.data.len();
self.incoming.push_back(datagram);
- The receive-side accounting is unchanged since it was moved into
DatagramState
(commit 2637) and predates that move, so older versions of quinn are also affected.
Datagram receive is enabled by default. TransportConfig::default() sets
datagram_receive_buffer_size: Some(STREAM_RWND) = 1,250,000
(quinn-proto/src/config/transport.rs:394). Any peer that has not explicitly called
datagram_receive_buffer_size(None) advertises max_datagram_frame_size
(transport_parameters.rs:170) and is exposed. This is not an opt-in feature.
PoC
Put this in quinn-proto/src/tests/mod.rs
#[test]
fn datagram_recv_buffer_bypassed_by_empty_datagrams() {
let _guard = subscribe();
const COUNT: usize = 1000;
let server = ServerConfig {
transport: Arc::new(TransportConfig {
datagram_receive_buffer_size: Some(0), // 0 can be whatever number, admission does not care, default config also vulnerable.
..TransportConfig::default()
}),
..server_config()
};
let mut pair = Pair::new(Default::default(), server);
let (client_ch, server_ch) = pair.connect();
for _ in 0..COUNT {
pair.client_datagrams(client_ch)
.send(Bytes::new(), false)
.unwrap();
}
pair.drive();
// A queue bounded by a 0-byte window cannot hold 1000 entries under any
// per-entry cost model. It does, because each entry was charged 0 bytes.
let mut drained = 0;
while pair.server_datagrams(server_ch).recv().is_some() {
drained += 1;
}
assert_eq!(drained, COUNT);
assert!(!pair.server_conn_mut(server_ch).is_closed());
}
Impact
All servers and clients that do not explicitly disable DATAGRAMS are potentially vulnerable.
Secondary: the same defect on the send path
Datagrams::send has the mirror-image accounting. If client is sending too much, empty datagrams enqueue forever and SendDatagramError::Blocked is never returned. This is a local-application footgun rather than a remote attack, but it shares the root cause.
Summary
quinn-protobounds its receive-side datagram queue by charging each queued frame's payload length againstTransportConfig::datagram_receive_buffer_size. A DATAGRAM frame with a zero-length payload is charged zero, so the eviction loop never runs and the frame is appended to aVecDequethat has no other bound.A peer that completes a handshake can send a stream of empty DATAGRAM frames and grow the
receiver's heap until the process is OOM-killed (unless application actively fetches DATAGRAM frames all the time).
DATAGRAM frames are not flow-controlled, so with
datagram_receive_buffer_sizebypassed there is nothing holding the queue down. So even if the application does read the incoming datagrams but is slower than sender, the OOM is still possible.Details
DatagramState::received,quinn-proto/src/connection/datagrams.rs:128:DatagramState(commit
2637) and predates that move, so older versions of quinn are also affected.Datagram receive is enabled by default.
TransportConfig::default()setsdatagram_receive_buffer_size: Some(STREAM_RWND)= 1,250,000(
quinn-proto/src/config/transport.rs:394). Any peer that has not explicitly calleddatagram_receive_buffer_size(None)advertisesmax_datagram_frame_size(
transport_parameters.rs:170) and is exposed. This is not an opt-in feature.PoC
Put this in quinn-proto/src/tests/mod.rs
Impact
All servers and clients that do not explicitly disable DATAGRAMS are potentially vulnerable.
Secondary: the same defect on the send path
Datagrams::sendhas the mirror-image accounting. If client is sending too much, empty datagrams enqueue forever andSendDatagramError::Blockedis never returned. This is a local-application footgun rather than a remote attack, but it shares the root cause.