Skip to content

Commit b079a68

Browse files
committed
More bugfixes for weak thread finalization
If a thread was held in an upvalue, then it was possible for the runtime to think that this thread was dead when it was not and reset it. To prevent this, the runtime has been changed to mark the *targets* of all live upvalues as live during finalization, even though they are held through a weak thread ptr. This requires two-phase finalization, one to make sure all live upvalues are actually marked as live, *then* one to reset every dead thread. Multi-phase finalization has always been on the roadmap so this is not really a problem.
1 parent 892bfa2 commit b079a68

File tree

5 files changed

+123
-15
lines changed

5 files changed

+123
-15
lines changed

src/finalizers.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use crate::{thread::ThreadInner, Thread};
77
pub struct Finalizers<'gc>(Gc<'gc, RefLock<FinalizersState<'gc>>>);
88

99
impl<'gc> Finalizers<'gc> {
10+
const THREAD_ERR: &'static str = "thread finalization was missed";
11+
1012
pub(crate) fn new(mc: &Mutation<'gc>) -> Self {
1113
Finalizers(Gc::new(mc, RefLock::default()))
1214
}
@@ -15,10 +17,29 @@ impl<'gc> Finalizers<'gc> {
1517
self.0.borrow_mut(mc).threads.push(Gc::downgrade(ptr));
1618
}
1719

20+
/// First stage of two-stage finalization.
21+
///
22+
/// This stage can cause resurrection, so the arena must be *fully re-marked* before stage two
23+
/// (`Finalizers::finalize`).
24+
pub(crate) fn prepare(&self, fc: &Finalization<'gc>) {
25+
let state = self.0.borrow();
26+
for &ptr in &state.threads {
27+
let thread = Thread::from_inner(ptr.upgrade(fc).expect(Self::THREAD_ERR));
28+
thread.resurrect_live_upvalues(fc).unwrap();
29+
}
30+
}
31+
32+
/// Second stage of two-stage finalization.
33+
///
34+
/// Assuming stage one was called (`Finalizers::prepare`) and the arena fully re-marked, this
35+
/// method will *not* cause any resurrection.
36+
///
37+
/// The arena must *immediately* transition to `CollectionPhase::Collecting` afterwards to not
38+
/// miss any finalizers.
1839
pub(crate) fn finalize(&self, fc: &Finalization<'gc>) {
1940
let mut state = self.0.borrow_mut(fc);
2041
state.threads.retain(|&ptr| {
21-
let ptr = ptr.upgrade(fc).expect("thread finalization was missed");
42+
let ptr = ptr.upgrade(fc).expect(Self::THREAD_ERR);
2243
if Gc::is_dead(fc, ptr) {
2344
Thread::from_inner(ptr).reset(fc).unwrap();
2445
false

src/lua.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ impl<'gc> ops::Deref for Context<'gc> {
8888

8989
pub struct Lua {
9090
arena: Arena<Rootable![State<'_>]>,
91-
finalized: bool,
9291
}
9392

9493
impl Default for Lua {
@@ -102,7 +101,6 @@ impl Lua {
102101
pub fn empty() -> Self {
103102
Lua {
104103
arena: Arena::<Rootable![State<'_>]>::new(|mc| State::new(mc)),
105-
finalized: false,
106104
}
107105
}
108106

@@ -156,15 +154,17 @@ impl Lua {
156154

157155
/// Finish the current collection cycle completely, calls `gc_arena::Arena::collect_all()`.
158156
pub fn gc_collect(&mut self) {
159-
if !self.finalized {
157+
if self.arena.collection_phase() != CollectionPhase::Collecting {
158+
self.arena.mark_all().unwrap().finalize(|fc, root| {
159+
root.finalizers.prepare(fc);
160+
});
160161
self.arena.mark_all().unwrap().finalize(|fc, root| {
161162
root.finalizers.finalize(fc);
162163
});
163164
}
164165

165166
self.arena.collect_all();
166167
assert!(self.arena.collection_phase() == CollectionPhase::Sleeping);
167-
self.finalized = false;
168168
}
169169

170170
pub fn gc_metrics(&self) -> &Metrics {
@@ -190,20 +190,18 @@ impl Lua {
190190

191191
let r = self.arena.mutate(move |mc, state| f(state.ctx(mc)));
192192
if self.arena.metrics().allocation_debt() > COLLECTOR_GRANULARITY {
193-
if self.finalized {
193+
if self.arena.collection_phase() == CollectionPhase::Collecting {
194194
self.arena.collect_debt();
195-
196-
if self.arena.collection_phase() == CollectionPhase::Sleeping {
197-
self.finalized = false;
198-
}
199195
} else {
200196
if let Some(marked) = self.arena.mark_debt() {
201197
marked.finalize(|fc, root| {
198+
root.finalizers.prepare(fc);
199+
});
200+
self.arena.mark_all().unwrap().finalize(|fc, root| {
202201
root.finalizers.finalize(fc);
203202
});
204203
// Immediately transition to `CollectionPhase::Collecting`.
205204
self.arena.mark_all().unwrap().start_collecting();
206-
self.finalized = true;
207205
}
208206
}
209207
}

src/thread/thread.rs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ use std::{
44
};
55

66
use allocator_api2::vec;
7-
use gc_arena::{allocator_api::MetricsAlloc, lock::RefLock, Collect, Gc, GcWeak, Mutation};
7+
use gc_arena::{
8+
allocator_api::MetricsAlloc, lock::RefLock, Collect, Finalization, Gc, GcWeak, Mutation,
9+
};
810
use thiserror::Error;
911

1012
use crate::{
1113
closure::{UpValue, UpValueState},
1214
meta_ops,
1315
types::{RegisterIndex, VarCount},
1416
BoxSequence, Callback, Closure, Context, Error, FromMultiValue, Fuel, Function, IntoMultiValue,
15-
TypeError, VMError, Value,
17+
String, Table, TypeError, UserData, VMError, Value,
1618
};
1719

1820
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -179,6 +181,27 @@ impl<'gc> Thread<'gc> {
179181
}
180182
}
181183

184+
/// For each open upvalue pointing to this thread, if the upvalue itself is live, then resurrect
185+
/// the actual value that it is pointing to.
186+
///
187+
/// Because open upvalues keep a *weak* pointer to their parent thread, their target values will
188+
/// not be properly marked as live until until they are manually marked with this method.
189+
pub(crate) fn resurrect_live_upvalues(
190+
self,
191+
fc: &Finalization<'gc>,
192+
) -> Result<(), BadThreadMode> {
193+
// If this thread is not dead, then none of the held stack values can be dead, so we don't
194+
// need to resurrect them.
195+
if Gc::is_dead(fc, self.0) {
196+
let state = self.0.try_borrow().map_err(|_| BadThreadMode {
197+
found: ThreadMode::Running,
198+
expected: None,
199+
})?;
200+
state.resurrect_live_upvalues(fc);
201+
}
202+
Ok(())
203+
}
204+
182205
fn check_mode(
183206
&self,
184207
mc: &Mutation<'gc>,
@@ -447,7 +470,7 @@ impl<'gc> ThreadState<'gc> {
447470
for &upval in &self.open_upvalues[start..] {
448471
match upval.get() {
449472
UpValueState::Open(open_upvalue) => {
450-
assert!(open_upvalue.thread.upgrade(mc).unwrap().as_ptr() == this_ptr);
473+
debug_assert!(open_upvalue.thread.upgrade(mc).unwrap().as_ptr() == this_ptr);
451474
upval.set(
452475
mc,
453476
UpValueState::Closed(self.stack[open_upvalue.stack_index]),
@@ -460,6 +483,31 @@ impl<'gc> ThreadState<'gc> {
460483
self.open_upvalues.truncate(start);
461484
}
462485

486+
fn resurrect_live_upvalues(&self, fc: &Finalization<'gc>) {
487+
for &upval in &self.open_upvalues {
488+
if !Gc::is_dead(fc, UpValue::into_inner(upval)) {
489+
match upval.get() {
490+
UpValueState::Open(open_upvalue) => {
491+
match self.stack[open_upvalue.stack_index] {
492+
Value::String(s) => Gc::resurrect(fc, String::into_inner(s)),
493+
Value::Table(t) => Gc::resurrect(fc, Table::into_inner(t)),
494+
Value::Function(Function::Closure(c)) => {
495+
Gc::resurrect(fc, Closure::into_inner(c))
496+
}
497+
Value::Function(Function::Callback(c)) => {
498+
Gc::resurrect(fc, Callback::into_inner(c))
499+
}
500+
Value::Thread(t) => Gc::resurrect(fc, Thread::into_inner(t)),
501+
Value::UserData(u) => Gc::resurrect(fc, UserData::into_inner(u)),
502+
_ => {}
503+
}
504+
}
505+
UpValueState::Closed(_) => panic!("upvalue is not open"),
506+
}
507+
}
508+
}
509+
}
510+
463511
fn reset(&mut self, mc: &Mutation<'gc>) {
464512
self.close_upvalues(mc, 0);
465513
assert!(self.open_upvalues.is_empty());

tests/table.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ fn test_table_iter() {
3434
assert!(matches!(pairs[5], (Value::String(s), Value::Integer(3)) if s == "3" ));
3535

3636
for (k, _) in table.iter() {
37-
dbg!(k);
3837
table.set(ctx, k, Value::Nil).unwrap();
3938
}
4039

tests/weak_threads.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,49 @@ fn weak_threads_close() -> Result<(), StaticError> {
3131
lua.gc_collect();
3232
let executor = lua.try_enter(|ctx| {
3333
let closure = Closure::load(ctx, None, format!("assert(closure() == {i})").as_bytes())?;
34+
Ok(ctx.stash(Executor::start(ctx, closure.into(), ())))
35+
})?;
36+
lua.execute::<()>(&executor)?;
37+
}
38+
39+
Ok(())
40+
}
41+
42+
#[test]
43+
fn live_upvalues_not_dead() -> Result<(), StaticError> {
44+
let mut lua = Lua::core();
45+
46+
let executor = lua.try_enter(|ctx| {
47+
let closure = Closure::load(
48+
ctx,
49+
None,
50+
&br#"
51+
local co = coroutine.create(function()
52+
local thread = coroutine.create(function()
53+
local i = 1
54+
while true do
55+
coroutine.yield(i)
56+
i = i + 1
57+
end
58+
end)
59+
60+
coroutine.yield(function()
61+
return coroutine.continue(thread)
62+
end)
63+
end)
64+
65+
_, go = coroutine.resume(co)
66+
"#[..],
67+
)?;
68+
69+
Ok(ctx.stash(Executor::start(ctx, closure.into(), ())))
70+
})?;
71+
lua.execute::<()>(&executor)?;
3472

73+
for i in 1..4 {
74+
lua.gc_collect();
75+
let executor = lua.try_enter(|ctx| {
76+
let closure = Closure::load(ctx, None, format!("assert(go() == {i})").as_bytes())?;
3577
Ok(ctx.stash(Executor::start(ctx, closure.into(), ())))
3678
})?;
3779
lua.execute::<()>(&executor)?;

0 commit comments

Comments
 (0)