Skip to content

Commit b0e02f0

Browse files
committed
Auto merge of #1568 - fusion-engineering-forks:futex, r=RalfJung
Implement futex_wait and futex_wake. Fixes rust-lang/rust#77406 and fixes #1562. This makes std's park(), park_timeout(), and unpark() work. That means std::sync::Once is usable again and the test pass again with the latest rustc. This also makes parking_lot work.
2 parents 266b75f + 68776d2 commit b0e02f0

File tree

11 files changed

+364
-3
lines changed

11 files changed

+364
-3
lines changed

src/helpers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ pub fn check_arg_count<'a, 'tcx, const N: usize>(args: &'a [OpTy<'tcx, Tag>]) ->
555555

556556
pub fn isolation_error(name: &str) -> InterpResult<'static> {
557557
throw_machine_stop!(TerminationInfo::UnsupportedInIsolation(format!(
558-
"`{}` not available when isolation is enabled",
558+
"{} not available when isolation is enabled",
559559
name,
560560
)))
561561
}

src/shims/posix/linux/foreign_items.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use rustc_middle::mir;
33
use crate::*;
44
use crate::helpers::check_arg_count;
55
use shims::posix::fs::EvalContextExt as _;
6+
use shims::posix::linux::sync::futex;
67
use shims::posix::sync::EvalContextExt as _;
78
use shims::posix::thread::EvalContextExt as _;
89

@@ -112,6 +113,17 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
112113

113114
// Dynamically invoked syscalls
114115
"syscall" => {
116+
// FIXME: The libc syscall() function is a variadic function.
117+
// It's valid to call it with more arguments than a syscall
118+
// needs, so none of these syscalls should use check_arg_count.
119+
// It's even valid to call it with the wrong type of arguments,
120+
// as long as they'd end up in the same place with the calling
121+
// convention used. (E.g. using a `usize` instead of a pointer.)
122+
// It's not directly clear which number, size, and type of arguments
123+
// are acceptable in which cases and which aren't. (E.g. some
124+
// types might take up the space of two registers.)
125+
// So this needs to be researched first.
126+
115127
let sys_getrandom = this
116128
.eval_libc("SYS_getrandom")?
117129
.to_machine_usize(this)?;
@@ -120,6 +132,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
120132
.eval_libc("SYS_statx")?
121133
.to_machine_usize(this)?;
122134

135+
let sys_futex = this
136+
.eval_libc("SYS_futex")?
137+
.to_machine_usize(this)?;
138+
123139
if args.is_empty() {
124140
throw_ub_format!("incorrect number of arguments for syscall: got 0, expected at least 1");
125141
}
@@ -139,6 +155,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
139155
let result = this.linux_statx(dirfd, pathname, flags, mask, statxbuf)?;
140156
this.write_scalar(Scalar::from_machine_isize(result.into(), this), dest)?;
141157
}
158+
// `futex` is used by some synchonization primitives.
159+
id if id == sys_futex => {
160+
futex(this, args, dest)?;
161+
}
142162
id => throw_unsup_format!("miri does not support syscall ID {}", id),
143163
}
144164
}

src/shims/posix/linux/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod foreign_items;
22
pub mod dlsym;
3+
pub mod sync;

src/shims/posix/linux/sync.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use crate::thread::Time;
2+
use crate::*;
3+
use rustc_target::abi::{Align, Size};
4+
use std::time::{Instant, SystemTime};
5+
6+
/// Implementation of the SYS_futex syscall.
7+
pub fn futex<'tcx>(
8+
this: &mut MiriEvalContext<'_, 'tcx>,
9+
args: &[OpTy<'tcx, Tag>],
10+
dest: PlaceTy<'tcx, Tag>,
11+
) -> InterpResult<'tcx> {
12+
// The amount of arguments used depends on the type of futex operation.
13+
// The full futex syscall takes six arguments (excluding the syscall
14+
// number), which is also the maximum amount of arguments a linux syscall
15+
// can take on most architectures.
16+
// However, not all futex operations use all six arguments. The unused ones
17+
// may or may not be left out from the `syscall()` call.
18+
// Therefore we don't use `check_arg_count` here, but only check for the
19+
// number of arguments to fall within a range.
20+
if !(4..=7).contains(&args.len()) {
21+
throw_ub_format!("incorrect number of arguments for futex syscall: got {}, expected between 4 and 7 (inclusive)", args.len());
22+
}
23+
24+
// The first three arguments (after the syscall number itself) are the same to all futex operations:
25+
// (int *addr, int op, int val).
26+
// We checked above that these definitely exist.
27+
let addr = this.read_immediate(args[1])?;
28+
let op = this.read_scalar(args[2])?.to_i32()?;
29+
let val = this.read_scalar(args[3])?.to_i32()?;
30+
31+
// The raw pointer value is used to identify the mutex.
32+
// Not all mutex operations actually read from this address or even require this address to exist.
33+
// This will make FUTEX_WAKE fail on an integer cast to a pointer. But FUTEX_WAIT on
34+
// such a pointer can never work anyway, so that seems fine.
35+
let futex_ptr = this.force_ptr(addr.to_scalar()?)?;
36+
37+
let thread = this.get_active_thread();
38+
39+
let futex_private = this.eval_libc_i32("FUTEX_PRIVATE_FLAG")?;
40+
let futex_wait = this.eval_libc_i32("FUTEX_WAIT")?;
41+
let futex_wake = this.eval_libc_i32("FUTEX_WAKE")?;
42+
let futex_realtime = this.eval_libc_i32("FUTEX_CLOCK_REALTIME")?;
43+
44+
// FUTEX_PRIVATE enables an optimization that stops it from working across processes.
45+
// Miri doesn't support that anyway, so we ignore that flag.
46+
match op & !futex_private {
47+
// FUTEX_WAIT: (int *addr, int op = FUTEX_WAIT, int val, const timespec *timeout)
48+
// Blocks the thread if *addr still equals val. Wakes up when FUTEX_WAKE is called on the same address,
49+
// or *timeout expires. `timeout == null` for an infinite timeout.
50+
op if op & !futex_realtime == futex_wait => {
51+
if args.len() < 5 {
52+
throw_ub_format!("incorrect number of arguments for FUTEX_WAIT syscall: got {}, expected at least 5", args.len());
53+
}
54+
let timeout = args[4];
55+
let timeout_time = if this.is_null(this.read_scalar(timeout)?.check_init()?)? {
56+
None
57+
} else {
58+
let duration = match this.read_timespec(timeout)? {
59+
Some(duration) => duration,
60+
None => {
61+
let einval = this.eval_libc("EINVAL")?;
62+
this.set_last_error(einval)?;
63+
this.write_scalar(Scalar::from_machine_isize(-1, this), dest)?;
64+
return Ok(());
65+
}
66+
};
67+
this.check_no_isolation("FUTEX_WAIT with timeout")?;
68+
Some(if op & futex_realtime != 0 {
69+
Time::RealTime(SystemTime::now().checked_add(duration).unwrap())
70+
} else {
71+
Time::Monotonic(Instant::now().checked_add(duration).unwrap())
72+
})
73+
};
74+
// Check the pointer for alignment and validity.
75+
// The API requires `addr` to be a 4-byte aligned pointer, and will
76+
// use the 4 bytes at the given address as an (atomic) i32.
77+
this.memory.check_ptr_access(addr.to_scalar()?, Size::from_bytes(4), Align::from_bytes(4).unwrap())?;
78+
// Read an `i32` through the pointer, regardless of any wrapper types.
79+
// It's not uncommon for `addr` to be passed as another type than `*mut i32`, such as `*const AtomicI32`.
80+
// FIXME: this fails if `addr` is not a pointer type.
81+
let futex_val = this.read_scalar_at_offset(addr.into(), 0, this.machine.layouts.i32)?.to_i32()?;
82+
if val == futex_val {
83+
// The value still matches, so we block the trait make it wait for FUTEX_WAKE.
84+
this.block_thread(thread);
85+
this.futex_wait(futex_ptr, thread);
86+
// Succesfully waking up from FUTEX_WAIT always returns zero.
87+
this.write_scalar(Scalar::from_machine_isize(0, this), dest)?;
88+
// Register a timeout callback if a timeout was specified.
89+
// This callback will override the return value when the timeout triggers.
90+
if let Some(timeout_time) = timeout_time {
91+
this.register_timeout_callback(
92+
thread,
93+
timeout_time,
94+
Box::new(move |this| {
95+
this.unblock_thread(thread);
96+
this.futex_remove_waiter(futex_ptr, thread);
97+
let etimedout = this.eval_libc("ETIMEDOUT")?;
98+
this.set_last_error(etimedout)?;
99+
this.write_scalar(Scalar::from_machine_isize(-1, this), dest)?;
100+
Ok(())
101+
}),
102+
);
103+
}
104+
} else {
105+
// The futex value doesn't match the expected value, so we return failure
106+
// right away without sleeping: -1 and errno set to EAGAIN.
107+
let eagain = this.eval_libc("EAGAIN")?;
108+
this.set_last_error(eagain)?;
109+
this.write_scalar(Scalar::from_machine_isize(-1, this), dest)?;
110+
}
111+
}
112+
// FUTEX_WAKE: (int *addr, int op = FUTEX_WAKE, int val)
113+
// Wakes at most `val` threads waiting on the futex at `addr`.
114+
// Returns the amount of threads woken up.
115+
// Does not access the futex value at *addr.
116+
op if op == futex_wake => {
117+
let mut n = 0;
118+
for _ in 0..val {
119+
if let Some(thread) = this.futex_wake(futex_ptr) {
120+
this.unblock_thread(thread);
121+
this.unregister_timeout_callback_if_exists(thread);
122+
n += 1;
123+
} else {
124+
break;
125+
}
126+
}
127+
this.write_scalar(Scalar::from_machine_isize(n, this), dest)?;
128+
}
129+
op => throw_unsup_format!("miri does not support SYS_futex operation {}", op),
130+
}
131+
132+
Ok(())
133+
}

src/sync.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,26 @@ struct Condvar {
9696
waiters: VecDeque<CondvarWaiter>,
9797
}
9898

99+
/// The futex state.
100+
#[derive(Default, Debug)]
101+
struct Futex {
102+
waiters: VecDeque<FutexWaiter>,
103+
}
104+
105+
/// A thread waiting on a futex.
106+
#[derive(Debug)]
107+
struct FutexWaiter {
108+
/// The thread that is waiting on this futex.
109+
thread: ThreadId,
110+
}
111+
99112
/// The state of all synchronization variables.
100113
#[derive(Default, Debug)]
101114
pub(super) struct SynchronizationState {
102115
mutexes: IndexVec<MutexId, Mutex>,
103116
rwlocks: IndexVec<RwLockId, RwLock>,
104117
condvars: IndexVec<CondvarId, Condvar>,
118+
futexes: HashMap<Pointer, Futex>,
105119
}
106120

107121
// Private extension trait for local helper methods
@@ -403,4 +417,24 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
403417
let this = self.eval_context_mut();
404418
this.machine.threads.sync.condvars[id].waiters.retain(|waiter| waiter.thread != thread);
405419
}
420+
421+
fn futex_wait(&mut self, addr: Pointer<stacked_borrows::Tag>, thread: ThreadId) {
422+
let this = self.eval_context_mut();
423+
let waiters = &mut this.machine.threads.sync.futexes.entry(addr.erase_tag()).or_default().waiters;
424+
assert!(waiters.iter().all(|waiter| waiter.thread != thread), "thread is already waiting");
425+
waiters.push_back(FutexWaiter { thread });
426+
}
427+
428+
fn futex_wake(&mut self, addr: Pointer<stacked_borrows::Tag>) -> Option<ThreadId> {
429+
let this = self.eval_context_mut();
430+
let waiters = &mut this.machine.threads.sync.futexes.get_mut(&addr.erase_tag())?.waiters;
431+
waiters.pop_front().map(|waiter| waiter.thread)
432+
}
433+
434+
fn futex_remove_waiter(&mut self, addr: Pointer<stacked_borrows::Tag>, thread: ThreadId) {
435+
let this = self.eval_context_mut();
436+
if let Some(futex) = this.machine.threads.sync.futexes.get_mut(&addr.erase_tag()) {
437+
futex.waiters.retain(|waiter| waiter.thread != thread);
438+
}
439+
}
406440
}

tests/compile-fail/fs/isolated_file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// ignore-windows: File handling is not implemented yet
2-
// error-pattern: `open` not available when isolation is enabled
2+
// error-pattern: open not available when isolation is enabled
33

44
fn main() {
55
let _file = std::fs::File::open("file.txt").unwrap();

tests/compile-fail/fs/isolated_stdin.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ extern crate libc;
77
fn main() -> std::io::Result<()> {
88
let mut bytes = [0u8; 512];
99
unsafe {
10-
libc::read(0, bytes.as_mut_ptr() as *mut libc::c_void, 512); //~ ERROR `read` not available when isolation is enabled
10+
libc::read(0, bytes.as_mut_ptr() as *mut libc::c_void, 512); //~ ERROR read not available when isolation is enabled
1111
}
1212
Ok(())
1313
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Unfortunately, the test framework does not support 'only-linux',
2+
// so we need to ignore Windows and macOS instead.
3+
// ignore-macos: Uses Linux-only APIs
4+
// ignore-windows: Uses Linux-only APIs
5+
// compile-flags: -Zmiri-disable-isolation
6+
7+
#![feature(rustc_private)]
8+
extern crate libc;
9+
10+
use std::ptr;
11+
use std::thread;
12+
use std::time::{Duration, Instant};
13+
14+
fn wake_nobody() {
15+
let futex = 0;
16+
17+
// Wake 1 waiter. Expect zero waiters woken up, as nobody is waiting.
18+
unsafe {
19+
assert_eq!(libc::syscall(
20+
libc::SYS_futex,
21+
&futex as *const i32,
22+
libc::FUTEX_WAKE,
23+
1,
24+
), 0);
25+
}
26+
27+
// Same, but without omitting the unused arguments.
28+
unsafe {
29+
assert_eq!(libc::syscall(
30+
libc::SYS_futex,
31+
&futex as *const i32,
32+
libc::FUTEX_WAKE,
33+
1,
34+
0,
35+
0,
36+
0,
37+
), 0);
38+
}
39+
}
40+
41+
fn wake_dangling() {
42+
let futex = Box::new(0);
43+
let ptr: *const i32 = &*futex;
44+
drop(futex);
45+
46+
// Wake 1 waiter. Expect zero waiters woken up, as nobody is waiting.
47+
unsafe {
48+
assert_eq!(libc::syscall(
49+
libc::SYS_futex,
50+
ptr,
51+
libc::FUTEX_WAKE,
52+
1,
53+
), 0);
54+
}
55+
}
56+
57+
fn wait_wrong_val() {
58+
let futex: i32 = 123;
59+
60+
// Only wait if the futex value is 456.
61+
unsafe {
62+
assert_eq!(libc::syscall(
63+
libc::SYS_futex,
64+
&futex as *const i32,
65+
libc::FUTEX_WAIT,
66+
456,
67+
ptr::null::<libc::timespec>(),
68+
), -1);
69+
assert_eq!(*libc::__errno_location(), libc::EAGAIN);
70+
}
71+
}
72+
73+
fn wait_timeout() {
74+
let start = Instant::now();
75+
76+
let futex: i32 = 123;
77+
78+
// Wait for 200ms, with nobody waking us up early.
79+
unsafe {
80+
assert_eq!(libc::syscall(
81+
libc::SYS_futex,
82+
&futex as *const i32,
83+
libc::FUTEX_WAIT,
84+
123,
85+
&libc::timespec {
86+
tv_sec: 0,
87+
tv_nsec: 200_000_000,
88+
},
89+
), -1);
90+
assert_eq!(*libc::__errno_location(), libc::ETIMEDOUT);
91+
}
92+
93+
assert!((200..500).contains(&start.elapsed().as_millis()));
94+
}
95+
96+
fn wait_wake() {
97+
let start = Instant::now();
98+
99+
static FUTEX: i32 = 0;
100+
101+
thread::spawn(move || {
102+
thread::sleep(Duration::from_millis(200));
103+
unsafe {
104+
assert_eq!(libc::syscall(
105+
libc::SYS_futex,
106+
&FUTEX as *const i32,
107+
libc::FUTEX_WAKE,
108+
10, // Wake up at most 10 threads.
109+
), 1); // Woken up one thread.
110+
}
111+
});
112+
113+
unsafe {
114+
assert_eq!(libc::syscall(
115+
libc::SYS_futex,
116+
&FUTEX as *const i32,
117+
libc::FUTEX_WAIT,
118+
0,
119+
ptr::null::<libc::timespec>(),
120+
), 0);
121+
}
122+
123+
assert!((200..500).contains(&start.elapsed().as_millis()));
124+
}
125+
126+
fn main() {
127+
wake_nobody();
128+
wake_dangling();
129+
wait_wrong_val();
130+
wait_timeout();
131+
wait_wake();
132+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
warning: thread support is experimental. For example, Miri does not detect data races yet.
2+

0 commit comments

Comments
 (0)