From 97b094539f2b9824de7991f1036b0d8ba6be6b3f Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Sat, 15 Aug 2026 14:57:21 +0700 Subject: [PATCH 1/2] refactor(client): unify Linux software timestamping Migrate the Linux client's receive kernel timing from SO_TIMESTAMPNS to SO_TIMESTAMPING with RX_SOFTWARE + SOFTWARE, reported via SCM_TIMESTAMPING instead of SCM_TIMESTAMPNS. Linux documents fallback quirks when SO_TIMESTAMP[NS] and SO_TIMESTAMPING are both enabled on a socket, so this establishes one unambiguous software-timestamp facility before client TX timestamp work builds on the same API. ReceiveMeta.kernel_rx_timestamp semantics, PacketMeta propagation, and all PR #100 downstream OWD/RTT/IPDV behavior are unchanged. The receive control buffer is now sized deliberately from cmsg_space::() instead of an unexplained fixed constant. Also add safe, test-only Linux primitives that a follow-up change needs for client TX timestamping: SO_TIMESTAMPING TX flag construction, nonblocking MSG_ERRQUEUE parsing, and extended-error classification that distinguishes a genuine send-timestamp completion from a malformed timestamp record and from a real socket/network error. Production sockets do not enable SOF_TIMESTAMPING_TX_SOFTWARE and nothing drains the error queue in production; these primitives only run under #[cfg(test)]. --- crates/irtt-client/src/receive/linux.rs | 79 +++- .../src/receive/linux/error_queue.rs | 437 ++++++++++++++++++ 2 files changed, 510 insertions(+), 6 deletions(-) create mode 100644 crates/irtt-client/src/receive/linux/error_queue.rs diff --git a/crates/irtt-client/src/receive/linux.rs b/crates/irtt-client/src/receive/linux.rs index db0e6dd..ed1eb6c 100644 --- a/crates/irtt-client/src/receive/linux.rs +++ b/crates/irtt-client/src/receive/linux.rs @@ -1,3 +1,21 @@ +//! Linux ancillary receive metadata. +//! +//! Receive kernel timing uses `SO_TIMESTAMPING` with `RX_SOFTWARE` + +//! `SOFTWARE` (reported via `SCM_TIMESTAMPING`), not the older +//! `SO_TIMESTAMPNS`/`SCM_TIMESTAMPNS`. Linux documents fallback quirks when +//! `SO_TIMESTAMP[NS]` and `SO_TIMESTAMPING` are both enabled on one socket, +//! so this establishes one unambiguous software-timestamp facility before +//! TX-side timing is introduced in a follow-up change. `ReceiveMeta`'s +//! semantics — an optional observed kernel receive wall time, used for +//! downstream one-way delay only when plausible relative to the userspace +//! receive sample — are unchanged by this. +//! +//! `SOF_TIMESTAMPING_TX_SOFTWARE` is never requested on the production +//! socket in [`configure_receive_metadata`]. TX flag construction, +//! `MSG_ERRQUEUE` parsing, and extended-error classification exist as +//! test-only primitives in [`error_queue`] for a follow-up change to build +//! on; production does not generate or drain TX timestamp records yet. + use std::{ io::{self, IoSliceMut}, net::{SocketAddr, UdpSocket}, @@ -6,16 +24,40 @@ use std::{ }; use nix::sys::{ - socket::{recvmsg, setsockopt, sockopt, ControlMessageOwned, MsgFlags, RecvMsg}, + socket::{ + cmsg_space, recvmsg, setsockopt, sockopt, ControlMessageOwned, MsgFlags, RecvMsg, + TimestampingFlag, Timestamps, + }, time::TimeSpec, }; use crate::{metadata::ReceiveMeta, receive::ReceivedDatagram, timing::ClientTimestamp}; -const CONTROL_LEN: usize = 128; +#[cfg(test)] +mod error_queue; + +/// `SO_TIMESTAMPING` flags requested on the production receive socket. +/// +/// `SOF_TIMESTAMPING_RX_SOFTWARE` asks the kernel to generate a software +/// receive timestamp for each datagram; `SOF_TIMESTAMPING_SOFTWARE` asks it +/// to report that timestamp back via `SCM_TIMESTAMPING`. Mixing this API with +/// the older `SO_TIMESTAMP`/`SO_TIMESTAMPNS` on the same socket has +/// documented fallback quirks, so the client uses `SO_TIMESTAMPING` +/// exclusively for receive timing. Hardware and TX-side flags are +/// deliberately excluded: this socket is receive-only. +const RX_TIMESTAMPING_FLAGS: TimestampingFlag = TimestampingFlag::SOF_TIMESTAMPING_RX_SOFTWARE + .union(TimestampingFlag::SOF_TIMESTAMPING_SOFTWARE); + +/// Control buffer capacity for a normal receive: one `SCM_TIMESTAMPING` +/// record (three `TimeSpec`s) plus one traffic-class record. Only one of +/// `IP_TOS` (`u8`) or `IPV6_TCLASS` (`i32`) is ever requested on a given +/// socket, but `cmsg_space` already rounds both up to the same aligned +/// space, so sizing on the larger of the two costs nothing and covers +/// either family. +const CONTROL_LEN: usize = cmsg_space::() + cmsg_space::(); pub(crate) fn configure_receive_metadata(socket: &UdpSocket, remote: SocketAddr) -> io::Result<()> { - setsockopt(socket, sockopt::ReceiveTimestampns, &true)?; + setsockopt(socket, sockopt::Timestamping, &RX_TIMESTAMPING_FLAGS)?; let socket = socket2::SockRef::from(socket); if remote.is_ipv4() { socket.set_recv_tos_v4(true) @@ -72,8 +114,8 @@ fn receive_meta(msg: &RecvMsg<'_, '_, S>) -> ReceiveMeta { for cmsg in cmsgs { match cmsg { - ControlMessageOwned::ScmTimestampns(timestamp) => { - meta.kernel_rx_timestamp = system_time_from_timespec(timestamp); + ControlMessageOwned::ScmTimestampsns(timestamps) => { + meta.kernel_rx_timestamp = system_time_from_timespec(timestamps.system); } ControlMessageOwned::Ipv4Tos(tos) => { meta.traffic_class = Some(tos); @@ -264,7 +306,7 @@ mod tests { assert_eq!(&buf[..datagram.len], b"stamp"); let Some(timestamp) = datagram.meta.kernel_rx_timestamp else { eprintln!( - "skipping kernel timestamp assertion: kernel did not provide SCM_TIMESTAMPNS" + "skipping kernel timestamp assertion: kernel did not provide SCM_TIMESTAMPING" ); return; }; @@ -273,6 +315,31 @@ mod tests { assert!(duration.as_nanos() > 0); } + #[test] + fn kernel_rx_timestamp_and_traffic_class_coexist() { + let (sender, receiver) = connected_ipv4_loopback_pair(); + configure_receive_metadata(&receiver, sender.local_addr().unwrap()).unwrap(); + apply_traffic_class_to_socket(&sender, receiver.local_addr().unwrap(), 184).unwrap(); + sender.send(b"both").unwrap(); + + let mut buf = [0_u8; 16]; + let datagram = recv_datagram(&receiver, &mut buf).unwrap(); + assert_eq!(&buf[..datagram.len], b"both"); + + // Both cmsgs are requested on this socket; the parser must not + // depend on the order the kernel emits them in to observe either. + if datagram.meta.kernel_rx_timestamp.is_none() { + eprintln!("skipping combined assertion: kernel did not provide SCM_TIMESTAMPING"); + return; + } + if datagram.meta.traffic_class.is_none() { + eprintln!("skipping combined assertion: kernel did not provide IP_TOS"); + return; + } + assert!(datagram.meta.kernel_rx_timestamp.is_some()); + assert_eq!(datagram.meta.traffic_class.unwrap() & 0xfc, 184); + } + #[test] fn ipv4_traffic_class_metadata_is_observed_when_kernel_provides_it() { let (sender, receiver) = connected_ipv4_loopback_pair(); diff --git a/crates/irtt-client/src/receive/linux/error_queue.rs b/crates/irtt-client/src/receive/linux/error_queue.rs new file mode 100644 index 0000000..ca34c6f --- /dev/null +++ b/crates/irtt-client/src/receive/linux/error_queue.rs @@ -0,0 +1,437 @@ +//! Safe `MSG_ERRQUEUE` primitives for the future client TX timestamp work +//! (see the parent module's TX-foundation note). +//! +//! Test-only for this PR: production sockets never enable +//! `SOF_TIMESTAMPING_TX_SOFTWARE` and never drain the error queue, so +//! nothing here runs outside `#[cfg(test)]`. It exists so the follow-up PR +//! that wires TX timestamps into probes does not need to rediscover flag +//! construction, error-queue parsing, or extended-error classification. + +use std::{ + io::{self, IoSliceMut}, + os::fd::RawFd, + time::SystemTime, +}; + +use nix::{ + errno::Errno, + sys::socket::{ + cmsg_space, recvmsg, ControlMessageOwned, MsgFlags, TimestampingFlag, Timestamps, + }, +}; + +use super::system_time_from_timespec; + +/// Desired `SO_TIMESTAMPING` configuration for a future TX-timestamped +/// socket. Combines this PR's RX flags with the TX-side flags the next PR +/// needs: `TX_SOFTWARE` to generate a send timestamp, `OPT_ID` for automatic +/// per-datagram correlation, and `OPT_TSONLY` so the notification does not +/// need to retain or copy the original payload. +/// +/// Not applied to any production socket in this PR. +pub(crate) const TX_TIMESTAMPING_FLAGS: TimestampingFlag = + TimestampingFlag::SOF_TIMESTAMPING_RX_SOFTWARE + .union(TimestampingFlag::SOF_TIMESTAMPING_SOFTWARE) + .union(TimestampingFlag::SOF_TIMESTAMPING_TX_SOFTWARE) + .union(TimestampingFlag::SOF_TIMESTAMPING_OPT_ID) + .union(TimestampingFlag::SOF_TIMESTAMPING_OPT_TSONLY); + +/// `SCM_TSTAMP_SND`, the send-timestamp completion kind reported in the +/// `ee_info` field of a `SO_EE_ORIGIN_TIMESTAMPING` extended error. +/// +/// Not exposed by `libc` or `nix`. Value verified against the kernel UAPI +/// enum in `include/uapi/linux/errqueue.h`: +/// `enum { SCM_TSTAMP_SND, SCM_TSTAMP_SCHED, SCM_TSTAMP_ACK };`, so +/// `SCM_TSTAMP_SND == 0`. This is a stable, long-documented UAPI value, not +/// a guess. +const SCM_TSTAMP_SND: u32 = 0; + +/// A large-enough placeholder for the offender address `sock_extended_err` +/// is followed by in the kernel's error-queue cmsg payload (the address is +/// present for network-originated errors, absent for local/timestamp +/// ones). Sized generously so `cmsg_space` reserves enough control-buffer +/// capacity for either an `IPv4` or `IPv6` offender address; never read. +#[repr(C)] +struct ExtendedErrWithAddr { + _err: nix::libc::sock_extended_err, + _addr: nix::libc::sockaddr_in6, +} + +/// Control buffer capacity for one error-queue receive: one extended-error +/// record (with room for the largest possible offender address) plus one +/// `SCM_TIMESTAMPING` record. +const ERROR_QUEUE_CONTROL_LEN: usize = + cmsg_space::() + cmsg_space::(); + +/// A classified record read from a socket's `MSG_ERRQUEUE`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ErrorQueueRecord { + /// A genuine send-timestamp completion: the extended error's origin was + /// `SO_EE_ORIGIN_TIMESTAMPING`, its kind was `SCM_TSTAMP_SND`, and a + /// usable software timestamp accompanied it. + TxTimestamp { id: u32, timestamp: SystemTime }, + /// A timestamp-origin extended error was present, but no usable + /// timestamp accompanied it: either the `SCM_TIMESTAMPING` cmsg was + /// missing, or its value failed to convert to a `SystemTime`. An + /// accuracy failure, not a socket failure. + MalformedTimestamp, + /// An extended error that is not a timestamp completion: a real + /// network, local, or ICMP error surfaced on the error queue. Must not + /// be misclassified as malformed timing metadata. + SocketError { errno: i32, origin: u8 }, + /// No extended error was present in this record's control data (e.g. + /// only unrelated/unknown cmsgs). + Ignored, +} + +/// Classify a decoded set of cmsgs from one `MSG_ERRQUEUE` datagram. +/// +/// Collects the extended error and any accompanying timestamp +/// independently before classifying, so cmsg order never matters. A +/// timestamp is only ever attached to a timestamp-origin extended error; +/// an ID is never reported without a timestamp, and a timestamp is never +/// accepted without a valid timestamp-origin extended error. +pub(crate) fn classify(cmsgs: impl Iterator) -> ErrorQueueRecord { + let mut extended_error: Option<(u32, u8, u32, u32)> = None; + let mut timestamp: Option = None; + + for cmsg in cmsgs { + match cmsg { + ControlMessageOwned::Ipv4RecvErr(err, _) => { + extended_error = Some((err.ee_errno, err.ee_origin, err.ee_info, err.ee_data)); + } + ControlMessageOwned::Ipv6RecvErr(err, _) => { + extended_error = Some((err.ee_errno, err.ee_origin, err.ee_info, err.ee_data)); + } + ControlMessageOwned::ScmTimestampsns(observed) => { + timestamp = Some(observed); + } + _ => {} + } + } + + let Some((errno, origin, info, id)) = extended_error else { + return ErrorQueueRecord::Ignored; + }; + + let is_timestamp_origin = origin == nix::libc::SO_EE_ORIGIN_TIMESTAMPING + && errno == nix::libc::ENOMSG as u32 + && info == SCM_TSTAMP_SND; + + if !is_timestamp_origin { + return ErrorQueueRecord::SocketError { + errno: errno as i32, + origin, + }; + } + + match timestamp.and_then(|observed| system_time_from_timespec(observed.system)) { + Some(timestamp) => ErrorQueueRecord::TxTimestamp { id, timestamp }, + None => ErrorQueueRecord::MalformedTimestamp, + } +} + +/// Nonblocking drain of a single record from `fd`'s `MSG_ERRQUEUE`. +/// +/// Returns `Ok(None)` when the queue is empty (`EAGAIN`/`EWOULDBLOCK`). +/// `SOF_TIMESTAMPING_OPT_TSONLY` notifications carry no meaningful payload, +/// so a zero-length iovec is enough: correlation is by `id`, never by +/// payload content. +pub(crate) fn try_recv_error_queue_record(fd: RawFd) -> io::Result> { + let mut payload: [u8; 0] = []; + let mut iov = [IoSliceMut::new(&mut payload)]; + let mut control = [0_u8; ERROR_QUEUE_CONTROL_LEN]; + + match recvmsg::<()>( + fd, + &mut iov, + Some(&mut control), + MsgFlags::MSG_ERRQUEUE | MsgFlags::MSG_DONTWAIT, + ) { + Ok(msg) => { + let cmsgs = msg + .cmsgs() + .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?; + Ok(Some(classify(cmsgs))) + } + Err(Errno::EWOULDBLOCK) => Ok(None), + Err(errno) => Err(io::Error::from(errno)), + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{SocketAddr, UdpSocket}, + os::fd::AsRawFd, + time::{Duration, Instant, UNIX_EPOCH}, + }; + + use nix::sys::{ + socket::{setsockopt, sockopt}, + time::TimeSpec, + }; + + use super::*; + + fn extended_error( + errno: u32, + origin: u8, + info: u32, + data: u32, + ) -> nix::libc::sock_extended_err { + nix::libc::sock_extended_err { + ee_errno: errno, + ee_origin: origin, + ee_type: 0, + ee_code: 0, + ee_pad: 0, + ee_info: info, + ee_data: data, + } + } + + fn timestamps_with_system(system: TimeSpec) -> Timestamps { + Timestamps { + system, + hw_trans: TimeSpec::new(0, 0), + hw_raw: TimeSpec::new(0, 0), + } + } + + fn send_timestamp_error(id: u32) -> ControlMessageOwned { + ControlMessageOwned::Ipv4RecvErr( + extended_error( + nix::libc::ENOMSG as u32, + nix::libc::SO_EE_ORIGIN_TIMESTAMPING, + SCM_TSTAMP_SND, + id, + ), + None, + ) + } + + #[test] + fn classifies_valid_send_timestamp_completion() { + let cmsgs = vec![ + send_timestamp_error(7), + ControlMessageOwned::ScmTimestampsns(timestamps_with_system(TimeSpec::new(1, 2))), + ]; + + let record = classify(cmsgs.into_iter()); + + assert_eq!( + record, + ErrorQueueRecord::TxTimestamp { + id: 7, + timestamp: UNIX_EPOCH + Duration::new(1, 2), + } + ); + } + + #[test] + fn classifies_timestamp_origin_without_timestamp_as_malformed() { + let cmsgs = vec![send_timestamp_error(3)]; + + assert_eq!( + classify(cmsgs.into_iter()), + ErrorQueueRecord::MalformedTimestamp + ); + } + + #[test] + fn classifies_timestamp_origin_with_invalid_timespec_as_malformed() { + let cmsgs = vec![ + send_timestamp_error(3), + ControlMessageOwned::ScmTimestampsns(timestamps_with_system(TimeSpec::new(-1, 0))), + ]; + + assert_eq!( + classify(cmsgs.into_iter()), + ErrorQueueRecord::MalformedTimestamp + ); + } + + #[test] + fn classifies_non_timestamp_extended_error_as_socket_error() { + let cmsgs = vec![ControlMessageOwned::Ipv4RecvErr( + extended_error( + nix::libc::ECONNREFUSED as u32, + nix::libc::SO_EE_ORIGIN_ICMP, + 0, + 0, + ), + None, + )]; + + assert_eq!( + classify(cmsgs.into_iter()), + ErrorQueueRecord::SocketError { + errno: nix::libc::ECONNREFUSED, + origin: nix::libc::SO_EE_ORIGIN_ICMP, + } + ); + } + + #[test] + fn classifies_timestamping_origin_with_wrong_completion_kind_as_socket_error() { + // Same origin byte as a send-timestamp completion, but a different + // `ee_info` (e.g. SCM_TSTAMP_SCHED/ACK) must not be mistaken for + // SCM_TSTAMP_SND. + let cmsgs = vec![ControlMessageOwned::Ipv4RecvErr( + extended_error( + nix::libc::ENOMSG as u32, + nix::libc::SO_EE_ORIGIN_TIMESTAMPING, + 1, + 0, + ), + None, + )]; + + assert_eq!( + classify(cmsgs.into_iter()), + ErrorQueueRecord::SocketError { + errno: nix::libc::ENOMSG, + origin: nix::libc::SO_EE_ORIGIN_TIMESTAMPING, + } + ); + } + + #[test] + fn ignores_records_without_any_extended_error() { + let cmsgs = vec![ControlMessageOwned::Ipv4Tos(0)]; + + assert_eq!(classify(cmsgs.into_iter()), ErrorQueueRecord::Ignored); + } + + #[test] + fn unknown_cmsgs_do_not_affect_classification() { + let cmsgs = vec![ + ControlMessageOwned::Ipv4Tos(0), + send_timestamp_error(4), + ControlMessageOwned::ScmTimestampsns(timestamps_with_system(TimeSpec::new(9, 0))), + ]; + + assert_eq!( + classify(cmsgs.into_iter()), + ErrorQueueRecord::TxTimestamp { + id: 4, + timestamp: UNIX_EPOCH + Duration::new(9, 0), + } + ); + } + + #[test] + fn cmsg_order_does_not_affect_classification() { + let forward = classify( + vec![ + send_timestamp_error(9), + ControlMessageOwned::ScmTimestampsns(timestamps_with_system(TimeSpec::new(5, 0))), + ] + .into_iter(), + ); + let reversed = classify( + vec![ + ControlMessageOwned::ScmTimestampsns(timestamps_with_system(TimeSpec::new(5, 0))), + send_timestamp_error(9), + ] + .into_iter(), + ); + + assert_eq!(forward, reversed); + } + + /// Bounded poll for one error-queue record, since software TX timestamp + /// delivery is asynchronous even on loopback. Never sleeps hoping + /// delivery "probably" happened; it keeps polling until either a + /// record appears or the overall deadline passes. + fn poll_for_record(fd: RawFd, overall_timeout: Duration) -> Option { + let deadline = Instant::now() + overall_timeout; + loop { + if let Some(record) = try_recv_error_queue_record(fd).unwrap() { + return Some(record); + } + if Instant::now() >= deadline { + return None; + } + std::thread::yield_now(); + } + } + + fn tx_timestamped_loopback_socket() -> Option<(UdpSocket, UdpSocket)> { + let socket = UdpSocket::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); + // Held for the caller's lifetime: if this peer socket were dropped, + // its port would close and subsequent sends would fail with + // ECONNREFUSED once the kernel reports ICMP port-unreachable. + let peer = UdpSocket::bind(SocketAddr::from(([127, 0, 0, 1], 0))).unwrap(); + socket.connect(peer.local_addr().unwrap()).unwrap(); + + match setsockopt(&socket, sockopt::Timestamping, &TX_TIMESTAMPING_FLAGS) { + Ok(()) => Some((socket, peer)), + Err(error) => { + eprintln!( + "skipping TX timestamping smoke test: kernel/container denied \ + SO_TIMESTAMPING TX flags: {error}" + ); + None + } + } + } + + #[test] + fn automatic_opt_id_increments_sequentially_across_separate_sends() { + let Some((socket, _peer)) = tx_timestamped_loopback_socket() else { + return; + }; + let fd = socket.as_raw_fd(); + + // Under heavy parallel test load, loopback UDP sends can + // occasionally surface a stale ICMP-derived socket error unrelated + // to this test's own sockets. That is an environmental condition, + // not a claim this test makes about kernel TX timestamp behavior, + // so it is a skip rather than a failure. + if let Err(error) = socket.send(b"first") { + eprintln!("skipping automatic OPT_ID assertion: first send failed: {error}"); + return; + } + let Some(first) = poll_for_record(fd, Duration::from_secs(2)) else { + eprintln!( + "skipping automatic OPT_ID assertion: no TX timestamp record was \ + delivered within the poll deadline" + ); + return; + }; + assert!( + matches!(first, ErrorQueueRecord::TxTimestamp { id: 0, .. }), + "expected first send to report id 0, got {first:?}" + ); + + if let Err(error) = socket.send(b"second") { + eprintln!("skipping automatic OPT_ID assertion: second send failed: {error}"); + return; + } + let Some(second) = poll_for_record(fd, Duration::from_secs(2)) else { + eprintln!( + "skipping automatic OPT_ID assertion: no TX timestamp record was \ + delivered for the second send within the poll deadline" + ); + return; + }; + assert!( + matches!(second, ErrorQueueRecord::TxTimestamp { id: 1, .. }), + "expected second send to report id 1, got {second:?}" + ); + } + + // NOTE: a deterministic test proving that a failed nonblocking `sendmsg` + // does not consume an automatic OPT_ID is not included here. Forcing a + // reliable, non-flaky `WouldBlock` on a connected loopback UDP socket + // (e.g. via socket-buffer saturation) has no deterministic OS-level + // mechanism available in this environment, and the repository's testing + // policy explicitly forbids faking coverage with sleep/saturation-based + // flakiness. This remains an open assumption for the follow-up PR that + // wires automatic ID correlation into production: it must verify, by + // construction or by a reliable test double, that a `WouldBlock` send + // never advances the ID the kernel will assign to the next + // successfully transmitted timestamped datagram. +} -- 2.54.0 From 12b4816e146e3044855a7605c814a6effb207a31 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Sat, 15 Aug 2026 15:08:30 +0700 Subject: [PATCH 2/2] fix(client): align the error-queue control buffer The MSG_ERRQUEUE control buffer was a plain [u8; N], which only guarantees byte alignment. recvmsg's cmsgs() walks cmsghdr references out of that storage and requires cmsghdr alignment, so this mirrors the parent module's #[repr(align(8))] ControlBuffer instead of relying on incidental stack alignment. --- .../src/receive/linux/error_queue.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/irtt-client/src/receive/linux/error_queue.rs b/crates/irtt-client/src/receive/linux/error_queue.rs index ca34c6f..c8e1ed9 100644 --- a/crates/irtt-client/src/receive/linux/error_queue.rs +++ b/crates/irtt-client/src/receive/linux/error_queue.rs @@ -131,6 +131,19 @@ pub(crate) fn classify(cmsgs: impl Iterator) -> Erro } } +/// Reusable per-receive control buffer. `recvmsg` control data must satisfy +/// `cmsghdr` alignment; a plain `[u8; N]` only guarantees byte alignment, so +/// this mirrors the parent module's `ControlBuffer` rather than reading +/// `cmsghdr`s out of unaligned storage. +#[repr(align(8))] +struct ErrorQueueControlBuffer([u8; ERROR_QUEUE_CONTROL_LEN]); + +impl ErrorQueueControlBuffer { + fn new() -> Self { + Self([0; ERROR_QUEUE_CONTROL_LEN]) + } +} + /// Nonblocking drain of a single record from `fd`'s `MSG_ERRQUEUE`. /// /// Returns `Ok(None)` when the queue is empty (`EAGAIN`/`EWOULDBLOCK`). @@ -140,12 +153,12 @@ pub(crate) fn classify(cmsgs: impl Iterator) -> Erro pub(crate) fn try_recv_error_queue_record(fd: RawFd) -> io::Result> { let mut payload: [u8; 0] = []; let mut iov = [IoSliceMut::new(&mut payload)]; - let mut control = [0_u8; ERROR_QUEUE_CONTROL_LEN]; + let mut control = ErrorQueueControlBuffer::new(); match recvmsg::<()>( fd, &mut iov, - Some(&mut control), + Some(&mut control.0), MsgFlags::MSG_ERRQUEUE | MsgFlags::MSG_DONTWAIT, ) { Ok(msg) => { -- 2.54.0