Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement futex_wait and futex_wake. #1568

Merged
merged 23 commits into from
Oct 3, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1ffc5bb
Implement futex_wait and futex_wake.
m-ou-se Oct 1, 2020
281a538
Move futex syscall to its own file.
m-ou-se Oct 1, 2020
6c2f36e
Erase tag from futex pointers.
m-ou-se Oct 1, 2020
69cea1d
Only check futex pointer in futex_wait and not in futex_wake.
m-ou-se Oct 1, 2020
1c582e7
Return correct value from futex_wait.
m-ou-se Oct 1, 2020
c2fa27c
Check maximum amount of arguments to SYS_futex.
m-ou-se Oct 2, 2020
712e800
Improve handling of the `addr` argument in SYS_futex.
m-ou-se Oct 2, 2020
ee3eb4b
Add comments that document SYS_futex better.
m-ou-se Oct 2, 2020
dabd980
Update note about number of arguments to SYS_futex.
m-ou-se Oct 2, 2020
422b505
Add note about arguments in futex implementation.
m-ou-se Oct 2, 2020
d5b3f54
Use force_ptr in futex implementation.
m-ou-se Oct 2, 2020
e64ead2
Implement timeouts for FUTEX_WAIT.
m-ou-se Oct 2, 2020
8113882
Add park/park_timeout/unpark test.
m-ou-se Oct 2, 2020
c9627b2
Use correct return type for syscall(SYS_futex).
m-ou-se Oct 2, 2020
924fd56
Only allow FUTEX_WAIT with timeout when isoloation is disabled.
m-ou-se Oct 3, 2020
6628275
Remove backtics from isolation error.
m-ou-se Oct 3, 2020
5880e7d
Update expected error messages in tests.
m-ou-se Oct 3, 2020
6df54c4
Use read_scalar_at_offset in futex_wait instead of memory.get_raw.
m-ou-se Oct 3, 2020
9d764c5
Add FIXME note about variadic syscall().
m-ou-se Oct 3, 2020
dc36988
Add test for futex syscall.
m-ou-se Oct 3, 2020
dfcb46a
Update syscall FIXME to include note about 'wrong' types.
m-ou-se Oct 3, 2020
c268ee2
Add note about use of force_ptr in futex implementation.
m-ou-se Oct 3, 2020
68776d2
Add FIXME about type of `addr` in futex implementation.
m-ou-se Oct 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/shims/posix/linux/foreign_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use rustc_middle::mir;
use crate::*;
use crate::helpers::check_arg_count;
use shims::posix::fs::EvalContextExt as _;
use shims::posix::linux::sync::futex;
use shims::posix::sync::EvalContextExt as _;
use shims::posix::thread::EvalContextExt as _;

Expand Down Expand Up @@ -120,6 +121,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
.eval_libc("SYS_statx")?
.to_machine_usize(this)?;

let sys_futex = this
.eval_libc("SYS_futex")?
.to_machine_usize(this)?;

if args.is_empty() {
throw_ub_format!("incorrect number of arguments for syscall: got 0, expected at least 1");
}
Expand All @@ -139,6 +144,10 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
let result = this.linux_statx(dirfd, pathname, flags, mask, statxbuf)?;
this.write_scalar(Scalar::from_machine_isize(result.into(), this), dest)?;
}
// `futex` is used by some synchonization primitives.
id if id == sys_futex => {
futex(this, args, dest)?;
}
id => throw_unsup_format!("miri does not support syscall ID {}", id),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/shims/posix/linux/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod foreign_items;
pub mod dlsym;
pub mod sync;
88 changes: 88 additions & 0 deletions src/shims/posix/linux/sync.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use crate::*;
use rustc_target::abi::{Align, Size};

/// Implementation of the SYS_futex syscall.
pub fn futex<'tcx>(
this: &mut MiriEvalContext<'_, 'tcx>,
args: &[OpTy<'tcx, Tag>],
dest: PlaceTy<'tcx, Tag>,
) -> InterpResult<'tcx> {
// The amount of arguments used depends on the type of futex operation.
// Some users always pass all arguments, even the unused ones, due to how they wrap this syscall in their code base.
// Some other users pass only the arguments the operation actually needs. So we don't use `check_arg_count` here.
if !(4..=7).contains(&args.len()) {
m-ou-se marked this conversation as resolved.
Show resolved Hide resolved
throw_ub_format!("incorrect number of arguments for futex syscall: got {}, expected between 4 and 7 (inclusive)", args.len());
}

// The first three arguments (after the syscall number itself) are the same to all futex operations:
// (int *addr, int op, int val).
m-ou-se marked this conversation as resolved.
Show resolved Hide resolved
// Although note that the first one is often passed as a different pointer type, e.g. `*const AtomicU32` or `*mut u32`.
let addr = this.deref_operand(args[1])?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, didn't you say that this point may in fact be dangling? That won't work like this. deref_operand requires that the pointer be dereferncable for the given type.

I am okay leaving this as a FIXME though. But eventually we should have a test for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, didn't you say that this point may in fact be dangling?

I did! I (mis)interpreted your comment above ..

Then later you can use this.read_scalar(futex_val_place.offset) to read the actual data, and futex_val_place.ptr.assert_ptr() to get the Pointer. (deref_operand does the necessary int-to-ptr conversion.)

.. as that only read_scalar() requires the address to be readable, and that deref_operand only does the conversion to a pointer.

Anyway, this was the reason I kept the original addr around as a TyOp, separate from the Pointer, such that the FUTEX_WAIT path can still call deref_operand (or read_scalar_at_offset, which already does deref_operand) on it.

What's a good way to do this? Both operations need addr as Pointer, but one also needs to dereference the pointer (as i32).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using force_ptr now instead of deref_operand. But using that Pointer to read the i32 pointee in FUTEX_WAIT seems tricky.

Now I'm using check_ptr_access followed by this.memory.get_raw(addr.alloc_id)?.read_scalar, but I don't know if that's a proper way to do it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is a tricky situation which I do not think we've had before. But get_raw feels like going down way too far the stack of abstractions.

I think the best way is to delay part of the decision: here at the top, just do let futex_val = this.read_immediate. Then also get the ptr value via force_ptr(futex_val.to_scalar()?). In the futex_wait branch, do deref_operand(futex_val.into())?. At least I think that should work.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That only works if I do:

this.read_scalar(this.deref_operand(addr.into())?.offset(Size::ZERO, MemPlaceMeta::None, this.machine.layouts.i32, this)?.into())?.to_i32()?

or

this.read_scalar_at_offset(addr.into(), 0, this.machine.layouts.i32)?.to_i32()?

in the futex_wait branch. Otherwise it refuses to read a AtomicI32 as i32 again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, now it fails if somebody calls syscall() with that futex address as a usize. Neither parking_lot nor std do that, but it might make sense as in Rust *const i32 is not Send, while usize is. And since this pointer may dangle, sending it to another thread might make sense.

I am confused actually, why does this fail? Without deref_operand I think this should work, but I might forget some check somewhere.

Copy link
Member

@RalfJung RalfJung Oct 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or was the actual problem that you passed 0usize? In that case the value would have been the problem, not the type -- i.e., 0usize as *const () would not work either. That is what the last comment that I added is about.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused actually, why does this fail?

Because in the example I also used the usize for the wait operation, which does dereference it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it doesn't do that with deref_operand. So as long as the pointer points to something valid, with your current code, I think it should work fine both with usize and with a pointer type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, read_scalar_at_offset uses deref_operand internally...

let op = this.read_scalar(args[2])?.to_i32()?;
let val = this.read_scalar(args[3])?.to_i32()?;

// The raw pointer value is used to identify the mutex.
// Not all mutex operations actually read from this address or even require this address to exist.
let futex_ptr = addr.ptr.assert_ptr();

let thread = this.get_active_thread();

let futex_private = this.eval_libc_i32("FUTEX_PRIVATE_FLAG")?;
let futex_wait = this.eval_libc_i32("FUTEX_WAIT")?;
let futex_wake = this.eval_libc_i32("FUTEX_WAKE")?;

// FUTEX_PRIVATE enables an optimization that stops it from working across processes.
// Miri doesn't support that anyway, so we ignore that flag.
match op & !futex_private {
// FUTEX_WAIT: (int *addr, int op = FUTEX_WAIT, int val, const timespec *timeout)
// Blocks the thread if *addr still equals val. Wakes up when FUTEX_WAKE is called on the same address,
// or *timeout expires. `timeout == null` for an infinite timeout.
op if op == futex_wait => {
if args.len() < 5 {
throw_ub_format!("incorrect number of arguments for FUTEX_WAIT syscall: got {}, expected at least 5", args.len());
}
let timeout = this.read_scalar(args[4])?.check_init()?;
if !this.is_null(timeout)? {
// FIXME: Implement timeouts. The condvar waiting code is probably a good example to start with.
// Note that a triggered timeout should have this syscall return with -1 and errno set to ETIMEOUT.
throw_ub_format!("miri does not support timeouts for futex operations");
}
// Check the pointer for alignment. Atomic operations are only available for fully aligned values.
this.memory.check_ptr_access(addr.ptr.into(), Size::from_bytes(4), Align::from_bytes(4).unwrap())?;
// Read an `i32` through the pointer, regardless of any wrapper types (e.g. `AtomicI32`).
let futex_val = this.read_scalar(addr.offset(Size::ZERO, MemPlaceMeta::None, this.machine.layouts.i32, this)?.into())?.to_i32()?;
if val == futex_val {
// The value still matches, so we block the trait make it wait for FUTEX_WAKE.
this.block_thread(thread);
this.futex_wait(futex_ptr, thread);
RalfJung marked this conversation as resolved.
Show resolved Hide resolved
// Succesfully waking up from FUTEX_WAIT always returns zero.
this.write_scalar(Scalar::from_i32(0), dest)?;
} else {
// The futex value doesn't match the expected value, so we return failure
// right away without sleeping: -1 and errno set to EAGAIN.
let eagain = this.eval_libc("EAGAIN")?;
this.set_last_error(eagain)?;
this.write_scalar(Scalar::from_i32(-1), dest)?;
}
}
// FUTEX_WAKE: (int *addr, int op = FUTEX_WAKE, int val)
// Wakes at most `val` threads waiting on the futex at `addr`.
// Returns the amount of threads woken up.
// Does not access the futex value at *addr.
op if op == futex_wake => {
let mut n = 0;
for _ in 0..val {
if let Some(thread) = this.futex_wake(futex_ptr) {
this.unblock_thread(thread);
n += 1;
} else {
break;
}
}
this.write_scalar(Scalar::from_i32(n), dest)?;
}
op => throw_unsup_format!("miri does not support SYS_futex operation {}", op),
}

Ok(())
}
27 changes: 27 additions & 0 deletions src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,26 @@ struct Condvar {
waiters: VecDeque<CondvarWaiter>,
}

/// The futex state.
#[derive(Default, Debug)]
struct Futex {
waiters: VecDeque<FutexWaiter>,
}

/// A thread waiting on a futex.
#[derive(Debug)]
struct FutexWaiter {
/// The thread that is waiting on this futex.
thread: ThreadId,
}

/// The state of all synchronization variables.
#[derive(Default, Debug)]
pub(super) struct SynchronizationState {
mutexes: IndexVec<MutexId, Mutex>,
rwlocks: IndexVec<RwLockId, RwLock>,
condvars: IndexVec<CondvarId, Condvar>,
futexes: HashMap<Pointer, Futex>,
}

// Private extension trait for local helper methods
Expand Down Expand Up @@ -403,4 +417,17 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
let this = self.eval_context_mut();
this.machine.threads.sync.condvars[id].waiters.retain(|waiter| waiter.thread != thread);
}

fn futex_wait(&mut self, addr: Pointer<stacked_borrows::Tag>, thread: ThreadId) {
let this = self.eval_context_mut();
let waiters = &mut this.machine.threads.sync.futexes.entry(addr.erase_tag()).or_default().waiters;
assert!(waiters.iter().all(|waiter| waiter.thread != thread), "thread is already waiting");
waiters.push_back(FutexWaiter { thread });
}

fn futex_wake(&mut self, addr: Pointer<stacked_borrows::Tag>) -> Option<ThreadId> {
let this = self.eval_context_mut();
let waiters = &mut this.machine.threads.sync.futexes.get_mut(&addr.erase_tag())?.waiters;
waiters.pop_front().map(|waiter| waiter.thread)
}
}