Skip to content

Commit 919d6bf

Browse files
authored
Rollup merge of #102589 - RalfJung:scoped-threads-dangling, r=m-ou-se
scoped threads: pass closure through MaybeUninit to avoid invalid dangling references The `main` function defined here looks roughly like this, if it were written as a more explicit stand-alone function: ```rust // Not showing all the `'lifetime` tracking, the point is that // this closure might live shorter than `thread`. fn thread(control: ..., closure: impl FnOnce() + 'lifetime) { closure(); control.signal_done(); // A lot of time can pass here. } ``` Note that `thread` continues to run even after `signal_done`! Now consider what happens if the `closure` captures a reference of lifetime `'lifetime`: - The type of `closure` is a struct (the implicit unnameable closure type) with a `&'lifetime mut T` field. References passed to a function are marked with `dereferenceable`, which is LLVM speak for *this reference will remain live for the entire duration of this function*. - The closure runs, `signal_done` runs. Then -- potentially -- this thread gets scheduled away and the main thread runs, seeing the signal and returning to the user. Now `'lifetime` ends and the memory the reference points to might be deallocated. - Now we have UB! The reference that as passed to `thread` with the promise of remaining live for the entire duration of the function, actually got deallocated while the function still runs. Oops. Long-term I think we should be able to use `ManuallyDrop` to fix this without `unsafe`, or maybe a new `MaybeDangling` type. I am working on an RFC for that. But in the mean time it'd be nice to fix this so that Miri with `-Zmiri-retag-fields` (which is needed for "full enforcement" of all the LLVM flags we generate) stops erroring on scoped threads. Fixes #101983 r? `@m-ou-se`
2 parents e0954ca + 78b577c commit 919d6bf

File tree

1 file changed

+33
-0
lines changed

1 file changed

+33
-0
lines changed

library/std/src/thread/mod.rs

+33
Original file line numberDiff line numberDiff line change
@@ -499,13 +499,40 @@ impl Builder {
499499
let output_capture = crate::io::set_output_capture(None);
500500
crate::io::set_output_capture(output_capture.clone());
501501

502+
// Pass `f` in `MaybeUninit` because actually that closure might *run longer than the lifetime of `F`*.
503+
// See <https://github.com/rust-lang/rust/issues/101983> for more details.
504+
// To prevent leaks we use a wrapper that drops its contents.
505+
#[repr(transparent)]
506+
struct MaybeDangling<T>(mem::MaybeUninit<T>);
507+
impl<T> MaybeDangling<T> {
508+
fn new(x: T) -> Self {
509+
MaybeDangling(mem::MaybeUninit::new(x))
510+
}
511+
fn into_inner(self) -> T {
512+
// SAFETY: we are always initiailized.
513+
let ret = unsafe { self.0.assume_init_read() };
514+
// Make sure we don't drop.
515+
mem::forget(self);
516+
ret
517+
}
518+
}
519+
impl<T> Drop for MaybeDangling<T> {
520+
fn drop(&mut self) {
521+
// SAFETY: we are always initiailized.
522+
unsafe { self.0.assume_init_drop() };
523+
}
524+
}
525+
526+
let f = MaybeDangling::new(f);
502527
let main = move || {
503528
if let Some(name) = their_thread.cname() {
504529
imp::Thread::set_name(name);
505530
}
506531

507532
crate::io::set_output_capture(output_capture);
508533

534+
// SAFETY: we constructed `f` initialized.
535+
let f = f.into_inner();
509536
// SAFETY: the stack guard passed is the one for the current thread.
510537
// This means the current thread's stack and the new thread's stack
511538
// are properly set and protected from each other.
@@ -518,6 +545,12 @@ impl Builder {
518545
// same `JoinInner` as this closure meaning the mutation will be
519546
// safe (not modify it and affect a value far away).
520547
unsafe { *their_packet.result.get() = Some(try_result) };
548+
// Here `their_packet` gets dropped, and if this is the last `Arc` for that packet that
549+
// will call `decrement_num_running_threads` and therefore signal that this thread is
550+
// done.
551+
drop(their_packet);
552+
// Here, the lifetime `'a` and even `'scope` can end. `main` keeps running for a bit
553+
// after that before returning itself.
521554
};
522555

523556
if let Some(scope_data) = &my_packet.scope {

0 commit comments

Comments
 (0)