From 5c518f2def861f727ce35d773f7184d67dced41a Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:25:53 +0100 Subject: [PATCH 1/2] fix(bug): ns->us overflow handling --- src/backend/libc/event/syscalls.rs | 12 +---- src/backend/libc/event/windows_syscalls.rs | 9 +--- src/backend/libc/net/sockopt.rs | 19 ++++---- src/backend/linux_raw/net/sockopt.rs | 14 ++++-- src/timespec.rs | 55 ++++++++++++++++++++++ tests/event/select.rs | 30 ++++++++++++ 6 files changed, 108 insertions(+), 31 deletions(-) diff --git a/src/backend/libc/event/syscalls.rs b/src/backend/libc/event/syscalls.rs index 3827a2f99..268638287 100644 --- a/src/backend/libc/event/syscalls.rs +++ b/src/backend/libc/event/syscalls.rs @@ -247,11 +247,7 @@ pub(crate) unsafe fn select( let timeout_data; let timeout_ptr = match timeout { Some(timeout) => { - // Convert from `Timespec` to `c::timeval`. - timeout_data = c::timeval { - tv_sec: timeout.tv_sec.try_into().map_err(|_| io::Errno::INVAL)?, - tv_usec: ((timeout.tv_nsec + 999) / 1000) as _, - }; + timeout_data = timeout.to_timeval()?; &timeout_data } None => null(), @@ -324,11 +320,7 @@ pub(crate) unsafe fn select( let timeout_data; let timeout_ptr = match timeout { Some(timeout) => { - // Convert from `Timespec` to `c::timeval`. - timeout_data = c::timeval { - tv_sec: timeout.tv_sec.try_into().map_err(|_| io::Errno::INVAL)?, - tv_usec: ((timeout.tv_nsec + 999) / 1000) as _, - }; + timeout_data = timeout.to_timeval()?; &timeout_data } None => null(), diff --git a/src/backend/libc/event/windows_syscalls.rs b/src/backend/libc/event/windows_syscalls.rs index 05394f55d..e615bdc4a 100644 --- a/src/backend/libc/event/windows_syscalls.rs +++ b/src/backend/libc/event/windows_syscalls.rs @@ -54,14 +54,7 @@ pub(crate) fn select( let timeout_data; let timeout_ptr = match timeout { Some(timeout) => { - // Convert from `Timespec` to `TIMEVAL`. - timeout_data = c::TIMEVAL { - tv_sec: timeout - .tv_sec - .try_into() - .map_err(|_| io::Errno::OPNOTSUPP)?, - tv_usec: ((timeout.tv_nsec + 999) / 1000) as _, - }; + timeout_data = timeout.to_timeval()?; &timeout_data } None => null(), diff --git a/src/backend/libc/net/sockopt.rs b/src/backend/libc/net/sockopt.rs index 0a8541461..0c271e370 100644 --- a/src/backend/libc/net/sockopt.rs +++ b/src/backend/libc/net/sockopt.rs @@ -233,18 +233,19 @@ pub(crate) fn set_socket_timeout( return Err(io::Errno::INVAL); } - // Rust's musl libc bindings deprecated `time_t` while they - // transition to 64-bit `time_t`. What we want here is just - // “whatever type `timeval`'s `tv_sec` is”, so we're ok using - // the deprecated type. + let ts = crate::timespec::Timespec { + tv_sec: timeout + .as_secs() + .try_into() + .unwrap_or(crate::timespec::Secs::MAX), + tv_nsec: timeout.subsec_nanos() as _, + }; + let (sec, usec) = ts.to_sec_usec(); #[allow(deprecated)] - let tv_sec = timeout.as_secs().try_into().unwrap_or(c::time_t::MAX); - - // `subsec_micros` rounds down, so we use `subsec_nanos` and - // manually round up. + let tv_sec = sec.try_into().unwrap_or(c::time_t::MAX); let mut timeout = c::timeval { tv_sec, - tv_usec: ((timeout.subsec_nanos() + 999) / 1000) as _, + tv_usec: usec as _, }; if timeout.tv_sec == 0 && timeout.tv_usec == 0 { timeout.tv_usec = 1; diff --git a/src/backend/linux_raw/net/sockopt.rs b/src/backend/linux_raw/net/sockopt.rs index 85c65b085..3add422a4 100644 --- a/src/backend/linux_raw/net/sockopt.rs +++ b/src/backend/linux_raw/net/sockopt.rs @@ -312,11 +312,17 @@ fn duration_to_linux_old_timeval(timeout: Option) -> io::Result<__kern return Err(io::Errno::INVAL); } - // `subsec_micros` rounds down, so we use `subsec_nanos` and - // manually round up. + let ts = crate::timespec::Timespec { + tv_sec: timeout + .as_secs() + .try_into() + .unwrap_or(crate::timespec::Secs::MAX), + tv_nsec: timeout.subsec_nanos() as _, + }; + let (sec, usec) = ts.to_sec_usec(); let mut timeout = __kernel_old_timeval { - tv_sec: timeout.as_secs().try_into().unwrap_or(c::c_long::MAX), - tv_usec: ((timeout.subsec_nanos() + 999) / 1000) as _, + tv_sec: sec.try_into().unwrap_or(c::c_long::MAX), + tv_usec: usec as _, }; if timeout.tv_sec == 0 && timeout.tv_usec == 0 { timeout.tv_usec = 1; diff --git a/src/timespec.rs b/src/timespec.rs index 44721958f..ee837a575 100644 --- a/src/timespec.rs +++ b/src/timespec.rs @@ -203,6 +203,41 @@ impl Timespec { }) .and_then(|millis| c::c_int::try_from(millis).ok()) } + + /// Convert `Timespec` to seconds and microseconds, rounding up fractional + /// microseconds and carrying any overflow into seconds. + #[inline] + pub(crate) fn to_sec_usec(&self) -> (Secs, u32) { + let mut sec = self.tv_sec; + let mut usec = (self.tv_nsec + 999) / 1000; + if usec >= 1_000_000 { + sec = sec.saturating_add(1); + usec -= 1_000_000; + } + (sec, usec as u32) + } + + /// Convert from `Timespec` to `c::timeval`, rounding up fractional + /// microseconds and carrying any overflow into seconds. + #[cfg(any(libc, target_os = "wasi"))] + pub(crate) fn to_timeval(&self) -> crate::io::Result { + let (sec, usec) = self.to_sec_usec(); + Ok(c::timeval { + tv_sec: sec.try_into().map_err(|_| crate::io::Errno::INVAL)?, + tv_usec: usec as _, + }) + } + + /// Convert from `Timespec` to `c::TIMEVAL`, rounding up fractional + /// microseconds and carrying any overflow into seconds. + #[cfg(windows)] + pub(crate) fn to_timeval(&self) -> crate::io::Result { + let (sec, usec) = self.to_sec_usec(); + Ok(c::TIMEVAL { + tv_sec: sec.try_into().map_err(|_| crate::io::Errno::OPNOTSUPP)?, + tv_usec: usec as _, + }) + } } impl TryFrom for Duration { @@ -396,6 +431,26 @@ mod tests { assert_eq!(t.tv_sec as u64, 0x1_0000_0000_u64); } + #[cfg(any(libc, target_os = "wasi", windows))] + #[test] + fn test_to_timeval() { + let ts = Timespec { + tv_sec: 4, + tv_nsec: 999_999_500, + }; + let tv = ts.to_timeval().unwrap(); + assert_eq!(tv.tv_sec, 5); + assert_eq!(tv.tv_usec, 0); + + let ts2 = Timespec { + tv_sec: 2, + tv_nsec: 500_000_000, + }; + let tv2 = ts2.to_timeval().unwrap(); + assert_eq!(tv2.tv_sec, 2); + assert_eq!(tv2.tv_usec, 500_000); + } + // Test that our workarounds are needed. #[cfg(fix_y2038)] #[test] diff --git a/tests/event/select.rs b/tests/event/select.rs index 13b502a34..0ccf6bb3c 100644 --- a/tests/event/select.rs +++ b/tests/event/select.rs @@ -429,3 +429,33 @@ fn test_select_iter() { fn fd_set_contains(fds: &[FdSetElement], fd: RawFd) -> bool { FdSetIter::new(fds).any(|x| x == fd) } + +#[cfg(feature = "pipe")] +#[cfg(not(windows))] +#[test] +fn test_select_near_second_boundary_timeout() { + use rustix::pipe::pipe; + + let (reader, _writer) = pipe().unwrap(); + let nfds = reader.as_raw_fd() + 1; + let mut readfds = vec![FdSetElement::default(); fd_set_num_elements(1, nfds)]; + fd_set_insert(&mut readfds, reader.as_raw_fd()); + + // 999_999_500 ns will ceiling-divide to 1_000_000 microseconds. + // Ensure that it rolls over to tv_sec = 1, tv_usec = 0 instead of tv_usec = 1_000_000 + // which fails on macOS with EINVAL. + let num = retry_on_intr(|| unsafe { + select( + nfds, + Some(&mut readfds), + None, + None, + Some(&Timespec { + tv_sec: 0, + tv_nsec: 999_999_500, + }), + ) + }) + .unwrap(); + assert_eq!(num, 0); +} From 4ca8d6e54100409ff092ba088cbc67ee0b4c7a4c Mon Sep 17 00:00:00 2001 From: Hal Frigaard <4559349+HalFrgrd@users.noreply.github.com> Date: Fri, 7 Aug 2026 21:40:30 +0100 Subject: [PATCH 2/2] Keep semantics --- src/backend/libc/net/sockopt.rs | 2 +- src/backend/linux_raw/net/sockopt.rs | 2 +- src/timespec.rs | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/backend/libc/net/sockopt.rs b/src/backend/libc/net/sockopt.rs index 0c271e370..bd60e3990 100644 --- a/src/backend/libc/net/sockopt.rs +++ b/src/backend/libc/net/sockopt.rs @@ -240,7 +240,7 @@ pub(crate) fn set_socket_timeout( .unwrap_or(crate::timespec::Secs::MAX), tv_nsec: timeout.subsec_nanos() as _, }; - let (sec, usec) = ts.to_sec_usec(); + let (sec, usec) = ts.to_sec_usec().unwrap_or((crate::timespec::Secs::MAX, 0)); #[allow(deprecated)] let tv_sec = sec.try_into().unwrap_or(c::time_t::MAX); let mut timeout = c::timeval { diff --git a/src/backend/linux_raw/net/sockopt.rs b/src/backend/linux_raw/net/sockopt.rs index 3add422a4..c1c099e67 100644 --- a/src/backend/linux_raw/net/sockopt.rs +++ b/src/backend/linux_raw/net/sockopt.rs @@ -319,7 +319,7 @@ fn duration_to_linux_old_timeval(timeout: Option) -> io::Result<__kern .unwrap_or(crate::timespec::Secs::MAX), tv_nsec: timeout.subsec_nanos() as _, }; - let (sec, usec) = ts.to_sec_usec(); + let (sec, usec) = ts.to_sec_usec().unwrap_or((crate::timespec::Secs::MAX, 0)); let mut timeout = __kernel_old_timeval { tv_sec: sec.try_into().unwrap_or(c::c_long::MAX), tv_usec: usec as _, diff --git a/src/timespec.rs b/src/timespec.rs index ee837a575..410221e4c 100644 --- a/src/timespec.rs +++ b/src/timespec.rs @@ -207,21 +207,21 @@ impl Timespec { /// Convert `Timespec` to seconds and microseconds, rounding up fractional /// microseconds and carrying any overflow into seconds. #[inline] - pub(crate) fn to_sec_usec(&self) -> (Secs, u32) { + pub(crate) fn to_sec_usec(&self) -> Option<(Secs, u32)> { let mut sec = self.tv_sec; let mut usec = (self.tv_nsec + 999) / 1000; if usec >= 1_000_000 { - sec = sec.saturating_add(1); + sec = sec.checked_add(1)?; usec -= 1_000_000; } - (sec, usec as u32) + Some((sec, usec as u32)) } /// Convert from `Timespec` to `c::timeval`, rounding up fractional /// microseconds and carrying any overflow into seconds. #[cfg(any(libc, target_os = "wasi"))] pub(crate) fn to_timeval(&self) -> crate::io::Result { - let (sec, usec) = self.to_sec_usec(); + let (sec, usec) = self.to_sec_usec().ok_or(crate::io::Errno::INVAL)?; Ok(c::timeval { tv_sec: sec.try_into().map_err(|_| crate::io::Errno::INVAL)?, tv_usec: usec as _, @@ -232,7 +232,7 @@ impl Timespec { /// microseconds and carrying any overflow into seconds. #[cfg(windows)] pub(crate) fn to_timeval(&self) -> crate::io::Result { - let (sec, usec) = self.to_sec_usec(); + let (sec, usec) = self.to_sec_usec().ok_or(crate::io::Errno::OPNOTSUPP)?; Ok(c::TIMEVAL { tv_sec: sec.try_into().map_err(|_| crate::io::Errno::OPNOTSUPP)?, tv_usec: usec as _,