Skip to content

Commit 66421d9

Browse files
authored
Rollup merge of rust-lang#84409 - mzohreva:mz/tls-dtors-before-join, r=jethrogb
Ensure TLS destructors run before thread joins in SGX The excellent test is from `@jethrogb` For context see: rust-lang#83416 (comment)
2 parents 377d1a9 + 2acd62d commit 66421d9

File tree

3 files changed

+173
-10
lines changed

3 files changed

+173
-10
lines changed

library/std/src/sys/sgx/abi/mod.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,12 @@ unsafe extern "C" fn tcs_init(secondary: bool) {
6262
extern "C" fn entry(p1: u64, p2: u64, p3: u64, secondary: bool, p4: u64, p5: u64) -> EntryReturn {
6363
// FIXME: how to support TLS in library mode?
6464
let tls = Box::new(tls::Tls::new());
65-
let _tls_guard = unsafe { tls.activate() };
65+
let tls_guard = unsafe { tls.activate() };
6666

6767
if secondary {
68-
super::thread::Thread::entry();
68+
let join_notifier = super::thread::Thread::entry();
69+
drop(tls_guard);
70+
drop(join_notifier);
6971

7072
EntryReturn(0, 0)
7173
} else {

library/std/src/sys/sgx/thread.rs

+61-8
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,37 @@ pub struct Thread(task_queue::JoinHandle);
99

1010
pub const DEFAULT_MIN_STACK_SIZE: usize = 4096;
1111

12+
pub use self::task_queue::JoinNotifier;
13+
1214
mod task_queue {
13-
use crate::sync::mpsc;
15+
use super::wait_notify;
1416
use crate::sync::{Mutex, MutexGuard, Once};
1517

16-
pub type JoinHandle = mpsc::Receiver<()>;
18+
pub type JoinHandle = wait_notify::Waiter;
19+
20+
pub struct JoinNotifier(Option<wait_notify::Notifier>);
21+
22+
impl Drop for JoinNotifier {
23+
fn drop(&mut self) {
24+
self.0.take().unwrap().notify();
25+
}
26+
}
1727

1828
pub(super) struct Task {
1929
p: Box<dyn FnOnce()>,
20-
done: mpsc::Sender<()>,
30+
done: JoinNotifier,
2131
}
2232

2333
impl Task {
2434
pub(super) fn new(p: Box<dyn FnOnce()>) -> (Task, JoinHandle) {
25-
let (done, recv) = mpsc::channel();
35+
let (done, recv) = wait_notify::new();
36+
let done = JoinNotifier(Some(done));
2637
(Task { p, done }, recv)
2738
}
2839

29-
pub(super) fn run(self) {
40+
pub(super) fn run(self) -> JoinNotifier {
3041
(self.p)();
31-
let _ = self.done.send(());
42+
self.done
3243
}
3344
}
3445

@@ -47,6 +58,48 @@ mod task_queue {
4758
}
4859
}
4960

61+
/// This module provides a synchronization primitive that does not use thread
62+
/// local variables. This is needed for signaling that a thread has finished
63+
/// execution. The signal is sent once all TLS destructors have finished at
64+
/// which point no new thread locals should be created.
65+
pub mod wait_notify {
66+
use super::super::waitqueue::{SpinMutex, WaitQueue, WaitVariable};
67+
use crate::sync::Arc;
68+
69+
pub struct Notifier(Arc<SpinMutex<WaitVariable<bool>>>);
70+
71+
impl Notifier {
72+
/// Notify the waiter. The waiter is either notified right away (if
73+
/// currently blocked in `Waiter::wait()`) or later when it calls the
74+
/// `Waiter::wait()` method.
75+
pub fn notify(self) {
76+
let mut guard = self.0.lock();
77+
*guard.lock_var_mut() = true;
78+
let _ = WaitQueue::notify_one(guard);
79+
}
80+
}
81+
82+
pub struct Waiter(Arc<SpinMutex<WaitVariable<bool>>>);
83+
84+
impl Waiter {
85+
/// Wait for a notification. If `Notifier::notify()` has already been
86+
/// called, this will return immediately, otherwise the current thread
87+
/// is blocked until notified.
88+
pub fn wait(self) {
89+
let guard = self.0.lock();
90+
if *guard.lock_var() {
91+
return;
92+
}
93+
WaitQueue::wait(guard, || {});
94+
}
95+
}
96+
97+
pub fn new() -> (Notifier, Waiter) {
98+
let inner = Arc::new(SpinMutex::new(WaitVariable::new(false)));
99+
(Notifier(inner.clone()), Waiter(inner))
100+
}
101+
}
102+
50103
impl Thread {
51104
// unsafe: see thread::Builder::spawn_unchecked for safety requirements
52105
pub unsafe fn new(_stack: usize, p: Box<dyn FnOnce()>) -> io::Result<Thread> {
@@ -57,7 +110,7 @@ impl Thread {
57110
Ok(Thread(handle))
58111
}
59112

60-
pub(super) fn entry() {
113+
pub(super) fn entry() -> JoinNotifier {
61114
let mut pending_tasks = task_queue::lock();
62115
let task = rtunwrap!(Some, pending_tasks.pop());
63116
drop(pending_tasks); // make sure to not hold the task queue lock longer than necessary
@@ -78,7 +131,7 @@ impl Thread {
78131
}
79132

80133
pub fn join(self) {
81-
let _ = self.0.recv();
134+
self.0.wait();
82135
}
83136
}
84137

library/std/src/thread/local/tests.rs

+108
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::cell::{Cell, UnsafeCell};
2+
use crate::sync::atomic::{AtomicU8, Ordering};
23
use crate::sync::mpsc::{channel, Sender};
34
use crate::thread::{self, LocalKey};
45
use crate::thread_local;
@@ -207,3 +208,110 @@ fn dtors_in_dtors_in_dtors_const_init() {
207208
});
208209
rx.recv().unwrap();
209210
}
211+
212+
// This test tests that TLS destructors have run before the thread joins. The
213+
// test has no false positives (meaning: if the test fails, there's actually
214+
// an ordering problem). It may have false negatives, where the test passes but
215+
// join is not guaranteed to be after the TLS destructors. However, false
216+
// negatives should be exceedingly rare due to judicious use of
217+
// thread::yield_now and running the test several times.
218+
#[test]
219+
fn join_orders_after_tls_destructors() {
220+
// We emulate a synchronous MPSC rendezvous channel using only atomics and
221+
// thread::yield_now. We can't use std::mpsc as the implementation itself
222+
// may rely on thread locals.
223+
//
224+
// The basic state machine for an SPSC rendezvous channel is:
225+
// FRESH -> THREAD1_WAITING -> MAIN_THREAD_RENDEZVOUS
226+
// where the first transition is done by the “receiving” thread and the 2nd
227+
// transition is done by the “sending” thread.
228+
//
229+
// We add an additional state `THREAD2_LAUNCHED` between `FRESH` and
230+
// `THREAD1_WAITING` to block until all threads are actually running.
231+
//
232+
// A thread that joins on the “receiving” thread completion should never
233+
// observe the channel in the `THREAD1_WAITING` state. If this does occur,
234+
// we switch to the “poison” state `THREAD2_JOINED` and panic all around.
235+
// (This is equivalent to “sending” from an alternate producer thread.)
236+
const FRESH: u8 = 0;
237+
const THREAD2_LAUNCHED: u8 = 1;
238+
const THREAD1_WAITING: u8 = 2;
239+
const MAIN_THREAD_RENDEZVOUS: u8 = 3;
240+
const THREAD2_JOINED: u8 = 4;
241+
static SYNC_STATE: AtomicU8 = AtomicU8::new(FRESH);
242+
243+
for _ in 0..10 {
244+
SYNC_STATE.store(FRESH, Ordering::SeqCst);
245+
246+
let jh = thread::Builder::new()
247+
.name("thread1".into())
248+
.spawn(move || {
249+
struct TlDrop;
250+
251+
impl Drop for TlDrop {
252+
fn drop(&mut self) {
253+
let mut sync_state = SYNC_STATE.swap(THREAD1_WAITING, Ordering::SeqCst);
254+
loop {
255+
match sync_state {
256+
THREAD2_LAUNCHED | THREAD1_WAITING => thread::yield_now(),
257+
MAIN_THREAD_RENDEZVOUS => break,
258+
THREAD2_JOINED => panic!(
259+
"Thread 1 still running after thread 2 joined on thread 1"
260+
),
261+
v => unreachable!("sync state: {}", v),
262+
}
263+
sync_state = SYNC_STATE.load(Ordering::SeqCst);
264+
}
265+
}
266+
}
267+
268+
thread_local! {
269+
static TL_DROP: TlDrop = TlDrop;
270+
}
271+
272+
TL_DROP.with(|_| {});
273+
274+
loop {
275+
match SYNC_STATE.load(Ordering::SeqCst) {
276+
FRESH => thread::yield_now(),
277+
THREAD2_LAUNCHED => break,
278+
v => unreachable!("sync state: {}", v),
279+
}
280+
}
281+
})
282+
.unwrap();
283+
284+
let jh2 = thread::Builder::new()
285+
.name("thread2".into())
286+
.spawn(move || {
287+
assert_eq!(SYNC_STATE.swap(THREAD2_LAUNCHED, Ordering::SeqCst), FRESH);
288+
jh.join().unwrap();
289+
match SYNC_STATE.swap(THREAD2_JOINED, Ordering::SeqCst) {
290+
MAIN_THREAD_RENDEZVOUS => return,
291+
THREAD2_LAUNCHED | THREAD1_WAITING => {
292+
panic!("Thread 2 running after thread 1 join before main thread rendezvous")
293+
}
294+
v => unreachable!("sync state: {:?}", v),
295+
}
296+
})
297+
.unwrap();
298+
299+
loop {
300+
match SYNC_STATE.compare_exchange_weak(
301+
THREAD1_WAITING,
302+
MAIN_THREAD_RENDEZVOUS,
303+
Ordering::SeqCst,
304+
Ordering::SeqCst,
305+
) {
306+
Ok(_) => break,
307+
Err(FRESH) => thread::yield_now(),
308+
Err(THREAD2_LAUNCHED) => thread::yield_now(),
309+
Err(THREAD2_JOINED) => {
310+
panic!("Main thread rendezvous after thread 2 joined thread 1")
311+
}
312+
v => unreachable!("sync state: {:?}", v),
313+
}
314+
}
315+
jh2.join().unwrap();
316+
}
317+
}

0 commit comments

Comments
 (0)