Skip to content

Commit ced476b

Browse files
committed
- clarify documentation of mock lazy_static implementation
- ensure that global statics are dropped after thread locals - ensure that `loom` does not panic inside a panic - add two regression tests for #152
1 parent f15f931 commit ced476b

File tree

4 files changed

+135
-8
lines changed

4 files changed

+135
-8
lines changed

src/lazy_static.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
//! Mock implementation of the `lazy_static` crate.
2+
//!
3+
//! Note: unlike the [semantics] of the `lazy_static` crate, this
4+
//! mock implementation *will* drop its value when the program finishes.
5+
//! This is due to an implementation detail in `loom`: it will create
6+
//! many instances of the same program and this would otherwise lead
7+
//! to unbounded memory leaks due to instantiating the lazy static
8+
//! many times over.
9+
//!
10+
//! [semantics]: https://docs.rs/lazy_static/latest/lazy_static/#semantics
211
312
use crate::rt;
413
pub use crate::rt::thread::AccessError;
@@ -12,6 +21,15 @@ use std::fmt;
1221
use std::marker::PhantomData;
1322

1423
/// Mock implementation of `lazy_static::Lazy`.
24+
///
25+
/// Note: unlike the [semantics] of the `lazy_static` crate, this
26+
/// mock implementation *will* drop its value when the program finishes.
27+
/// This is due to an implementation detail in `loom`: it will create
28+
/// many instances of the same program and this would otherwise lead
29+
/// to unbounded memory leaks due to instantiating the lazy static
30+
/// many times over.
31+
///
32+
/// [semantics]: https://docs.rs/lazy_static/latest/lazy_static/#semantics
1533
pub struct Lazy<T> {
1634
// Sadly, these fields have to be public, since function pointers in const
1735
// fns are unstable. When fn pointer arguments to const fns stabilize, these

src/model.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,37 @@ impl Builder {
182182
let f = f.clone();
183183

184184
scheduler.run(&mut execution, move || {
185-
f();
185+
/// the given closure `f` may panic when executed.
186+
/// when this happens, we still want to ensure
187+
/// that thread locals are destructed before the
188+
/// global statics are dropped. therefore, we set
189+
/// up a guard that is dropped as part of the unwind
190+
/// logic when `f` panics. this guard in turn ensures,
191+
/// through the implementation of `rt::thread_done`,
192+
/// that thread local fields are dropped before the
193+
/// global statics are dropped. without this guard,
194+
/// a `Drop` implementation on a type that is stored
195+
/// in a thread local field could access the lazy static,
196+
/// and this then would in turn panic from the method
197+
/// [`Lazy::get`](crate::lazy_static::Lazy), which
198+
/// causes a panic inside a panic, which in turn causes
199+
/// Rust to abort, triggering a segfault in `loom`.
200+
struct PanicGuard;
201+
impl Drop for PanicGuard {
202+
fn drop(&mut self) {
203+
rt::thread_done(true);
204+
}
205+
}
186206

187-
let lazy_statics = rt::execution(|execution| execution.lazy_statics.drop());
207+
// set up the panic guard
208+
let panic_guard = PanicGuard;
188209

189-
// drop outside of execution
190-
drop(lazy_statics);
210+
// execute the closure, note that `f()` may panic!
211+
f();
191212

192-
rt::thread_done();
213+
// if `f()` didn't panic, then we terminate the
214+
// main thread by dropping the guard ourselves.
215+
drop(panic_guard);
193216
});
194217

195218
execution.check_for_leaks();

src/rt/mod.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ where
7474

7575
Scheduler::spawn(Box::new(move || {
7676
f();
77-
thread_done();
77+
thread_done(false);
7878
}));
7979

8080
id
@@ -171,7 +171,7 @@ where
171171
Scheduler::with_execution(f)
172172
}
173173

174-
pub fn thread_done() {
174+
pub fn thread_done(is_main_thread: bool) {
175175
let locals = execution(|execution| {
176176
let thread = execution.threads.active_id();
177177

@@ -180,9 +180,28 @@ pub fn thread_done() {
180180
execution.threads.active_mut().drop_locals()
181181
});
182182

183-
// Drop outside of the execution context
183+
// Drop locals outside of the execution context
184184
drop(locals);
185185

186+
if is_main_thread {
187+
// Note that the statics must be dropped AFTER
188+
// dropping the thread local fields. The `Drop`
189+
// implementation of a type stored in a thread
190+
// local field may still try to access the global
191+
// statics on drop, so we need to take extra care
192+
// that the global statics outlive the thread locals.
193+
let statics = execution(|execution| {
194+
let thread = execution.threads.active_id();
195+
196+
trace!(?thread, "thread_done: drop statics from the main thread");
197+
198+
execution.lazy_statics.drop()
199+
});
200+
201+
// Drop statics outside of the execution context
202+
drop(statics);
203+
}
204+
186205
execution(|execution| {
187206
let thread = execution.threads.active_id();
188207

tests/thread_local.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,70 @@ fn drop() {
107107
// should also be dropped.
108108
assert_eq!(DROPS.load(Ordering::Acquire), 3);
109109
}
110+
111+
/// Test that TLS destructors are allowed to access global statics
112+
/// when the TLS type is dropped.
113+
///
114+
/// This is a regression test for:
115+
/// <https://github.com/tokio-rs/loom/issues/152>
116+
#[test]
117+
fn lazy_static() {
118+
loom::lazy_static! {
119+
static ref ID: usize = 0x42;
120+
}
121+
122+
loom::thread_local! {
123+
static BAR: Bar = Bar;
124+
}
125+
126+
struct Bar;
127+
128+
impl Drop for Bar {
129+
fn drop(&mut self) {
130+
let _ = &*ID;
131+
}
132+
}
133+
134+
loom::model(|| {
135+
BAR.with(|_| ());
136+
});
137+
}
138+
139+
/// When a thread panics it runs the TLS destructors, which
140+
/// in turn may try to access a global static. If the drop
141+
/// order of TLS fields and global statics is switched, then
142+
/// this will trigger a panic from the TLS destructor, too.
143+
/// This causes a panic inside another panic, which will lead
144+
/// to loom triggering a segfault. This should not happen,
145+
/// because it should be allowed for TLS destructors to always
146+
/// access a global static.
147+
///
148+
/// This is a regression test for a slight varation of
149+
/// <https://github.com/tokio-rs/loom/issues/152>.
150+
#[test]
151+
#[should_panic(expected = "loom should not panic inside another panic")]
152+
fn lazy_static_panic() {
153+
loom::lazy_static! {
154+
static ref ID: usize = 0x42;
155+
}
156+
157+
loom::thread_local! {
158+
static BAR: Bar = Bar;
159+
}
160+
161+
struct Bar;
162+
163+
impl Drop for Bar {
164+
fn drop(&mut self) {
165+
let _ = &*ID;
166+
}
167+
}
168+
169+
loom::model(|| {
170+
// initialize the TLS destructor:
171+
BAR.with(|_| ());
172+
println!("about to panic");
173+
// intentionally panic:
174+
panic!("loom should not panic inside another panic");
175+
});
176+
}

0 commit comments

Comments
 (0)