Skip to content

Commit 09c2a34

Browse files
authored
Rollup merge of #146503 - joboet:macos-condvar-timeout, r=ibraheemdev
std: improve handling of timed condition variable waits on macOS Fixes rust-lang/rust#37440 (for good). This fixes two issues with `Condvar::wait_timeout` on macOS: Apple's implementation of `pthread_cond_timedwait` internally converts the absolute timeout to a relative one, measured in nanoseconds, but fails to consider overflow when doing so. This results in `wait_timeout` returning much earlier than anticipated when passed a duration that is slightly longer than `u64::MAX` nanoseconds (around 584 years). The existing clamping introduced by rust-lang/rust#42604 to address rust-lang/rust#37440 unfortunately used a maximum duration of 1000 years and thus still runs into the bug when run on older macOS versions (or with `PTHREAD_MUTEX_USE_ULOCK` set to a value other than "1"). See rust-lang/rust#37440 (comment) for context. Reducing the maximum duration alone however would not be enough to make the implementation completely correct. As macOS does not support `pthread_condattr_setclock`, the deadline passed to `pthread_cond_timedwait` is measured against the wall-time clock. `std` currently calculates the deadline by retrieving the current time and adding the duration to that, only for macOS to convert the deadline back to a relative duration by [retrieving the current time itself](https://github.com/apple-oss-distributions/libpthread/blob/1ebf56b3a702df53213c2996e5e128a535d2577e/src/pthread_cond.c#L802-L819) (this conversion is performed before the aforementioned problematic one). Thus, if the wall-time clock is adjusted between the `std` lookup and the system lookup, the relative duration could have changed, possibly even to a value larger than $2^{64}\ \textrm{ns}$. Luckily however, macOS supports the non-standard, tongue-twisting `pthread_cond_timedwait_relative_np` function which avoids the wall-clock-time roundtrip by taking a relative timeout. Even apart from that, this function is perfectly suited for `std`'s purposes: it is public (albeit badly-documented) API, [available since macOS 10.4](https://github.com/apple-oss-distributions/libpthread/blob/1ebf56b3a702df53213c2996e5e128a535d2577e/include/pthread/pthread.h#L555-L559) (that's way below our minimum of 10.12) and completely resilient against wall-time changes as all timeouts are [measured against the monotonic clock](https://github.com/apple-oss-distributions/xnu/blob/e3723e1f17661b24996789d8afc084c0c3303b26/bsd/kern/sys_ulock.c#L741) inside the kernel. Thus, this PR switches `Condvar::wait_timeout` to `pthread_cond_timedwait_relative_np`, making sure to clamp the duration to a maximum of $2^{64} - 1 \ \textrm{ns}$. I've added a miri shim as well, so the only thing missing is a definition of `pthread_cond_timedwait_relative_np` inside `libc`.
2 parents 1163c28 + fdc4932 commit 09c2a34

File tree

4 files changed

+64
-7
lines changed

4 files changed

+64
-7
lines changed

src/shims/unix/foreign_items.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,7 +815,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
815815
"pthread_cond_timedwait" => {
816816
let [cond, mutex, abstime] =
817817
this.check_shim_sig_lenient(abi, CanonAbi::C, link_name, args)?;
818-
this.pthread_cond_timedwait(cond, mutex, abstime, dest)?;
818+
this.pthread_cond_timedwait(cond, mutex, abstime, dest, /* macos_relative_np */ false)?;
819819
}
820820
"pthread_cond_destroy" => {
821821
let [cond] = this.check_shim_sig_lenient(abi, CanonAbi::C, link_name, args)?;

src/shims/unix/macos/foreign_items.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
307307
this.os_unfair_lock_assert_not_owner(lock_op)?;
308308
}
309309

310+
"pthread_cond_timedwait_relative_np" => {
311+
let [cond, mutex, reltime] =
312+
this.check_shim_sig_lenient(abi, CanonAbi::C, link_name, args)?;
313+
this.pthread_cond_timedwait(cond, mutex, reltime, dest, /* macos_relative_np */ true)?;
314+
}
315+
310316
_ => return interp_ok(EmulateItemResult::NotSupported),
311317
};
312318

src/shims/unix/sync.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -834,8 +834,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
834834
&mut self,
835835
cond_op: &OpTy<'tcx>,
836836
mutex_op: &OpTy<'tcx>,
837-
abstime_op: &OpTy<'tcx>,
837+
timeout_op: &OpTy<'tcx>,
838838
dest: &MPlaceTy<'tcx>,
839+
macos_relative_np: bool,
839840
) -> InterpResult<'tcx> {
840841
let this = self.eval_context_mut();
841842

@@ -844,7 +845,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
844845

845846
// Extract the timeout.
846847
let duration = match this
847-
.read_timespec(&this.deref_pointer_as(abstime_op, this.libc_ty_layout("timespec"))?)?
848+
.read_timespec(&this.deref_pointer_as(timeout_op, this.libc_ty_layout("timespec"))?)?
848849
{
849850
Some(duration) => duration,
850851
None => {
@@ -853,14 +854,23 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
853854
return interp_ok(());
854855
}
855856
};
856-
if data.clock == TimeoutClock::RealTime {
857-
this.check_no_isolation("`pthread_cond_timedwait` with `CLOCK_REALTIME`")?;
858-
}
857+
858+
let (clock, anchor) = if macos_relative_np {
859+
// `pthread_cond_timedwait_relative_np` always measures time against the
860+
// monotonic clock, regardless of the condvar clock.
861+
(TimeoutClock::Monotonic, TimeoutAnchor::Relative)
862+
} else {
863+
if data.clock == TimeoutClock::RealTime {
864+
this.check_no_isolation("`pthread_cond_timedwait` with `CLOCK_REALTIME`")?;
865+
}
866+
867+
(data.clock, TimeoutAnchor::Absolute)
868+
};
859869

860870
this.condvar_wait(
861871
data.condvar_ref,
862872
mutex_ref,
863-
Some((data.clock, TimeoutAnchor::Absolute, duration)),
873+
Some((clock, anchor, duration)),
864874
Scalar::from_i32(0),
865875
this.eval_libc("ETIMEDOUT"), // retval_timeout
866876
dest.clone(),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//@only-target: apple # `pthread_cond_timedwait_relative_np` is a non-standard extension
2+
3+
use std::time::Instant;
4+
5+
// FIXME: remove once this is in libc.
6+
mod libc {
7+
pub use ::libc::*;
8+
unsafe extern "C" {
9+
pub unsafe fn pthread_cond_timedwait_relative_np(
10+
cond: *mut libc::pthread_cond_t,
11+
lock: *mut libc::pthread_mutex_t,
12+
timeout: *const libc::timespec,
13+
) -> libc::c_int;
14+
}
15+
}
16+
17+
fn main() {
18+
unsafe {
19+
let mut mutex: libc::pthread_mutex_t = libc::PTHREAD_MUTEX_INITIALIZER;
20+
let mut cond: libc::pthread_cond_t = libc::PTHREAD_COND_INITIALIZER;
21+
22+
// Wait for 100 ms.
23+
let timeout = libc::timespec { tv_sec: 0, tv_nsec: 100_000_000 };
24+
25+
assert_eq!(libc::pthread_mutex_lock(&mut mutex as *mut _), 0);
26+
27+
let current_time = Instant::now();
28+
assert_eq!(
29+
libc::pthread_cond_timedwait_relative_np(&mut cond, &mut mutex, &timeout),
30+
libc::ETIMEDOUT
31+
);
32+
let elapsed_time = current_time.elapsed().as_millis();
33+
// This is actually deterministic (since isolation remains enabled),
34+
// but can change slightly with Rust updates.
35+
assert!(90 <= elapsed_time && elapsed_time <= 110);
36+
37+
assert_eq!(libc::pthread_mutex_unlock(&mut mutex), 0);
38+
assert_eq!(libc::pthread_mutex_destroy(&mut mutex), 0);
39+
assert_eq!(libc::pthread_cond_destroy(&mut cond), 0);
40+
}
41+
}

0 commit comments

Comments
 (0)