Skip to content

Commit

Permalink
store futexes in per-allocation data rather than globally
Browse files Browse the repository at this point in the history
  • Loading branch information
RalfJung committed Oct 15, 2024
1 parent e1964fe commit ba11d43
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 37 deletions.
39 changes: 24 additions & 15 deletions src/concurrency/sync.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::cell::RefCell;
use std::collections::VecDeque;
use std::collections::hash_map::Entry;
use std::ops::Not;
use std::rc::Rc;
use std::time::Duration;

use rustc_data_structures::fx::FxHashMap;
Expand Down Expand Up @@ -121,6 +123,15 @@ struct Futex {
clock: VClock,
}

#[derive(Default, Clone)]
pub struct FutexRef(Rc<RefCell<Futex>>);

impl VisitProvenance for FutexRef {
fn visit_provenance(&self, _visit: &mut VisitWith<'_>) {
// No provenance
}
}

/// A thread waiting on a futex.
#[derive(Debug)]
struct FutexWaiter {
Expand All @@ -137,9 +148,6 @@ pub struct SynchronizationObjects {
rwlocks: IndexVec<RwLockId, RwLock>,
condvars: IndexVec<CondvarId, Condvar>,
pub(super) init_onces: IndexVec<InitOnceId, InitOnce>,

/// Futex info for the futex at the given address.
futexes: FxHashMap<u64, Futex>,
}

// Private extension trait for local helper methods
Expand Down Expand Up @@ -184,7 +192,7 @@ impl SynchronizationObjects {
}

impl<'tcx> AllocExtra<'tcx> {
pub fn get_sync<T: 'static>(&self, offset: Size) -> Option<&T> {
fn get_sync<T: 'static>(&self, offset: Size) -> Option<&T> {
self.sync.get(&offset).and_then(|s| s.downcast_ref::<T>())
}
}
Expand Down Expand Up @@ -690,7 +698,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
/// On a timeout, `retval_timeout` is written to `dest` and `errno_timeout` is set as the last error.
fn futex_wait(
&mut self,
addr: u64,
futex_ref: FutexRef,
bitset: u32,
timeout: Option<(TimeoutClock, TimeoutAnchor, Duration)>,
retval_succ: Scalar,
Expand All @@ -700,23 +708,25 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
) {
let this = self.eval_context_mut();
let thread = this.active_thread();
let futex = &mut this.machine.sync.futexes.entry(addr).or_default();
let mut futex = futex_ref.0.borrow_mut();
let waiters = &mut futex.waiters;
assert!(waiters.iter().all(|waiter| waiter.thread != thread), "thread is already waiting");
waiters.push_back(FutexWaiter { thread, bitset });
drop(futex);

this.block_thread(
BlockReason::Futex { addr },
BlockReason::Futex,
timeout,
callback!(
@capture<'tcx> {
addr: u64,
futex_ref: FutexRef,
retval_succ: Scalar,
retval_timeout: Scalar,
dest: MPlaceTy<'tcx>,
errno_timeout: Scalar,
}
@unblock = |this| {
let futex = this.machine.sync.futexes.get(&addr).unwrap();
let futex = futex_ref.0.borrow();
// Acquire the clock of the futex.
if let Some(data_race) = &this.machine.data_race {
data_race.acquire_clock(&futex.clock, &this.machine.threads);
Expand All @@ -728,7 +738,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
@timeout = |this| {
// Remove the waiter from the futex.
let thread = this.active_thread();
let futex = this.machine.sync.futexes.get_mut(&addr).unwrap();
let mut futex = futex_ref.0.borrow_mut();
futex.waiters.retain(|waiter| waiter.thread != thread);
// Set errno and write return value.
this.set_last_error(errno_timeout)?;
Expand All @@ -740,11 +750,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
}

/// Returns whether anything was woken.
fn futex_wake(&mut self, addr: u64, bitset: u32) -> InterpResult<'tcx, bool> {
fn futex_wake(&mut self, futex_ref: &FutexRef, bitset: u32) -> InterpResult<'tcx, bool> {
let this = self.eval_context_mut();
let Some(futex) = this.machine.sync.futexes.get_mut(&addr) else {
return interp_ok(false);
};
let mut futex = futex_ref.0.borrow_mut();
let data_race = &this.machine.data_race;

// Each futex-wake happens-before the end of the futex wait
Expand All @@ -757,7 +765,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
return interp_ok(false);
};
let waiter = futex.waiters.remove(i).unwrap();
this.unblock_thread(waiter.thread, BlockReason::Futex { addr })?;
drop(futex);
this.unblock_thread(waiter.thread, BlockReason::Futex)?;
interp_ok(true)
}
}
2 changes: 1 addition & 1 deletion src/concurrency/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ pub enum BlockReason {
/// Blocked on a reader-writer lock.
RwLock(RwLockId),
/// Blocked on a Futex variable.
Futex { addr: u64 },
Futex,
/// Blocked on an InitOnce.
InitOnce(InitOnceId),
/// Blocked on epoll.
Expand Down
17 changes: 14 additions & 3 deletions src/shims/unix/linux/sync.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use crate::concurrency::sync::FutexRef;
use crate::helpers::check_min_arg_count;
use crate::*;

struct LinuxFutex {
futex: FutexRef,
}

/// Implementation of the SYS_futex syscall.
/// `args` is the arguments *including* the syscall number.
pub fn futex<'tcx>(
Expand All @@ -25,9 +30,15 @@ pub fn futex<'tcx>(
let op = this.read_scalar(op)?.to_i32()?;
let val = this.read_scalar(val)?.to_i32()?;

// Storing this inside the allocation means that if an address gets reused by a new allocation,
// we'll use an independent futex queue for this... that seems acceptable.
let futex_ref = this
.get_sync_or_init(addr, |_| interp_ok(LinuxFutex { futex: Default::default() }))?
.futex
.clone();

// This is a vararg function so we have to bring our own type for this pointer.
let addr = this.ptr_to_mplace(addr, this.machine.layouts.i32);
let addr_usize = addr.ptr().addr().bytes();

let futex_private = this.eval_libc_i32("FUTEX_PRIVATE_FLAG");
let futex_wait = this.eval_libc_i32("FUTEX_WAIT");
Expand Down Expand Up @@ -147,7 +158,7 @@ pub fn futex<'tcx>(
if val == futex_val {
// The value still matches, so we block the thread and make it wait for FUTEX_WAKE.
this.futex_wait(
addr_usize,
futex_ref,
bitset,
timeout,
Scalar::from_target_isize(0, this), // retval_succ
Expand Down Expand Up @@ -191,7 +202,7 @@ pub fn futex<'tcx>(
let mut n = 0;
#[allow(clippy::arithmetic_side_effects)]
for _ in 0..val {
if this.futex_wake(addr_usize, bitset)? {
if this.futex_wake(&futex_ref, bitset)? {
n += 1;
} else {
break;
Expand Down
26 changes: 20 additions & 6 deletions src/shims/windows/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ use std::time::Duration;
use rustc_target::abi::Size;

use crate::concurrency::init_once::InitOnceStatus;
use crate::concurrency::sync::FutexRef;
use crate::*;

#[derive(Copy, Clone)]
struct WindowsInitOnce {
id: InitOnceId,
}

struct WindowsFutex {
futex: FutexRef,
}

impl<'tcx> EvalContextExtPriv<'tcx> for crate::MiriInterpCx<'tcx> {}
trait EvalContextExtPriv<'tcx>: crate::MiriInterpCxExt<'tcx> {
// Windows sync primitives are pointer sized.
Expand Down Expand Up @@ -168,7 +173,10 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
let size = this.read_target_usize(size_op)?;
let timeout_ms = this.read_scalar(timeout_op)?.to_u32()?;

let addr = ptr.addr().bytes();
let futex_ref = this
.get_sync_or_init(ptr, |_| interp_ok(WindowsFutex { futex: Default::default() }))?
.futex
.clone();

if size > 8 || !size.is_power_of_two() {
let invalid_param = this.eval_windows("c", "ERROR_INVALID_PARAMETER");
Expand Down Expand Up @@ -196,7 +204,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
if futex_val == compare_val {
// If the values are the same, we have to block.
this.futex_wait(
addr,
futex_ref,
u32::MAX, // bitset
timeout,
Scalar::from_i32(1), // retval_succ
Expand All @@ -219,8 +227,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
// See the Linux futex implementation for why this fence exists.
this.atomic_fence(AtomicFenceOrd::SeqCst)?;

let addr = ptr.addr().bytes();
this.futex_wake(addr, u32::MAX)?;
let futex_ref = this
.get_sync_or_init(ptr, |_| interp_ok(WindowsFutex { futex: Default::default() }))?
.futex
.clone();
this.futex_wake(&futex_ref, u32::MAX)?;

interp_ok(())
}
Expand All @@ -232,8 +243,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
// See the Linux futex implementation for why this fence exists.
this.atomic_fence(AtomicFenceOrd::SeqCst)?;

let addr = ptr.addr().bytes();
while this.futex_wake(addr, u32::MAX)? {}
let futex_ref = this
.get_sync_or_init(ptr, |_| interp_ok(WindowsFutex { futex: Default::default() }))?
.futex
.clone();
while this.futex_wake(&futex_ref, u32::MAX)? {}

interp_ok(())
}
Expand Down
12 changes: 0 additions & 12 deletions tests/pass-dep/concurrency/linux-futex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,6 @@ fn wake_nobody() {
}
}

fn wake_dangling() {
let futex = Box::new(0);
let ptr: *const i32 = &*futex;
drop(futex);

// Wake 1 waiter. Expect zero waiters woken up, as nobody is waiting.
unsafe {
assert_eq!(libc::syscall(libc::SYS_futex, ptr, libc::FUTEX_WAKE, 1), 0);
}
}

fn wait_wrong_val() {
let futex: i32 = 123;

Expand Down Expand Up @@ -286,7 +275,6 @@ fn concurrent_wait_wake() {

fn main() {
wake_nobody();
wake_dangling();
wait_wrong_val();
wait_timeout();
wait_absolute_timeout();
Expand Down

0 comments on commit ba11d43

Please sign in to comment.