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..bd60e3990 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().unwrap_or((crate::timespec::Secs::MAX, 0)); #[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..c1c099e67 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().unwrap_or((crate::timespec::Secs::MAX, 0)); 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..410221e4c 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) -> Option<(Secs, u32)> { + let mut sec = self.tv_sec; + let mut usec = (self.tv_nsec + 999) / 1000; + if usec >= 1_000_000 { + sec = sec.checked_add(1)?; + usec -= 1_000_000; + } + 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().ok_or(crate::io::Errno::INVAL)?; + 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_or(crate::io::Errno::OPNOTSUPP)?; + 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); +}