refactor(client): replace unsafe libc calls with nix for Linux ancillary data #65

Merged
Lochnair merged 1 commits from fix/ancillary-safe into main 2026-08-11 06:58:23 +00:00
4 changed files with 57 additions and 150 deletions
Showing only changes of commit 373bf32d0f - Show all commits

No files matched your search

Generated
+2 -1
View File
@@ -710,7 +710,7 @@ name = "irtt-client"
version = "0.4.0" version = "0.4.0"
dependencies = [ dependencies = [
"irtt-proto", "irtt-proto",
"libc", "nix 0.31.3",
"socket2", "socket2",
"thiserror 2.0.20", "thiserror 2.0.20",
"tokio", "tokio",
@@ -927,6 +927,7 @@ dependencies = [
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"memoffset",
] ]
[[package]] [[package]]
+4 -2
View File
@@ -13,14 +13,16 @@ categories = ["network-programming"]
[dependencies] [dependencies]
irtt-proto.workspace = true irtt-proto.workspace = true
libc = { version = "0.2.189", optional = true }
socket2 = { version = "0.6.5", features = ["all"] } socket2 = { version = "0.6.5", features = ["all"] }
thiserror = "2.0.20" thiserror = "2.0.20"
tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "sync"], optional = true} tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "sync"], optional = true}
[target.'cfg(target_os = "linux")'.dependencies]
nix = { version = "0.31", default-features = false, features = ["net", "uio"], optional = true }
[features] [features]
default = [] default = []
ancillary = ["dep:libc"] ancillary = ["dep:nix"]
tokio = ["dep:tokio"] tokio = ["dep:tokio"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
+1 -5
View File
@@ -18,11 +18,7 @@
//! [`managed`] contains the unified managed task/handle implementation, //! [`managed`] contains the unified managed task/handle implementation,
//! including the dedicated-runtime blocking frontend for synchronous callers. //! including the dedicated-runtime blocking frontend for synchronous callers.
//! //!
#![cfg_attr( #![forbid(unsafe_code)]
not(all(target_os = "linux", feature = "ancillary")),
forbid(unsafe_code)
)]
#![cfg_attr(all(target_os = "linux", feature = "ancillary"), deny(unsafe_code))]
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
mod async_client; mod async_client;
+50 -142
View File
@@ -1,18 +1,21 @@
#![allow(unsafe_code)]
use std::{ use std::{
io, mem, io::{self, IoSliceMut},
net::{SocketAddr, UdpSocket}, net::{SocketAddr, UdpSocket},
os::fd::{AsRawFd, RawFd}, os::fd::{AsRawFd, RawFd},
ptr,
time::{Duration, SystemTime, UNIX_EPOCH}, time::{Duration, SystemTime, UNIX_EPOCH},
}; };
use nix::sys::{
socket::{recvmsg, setsockopt, sockopt, ControlMessageOwned, MsgFlags, RecvMsg},
time::TimeSpec,
};
use crate::{metadata::ReceiveMeta, receive::ReceivedDatagram, timing::ClientTimestamp}; use crate::{metadata::ReceiveMeta, receive::ReceivedDatagram, timing::ClientTimestamp};
const CONTROL_LEN: usize = 128; const CONTROL_LEN: usize = 128;
pub(crate) fn configure_receive_metadata(socket: &UdpSocket, remote: SocketAddr) -> io::Result<()> { pub(crate) fn configure_receive_metadata(socket: &UdpSocket, remote: SocketAddr) -> io::Result<()> {
enable_kernel_rx_timestamps(socket)?; setsockopt(socket, sockopt::ReceiveTimestampns, &true)?;
let socket = socket2::SockRef::from(socket); let socket = socket2::SockRef::from(socket);
if remote.is_ipv4() { if remote.is_ipv4() {
socket.set_recv_tos_v4(true) socket.set_recv_tos_v4(true)
@@ -21,32 +24,11 @@ pub(crate) fn configure_receive_metadata(socket: &UdpSocket, remote: SocketAddr)
} }
} }
fn enable_kernel_rx_timestamps(socket: &UdpSocket) -> io::Result<()> {
let enabled: libc::c_int = 1;
let result = unsafe {
// SAFETY: The file descriptor is borrowed from a valid `UdpSocket`.
// `enabled` is a properly aligned `c_int`, and the length matches the
// pointed-to value for the `SO_TIMESTAMPNS` boolean socket option.
libc::setsockopt(
socket.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_TIMESTAMPNS,
(&enabled as *const libc::c_int).cast(),
mem::size_of_val(&enabled) as libc::socklen_t,
)
};
if result == -1 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
pub(crate) fn recv_datagram( pub(crate) fn recv_datagram(
socket: &UdpSocket, socket: &UdpSocket,
buf: &mut [u8], buf: &mut [u8],
) -> Result<ReceivedDatagram, io::Error> { ) -> Result<ReceivedDatagram, io::Error> {
recv_datagram_fd(socket.as_raw_fd(), buf, 0) recv_datagram_fd(socket.as_raw_fd(), buf, MsgFlags::empty())
} }
#[cfg(feature = "tokio")] #[cfg(feature = "tokio")]
@@ -55,120 +37,67 @@ pub(crate) fn try_recv_tokio_datagram(
buf: &mut [u8], buf: &mut [u8],
) -> Result<ReceivedDatagram, io::Error> { ) -> Result<ReceivedDatagram, io::Error> {
socket.try_io(tokio::io::Interest::READABLE, || { socket.try_io(tokio::io::Interest::READABLE, || {
recv_datagram_fd(socket.as_raw_fd(), buf, libc::MSG_DONTWAIT) recv_datagram_fd(socket.as_raw_fd(), buf, MsgFlags::MSG_DONTWAIT)
}) })
} }
fn recv_datagram_fd( fn recv_datagram_fd(
socket_fd: RawFd, socket_fd: RawFd,
buf: &mut [u8], buf: &mut [u8],
flags: libc::c_int, flags: MsgFlags,
) -> Result<ReceivedDatagram, io::Error> { ) -> Result<ReceivedDatagram, io::Error> {
let mut control = ControlBuffer::new(); let mut control = ControlBuffer::new();
let mut iov = libc::iovec { let mut iov = [IoSliceMut::new(buf)];
iov_base: buf.as_mut_ptr().cast(),
iov_len: buf.len(),
};
let mut msg: libc::msghdr = unsafe { mem::zeroed() };
msg.msg_iov = &mut iov;
msg.msg_iovlen = 1;
msg.msg_control = control.as_mut_ptr().cast();
msg.msg_controllen = control.len();
let len = unsafe { // The socket is connected, so the source address is not needed; `()` skips
// SAFETY: `msg` points to one writable iovec backed by `buf`, and the // copying it out.
// control buffer is writable and lives until `recvmsg` returns. The let msg = recvmsg::<()>(socket_fd, &mut iov, Some(control.as_mut_slice()), flags)?;
// socket file descriptor is borrowed from a valid UDP socket.
libc::recvmsg(socket_fd, &mut msg, flags)
};
if len < 0 {
return Err(io::Error::last_os_error());
}
let len = usize::try_from(len).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"recvmsg returned an unrepresentable datagram length",
)
})?;
let received_at = ClientTimestamp::now(); let received_at = ClientTimestamp::now();
let meta = unsafe {
// SAFETY: `msg` was initialized by a successful `recvmsg` call. The
// parser only reads cmsghdr entries within `msg_controllen` and ignores
// short or unrelated control messages.
parse_receive_meta(&msg)
};
Ok(ReceivedDatagram { Ok(ReceivedDatagram {
len, len: msg.bytes,
received_at, received_at,
meta, meta: receive_meta(&msg),
}) })
} }
unsafe fn parse_receive_meta(msg: &libc::msghdr) -> ReceiveMeta { fn receive_meta<S>(msg: &RecvMsg<'_, '_, S>) -> ReceiveMeta {
let mut meta = ReceiveMeta::default(); let mut meta = ReceiveMeta::default();
let mut cmsg = libc::CMSG_FIRSTHDR(msg); // A control buffer the kernel had to truncate (`MSG_CTRUNC`) cannot be
while !cmsg.is_null() { // walked reliably, so `cmsgs` refuses to parse it. The datagram itself is
let cmsg_ref = &*cmsg; // still valid; report it without ancillary metadata.
let header_len = libc::CMSG_LEN(0) as usize; let Ok(cmsgs) = msg.cmsgs() else {
if cmsg_ref.cmsg_len < header_len { return meta;
cmsg = libc::CMSG_NXTHDR(msg, cmsg); };
continue;
for cmsg in cmsgs {
match cmsg {
ControlMessageOwned::ScmTimestampns(timestamp) => {
meta.kernel_rx_timestamp = system_time_from_timespec(timestamp);
}
ControlMessageOwned::Ipv4Tos(tos) => {
meta.traffic_class = Some(tos);
}
ControlMessageOwned::Ipv6TClass(traffic_class) => {
meta.traffic_class = u8::try_from(traffic_class).ok();
}
_ => {}
} }
let data_len = cmsg_ref.cmsg_len - header_len;
if is_traffic_class_cmsg(cmsg_ref) {
meta.traffic_class = read_int_cmsg_low_byte(cmsg, data_len);
} else if is_kernel_rx_timestamp_cmsg(cmsg_ref) {
meta.kernel_rx_timestamp = read_timespec_cmsg(cmsg, data_len);
}
cmsg = libc::CMSG_NXTHDR(msg, cmsg);
} }
meta meta
} }
fn is_traffic_class_cmsg(cmsg: &libc::cmsghdr) -> bool { fn system_time_from_timespec(timespec: TimeSpec) -> Option<SystemTime> {
(cmsg.cmsg_level == libc::IPPROTO_IP && cmsg.cmsg_type == libc::IP_TOS) let seconds = u64::try_from(timespec.tv_sec()).ok()?;
|| (cmsg.cmsg_level == libc::IPPROTO_IPV6 && cmsg.cmsg_type == libc::IPV6_TCLASS) let nanos = u32::try_from(timespec.tv_nsec()).ok()?;
} if nanos >= 1_000_000_000 {
fn is_kernel_rx_timestamp_cmsg(cmsg: &libc::cmsghdr) -> bool {
cmsg.cmsg_level == libc::SOL_SOCKET && cmsg.cmsg_type == libc::SCM_TIMESTAMPNS
}
unsafe fn read_u8_cmsg(cmsg: *mut libc::cmsghdr, data_len: usize) -> Option<u8> {
if data_len >= mem::size_of::<u8>() {
Some(*libc::CMSG_DATA(cmsg).cast::<u8>())
} else {
None
}
}
unsafe fn read_int_cmsg_low_byte(cmsg: *mut libc::cmsghdr, data_len: usize) -> Option<u8> {
if data_len >= mem::size_of::<libc::c_int>() {
let value = ptr::read_unaligned(libc::CMSG_DATA(cmsg).cast::<libc::c_int>());
Some((value & 0xff) as u8)
} else {
read_u8_cmsg(cmsg, data_len)
}
}
unsafe fn read_timespec_cmsg(cmsg: *mut libc::cmsghdr, data_len: usize) -> Option<SystemTime> {
if data_len < mem::size_of::<libc::timespec>() {
return None; return None;
} }
let timespec = ptr::read_unaligned(libc::CMSG_DATA(cmsg).cast::<libc::timespec>());
system_time_from_timespec(timespec)
}
fn system_time_from_timespec(timespec: libc::timespec) -> Option<SystemTime> {
if timespec.tv_sec < 0 || timespec.tv_nsec < 0 || timespec.tv_nsec >= 1_000_000_000 {
return None;
}
let seconds = u64::try_from(timespec.tv_sec).ok()?;
let nanos = u32::try_from(timespec.tv_nsec).ok()?;
UNIX_EPOCH.checked_add(Duration::new(seconds, nanos)) UNIX_EPOCH.checked_add(Duration::new(seconds, nanos))
} }
/// Reusable per-receive control buffer. `recvmsg` control data must satisfy
/// `cmsghdr` alignment, which 8-byte alignment covers on supported targets.
#[repr(align(8))] #[repr(align(8))]
struct ControlBuffer([u8; CONTROL_LEN]); struct ControlBuffer([u8; CONTROL_LEN]);
@@ -177,12 +106,8 @@ impl ControlBuffer {
Self([0; CONTROL_LEN]) Self([0; CONTROL_LEN])
} }
fn as_mut_ptr(&mut self) -> *mut u8 { fn as_mut_slice(&mut self) -> &mut [u8] {
self.0.as_mut_ptr() &mut self.0
}
fn len(&self) -> usize {
self.0.len()
} }
} }
@@ -194,6 +119,8 @@ mod tests {
time::{Duration, UNIX_EPOCH}, time::{Duration, UNIX_EPOCH},
}; };
use nix::sys::time::TimeSpec;
use crate::{ use crate::{
event::PacketMeta, event::PacketMeta,
receive::{configure_receive_metadata, recv_datagram}, receive::{configure_receive_metadata, recv_datagram},
@@ -412,11 +339,7 @@ mod tests {
#[test] #[test]
fn timespec_conversion_accepts_valid_unix_timestamp() { fn timespec_conversion_accepts_valid_unix_timestamp() {
let timestamp = super::system_time_from_timespec(libc::timespec { let timestamp = super::system_time_from_timespec(TimeSpec::new(1, 2)).unwrap();
tv_sec: 1,
tv_nsec: 2,
})
.unwrap();
assert_eq!( assert_eq!(
timestamp.duration_since(UNIX_EPOCH).unwrap(), timestamp.duration_since(UNIX_EPOCH).unwrap(),
@@ -426,25 +349,10 @@ mod tests {
#[test] #[test]
fn timespec_conversion_rejects_negative_or_invalid_values() { fn timespec_conversion_rejects_negative_or_invalid_values() {
assert_eq!(super::system_time_from_timespec(TimeSpec::new(-1, 0)), None);
assert_eq!(super::system_time_from_timespec(TimeSpec::new(0, -1)), None);
assert_eq!( assert_eq!(
super::system_time_from_timespec(libc::timespec { super::system_time_from_timespec(TimeSpec::new(0, 1_000_000_000)),
tv_sec: -1,
tv_nsec: 0,
}),
None
);
assert_eq!(
super::system_time_from_timespec(libc::timespec {
tv_sec: 0,
tv_nsec: -1,
}),
None
);
assert_eq!(
super::system_time_from_timespec(libc::timespec {
tv_sec: 0,
tv_nsec: 1_000_000_000,
}),
None None
); );
} }