Skip to content

fix(socket): restore buffer capacities before reusing recvmmsg headers - #2813

Open
maxlapshin wants to merge 2 commits into
nix-rust:masterfrom
maxlapshin:recvmmsg-loses-cmsg
Open

fix(socket): restore buffer capacities before reusing recvmmsg headers#2813
maxlapshin wants to merge 2 commits into
nix-rust:masterfrom
maxlapshin:recvmmsg-loses-cmsg

Conversation

@maxlapshin

Copy link
Copy Markdown

Description

recvmmsg loses control messages when MultiHeaders are reused

Expected

MultiHeaders is documented as a preallocated structure to be reused across recvmmsg calls, so every call should be able to receive as much control data as the buffer given to preallocate holds.

Actual

only the first call can. msg_namelen and msg_controllen are in-out parameters of recvmsg(2): going in they are the capacity of the buffers, coming out the kernel replaces them with the number of bytes it actually wrote. recvmmsg refills only msg_iov/msg_iovlen between calls, so once a slot receives a datagram carrying no control message, its control capacity stays at zero forever. Every later datagram in that slot comes back with MSG_CTRUNC and cmsgs() returns ENOBUFS — and code that reads control data as an optional extra (.cmsgs().ok()) simply stops seeing it.

Reproduction (added as test_recvmmsg_cmsgs_after_reuse)

bind a UDP socket, send and receive one datagram with ReceiveTimestampns off, then turn it on and receive a second one through the same MultiHeaders. The timestamp never arrives.

Found in the wild

a WebRTC load generator that reads with UDP_GRO — the segment size arrives in a control message. After the first non-coalesced datagram the size stopped arriving, each coalesced batch was counted as a single datagram, and the measurement reported 72% packet loss that did not exist.

Versions

reproduced on 0.29.0 and on master (0.31.3), Linux 6.8 and 7.0.

Fix

remember the capacities in MultiHeaders at preallocate time and restore msg_namelen/msg_controllen (and clear msg_flags) in the recvmmsg loop. msg_namelen matters on the BSDs, where the input value bounds the address copy; on Linux the kernel resets it itself.

Checklist:

  • I have read CONTRIBUTING.md
  • I have written necessary tests and rustdoc comments
  • A change log has been added if this PR modifies nix's API

maxlapshin added a commit to maxlapshin/nix that referenced this pull request Aug 16, 2026

@pacak pacak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think sendmmsg suffers from the same problem - it can write control messages to return timestamps for sent messages.

Comment thread src/sys/socket/mod.rs Outdated
_cmsg_buffers: Option<Box<[u8]>>,
msg_controllen: usize,
// the capacity of every address buffer, needed to restore `msg_namelen`
// before reusing the headers, see `reset_for_receive`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is reset_for_receive?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Changed the code

Comment thread src/sys/socket/mod.rs Outdated
let p = &mut mmsghdr.msg_hdr;
p.msg_iov = slice.as_mut().as_mut_ptr().cast();
p.msg_iovlen = slice.as_mut().len() as _;
p.msg_namelen = msg_namelen;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Name length is restored, but there's no test for it. Doing something with unix sockets might do the trick.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added test_recvmmsg_address_after_reuse

`msg_namelen` and `msg_controllen` are in-out parameters of `recvmsg(2)`:
the kernel replaces the capacity with the length it actually used, and
for the address it also copies no more than the capacity it was given.
Since `recvmmsg` reuses the same `MultiHeaders` across calls and only
refilled `msg_iov`, a slot that once received a datagram without control
messages was left with a control capacity of zero, and every later
datagram in that slot came back with `MSG_CTRUNC` and no control message
at all; a slot that once received from a peer with a short address
truncated the address of every later peer.

`sendmmsg` never sets `msg_controllen` either, so headers that had gone
through `recvmmsg` made it encode control messages into no room at all.

Refill both capacities, and clear `msg_flags`, before every call of both.
@maxlapshin
maxlapshin force-pushed the recvmmsg-loses-cmsg branch from 8400c9b to 1ecff38 Compare August 29, 2026 06:42
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