From 0cc05e744c8aa24bfce88e341d3f55589181971a Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Sat, 6 Jun 2026 20:06:34 -0700 Subject: [PATCH] fix(quinn-udp): drain full batch on partial sendmsg_x (Apple) `sendmsg_x` behaves like `sendmmsg`: it delivers as many messages as the interface queue accepts and returns that count, which can be fewer than requested when the queue fills mid-batch. The Apple fast-datapath send discarded the count and always returned `Ok(())`, silently dropping the un-sent tail of a partially-sent batch. Under load this shows up as upstream UDP loss (and collapsed TCP throughput) with no NIC-level drops. Loop until the whole batch is drained, surfacing the error only when no messages were sent so the caller can still back-pressure on `ENOBUFS`. Co-Authored-By: Claude Opus 4.8 --- quinn-udp/src/apple_fast.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/quinn-udp/src/apple_fast.rs b/quinn-udp/src/apple_fast.rs index 28d530e28c..3c7c0bcd19 100644 --- a/quinn-udp/src/apple_fast.rs +++ b/quinn-udp/src/apple_fast.rs @@ -65,7 +65,27 @@ fn send_via_sendmsg_x( let Some(sendmsg_x) = state.resolve_apple_fast_fn(sendmsg_x_fn) else { return send_single(state, io, transmit); }; - retry_if_interrupted(|| unsafe { sendmsg_x(io.as_raw_fd(), hdrs.as_ptr(), cnt as u32, 0) })?; + // `sendmsg_x` behaves like `sendmmsg`: it delivers as many messages as the interface queue + // can accept and returns that count, which may be fewer than requested when the queue fills + // mid-batch. We must drain the remainder ourselves; otherwise the un-sent tail is silently + // dropped (manifesting as upstream packet loss under load). + let total = cnt as usize; + let mut sent = 0; + while sent < total { + match retry_if_interrupted(|| unsafe { + sendmsg_x(io.as_raw_fd(), hdrs[sent..].as_ptr(), (total - sent) as u32, 0) + }) { + Ok(n) => sent += n as usize, + // Nothing sent yet: surface the error (e.g. `ENOBUFS`) so the caller can back-pressure. + Err(e) if sent == 0 => return Err(e), + // We already delivered part of the batch; returning would risk a duplicate re-send by + // the caller. Retry transient pressure; on a hard error keep what we sent, drop the rest. + Err(e) => match e.raw_os_error() { + Some(libc::ENOBUFS) | Some(libc::EAGAIN) => continue, + _ => return Ok(()), + }, + } + } Ok(()) }