luna_core/runtime/heap.rs
1//! GC heap v1: precise stop-the-world mark & sweep over an intrusive
2//! all-objects list (PUC `allgc` shape). All unsafe object plumbing is
3//! confined to this module and `string`/`table` internals.
4//!
5//! `Gc<T>` safety contract: the runtime is single-threaded; a `Gc` pointer is
6//! valid until a `collect()` call that does not reach it from the given
7//! roots. Callers must root every value they keep across a collect.
8
9use std::fmt;
10use std::ops::Deref;
11use std::ptr::{self, NonNull};
12
13use crate::runtime::function::{LuaClosure, NativeClosure, Proto, UpvalState, Upvalue};
14use crate::runtime::string::{self, LuaStr, StringTable};
15use crate::runtime::table::Table;
16use crate::runtime::userdata::{Userdata, UserdataPayload};
17use crate::runtime::value::Value;
18
19/// Discriminator the GC stores in every [`GcHeader`] so a raw header pointer
20/// can be cast back to the right object kind during tracing and sweeping.
21#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22#[repr(u8)]
23pub enum ObjTag {
24 /// [`crate::runtime::string::LuaStr`].
25 Str,
26 /// [`crate::runtime::table::Table`].
27 Table,
28 /// [`crate::runtime::function::Proto`].
29 Proto,
30 /// [`crate::runtime::function::LuaClosure`].
31 Closure,
32 /// [`crate::runtime::function::Upvalue`].
33 Upvalue,
34 /// [`crate::runtime::function::NativeClosure`].
35 Native,
36 /// [`crate::runtime::coroutine::Coro`].
37 Coro,
38 /// [`crate::runtime::userdata::Userdata`].
39 Userdata,
40}
41
42/// Header prefix on every GC-managed object: intrusive next-link + type tag +
43/// mark bits. Always at offset 0 of the containing struct (`#[repr(C)]`).
44#[repr(C)]
45pub struct GcHeader {
46 next: *mut GcHeader,
47 tag: ObjTag,
48 /// tricolor + finalizer state. PUC `gch.marked` layout (lgc.h):
49 /// bit 0 WHITE0 — current-white-A
50 /// bit 1 WHITE1 — current-white-B (the unused white in any given cycle
51 /// is the "other-white" / dead-white at sweep time)
52 /// bit 2 BLACK — propagated; outgoing refs already traced
53 /// bit 3 FIN — registered for `__gc` (tracked in `finalize`)
54 /// bit 4 FINALIZED — already enqueued or finalized once this lifetime
55 /// bit 5 DEFERRED — 5.3 cycle-finalize deferral marker (gc.lua :502)
56 /// Gray = no white bits, no BLACK; that is the in-stack state between the
57 /// time a Marker visits an object and the time it traces it.
58 flags: u8,
59}
60
61const WHITE0: u8 = 1;
62const WHITE1: u8 = 2;
63const BLACK: u8 = 4;
64const WHITE_BITS: u8 = WHITE0 | WHITE1;
65const COLOR_BITS: u8 = WHITE_BITS | BLACK;
66
67/// registered for finalization (`__gc`): the object is tracked in `finalize`.
68const FIN: u8 = 8;
69/// finalization already scheduled/run: never finalize this object again (PUC
70/// FINALIZEDBIT). Set when it moves to `tobefnz`.
71const FINALIZED: u8 = 16;
72/// resurrected once because a reference cycle through a coroutine kept the
73/// finalizable alive (PUC 5.3 gc.lua :502 "two collections are needed to
74/// break cycle"). The next time the object is found unreachable it is moved
75/// to `tobefnz` without re-deferring.
76const DEFERRED: u8 = 32;
77
78#[inline(always)]
79fn is_white(flags: u8) -> bool {
80 flags & WHITE_BITS != 0
81}
82#[inline(always)]
83fn is_black(flags: u8) -> bool {
84 flags & BLACK != 0
85}
86
87/// True when an object header has been reached by marking (gray or black).
88/// `pub(crate)` so other runtime modules (e.g. `Table::refs_contain_unmarked_coro`)
89/// can probe reachability without owning the bit constants. Equivalent to
90/// `isgray(o) || isblack(o)` in PUC.
91pub(crate) fn header_is_marked(h: *mut GcHeader) -> bool {
92 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
93 unsafe { !is_white((*h).flags) }
94}
95
96impl GcHeader {
97 pub(crate) fn new(tag: ObjTag) -> GcHeader {
98 GcHeader {
99 next: ptr::null_mut(),
100 tag,
101 flags: 0,
102 }
103 }
104}
105
106/// `Copy` handle to a heap-allocated GC-managed object. Layout is a single
107/// `NonNull<T>`; the GC walks reachability via root scanning and intrusive
108/// linkage on [`GcHeader`], not via reference counts.
109pub struct Gc<T> {
110 ptr: NonNull<T>,
111}
112
113impl<T> Clone for Gc<T> {
114 fn clone(&self) -> Self {
115 *self
116 }
117}
118impl<T> Copy for Gc<T> {}
119
120impl<T> Gc<T> {
121 #[doc(hidden)]
122 pub fn from_ptr(p: *mut T) -> Gc<T> {
123 Gc {
124 ptr: NonNull::new(p).expect("gc pointer must be non-null"),
125 }
126 }
127
128 /// Raw pointer to the referent. Always non-null; valid for the lifetime
129 /// of the [`Heap`] that allocated it as long as the object is reachable.
130 pub fn as_ptr(self) -> *mut T {
131 self.ptr.as_ptr()
132 }
133
134 /// Pointer-identity equality (PUC `rawequal` for reference types).
135 pub fn ptr_eq(self, other: Gc<T>) -> bool {
136 self.ptr == other.ptr
137 }
138
139 /// SAFETY: caller must ensure no other live reference to the object and
140 /// no collect() while the borrow is held (single-threaded runtime).
141 ///
142 /// `#[doc(hidden)]` (Track A4 — pub-surface 0 unsafe): embedders should
143 /// not see this in rustdoc. The safe path for mutating freshly-allocated
144 /// tables is the `TableBuilder` / `vm.table_of(...)` API (Track B3).
145 /// Cross-crate access from `luna` (e.g. `jit_backend`, `capi`) keeps
146 /// working — `#[doc(hidden)] pub` doesn't demote visibility, just docs.
147 #[doc(hidden)]
148 pub unsafe fn as_mut<'a>(self) -> &'a mut T {
149 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
150 unsafe { &mut *self.ptr.as_ptr() }
151 }
152}
153
154impl<T> Deref for Gc<T> {
155 type Target = T;
156 fn deref(&self) -> &T {
157 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
158 unsafe { self.ptr.as_ref() }
159 }
160}
161
162impl<T> fmt::Debug for Gc<T> {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 write!(f, "Gc({:p})", self.ptr.as_ptr())
165 }
166}
167
168/// Incremental GC phase.
169/// * `Pause` — no cycle in progress; all objects current-white.
170/// * `Propagate` — gray queue + propagate-state populated; mutator runs
171/// alongside `gc_step_propagate(budget)` calls. Born objects
172/// stamp the current-white; barriers re-gray modified
173/// parents. Transitions to `Sweep` via `gc_finish_atomic`.
174/// * `Sweep` — `sweep_cur` is the detached old heap being budget-swept.
175///
176/// `mark_all` (the STW path used by `collect_ex`) sequences start_propagate +
177/// drain_all + finish_atomic inline, never crossing a step boundary.
178#[derive(Clone, Copy, PartialEq, Eq, Debug)]
179enum GcPhase {
180 Pause,
181 Propagate,
182 Sweep,
183}
184
185/// Cross-step traversal state for the incremental Propagate phase. Owned by
186/// `Heap.propagate` (`Some` between `gc_start_propagate` and `gc_finish_atomic`,
187/// `None` otherwise). The gray queue itself lives in `Heap.gray` so write
188/// barriers can push directly without going through the Option.
189struct PropagateState {
190 weak: Vec<*mut Table>,
191 ephemeron: Vec<*mut Table>,
192 cached_protos: Vec<*mut Proto>,
193 no_ephemeron: bool,
194}
195
196/// luna's incremental mark-sweep GC heap. Owns every [`Gc<T>`] allocation
197/// (one per Vm); produces handles via the `new_*` constructors and traces
198/// reachability through registered roots. Holds the string-intern table and
199/// the auto-GC pacing state.
200pub struct Heap {
201 all: *mut GcHeader,
202 strings: StringTable,
203 seed: u32,
204 live: usize,
205 /// approximate allocated bytes — shells only. Each `link()` adds
206 /// `size_of::<T>` for its tag; each `free_obj` subtracts the same.
207 /// Internal Vec/Box growth (table array/hash parts, proto code,
208 /// closure upvals slice) is NOT auto-tracked, so this is a lower
209 /// bound on real memory. PUC's `g->GCtotalbytes` is exact because
210 /// `lmem.c` routes every malloc/free through one helper; luna pays
211 /// for that uniformity in exchange for a smaller, drift-free count.
212 bytes: usize,
213 /// byte threshold at which the VM should run a collection (auto-GC pacing)
214 next_gc: usize,
215 /// PUC `g->currentwhite`: which white bit (WHITE0 or WHITE1) means
216 /// "born / surviving this cycle". The other white is the dead-white that
217 /// sweep collects. Flipped at the end of each mark cycle (`atomic`).
218 current_white: u8,
219 /// Persistent gray queue: holds objects grayed by write barriers between
220 /// the time the marker first reached them and the next propagate step.
221 /// Lives outside `propagate` so barriers can push without going through
222 /// the Option; `gc_step_propagate` and `gc_finish_atomic` drain it.
223 gray: Vec<*mut GcHeader>,
224 /// Incremental traversal state. `Some` between `gc_start_propagate` and
225 /// `gc_finish_atomic` (and inline within `mark_all`); `None` otherwise.
226 propagate: Option<PropagateState>,
227 /// incremental-sweep phase (Pause unless a `step` cycle is mid-sweep)
228 phase: GcPhase,
229 /// the remaining detached object list being swept during `GcPhase::Sweep`;
230 /// survivors are spliced back onto `all`, garbage is freed
231 sweep_cur: *mut GcHeader,
232 /// `collectgarbage("stop")`: auto-GC is suspended while true
233 gc_stopped: bool,
234 /// objects registered for finalization (a live `__gc` metamethod was set);
235 /// parallel-tracked — ownership stays on `all` (PUC `finobj`).
236 finalize: Vec<*mut GcHeader>,
237 /// dead finalizables resurrected this cycle, awaiting their `__gc` call by
238 /// the VM (PUC `tobefnz`). Drained via `take_tobefnz`.
239 tobefnz: Vec<*mut GcHeader>,
240 /// PUC 5.1 has no ephemeron pass: a `__mode='k'` table marks its values
241 /// strongly during traversal, so entries like `a[t]=t` (key and value the
242 /// same fresh object) survive even with nothing else referencing `t`.
243 /// 5.2+ replaced that with ephemeron convergence. gc.lua's "weak tables"
244 /// section in 5.1 asserts 3*lim survivors, 5.4 only 2*lim — the loop2
245 /// pair was retired from the newer test as a result.
246 pub(crate) no_ephemeron: bool,
247 /// PUC 5.3 finalizes a table caught in a cycle through an unreachable
248 /// coroutine one GC round later than the unreachability is detected
249 /// ("two collections are needed to break cycle", gc.lua :502). 5.4 and 5.5
250 /// rewrote the GC and finalize the same cycle in a single pass (their
251 /// gc.lua :544 asserts collected after one `collectgarbage()`). 5.1/5.2
252 /// don't exercise this path. Set by the VM at construction.
253 pub(crate) defer_thread_cycle_finalize: bool,
254 /// P17-D v2 layer-15 attack: pool of freed Table allocations.
255 /// btrees-style workloads create + free ~32k tables per iter;
256 /// jemalloc's malloc/free roundtrip costs ~30ns per table = ~960µs
257 /// total per iter. Pool recycle: free_obj pushes the raw Table
258 /// pointer here instead of dropping; new_table pops + resets fields.
259 /// Cap at 4096 entries to avoid unbounded growth (worst-case: 4096
260 /// × sizeof(Table) ≈ 460 KB resident memory in idle pool).
261 table_pool: Vec<std::ptr::NonNull<crate::runtime::table::Table>>,
262 /// v2.13 WUC `gc-verify` — headers freed since the last collect
263 /// began. O(1) read-time dangling probes (`Vm::op_index`) test
264 /// membership here; cleared when the next mark starts. Only exact
265 /// under ASAN-style quarantining allocators (no immediate reuse).
266 #[cfg(feature = "gc-verify")]
267 pub(crate) recently_freed: std::collections::HashSet<usize>,
268 /// P09 embedding memory cap. When `Some(n)`, the VM's run loop watches
269 /// `bytes` between dispatch turns and, on overshoot, runs a full collect
270 /// and (still overshooting) raises a catchable "memory cap exceeded"
271 /// Lua error. A soft cap, not a hard alloc-time refusal: a single
272 /// allocation can briefly push `bytes` past `n`, but the embedder gets
273 /// control back at the next safe point — host policy.
274 pub(crate) mem_cap: Option<usize>,
275}
276
277/// Initial auto-GC threshold and floor (PUC GCSTEPSIZE-ish pacing).
278const GC_MIN_THRESHOLD: usize = 1 << 20;
279
280impl Heap {
281 /// Build a fresh empty heap with default GC pacing and no memory cap.
282 pub fn new() -> Heap {
283 Heap {
284 all: ptr::null_mut(),
285 strings: StringTable::new(),
286 seed: make_seed(),
287 live: 0,
288 bytes: 0,
289 next_gc: GC_MIN_THRESHOLD,
290 current_white: WHITE0,
291 gray: Vec::new(),
292 propagate: None,
293 phase: GcPhase::Pause,
294 sweep_cur: ptr::null_mut(),
295 gc_stopped: false,
296 finalize: Vec::new(),
297 tobefnz: Vec::new(),
298 no_ephemeron: false,
299 defer_thread_cycle_finalize: false,
300 mem_cap: None,
301 table_pool: Vec::new(),
302 #[cfg(feature = "gc-verify")]
303 recently_freed: std::collections::HashSet::new(),
304 }
305 }
306
307 fn link(&mut self, h: *mut GcHeader) {
308 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
309 unsafe {
310 (*h).next = self.all;
311 // Born color depends on phase:
312 // * Pause / Sweep — born current-white (PUC `luaC_white(g)`);
313 // reachable from roots gets marked next cycle.
314 // * Propagate — born BLACK (PUC `LUAGCRYOUNG` / sasimpl
315 // of new-during-cycle). Born-current-white during Propagate
316 // would lose the WHITE bits at the upcoming atomic flip and
317 // be swept this same cycle even when reachable from a
318 // barrier-grayed root. Born BLACK skips the trace and
319 // transitions to current-white at sweep, matching the
320 // reachable-survivor flow.
321 let born = if self.phase == GcPhase::Propagate {
322 BLACK
323 } else {
324 self.current_white
325 };
326 (*h).flags = ((*h).flags & !COLOR_BITS) | born;
327 }
328 self.all = h;
329 self.live += 1;
330 }
331
332 /// Take ownership of a boxed object and put it under GC management.
333 /// SAFETY-by-convention: `T` must be `repr(C)` with a `GcHeader` first
334 /// field whose tag matches `T` (enforced by the typed constructors).
335 pub(crate) fn adopt<T>(&mut self, obj: Box<T>) -> Gc<T> {
336 let p = Box::into_raw(obj);
337 self.link(p as *mut GcHeader);
338 self.bytes += std::mem::size_of::<T>();
339 Gc::from_ptr(p)
340 }
341
342 /// Allocate and adopt a fresh empty [`Table`].
343 pub fn new_table(&mut self) -> Gc<Table> {
344 // P17-D v2 layer-15 attack — table_pool fast path. When btrees-
345 // style alloc bursts have left freed Tables in the pool, pop a
346 // recycled one and reset its fields instead of mallocing fresh.
347 // Saves ~30ns per alloc (malloc roundtrip elided).
348 let p = if let Some(ptr) = self.table_pool.pop() {
349 let t = ptr.as_ptr();
350 // gc-verify: the pointer is alive again — drop it from the
351 // freed log so read-time probes don't flag the recycled table.
352 #[cfg(feature = "gc-verify")]
353 {
354 self.recently_freed.remove(&(t as usize));
355 crate::runtime::gc_verify_probe::FREED
356 .with(|f| f.borrow_mut().remove(&(t as usize)));
357 }
358 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
359 unsafe {
360 // Reset to fresh-Table state. Box-owned slab/nodes/
361 // metatable were already cleared in `free_obj` before
362 // pool push, so we only reset stack-resident fields here.
363 (*t).hdr = GcHeader::new(ObjTag::Table);
364 (*t).array_ptr = std::ptr::null_mut();
365 (*t).asize = 0;
366 (*t).inline_storage =
367 std::cell::UnsafeCell::new([0; crate::runtime::table::INLINE_U64S]);
368 (*t).lastfree = 0;
369 (*t).flags = 0;
370 }
371 t
372 } else {
373 Box::into_raw(Box::new(Table::new(GcHeader::new(ObjTag::Table))))
374 };
375 // Link + bytes accounting (same as adopt path).
376 self.link(p as *mut GcHeader);
377 self.bytes += std::mem::size_of::<Table>();
378 let g = Gc::from_ptr(p);
379 // P11-S5d.I — the Table is now at its final heap address; wire
380 // `array_ptr` to point at the inline storage that lives inside
381 // the boxed Table.
382 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
383 unsafe { g.as_mut() }.init_array_ptr();
384 g
385 }
386
387 /// P11-S5c.B — adopt an empty table and pre-allocate `asize`
388 /// NIL slots in the array part. Equivalent to `new_table()`
389 /// followed by `set_int(N, Nil)` worth of `rehash`es, except
390 /// the array reaches its final size in one allocation rather
391 /// than O(log N) doubling rounds.
392 ///
393 /// `asize == 0` is identical to `new_table()`. Larger sizes
394 /// are clamped at the array part's hard ceiling
395 /// `MAX_ASIZE = 2^27`; requests beyond that fall back to the
396 /// empty table, which the interpreter would have grown
397 /// gracefully via rehash anyway.
398 pub fn new_table_sized(&mut self, asize: usize) -> Gc<Table> {
399 const MAX_ASIZE_HINT: usize = 1 << 27;
400 let g = self.new_table();
401 let clamped = asize.min(MAX_ASIZE_HINT);
402 if clamped > 0 {
403 // SAFETY: the freshly adopted table has no live borrow
404 // anywhere else; we hold the only `Gc<Table>` handle.
405 unsafe { g.as_mut() }.resize(self, clamped, 0);
406 }
407 g
408 }
409
410 /// Adopt a compiler-built prototype (its `hdr` must carry ObjTag::Proto).
411 pub fn adopt_proto(&mut self, proto: Proto) -> Gc<Proto> {
412 debug_assert!(proto.hdr.tag == ObjTag::Proto);
413 self.adopt(Box::new(proto))
414 }
415
416 /// P11-S5d.M — back-compat constructor for callers that already
417 /// built a `Box<[Gc<Upvalue>]>`. Internally re-routes through
418 /// `new_closure_inline` so small-upval cases also pick the
419 /// inline path (the input Box is freed after the copy).
420 pub fn new_closure(&mut self, proto: Gc<Proto>, upvals: Box<[Gc<Upvalue>]>) -> Gc<LuaClosure> {
421 use crate::runtime::function::INLINE_UPVALS_N;
422 let n = upvals.len();
423 if n <= INLINE_UPVALS_N {
424 let g = self.new_closure_inline(proto, &upvals);
425 drop(upvals);
426 g
427 } else {
428 // Large closure — store the input Box directly in
429 // `overflow`, no copy.
430 self.adopt_closure_with(proto, n as u32, |c| {
431 c.overflow = upvals;
432 })
433 }
434 }
435
436 /// P11-S5d.M — hot-path constructor for the `Op::Closure` handler.
437 /// Takes a slice (typically backed by a stack array) so the caller
438 /// doesn't allocate a Vec/Box just to hand it over. Upvals are
439 /// copied into `inline_storage` for small closures, or into a
440 /// freshly-allocated `Box<[..]>` for the rare overflow case.
441 pub fn new_closure_inline(
442 &mut self,
443 proto: Gc<Proto>,
444 upvals: &[Gc<Upvalue>],
445 ) -> Gc<LuaClosure> {
446 use crate::runtime::function::INLINE_UPVALS_N;
447 let n = upvals.len();
448 self.adopt_closure_with(proto, n as u32, |c| {
449 if n <= INLINE_UPVALS_N {
450 for (i, &uv) in upvals.iter().enumerate() {
451 // SAFETY: exclusive &mut c inside the constructor;
452 // write through the cell so no direct borrow of
453 // the inline array is formed (see the field doc).
454 unsafe {
455 (*c.inline_storage.get())[i] = std::mem::MaybeUninit::new(uv);
456 }
457 }
458 } else {
459 c.overflow = upvals.to_vec().into_boxed_slice();
460 }
461 })
462 }
463
464 fn adopt_closure_with<F: FnOnce(&mut LuaClosure)>(
465 &mut self,
466 proto: Gc<Proto>,
467 upvals_len: u32,
468 fill: F,
469 ) -> Gc<LuaClosure> {
470 let mut boxed = Box::new(LuaClosure {
471 hdr: GcHeader::new(ObjTag::Closure),
472 proto,
473 upvals_ptr: std::ptr::null_mut(),
474 upvals_len,
475 inline_storage: std::cell::UnsafeCell::new(
476 [std::mem::MaybeUninit::<Gc<Upvalue>>::uninit();
477 crate::runtime::function::INLINE_UPVALS_N],
478 ),
479 overflow: Box::new([]),
480 });
481 // Box is heap-stable now — populate storage at the final
482 // address so `upvals_ptr` will be valid.
483 fill(&mut boxed);
484 let g = self.adopt(boxed);
485 // SAFETY: Gc<T> is NonNull<T> over the GC heap; the heap is single-threaded and the pointer is live as long as it is reachable from active roots (see heap.rs:5-7).
486 unsafe { g.as_mut() }.init_upvals_ptr();
487 g
488 }
489
490 /// Allocate a [`NativeClosure`] wrapping host function `f` with the
491 /// given captured upvalues.
492 pub fn new_native(
493 &mut self,
494 f: crate::runtime::value::NativeFn,
495 upvals: Box<[Value]>,
496 ) -> Gc<NativeClosure> {
497 self.adopt(Box::new(NativeClosure {
498 hdr: GcHeader::new(ObjTag::Native),
499 f,
500 upvals,
501 is_async: false,
502 }))
503 }
504
505 /// v1.1 B10 Stage 2 — like [`Heap::new_native`] but tags the
506 /// closure with `is_async = true`. The dispatcher's native-call
507 /// path then transmutes `f` to `AsyncNativeFn` and routes through
508 /// the cooperative-yield path. The caller is responsible for
509 /// having transmuted the `AsyncNativeFn` pointer to `NativeFn`
510 /// shape (both are `fn` pointers of the same size); see
511 /// [`crate::vm::async_drive`] for the helper that does this.
512 pub fn new_async_native(
513 &mut self,
514 f: crate::runtime::value::NativeFn,
515 upvals: Box<[Value]>,
516 ) -> Gc<NativeClosure> {
517 self.adopt(Box::new(NativeClosure {
518 hdr: GcHeader::new(ObjTag::Native),
519 f,
520 upvals,
521 is_async: true,
522 }))
523 }
524
525 /// Allocate a fresh [`Upvalue`] cell in the given `state` (open / closed).
526 pub fn new_upvalue(&mut self, state: UpvalState) -> Gc<Upvalue> {
527 self.adopt(Box::new(Upvalue {
528 hdr: GcHeader::new(ObjTag::Upvalue),
529 state,
530 }))
531 }
532
533 /// Create a fresh suspended coroutine wrapping `body`. The new thread
534 /// inherits the creator's globals table; a `setfenv(0, env)` inside it
535 /// will retune that copy without affecting the creator.
536 pub fn new_coro(
537 &mut self,
538 body: Value,
539 globals: Gc<crate::runtime::Table>,
540 ) -> Gc<crate::runtime::Coro> {
541 self.adopt(Box::new(crate::runtime::Coro {
542 hdr: GcHeader::new(ObjTag::Coro),
543 status: crate::runtime::CoroStatus::Suspended,
544 body,
545 started: false,
546 resumer: None,
547 resume_at: None,
548 error_value: None,
549 error_traceback: None,
550 stack: Vec::new(),
551 frames: Vec::new(),
552 open_upvals: Vec::new(),
553 tbc: Vec::new(),
554 top: 0,
555 pcall_depth: 0,
556 hook: crate::vm::exec::HookState::default(),
557 globals,
558 }))
559 }
560
561 /// Create a userdata (an io file handle — luna's only userdata) with no
562 /// metatable yet; the io library installs the shared `FILE*` metatable.
563 pub fn new_userdata(&mut self, payload: UserdataPayload, writable: bool) -> Gc<Userdata> {
564 self.adopt(Box::new(Userdata::new(
565 GcHeader::new(ObjTag::Userdata),
566 payload,
567 writable,
568 )))
569 }
570
571 /// Create (or find) a string. Short strings (≤ 40 bytes) are interned.
572 pub fn intern(&mut self, bytes: &[u8]) -> Gc<LuaStr> {
573 if bytes.len() <= string::MAX_SHORT_LEN {
574 let (p, is_new) = self.strings.intern(bytes, self.seed);
575 if is_new {
576 self.link(p as *mut GcHeader);
577 self.bytes += string::alloc_size(bytes.len());
578 } else {
579 // PUC `luaS_new` resurrect guard (lstring.c).
580 // The bucket-chain is walked open-loop without consulting GC
581 // color; during incremental sweep an existing entry may be
582 // dead-white (in `sweep_cur`, scheduled for `free_obj`). If we
583 // hand its pointer back, the budget-paced sweep frees it out
584 // from under the mutator and the next bucket walk dereferences
585 // a libc-recycled slot — the symptom recorded in
586 // `.dev/known-bugs/stringtable-intern-uaf.md` (misaligned ptr
587 // `0x800002a80000002d` deep in `StringTable::intern`).
588 //
589 // Flip the white bits to `current_white` so the upcoming sweep
590 // skips it (PUC `changewhite`). Black / not-white objects are
591 // already safe and untouched.
592 // SAFETY: `p` came from `StringTable::intern` and is a valid
593 // `LuaStr` header (its bucket chain is consistent under our
594 // single-threaded heap).
595 unsafe {
596 let f = (*(p as *mut GcHeader)).flags;
597 if is_white(f) && (f & self.current_white) == 0 {
598 (*(p as *mut GcHeader)).flags = (f & !WHITE_BITS) | self.current_white;
599 }
600 }
601 }
602 Gc::from_ptr(p)
603 } else {
604 let p = string::alloc_long(bytes, self.seed);
605 self.link(p as *mut GcHeader);
606 self.bytes += string::alloc_size(bytes.len());
607 Gc::from_ptr(p)
608 }
609 }
610
611 /// Number of GC-managed objects currently linked into the heap (live + not
612 /// yet swept). Useful for `collectgarbage("count")`-style introspection.
613 pub fn live_objects(&self) -> usize {
614 self.live
615 }
616
617 /// Approximate heap size in bytes.
618 pub fn bytes(&self) -> usize {
619 self.bytes
620 }
621
622 /// v2.0 Track TL — pure-read walk over the intrusive `all`
623 /// objects list, invoking `visit(tag)` once per live (or
624 /// not-yet-swept) GC-managed object. Used by `luna-tools`'s
625 /// `luna-heap-dump` to build a per-type histogram; embedders
626 /// can reuse it for ad-hoc heap introspection.
627 ///
628 /// # Read-only contract
629 ///
630 /// The callback receives only the [`ObjTag`] discriminant and
631 /// is invoked under a `&self` borrow on the heap: no pointer
632 /// to the GC payload escapes, no `as_mut`-style aliasing is
633 /// available, and the walk performs zero allocation in the
634 /// loop. Safe to call between dispatch ticks (the only allocs
635 /// happen in the caller's bookkeeping).
636 ///
637 /// The walk visits both the live `all` list and the
638 /// `sweep_cur` detached list so a mid-cycle invocation reports
639 /// the same total as [`Heap::live_objects`].
640 pub fn walk_objects(&self, mut visit: impl FnMut(ObjTag)) {
641 for head in [self.all, self.sweep_cur] {
642 let mut cur = head;
643 while !cur.is_null() {
644 // SAFETY: pointers come from the runtime's
645 // intrusive all-objects list. `&self` borrow on
646 // the heap prevents concurrent mutation; the GC
647 // cannot run while this walk holds the borrow,
648 // so every `next` link is valid until consumed.
649 let (tag, next) = unsafe { ((*cur).tag, (*cur).next) };
650 visit(tag);
651 cur = next;
652 }
653 }
654 }
655
656 /// Whether allocation has crossed the auto-GC threshold (cheap safe-point
657 /// check for the interpreter loop).
658 #[inline(always)]
659 pub fn gc_due(&self) -> bool {
660 !self.gc_stopped && self.bytes >= self.next_gc
661 }
662
663 /// `collectgarbage("stop"/"restart")`: suspend or resume auto-GC.
664 pub(crate) fn gc_is_stopped(&self) -> bool {
665 self.gc_stopped
666 }
667
668 pub(crate) fn gc_set_stopped(&mut self, stopped: bool) {
669 self.gc_stopped = stopped;
670 }
671
672 /// Re-arm with caller-supplied `pause` (PUC param, % of live bytes). The
673 /// next cycle fires once `bytes >= live * pause / 100`. `pause=200` (PUC
674 /// default) waits for the heap to double; `pause=100` fires immediately
675 /// when alloc resumes; `pause=300` is 3× — lower pause = more aggressive.
676 pub fn rearm_gc_pause(&mut self, pause: i64) {
677 let pause = pause.max(0) as usize;
678 let target = self
679 .bytes
680 .saturating_mul(pause)
681 .saturating_div(100)
682 .max(GC_MIN_THRESHOLD);
683 self.next_gc = target;
684 }
685
686 /// Re-arm the auto-GC threshold after a collection (PUC pause-style: next
687 /// collection once the live set roughly doubles).
688 pub fn rearm_gc(&mut self) {
689 self.next_gc = self.bytes.saturating_mul(2).max(GC_MIN_THRESHOLD);
690 }
691
692 /// Apply a `before → after` box-size delta from a Table mutation
693 /// (`set`/`rehash`/`ensure_*`). Grows credit `Heap.bytes`; shrinks
694 /// debit it. `free_obj` for `ObjTag::Table` then subtracts the table's
695 /// final `internal_bytes()` so the round-trip is symmetric across the
696 /// table's whole lifetime.
697 pub(crate) fn apply_bytes_delta(&mut self, before: usize, after: usize) {
698 if after > before {
699 self.bytes += after - before;
700 } else if before > after {
701 self.bytes = self.bytes.saturating_sub(before - after);
702 }
703 }
704
705 /// Mark from `roots`, sweep everything unreachable. Returns the number of
706 /// objects freed.
707 pub fn collect(&mut self, roots: &[Value]) -> usize {
708 self.collect_ex(roots, &[])
709 }
710
711 /// Like `collect`, with additional bare-object roots (e.g. the VM's open
712 /// upvalues, which are not first-class Values).
713 pub(crate) fn collect_ex(&mut self, roots: &[Value], extra: &[*mut GcHeader]) -> usize {
714 // a full STW collection subsumes any in-flight incremental cycle:
715 // drive it to completion (Propagate → atomic → Sweep → Pause) so `all`
716 // holds the whole heap again with all marks cleared, then run a fresh
717 // STW cycle. Any tobefnz from the finished cycle stays queued and is
718 // re-marked (kept alive) by the upcoming mark_all so the VM's
719 // run_finalizers can still see them.
720 if self.phase == GcPhase::Propagate {
721 self.gc_finish_atomic();
722 }
723 if self.phase == GcPhase::Sweep {
724 self.gc_sweep_step(usize::MAX);
725 }
726 self.mark_all(roots, extra);
727 self.full_sweep()
728 }
729
730 /// Stop-the-world mark from `roots`/`extra`. Builds an ephemeral marker,
731 /// seeds from roots + extra + tobefnz + any barrier-carried gray queue,
732 /// propagates to completion, then runs the atomic tail (weak / ephemeron
733 /// / finalizer resurrection / current-white flip). After return all
734 /// reachable objects are BLACK and `current_white` has flipped, so the
735 /// caller's sweep tests `other-white` for dead. Does NOT change `phase`.
736 fn mark_all(&mut self, roots: &[Value], extra: &[*mut GcHeader]) {
737 let mut m = Marker {
738 stack: Vec::new(),
739 weak: Vec::new(),
740 ephemeron: Vec::new(),
741 no_ephemeron: self.no_ephemeron,
742 cached_protos: Vec::new(),
743 };
744 // Drain any barrier-grayed objects carried over: each was demoted from
745 // BLACK back to gray by a write barrier and is awaiting (re-)trace.
746 m.stack.append(&mut self.gray);
747 for &r in roots {
748 m.value(r);
749 }
750 for &h in extra {
751 m.header(h);
752 }
753 // objects already queued for finalization but not yet run must stay
754 // alive until the VM calls their `__gc` (they may be unreachable now).
755 for &h in &self.tobefnz {
756 m.header(h);
757 }
758 drain_marker(&mut m);
759 // ephemeron convergence: a weak-key entry's value is reachable only if
760 // the key is. Marking a value can make another key reachable, so repeat
761 // until no value is newly marked (PUC convergeephemerons).
762 if !m.ephemeron.is_empty() {
763 loop {
764 let mut changed = false;
765 let eph = m.ephemeron.clone();
766 for t in eph {
767 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
768 changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, &mut m) };
769 }
770 drain_marker(&mut m);
771 if !changed {
772 break;
773 }
774 }
775 }
776 self.atomic_tail(&mut m);
777 }
778
779 /// PUC `atomic()` tail: weak-table value-clear, finalizer resurrection,
780 /// post-resurrection ephemeron convergence, proto cache, key-clear, late
781 /// value-clear, and current-white flip. Marker is consumed; `weak` is
782 /// empty on return.
783 ///
784 /// Shared between the STW path (`mark_all`) and the incremental path
785 /// (`gc_finish_atomic`). PUC 5.5 `lgc.c::atomic` mirror:
786 /// propagate → remarkupvals → convergeephemerons
787 /// → clearbyvalues(weak, NULL) ─ early value-clear
788 /// → clearbyvalues(allweak, NULL) ─ (same pass under luna)
789 /// → origweak = g->weak ─ snapshot pre-resurrection
790 /// → separatetobefnz(0) + markbeingfnz ─ separate_finalizables
791 /// → propagateall + convergeephemerons ─ post-resurrection
792 /// → clearbykeys(ephemeron) + clearbykeys(allweak)
793 /// → clearbyvalues(weak, origweak) ─ NEW (post-resurrect) only
794 /// → clearbyvalues(allweak, origall) ─ (same)
795 /// The `origweak` split matters because finalizer resurrection can
796 /// re-trace fresh proto/closure → new weak tables joining `m.weak`;
797 /// PUC limits the late value-clear to those new heads.
798 fn atomic_tail(&mut self, m: &mut Marker) {
799 let early_is_dead = |v: Value| -> bool {
800 let h = match v {
801 Value::Str(_) => return false,
802 Value::Table(t) => t.as_ptr() as *mut GcHeader,
803 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
804 Value::Native(n) => n.as_ptr() as *mut GcHeader,
805 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
806 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
807 _ => return false,
808 };
809 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
810 unsafe { is_white((*h).flags) }
811 };
812 let mark_string = |v: Value| {
813 if let Value::Str(s) = v {
814 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
815 unsafe {
816 let h = s.as_ptr() as *mut GcHeader;
817 // strings are leaves: skip gray and go straight to black
818 (*h).flags = ((*h).flags & !COLOR_BITS) | BLACK;
819 }
820 }
821 };
822 // (1) early clearbyvalues — drop dead-value entries from every weak
823 // table on `m.weak` (PUC's combined `clearbyvalues(weak, NULL) +
824 // clearbyvalues(allweak, NULL)`). Keys are deferred to the
825 // post-resurrection sweep below.
826 for t in &m.weak {
827 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
828 unsafe {
829 let (_wk, wv) = (**t).weak_mode();
830 if wv {
831 (**t).clear_weak(false, true, &early_is_dead, &mark_string);
832 }
833 }
834 }
835 // (2) `origweak` snapshot — PUC takes the list head; luna's `m.weak`
836 // is a Vec, so the equivalent is its length before resurrection.
837 // Anything appended past this index is a "NEW" weak table that the
838 // resurrection pass brought into view.
839 let origweak_n = m.weak.len();
840 // (3) separate + markbeingfnz — resurrect every registered finalizable
841 // that ended up unmarked. `m.header(h)` enqueues each into the marker
842 // so the following drain_marker propagates through it.
843 self.separate_finalizables(m);
844 drain_marker(m);
845 // (4) post-resurrection ephemeron convergence — a resurrected
846 // finalizable may bring new keys into reach, which in turn marks new
847 // ephemeron values.
848 if !m.ephemeron.is_empty() {
849 loop {
850 let mut changed = false;
851 let eph = m.ephemeron.clone();
852 for t in eph {
853 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
854 changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, m) };
855 }
856 drain_marker(m);
857 if !changed {
858 break;
859 }
860 }
861 }
862 // (5) closure-cache weak refs — PUC `traverseproto` clears
863 // `Proto.cache` when the cached LClosure ended the cycle unmarked.
864 // Without this, an LClosure whose only outstanding reference is the
865 // proto's cache would survive forever and its upvalues' `__gc`
866 // finalisers would never run.
867 for &p in &m.cached_protos {
868 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
869 unsafe {
870 if let Some(c) = (*p).cache.get() {
871 let h = c.as_ptr() as *mut GcHeader;
872 if is_white((*h).flags) {
873 (*p).cache.set(None);
874 }
875 }
876 }
877 }
878 // (6) clearbykeys — drop entries whose weak key did not survive
879 // marking, across every weak table (PUC's `clearbykeys(ephemeron)
880 // + clearbykeys(allweak)`). Pure key sweep — value-dead entries are
881 // either already nil from step (1) or wait for step (7).
882 for t in &m.weak {
883 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
884 unsafe {
885 let (wk, _wv) = (**t).weak_mode();
886 if wk {
887 (**t).clear_weak(true, false, &early_is_dead, &mark_string);
888 }
889 }
890 }
891 // (7) late clearbyvalues — PUC's `clearbyvalues(weak, origweak) +
892 // clearbyvalues(allweak, origall)`. Limit the sweep to NEW heads so
893 // we don't redo work already done in step (1) for the pre-resurrect
894 // tables (they were drained by then and re-marking happens through
895 // mark_string in step (6)). resurrected weak tables joining `m.weak`
896 // past `origweak_n` get their first value-clear here.
897 let weak_snapshot = std::mem::take(&mut m.weak);
898 for t in &weak_snapshot[origweak_n..] {
899 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
900 unsafe {
901 let (_wk, wv) = (**t).weak_mode();
902 if wv {
903 (**t).clear_weak(false, true, &early_is_dead, &mark_string);
904 }
905 }
906 }
907 // PUC 5.5 `atomic` end: flip currentwhite so survivors (presently
908 // BLACK) get transitioned into the *new* current-white during sweep,
909 // and the pre-flip current-white becomes the dead-white (the bit the
910 // sweep tests for). Born-during-sweep allocations stamp the new
911 // current-white via `Heap::link`, so they survive this cycle.
912 self.current_white ^= WHITE_BITS;
913 // v2.13 WUC `gc-verify` — tricolor invariant check at the one
914 // moment it is exact: marking is complete, nothing is freed yet.
915 // A BLACK (surviving) table holding a dead-white child means a
916 // write barrier was missed; the child will be freed by the
917 // upcoming sweep and the table left holding a dangling pointer.
918 // Nothing is dangling *yet*, so the reporter may safely print
919 // string contents to identify the key.
920 #[cfg(feature = "gc-verify")]
921 self.verify_tricolor("atomic_tail");
922 }
923
924 /// v2.13 WUC `gc-verify` — the set of every live object header
925 /// (all + sweep_cur + finalizer queues), for callers that need to
926 /// audit their own containers (e.g. the VM auditing its register
927 /// stack after a collect).
928 #[cfg(feature = "gc-verify")]
929 pub fn debug_live_set(&self) -> std::collections::HashSet<usize> {
930 let mut live = std::collections::HashSet::new();
931 // SAFETY: heap-owned intrusive lists; all elements are live
932 // allocations.
933 unsafe {
934 let mut cur = self.all;
935 while !cur.is_null() {
936 live.insert(cur as usize);
937 cur = (*cur).next;
938 }
939 let mut cur = self.sweep_cur;
940 while !cur.is_null() {
941 live.insert(cur as usize);
942 cur = (*cur).next;
943 }
944 }
945 for &h in &self.finalize {
946 live.insert(h as usize);
947 }
948 for &h in &self.tobefnz {
949 live.insert(h as usize);
950 }
951 live
952 }
953
954 /// See call site in [`Heap::atomic_tail`]. Panics on the first
955 /// BLACK table whose collectable child is dead-white (missed
956 /// barrier — that child is about to be swept while still
957 /// referenced).
958 #[cfg(feature = "gc-verify")]
959 fn verify_tricolor(&self, ctx: &str) {
960 let new_white = self.current_white;
961 // SAFETY: `all` is the heap's own intrusive list; every element
962 // is a live allocation (nothing has been freed this cycle yet).
963 unsafe {
964 let is_live = |v: Value| -> bool {
965 let h = match v {
966 Value::Str(s) => s.as_ptr() as *mut GcHeader,
967 Value::Table(t) => t.as_ptr() as *mut GcHeader,
968 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
969 Value::Native(n) => n.as_ptr() as *mut GcHeader,
970 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
971 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
972 _ => return true,
973 };
974 let f = (*h).flags;
975 !(is_white(f) && (f & new_white) == 0)
976 };
977 let mut cur = self.all;
978 while !cur.is_null() {
979 let f = (*cur).flags;
980 if (*cur).tag == ObjTag::Table && (f & BLACK) != 0 {
981 let t = &*(cur as *const Table);
982 // Weak tables may legitimately reference dead objects
983 // between clear passes; they were just cleared above,
984 // but their sanctioned dead_key nodes are skipped by
985 // verify_refs anyway. Only strong tables assert.
986 let (wk, wv) = t.weak_mode();
987 if wk || wv {
988 cur = (*cur).next;
989 continue;
990 }
991 let tp = cur as usize;
992 t.verify_refs(&is_live, &|what, idx, v| {
993 // Pre-sweep: dereferencing v is still safe — name
994 // the key when it is a string.
995 let detail = match v.as_bytes() {
996 Some(b) => format!("str {:?}", String::from_utf8_lossy(b)),
997 None => format!(
998 "non-str {:#x}",
999 match v {
1000 Value::Table(t) => t.as_ptr() as usize,
1001 Value::Closure(c) => c.as_ptr() as usize,
1002 Value::Coro(c) => c.as_ptr() as usize,
1003 Value::Userdata(u) => u.as_ptr() as usize,
1004 Value::Native(n) => n.as_ptr() as usize,
1005 _ => 0,
1006 }
1007 ),
1008 };
1009 panic!(
1010 "[gc-verify] {ctx}: BLACK table {tp:#x} holds dead-white {what} \
1011 (slot {idx}): {detail} — missed write barrier"
1012 );
1013 });
1014 }
1015 cur = (*cur).next;
1016 }
1017 }
1018 }
1019
1020 /// Borrow Heap's persistent propagate state as an ephemeral Marker.
1021 /// Caller MUST call `stash_marker` with the same Marker after work to
1022 /// write the (potentially mutated) state back. Used by the incremental
1023 /// Propagate path to avoid lifetime entanglement between `&mut self` and
1024 /// `&mut self.propagate`.
1025 fn loan_marker(&mut self) -> Marker {
1026 let mut prop = self
1027 .propagate
1028 .take()
1029 .expect("propagate state taken outside Propagate phase");
1030 Marker {
1031 stack: std::mem::take(&mut self.gray),
1032 weak: std::mem::take(&mut prop.weak),
1033 ephemeron: std::mem::take(&mut prop.ephemeron),
1034 no_ephemeron: prop.no_ephemeron,
1035 cached_protos: std::mem::take(&mut prop.cached_protos),
1036 }
1037 }
1038
1039 fn stash_marker(&mut self, m: Marker) {
1040 let no_ephemeron = m.no_ephemeron;
1041 self.gray = m.stack;
1042 self.propagate = Some(PropagateState {
1043 weak: m.weak,
1044 ephemeron: m.ephemeron,
1045 cached_protos: m.cached_protos,
1046 no_ephemeron,
1047 });
1048 }
1049
1050 /// Begin an incremental mark cycle: seed the persistent gray queue from
1051 /// roots + extra + tobefnz + any barrier-carried gray, install a fresh
1052 /// PropagateState, and enter `GcPhase::Propagate`. Precondition: `Pause`.
1053 pub(crate) fn gc_start_propagate(&mut self, roots: &[Value], extra: &[*mut GcHeader]) {
1054 debug_assert!(self.phase == GcPhase::Pause);
1055 self.phase = GcPhase::Propagate;
1056 self.propagate = Some(PropagateState {
1057 weak: Vec::new(),
1058 ephemeron: Vec::new(),
1059 cached_protos: Vec::new(),
1060 no_ephemeron: self.no_ephemeron,
1061 });
1062 let mut m = self.loan_marker();
1063 for &r in roots {
1064 m.value(r);
1065 }
1066 for &h in extra {
1067 m.header(h);
1068 }
1069 for &h in &self.tobefnz {
1070 m.header(h);
1071 }
1072 self.stash_marker(m);
1073 }
1074
1075 /// Drain up to `budget` gray objects (blacken + trace). Returns true if
1076 /// the gray queue is now empty (caller should follow up with
1077 /// `gc_finish_atomic`). PUC `propagatemark` budgeted loop.
1078 pub(crate) fn gc_step_propagate(&mut self, budget: usize) -> bool {
1079 debug_assert!(self.phase == GcPhase::Propagate);
1080 let mut m = self.loan_marker();
1081 let mut n = 0;
1082 while n < budget {
1083 let Some(h) = m.stack.pop() else {
1084 break;
1085 };
1086 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1087 unsafe {
1088 (*h).flags = ((*h).flags & !WHITE_BITS) | BLACK;
1089 match (*h).tag {
1090 ObjTag::Str => {}
1091 ObjTag::Table => (*(h as *mut Table)).trace(&mut m),
1092 ObjTag::Proto => (*(h as *mut Proto)).trace(&mut m),
1093 ObjTag::Closure => (*(h as *mut LuaClosure)).trace(&mut m),
1094 ObjTag::Upvalue => (*(h as *mut Upvalue)).trace(&mut m),
1095 ObjTag::Native => (*(h as *mut NativeClosure)).trace(&mut m),
1096 ObjTag::Coro => (*(h as *mut crate::runtime::Coro)).trace(&mut m),
1097 ObjTag::Userdata => (*(h as *mut Userdata)).trace(&mut m),
1098 }
1099 }
1100 n += 1;
1101 }
1102 let exhausted = m.stack.is_empty();
1103 self.stash_marker(m);
1104 exhausted
1105 }
1106
1107 /// Conclude a Propagate cycle: drain any residual gray, run the atomic
1108 /// tail (weak / ephemeron / finalizer / proto-cache / flip), detach `all`
1109 /// into `sweep_cur`, and enter `GcPhase::Sweep`. Releases `propagate`.
1110 /// PUC `atomic` + `entersweep` transition.
1111 pub(crate) fn gc_finish_atomic(&mut self) {
1112 debug_assert!(self.phase == GcPhase::Propagate);
1113 let mut m = self.loan_marker();
1114 // any residual gray (caller may not have drained to empty)
1115 drain_marker(&mut m);
1116 // pre-atomic ephemeron convergence
1117 if !m.ephemeron.is_empty() {
1118 loop {
1119 let mut changed = false;
1120 let eph = m.ephemeron.clone();
1121 for t in eph {
1122 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1123 changed |= unsafe { (*t).converge_ephemeron(&weak_key_alive, &mut m) };
1124 }
1125 drain_marker(&mut m);
1126 if !changed {
1127 break;
1128 }
1129 }
1130 }
1131 self.atomic_tail(&mut m);
1132 // PropagateState consumed; transition to Sweep phase by detaching
1133 // the whole heap into sweep_cur (mirrors gc_mark_atomic). Anything
1134 // allocated past this point links onto fresh `all` and survives.
1135 self.propagate = None;
1136 debug_assert!(self.gray.is_empty(), "gray queue not drained at atomic");
1137 self.sweep_cur = std::mem::replace(&mut self.all, ptr::null_mut());
1138 self.phase = GcPhase::Sweep;
1139 }
1140
1141 /// Phase peek (for the VM-side step driver).
1142 pub(crate) fn gc_phase_is_pause(&self) -> bool {
1143 self.phase == GcPhase::Pause
1144 }
1145 pub(crate) fn gc_phase_is_propagate(&self) -> bool {
1146 self.phase == GcPhase::Propagate
1147 }
1148 #[allow(dead_code)] // public phase-peek API trio; sweep variant unused internally
1149 pub(crate) fn gc_phase_is_sweep(&self) -> bool {
1150 self.phase == GcPhase::Sweep
1151 }
1152
1153 /// Forward write barrier: when a BLACK `parent` acquires a fresh reference
1154 /// to a WHITE `child`, gray the child (strings go straight to BLACK as
1155 /// leaves) and push onto the persistent gray queue so the next propagate
1156 /// step traces it. Mirrors PUC `luaC_barrier_`. No-op outside Propagate
1157 /// (parent is gray or white — the mutator never sees a BLACK object live
1158 /// outside an incremental cycle).
1159 #[allow(clippy::not_unsafe_ptr_arg_deref)] // Internal GC barrier; caller (Gc<T>::write_*) guarantees ptr validity per SAFETY below.
1160 pub fn barrier_forward(&mut self, parent: *mut GcHeader, child: Value) {
1161 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1162 unsafe {
1163 if !is_black((*parent).flags) {
1164 return;
1165 }
1166 let ch = match child {
1167 Value::Str(s) => s.as_ptr() as *mut GcHeader,
1168 Value::Table(t) => t.as_ptr() as *mut GcHeader,
1169 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1170 Value::Native(n) => n.as_ptr() as *mut GcHeader,
1171 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1172 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1173 _ => return,
1174 };
1175 let cf = (*ch).flags;
1176 if !is_white(cf) {
1177 return;
1178 }
1179 if (*ch).tag == ObjTag::Str {
1180 (*ch).flags = (cf & !COLOR_BITS) | BLACK;
1181 } else {
1182 (*ch).flags = cf & !WHITE_BITS;
1183 self.gray.push(ch);
1184 }
1185 }
1186 }
1187
1188 /// Backward write barrier for objects with many fields (tables, threads):
1189 /// demote the parent itself back to gray so propagate re-traces it.
1190 /// Mirrors PUC `luaC_barrierback_`. One call covers any number of
1191 /// subsequent stores until the next propagate finishes — much cheaper for
1192 /// tables than per-child forward barriers. No-op outside Propagate.
1193 #[allow(clippy::not_unsafe_ptr_arg_deref)] // Internal GC barrier; caller (Gc<T>::write_*) guarantees ptr validity per SAFETY below.
1194 pub fn barrier_back(&mut self, parent: *mut GcHeader) {
1195 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1196 unsafe {
1197 let f = (*parent).flags;
1198 if !is_black(f) {
1199 return;
1200 }
1201 (*parent).flags = f & !COLOR_BITS;
1202 self.gray.push(parent);
1203 }
1204 }
1205
1206 /// Move every registered finalizable that is now unreachable to `tobefnz`
1207 /// and resurrect it (mark it via `m`) so it — and the data its `__gc` needs
1208 /// — survives this cycle. Survivors stay registered in `finalize`. PUC's
1209 /// `separatetobefnz` walks `g->finobj` head-first, but `g->finobj` is a
1210 /// linked list that registration *prepends* to — so dead objects end up
1211 /// in `tobefnz` in reverse registration order, and `__gc` ultimately
1212 /// runs LIFO. luna's `finalize` is a Vec that grows forward, so iterate
1213 /// it in reverse here to match the LIFO contract (gc.lua's userdata
1214 /// section asserts the finalizers fire from value 10 back to 0).
1215 fn separate_finalizables(&mut self, m: &mut Marker) {
1216 let mut i = self.finalize.len();
1217 while i > 0 {
1218 i -= 1;
1219 let h = self.finalize[i];
1220 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1221 if unsafe { is_white((*h).flags) } {
1222 // Two-pass cycle-finalize (PUC 5.3 gc.lua :502): when a
1223 // finalizable table holds onto an unreachable coroutine, the
1224 // cycle (table → coroutine.stack → closure → table) keeps the
1225 // mark phase from reaching the table even though it is still
1226 // logically alive for one more GC pass. PUC's mark-sweep wakes
1227 // it via `markbeingfnz` *after* sweeping, so the actual `__gc`
1228 // call lands one cycle later. luna mirrors this by resurrecting
1229 // the table on the first sighting and only enqueuing it for
1230 // `__gc` on the second.
1231 let in_thread_cycle = self.defer_thread_cycle_finalize
1232 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1233 && unsafe { (*h).tag } == ObjTag::Table
1234 && {
1235 let t = h as *mut Table;
1236 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1237 unsafe { (*t).refs_contain_unmarked_coro() }
1238 };
1239 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1240 let already_deferred = unsafe { (*h).flags & DEFERRED != 0 };
1241 if in_thread_cycle && !already_deferred {
1242 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1243 unsafe { (*h).flags |= DEFERRED };
1244 m.header(h);
1245 continue;
1246 }
1247 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1248 unsafe { (*h).flags = ((*h).flags & !(FIN | DEFERRED)) | FINALIZED };
1249 self.tobefnz.push(h);
1250 m.header(h);
1251 self.finalize.swap_remove(i);
1252 } else {
1253 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1254 unsafe { (*h).flags &= !DEFERRED };
1255 }
1256 }
1257 }
1258
1259 /// Register a table for finalization (a live `__gc` metamethod was just set
1260 /// via setmetatable). No-op if it is already pending a finalize (FIN bit).
1261 /// PUC 5.5 reference manual §2.5.3: "An object can be marked again for
1262 /// finalization by calling setmetatable with a different metatable, or
1263 /// with the same metatable but with a different __gc field" — so a
1264 /// previously finalized object (FINALIZED bit set then reset by
1265 /// `take_tobefnz`) can re-register. PUC's `luaC_checkfinalizer` is gated
1266 /// on `tofinalize(o)` only, which mirrors checking the FIN bit.
1267 pub(crate) fn register_finalizable(&mut self, t: Gc<Table>) {
1268 let h = t.as_ptr() as *mut GcHeader;
1269 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1270 unsafe {
1271 if (*h).flags & FIN == 0 {
1272 (*h).flags |= FIN;
1273 self.finalize.push(h);
1274 }
1275 }
1276 }
1277
1278 /// Register a userdata for finalization. PUC 5.1 `newproxy(true)` plus a
1279 /// metatable carrying `__gc` lets a Lua script attach a finalizer to a
1280 /// proxy object — gc.lua's "testing userdata" section binds this
1281 /// behaviour together with weak tables.
1282 pub(crate) fn register_finalizable_userdata(&mut self, u: Gc<crate::runtime::Userdata>) {
1283 let h = u.as_ptr() as *mut GcHeader;
1284 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1285 unsafe {
1286 if (*h).flags & FIN == 0 {
1287 (*h).flags |= FIN;
1288 self.finalize.push(h);
1289 }
1290 }
1291 }
1292
1293 /// Take the objects awaiting their `__gc` call (the VM runs the
1294 /// finalizers). Each entry is dispatched on its `ObjTag` so the caller
1295 /// can look up `__gc` for either a table or a proxy userdata.
1296 ///
1297 /// Mirrors PUC 5.5 `udata2finalize`: the FINALIZED bit is reset on the
1298 /// way out so a `setmetatable(obj, mt_with___gc)` inside (or after) the
1299 /// finalizer can re-register the object for a future round, per the Lua
1300 /// 5.5 reference manual §2.5.3 re-finalize semantics.
1301 pub(crate) fn take_tobefnz(&mut self) -> Vec<crate::runtime::Value> {
1302 use crate::runtime::Value;
1303 std::mem::take(&mut self.tobefnz)
1304 .into_iter()
1305 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1306 .map(|h| unsafe {
1307 (*h).flags &= !FINALIZED;
1308 match (*h).tag {
1309 ObjTag::Table => Value::Table(Gc::from_ptr(h as *mut Table)),
1310 ObjTag::Userdata => {
1311 Value::Userdata(Gc::from_ptr(h as *mut crate::runtime::Userdata))
1312 }
1313 _ => unreachable!("non-finalizable object queued for finalization"),
1314 }
1315 })
1316 .collect()
1317 }
1318
1319 /// Move ALL still-registered finalizables to the pending queue regardless of
1320 /// reachability (PUC separatetobefnz(g, 1) at state close), so the VM can run
1321 /// every `__gc` before the heap is torn down.
1322 pub(crate) fn queue_all_finalizers(&mut self) {
1323 for h in std::mem::take(&mut self.finalize) {
1324 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1325 unsafe { (*h).flags = ((*h).flags & !FIN) | FINALIZED };
1326 self.tobefnz.push(h);
1327 }
1328 }
1329
1330 /// Sweep the whole `all` list in one pass: free dead-white objects,
1331 /// transition survivors (BLACK or current-white) to the new current-white
1332 /// so the next cycle can re-mark them. Returns the number of objects freed.
1333 fn full_sweep(&mut self) -> usize {
1334 // detach the list first so freeing (which needs &mut self for the
1335 // string table) never aliases a pointer into self
1336 let mut freed = 0;
1337 let new_white = self.current_white;
1338 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1339 unsafe {
1340 let mut cur = std::mem::replace(&mut self.all, ptr::null_mut());
1341 let mut kept_head: *mut GcHeader = ptr::null_mut();
1342 let mut kept_tail: *mut GcHeader = ptr::null_mut();
1343 while !cur.is_null() {
1344 let next = (*cur).next;
1345 let f = (*cur).flags;
1346 // dead = other-white (i.e. white but not current-white).
1347 // Survivors are BLACK (just-marked) or current-white (born
1348 // during the sweep itself).
1349 let dead = is_white(f) && (f & new_white) == 0;
1350 if !dead {
1351 (*cur).flags = (f & !COLOR_BITS) | new_white;
1352 (*cur).next = ptr::null_mut();
1353 if kept_tail.is_null() {
1354 kept_head = cur;
1355 } else {
1356 (*kept_tail).next = cur;
1357 }
1358 kept_tail = cur;
1359 } else {
1360 self.free_obj(cur);
1361 freed += 1;
1362 }
1363 cur = next;
1364 }
1365 self.all = kept_head;
1366 }
1367 self.live -= freed;
1368 #[cfg(feature = "gc-verify")]
1369 self.verify_no_dangling("full_sweep");
1370 freed
1371 }
1372
1373 /// Sweep up to `budget` objects from the detached `sweep_cur` list: free
1374 /// unmarked ones, splice marked survivors back onto `all` (clearing their
1375 /// MARK bit). Returns true once the list is exhausted (cycle complete →
1376 /// back to `Pause`). Safe with no write barrier: marking was atomic, so any
1377 /// object still unmarked here was unreachable at mark time and the mutator
1378 /// holds no reference that could have resurrected it.
1379 pub(crate) fn gc_sweep_step(&mut self, budget: usize) -> bool {
1380 let mut n = 0;
1381 let new_white = self.current_white;
1382 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1383 unsafe {
1384 while n < budget && !self.sweep_cur.is_null() {
1385 let cur = self.sweep_cur;
1386 let next = (*cur).next;
1387 let f = (*cur).flags;
1388 let dead = is_white(f) && (f & new_white) == 0;
1389 if !dead {
1390 (*cur).flags = (f & !COLOR_BITS) | new_white;
1391 (*cur).next = self.all;
1392 self.all = cur;
1393 } else {
1394 self.free_obj(cur);
1395 self.live -= 1;
1396 }
1397 self.sweep_cur = next;
1398 n += 1;
1399 }
1400 }
1401 // Verify after EVERY step, not just cycle completion: the
1402 // mutator runs between steps, so a reference dangling mid-cycle
1403 // is already a live bug (this is exactly UAF-C's window).
1404 #[cfg(feature = "gc-verify")]
1405 self.verify_no_dangling("sweep_step");
1406 if self.sweep_cur.is_null() {
1407 self.phase = GcPhase::Pause;
1408 true
1409 } else {
1410 false
1411 }
1412 }
1413
1414 /// v2.13 WUC `gc-verify` — post-sweep dangling-reference check
1415 /// (PUC `lua_checkmemory` analogue). Builds the live set from the
1416 /// `all` list + finalizer queues, then walks every live table's
1417 /// collectable refs via [`Table::verify_refs`]. Panics with table
1418 /// pointer + slot detail on the first reference whose target is no
1419 /// longer live — i.e. at the collect that CREATED the dangling
1420 /// pointer, not at the later allocator-dependent dereference.
1421 #[cfg(feature = "gc-verify")]
1422 pub fn verify_no_dangling(&self, ctx: &str) {
1423 use std::collections::HashSet;
1424 fn value_header(v: Value) -> Option<*mut GcHeader> {
1425 match v {
1426 Value::Str(s) => Some(s.as_ptr() as *mut GcHeader),
1427 Value::Table(t) => Some(t.as_ptr() as *mut GcHeader),
1428 Value::Closure(c) => Some(c.as_ptr() as *mut GcHeader),
1429 Value::Native(n) => Some(n.as_ptr() as *mut GcHeader),
1430 Value::Coro(c) => Some(c.as_ptr() as *mut GcHeader),
1431 Value::Userdata(u) => Some(u.as_ptr() as *mut GcHeader),
1432 _ => None,
1433 }
1434 }
1435 let mut live: HashSet<usize> = HashSet::new();
1436 // SAFETY: `all` / `sweep_cur` / finalizer-queue pointers are the
1437 // heap's own intrusive lists; every element is a live allocation
1438 // until free_obj unlinks it.
1439 unsafe {
1440 let mut cur = self.all;
1441 while !cur.is_null() {
1442 live.insert(cur as usize);
1443 cur = (*cur).next;
1444 }
1445 let mut cur = self.sweep_cur;
1446 while !cur.is_null() {
1447 live.insert(cur as usize);
1448 cur = (*cur).next;
1449 }
1450 for &h in &self.finalize {
1451 live.insert(h as usize);
1452 }
1453 for &h in &self.tobefnz {
1454 live.insert(h as usize);
1455 }
1456 let is_live = |v: Value| -> bool {
1457 match value_header(v) {
1458 None => true,
1459 Some(h) => live.contains(&(h as usize)),
1460 }
1461 };
1462 // Walk BOTH lists: `all` (already-swept survivors) and
1463 // `sweep_cur` (not-yet-swept remainder of an incremental
1464 // cycle). A dangling reference can live in either while the
1465 // mutator runs between sweep steps.
1466 let new_white = self.current_white;
1467 for head in [self.all, self.sweep_cur] {
1468 let mut cur = head;
1469 while !cur.is_null() {
1470 // Skip dead-but-not-yet-swept objects on `sweep_cur`:
1471 // they are unreachable, so a dangling ref inside them
1472 // is benign (their peers may already be freed).
1473 let f = (*cur).flags;
1474 if is_white(f) && (f & new_white) == 0 {
1475 cur = (*cur).next;
1476 continue;
1477 }
1478 if (*cur).tag == ObjTag::Table {
1479 let t = &*(cur as *const Table);
1480 let tp = cur as usize;
1481 t.verify_refs(&is_live, &|what, idx, v| {
1482 // Do NOT Debug-format `v` — for Value::Str that
1483 // would dereference the (dangling) payload.
1484 let p = value_header(v).map(|h| h as usize).unwrap_or(0);
1485 panic!(
1486 "[gc-verify] {ctx}: table {tp:#x} holds dangling {what} \
1487 (slot {idx}, target {p:#x})"
1488 );
1489 });
1490 }
1491 cur = (*cur).next;
1492 }
1493 }
1494 }
1495 }
1496
1497 unsafe fn free_obj(&mut self, h: *mut GcHeader) {
1498 #[cfg(feature = "gc-verify")]
1499 {
1500 self.recently_freed.insert(h as usize);
1501 crate::runtime::gc_verify_probe::FREED.with(|f| f.borrow_mut().insert(h as usize));
1502 }
1503 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1504 unsafe {
1505 match (*h).tag {
1506 ObjTag::Table => {
1507 let t = h as *mut Table;
1508 let internal = (*t).internal_bytes();
1509 self.bytes = self
1510 .bytes
1511 .saturating_sub(std::mem::size_of::<Table>() + internal);
1512 // P17-D v2 layer-15 attack — pool recycle. Drop the
1513 // Box-owned interior (slab, nodes, metatable) so the
1514 // Table struct itself can be re-handed-out by a
1515 // future `new_table` without re-mallocing. Cap pool
1516 // at 4096 entries to bound idle memory.
1517 const TABLE_POOL_CAP: usize = 4096;
1518 if self.table_pool.len() < TABLE_POOL_CAP {
1519 // Free interior heap allocations now (slab + nodes).
1520 // Each Box::new([]) is a dangling no-alloc empty
1521 // slice, so reassigning is just a pointer move.
1522 (*t).slab = Box::new([]);
1523 (*t).nodes = Box::new([]);
1524 // C3 — drop SoA Robin Hood parallel arrays too.
1525 // These are Box::new([]) dangling stubs until
1526 // Phase D cuts over (Phase B initial state).
1527 (*t).keys = Box::new([]);
1528 (*t).vals = Box::new([]);
1529 (*t).meta = Box::new([]);
1530 (*t).tombstones = 0;
1531 (*t).iter_depth = 0;
1532 (*t).metatable = None;
1533 // Stash the raw pointer for future reuse.
1534 // SAFETY: t is non-null (came from a live Gc<Table>);
1535 // pool owns it until reuse or Heap::Drop.
1536 self.table_pool.push(std::ptr::NonNull::new_unchecked(t));
1537 } else {
1538 drop(Box::from_raw(t));
1539 }
1540 }
1541 ObjTag::Proto => {
1542 self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Proto>());
1543 drop(Box::from_raw(h as *mut Proto));
1544 }
1545 ObjTag::Closure => {
1546 self.bytes = self.bytes.saturating_sub(std::mem::size_of::<LuaClosure>());
1547 drop(Box::from_raw(h as *mut LuaClosure));
1548 }
1549 ObjTag::Upvalue => {
1550 self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Upvalue>());
1551 drop(Box::from_raw(h as *mut Upvalue));
1552 }
1553 ObjTag::Native => {
1554 self.bytes = self
1555 .bytes
1556 .saturating_sub(std::mem::size_of::<NativeClosure>());
1557 drop(Box::from_raw(h as *mut NativeClosure));
1558 }
1559 ObjTag::Coro => {
1560 self.bytes = self
1561 .bytes
1562 .saturating_sub(std::mem::size_of::<crate::runtime::Coro>());
1563 drop(Box::from_raw(h as *mut crate::runtime::Coro));
1564 }
1565 ObjTag::Userdata => {
1566 self.bytes = self.bytes.saturating_sub(std::mem::size_of::<Userdata>());
1567 drop(Box::from_raw(h as *mut Userdata));
1568 }
1569 ObjTag::Str => {
1570 let s = h as *mut LuaStr;
1571 self.bytes = self.bytes.saturating_sub(string::alloc_size((*s).len()));
1572 if (*s).is_short() {
1573 self.strings.remove(s);
1574 }
1575 string::free(s);
1576 }
1577 }
1578 }
1579 }
1580}
1581
1582impl Drop for Heap {
1583 fn drop(&mut self) {
1584 // free everything regardless of reachability, including any list still
1585 // detached for an in-flight incremental sweep
1586 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1587 unsafe {
1588 for mut cur in [self.all, self.sweep_cur] {
1589 while !cur.is_null() {
1590 let next = (*cur).next;
1591 self.free_obj(cur);
1592 cur = next;
1593 }
1594 }
1595 // P17-D v2 layer-15 attack — release the table_pool's
1596 // dangling Box<Table> ptrs. Each was Box::into_raw'd into
1597 // the pool (via free_obj recycle path); without this, the
1598 // Tables would leak. The pool's Tables had their interior
1599 // Box-owned fields (slab/nodes/metatable) already cleared
1600 // when they were recycled, so dropping the Table now only
1601 // releases the Table struct itself.
1602 for ptr in self.table_pool.drain(..) {
1603 drop(Box::from_raw(ptr.as_ptr()));
1604 }
1605 }
1606 }
1607}
1608
1609impl Default for Heap {
1610 fn default() -> Heap {
1611 Heap::new()
1612 }
1613}
1614
1615/// Mark accumulator: gray stack plus entry points for Values and bare
1616/// object headers (Protos/Upvalues are not first-class Values).
1617pub(crate) struct Marker {
1618 stack: Vec<*mut GcHeader>,
1619 /// live tables with a weak `__mode`, collected during marking and processed
1620 /// (dead weak entries cleared) before the sweep
1621 pub(crate) weak: Vec<*mut Table>,
1622 /// ephemeron tables (weak keys, strong values): their hash values are not
1623 /// marked during trace but in a fixpoint pass keyed on key-reachability
1624 pub(crate) ephemeron: Vec<*mut Table>,
1625 /// PUC 5.1 mode: skip ephemeron handling — `__mode='k'` tables mark their
1626 /// values strongly during the normal trace pass (see [`Heap::no_ephemeron`]).
1627 pub(crate) no_ephemeron: bool,
1628 /// Protos with a non-null closure cache (PUC `Proto.cache`). After
1629 /// marking is done, any cached LClosure that ended the cycle unmarked is
1630 /// cleared so the sweep can collect it — the cache is a *weak* reference
1631 /// (PUC `traverseproto` checks `iswhite(cache)`). Seen via [`Proto::trace`].
1632 pub(crate) cached_protos: Vec<*mut crate::runtime::Proto>,
1633}
1634
1635/// Drain the gray stack: pop each marked object and trace its children until
1636/// the worklist is empty (iterative, so deep graphs don't overflow the Rust
1637/// stack). Shared by the root mark and the post-resurrection remark.
1638fn drain_marker(m: &mut Marker) {
1639 while let Some(h) = m.stack.pop() {
1640 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1641 unsafe {
1642 // PUC `propagatemark`: gray → black before scanning children, so a
1643 // child that points back at us (cycle) re-traces us as already
1644 // black and does not loop. White bits were cleared on push.
1645 (*h).flags = ((*h).flags & !WHITE_BITS) | BLACK;
1646 match (*h).tag {
1647 ObjTag::Str => {}
1648 ObjTag::Table => (*(h as *mut Table)).trace(m),
1649 ObjTag::Proto => (*(h as *mut Proto)).trace(m),
1650 ObjTag::Closure => (*(h as *mut LuaClosure)).trace(m),
1651 ObjTag::Upvalue => (*(h as *mut Upvalue)).trace(m),
1652 ObjTag::Native => (*(h as *mut NativeClosure)).trace(m),
1653 ObjTag::Coro => (*(h as *mut crate::runtime::Coro)).trace(m),
1654 ObjTag::Userdata => (*(h as *mut Userdata)).trace(m),
1655 }
1656 }
1657 }
1658}
1659
1660impl Marker {
1661 /// Mark a value, returning true if it was newly marked (was white).
1662 pub(crate) fn value(&mut self, v: Value) -> bool {
1663 let h = match v {
1664 Value::Str(s) => s.as_ptr() as *mut GcHeader,
1665 Value::Table(t) => t.as_ptr() as *mut GcHeader,
1666 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1667 Value::Native(n) => n.as_ptr() as *mut GcHeader,
1668 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1669 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1670 _ => return false,
1671 };
1672 self.header(h)
1673 }
1674
1675 /// Mark a bare header, returning true if it was newly marked (was white).
1676 /// Transitions white → gray (in PUC `reallymarkobject` terms): clears the
1677 /// current-white bit and pushes onto the gray stack. `drain_marker` later
1678 /// pops it, traces children, and stamps it BLACK.
1679 pub(crate) fn header(&mut self, h: *mut GcHeader) -> bool {
1680 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1681 unsafe {
1682 let f = (*h).flags;
1683 if is_white(f) {
1684 (*h).flags = f & !WHITE_BITS;
1685 self.stack.push(h);
1686 true
1687 } else {
1688 false
1689 }
1690 }
1691 }
1692}
1693
1694/// Whether a value is "alive" for ephemeron key purposes: non-collectable
1695/// values and strings are always alive (strings are never weakly cleared);
1696/// a collectable object is alive only once marked (gray or black).
1697fn weak_key_alive(v: Value) -> bool {
1698 let h = match v {
1699 Value::Table(t) => t.as_ptr() as *mut GcHeader,
1700 Value::Closure(c) => c.as_ptr() as *mut GcHeader,
1701 Value::Native(n) => n.as_ptr() as *mut GcHeader,
1702 Value::Coro(c) => c.as_ptr() as *mut GcHeader,
1703 Value::Userdata(u) => u.as_ptr() as *mut GcHeader,
1704 _ => return true, // strings, numbers, booleans: never weak-collected
1705 };
1706 // SAFETY: `h` is a GcHeader pointer drawn from the runtime's all-objects intrusive list (or from a live `Gc<T>` cast above); it is non-null and remains live for the duration of this GC step (heap.rs:5-7).
1707 unsafe { !is_white((*h).flags) }
1708}
1709
1710/// Hash seed from address entropy (ASLR) and clock, luaL_makeseed style.
1711fn make_seed() -> u32 {
1712 let stack_var = 0u8;
1713 let mut h = &stack_var as *const u8 as u64;
1714 h ^= (make_seed as *const () as u64) << 16;
1715 if let Ok(d) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
1716 h ^= (d.subsec_nanos() as u64) << 32 ^ d.as_secs();
1717 }
1718 h ^= h >> 33;
1719 h = h.wrapping_mul(0xff51_afd7_ed55_8ccd);
1720 h ^= h >> 33;
1721 h as u32
1722}
1723
1724#[cfg(test)]
1725mod tests {
1726 use super::*;
1727 use crate::vm::isa::{Inst, Op};
1728
1729 #[test]
1730 fn collect_traces_function_objects() {
1731 let mut heap = Heap::new();
1732 let source = heap.intern(b"@test");
1733 let kstr = heap.intern(b"a-constant-string");
1734 let inner = Proto {
1735 hdr: GcHeader::new(ObjTag::Proto),
1736 code: Box::new([Inst::iabc(Op::Return0, 0, 0, 0, false)]),
1737 consts: Box::new([]),
1738 protos: Box::new([]),
1739 upvals: Box::new([]),
1740 num_params: 0,
1741 is_vararg: false,
1742 has_vararg_table_pseudo: false,
1743 has_compat_vararg_arg: false,
1744 max_stack: 2,
1745 lines: Box::new([1]),
1746 source,
1747 line_defined: 1,
1748 last_line_defined: 1,
1749 locvars: Box::new([]),
1750 cache: std::cell::Cell::new(None),
1751 jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
1752 env_upval_idx: u8::MAX,
1753 trace_hot_count: std::cell::Cell::new(0),
1754 call_hot_count: std::cell::Cell::new(0),
1755 trace_discard_count: std::cell::Cell::new(0),
1756 trace_gave_up: std::cell::Cell::new(false),
1757 traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
1758 };
1759 let inner = heap.adopt_proto(inner);
1760 let outer = Proto {
1761 hdr: GcHeader::new(ObjTag::Proto),
1762 code: Box::new([Inst::iabc(Op::Return0, 0, 0, 0, false)]),
1763 consts: Box::new([Value::Str(kstr)]),
1764 protos: Box::new([inner]),
1765 upvals: Box::new([]),
1766 num_params: 0,
1767 is_vararg: true,
1768 has_vararg_table_pseudo: false,
1769 has_compat_vararg_arg: false,
1770 max_stack: 2,
1771 lines: Box::new([1]),
1772 source,
1773 line_defined: 0,
1774 last_line_defined: 0,
1775 locvars: Box::new([]),
1776 cache: std::cell::Cell::new(None),
1777 jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
1778 env_upval_idx: u8::MAX,
1779 trace_hot_count: std::cell::Cell::new(0),
1780 call_hot_count: std::cell::Cell::new(0),
1781 trace_discard_count: std::cell::Cell::new(0),
1782 trace_gave_up: std::cell::Cell::new(false),
1783 traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
1784 };
1785 let outer = heap.adopt_proto(outer);
1786 let captured = heap.intern(b"captured-value-string-xxxxxxxxxxxxxxxxxxxxxxxxx");
1787 let uv = heap.new_upvalue(UpvalState::Closed(Value::Str(captured)));
1788 let cl = heap.new_closure(outer, Box::new([uv]));
1789 // objects: source, kstr, inner, outer, captured, uv, cl
1790 assert_eq!(heap.live_objects(), 7);
1791 // rooting the closure keeps the whole graph alive
1792 assert_eq!(heap.collect(&[Value::Closure(cl)]), 0);
1793 assert_eq!(heap.live_objects(), 7);
1794 assert_eq!(heap.collect(&[]), 7);
1795 assert_eq!(heap.live_objects(), 0);
1796 }
1797
1798 #[test]
1799 fn collect_unreachable() {
1800 let mut heap = Heap::new();
1801 let s = heap.intern(b"hello");
1802 let t = heap.new_table();
1803 assert_eq!(heap.live_objects(), 2);
1804 // both rooted: nothing freed
1805 assert_eq!(heap.collect(&[Value::Str(s), Value::Table(t)]), 0);
1806 // only table rooted: string freed
1807 assert_eq!(heap.collect(&[Value::Table(t)]), 1);
1808 assert_eq!(heap.live_objects(), 1);
1809 // nothing rooted
1810 assert_eq!(heap.collect(&[]), 1);
1811 assert_eq!(heap.live_objects(), 0);
1812 }
1813
1814 #[test]
1815 fn collect_traces_table_contents() {
1816 let mut heap = Heap::new();
1817 let t = heap.new_table();
1818 let k = heap.intern(b"key-string-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); // long
1819 let v = heap.intern(b"val");
1820 unsafe { t.as_mut() }
1821 .set(&mut heap, Value::Str(k), Value::Str(v))
1822 .unwrap();
1823 let inner = heap.new_table();
1824 unsafe { t.as_mut() }
1825 .set(&mut heap, Value::Int(1), Value::Table(inner))
1826 .unwrap();
1827 unsafe { inner.as_mut() }.set_metatable(Some(t));
1828 assert_eq!(heap.live_objects(), 4);
1829 // root only the outer table: everything reachable through it survives
1830 assert_eq!(heap.collect(&[Value::Table(t)]), 0);
1831 assert_eq!(heap.live_objects(), 4);
1832 assert_eq!(heap.collect(&[]), 4);
1833 }
1834
1835 #[test]
1836 fn interned_string_reclaimed_and_reinternable() {
1837 let mut heap = Heap::new();
1838 heap.intern(b"transient");
1839 assert_eq!(heap.collect(&[]), 1);
1840 let s2 = heap.intern(b"transient");
1841 assert_eq!(s2.as_bytes(), b"transient");
1842 assert_eq!(heap.live_objects(), 1);
1843 }
1844
1845 #[test]
1846 fn bytes_and_live_round_trip_to_zero() {
1847 // Memory-invariant audit: after a churn of table allocation, rehash-
1848 // driven growth, and full collection of an empty root set, both
1849 // `heap.bytes` and `heap.live_objects` must return to 0. Catches any
1850 // alloc / free asymmetry in the Table internal-Box delta tracking
1851 // (4bab3c5) or the live counter (link/sweep symmetry).
1852 let mut heap = Heap::new();
1853 assert_eq!(heap.bytes(), 0);
1854 assert_eq!(heap.live_objects(), 0);
1855 // Build a churn: 50 tables, each filled with 200 int keys (forces
1856 // multiple rehashes); plus interned strings spliced through the
1857 // hash part. Bytes should grow well past the empty baseline.
1858 let mut roots: Vec<Value> = Vec::new();
1859 for ti in 0..50 {
1860 let t = heap.new_table();
1861 for k in 1..=200 {
1862 let _ =
1863 unsafe { t.as_mut() }.set(&mut heap, Value::Int(k), Value::Int(ti * 1000 + k));
1864 }
1865 for sk in 0..32 {
1866 let key = Value::Str(heap.intern(format!("k{ti}-{sk}").as_bytes()));
1867 let _ = unsafe { t.as_mut() }.set(&mut heap, key, Value::Int(sk));
1868 }
1869 roots.push(Value::Table(t));
1870 }
1871 let live_peak = heap.live_objects();
1872 let bytes_peak = heap.bytes();
1873 assert!(live_peak > 0, "live should be >0 after churn");
1874 assert!(bytes_peak > 0, "bytes should be >0 after churn");
1875 // Root only half — the other half should be collected.
1876 let half = roots.len() / 2;
1877 let freed = heap.collect(&roots[..half]);
1878 assert!(freed > 0, "some objects should have been freed");
1879 assert!(
1880 heap.bytes() < bytes_peak,
1881 "bytes must drop after partial collect"
1882 );
1883 assert!(
1884 heap.live_objects() < live_peak,
1885 "live must drop after partial collect"
1886 );
1887 // Drop everything: counters must return to 0 exactly.
1888 drop(roots);
1889 let _ = heap.collect(&[]);
1890 assert_eq!(heap.live_objects(), 0, "live not zero after full collect");
1891 assert_eq!(
1892 heap.bytes(),
1893 0,
1894 "bytes not zero after full collect — asymmetric alloc/free"
1895 );
1896 }
1897
1898 /// Regression for `.dev/known-bugs/stringtable-intern-uaf.md`:
1899 /// `StringTable::intern` must NOT return a dead-white (about-to-be-swept)
1900 /// short-string pointer. Mirrors PUC `luaS_new`'s resurrect-on-hit guard
1901 /// (lstring.c — `if (isdead(g, ts)) changewhite(ts);`).
1902 ///
1903 /// Without the guard, the bucket-chain still references the unswept
1904 /// short string after the atomic flip; a re-`intern` of the same bytes
1905 /// hands back that pointer; the budget-paced sweep then frees it and
1906 /// the next bucket walk dereferences libc-recycled garbage (the
1907 /// `0x800002a80000002d` misaligned pointer seen in the audit).
1908 #[test]
1909 fn intern_resurrects_dead_white_short_string() {
1910 let mut heap = Heap::new();
1911 let alive = heap.intern(b"keep-me-alive-1");
1912 let dying = heap.intern(b"transient-x");
1913 let dying_ptr = dying.as_ptr();
1914 let dying_bytes = dying.as_bytes().to_vec();
1915 // Drive an incremental cycle by hand to reproduce the race:
1916 // 1. mark-propagate with `alive` only as a root → `dying` stays white
1917 // 2. atomic flip → `dying` becomes dead-white, bucket still points at it
1918 // 3. RE-INTERN the same bytes BEFORE sweep clears the bucket
1919 // 4. fix must either (a) skip the dead entry & alloc fresh, or
1920 // (b) resurrect dying back to current-white
1921 let alive_root = [Value::Str(alive)];
1922 heap.gc_start_propagate(&alive_root, &[]);
1923 while !heap.gc_step_propagate(usize::MAX) {}
1924 heap.gc_finish_atomic();
1925 // At this point sweep_cur holds the detached old-heap list and the
1926 // dying string is dead-white. The bucket chain in `self.strings`
1927 // still references it.
1928 let resurrected = heap.intern(&dying_bytes);
1929 // Two valid outcomes:
1930 // * resurrect: same pointer, but flagged current-white so sweep
1931 // will keep it alive (PUC luaS_new shape)
1932 // * skip-and-alloc-fresh: different pointer, dying gets swept
1933 // normally as it should
1934 // Either way, after completing the sweep the heap must NOT crash
1935 // when we try to intern more short strings (which walks bucket chains).
1936 while !heap.gc_sweep_step(usize::MAX) {}
1937 // Smoke: bucket-chain walk for a fresh string must not deref a
1938 // freed pointer.
1939 let _fresh = heap.intern(b"after-sweep-canary");
1940 // If the fix is "resurrect", same pointer + bytes preserved:
1941 if resurrected.as_ptr() == dying_ptr {
1942 assert_eq!(resurrected.as_bytes(), dying_bytes.as_slice());
1943 }
1944 // Final cleanup: full collect must complete without UAF.
1945 drop(heap);
1946 }
1947
1948 #[test]
1949 fn deep_table_chain_marks_iteratively() {
1950 // deep chain: explicit mark stack must not overflow (smaller under
1951 // miri — the interpreter makes 100k tables take ~30 minutes)
1952 let n = if cfg!(miri) { 2_000 } else { 100_000 };
1953 let mut heap = Heap::new();
1954 let head = heap.new_table();
1955 let mut cur = head;
1956 for _ in 0..n {
1957 let next = heap.new_table();
1958 unsafe { cur.as_mut() }
1959 .set(&mut heap, Value::Int(1), Value::Table(next))
1960 .unwrap();
1961 cur = next;
1962 }
1963 assert_eq!(heap.collect(&[Value::Table(head)]), 0);
1964 assert_eq!(heap.collect(&[]), n + 1);
1965 }
1966}