Change:
Scopes threads as currently implemented are potentially unsound due to passing a reference to a function where the memory the reference points to is potentially deallocated while the function still runs. Specifically, takes as implicit argument its closure environment, which contains . That environment might just be a reference to some memory that is allocated in the caller stack frame. However the function can keep running after got dropped (implicitly at the end of ). Then the scope might end and the memory might be deallocated all while is still running. If the environment is just a reference, it ends up being a newtype and we will (AFAIK) add the attribute, meaning deallocation while the function runs is actually LLVM UB. Miri found the error in , where the invalidates the reference that was passed to the forked-off threads. ( doesn't actually get deallocated, it just has a unique reference created to it, but deallocation is also a possibility). To fix this properly, I think we need a language extension: we want a way to store references in a type without having alias guarantees for that reference. In terms of Stacked Borrows this means Stacked Borrows will stop recursing at that type during retags. In terms of the LLVM IR we generate it means we must not add or attributes for references inside that type. We don't have such a type currently but I think it makes sense to make that type (or even if we add a new type for this, should use it). Also see .
I believe included a fix for this.
Doesn't look like it -- the user-provided closure seems to be handled exactly the same as currently?
Ah, I read too quickly and misunderstood, apologies. It solves a different potential unsoundness (one more similar to the Arc drop issue).
Yeah, it looks like an alternative fix for the part of
While the , I am proposing a preliminary fix in

Discussion:
let output_capture = crate::io::set_output_capture(None); crate::io::set_output_capture(output_capture.clone()); <ins> // Pass `f` in `MaybeUninit` because actually that closure might *run longer than the lifetime of `F`*. // See <https://github.com/rust-lang/rust/issues/101983> for more details. // To prevent leaks we use a wrapper that drops its contents. #[repr(transparent)] struct MaybeDangling<T>(mem::MaybeUninit<T>); impl<T> MaybeDangling<T> { fn new(x: T) -> Self { MaybeDangling(mem::MaybeUninit::new(x)) } fn into_inner(self) -> T { // SAFETY: we are always initiailized. let ret = unsafe { self.0.assume_init_read() }; // Make sure we don't drop. mem::forget(self); ret } } impl<T> Drop for MaybeDangling<T> { fn drop(&mut self) { // SAFETY: we are always initiailized. unsafe { self.0.assume_init_drop() }; } } let f = MaybeDangling::new(f); </ins> let main = move || { if let Some(name) = their_thread.cname() { imp::Thread::set_name(name);

crate::io::set_output_capture(output_capture); <ins> // SAFETY: we constructed `f` initialized. let f = f.into_inner(); </ins> // SAFETY: the stack guard passed is the one for the current thread. // This means the current thread's stack and the new thread's stack // are properly set and protected from each other.

// same `JoinInner` as this closure meaning the mutation will be // safe (not modify it and affect a value far away). unsafe { *their_packet.result.get() = Some(try_result) }; <ins> // Here `their_packet` gets dropped, and if this is the last `Arc` for that packet that // will call `decrement_num_running_threads` and therefore signal that this thread is // done. drop(their_packet); // Here, the lifetime `'a` and even `'scope` can end. `main` keeps running for a bit // after that before returning itself. </ins> }; if let Some(scope_data) = &my_packet.scope {