lua_types/gc.rs
1//! `GcRef<T>` — the GC-managed reference handle.
2//!
3//! Newtype around `lua_gc::Gc<T>` — Copy under the hood, tracks allocation
4//! in the active `Heap` (via `lua_gc::with_current_heap(...)`). An earlier
5//! revision was a thin `Rc<T>` newtype instead; the surface stayed stable
6//! across the swap (`new`, `ptr_eq`, `identity`, `strong_count`,
7//! `weak_count`, `downgrade`), and existing code touching `gc.0` continues
8//! to work — `.0` is now `Gc<T>` instead of `Rc<T>`.
9//!
10//! # Weak refs
11//!
12//! Heap-tracked `GcWeak<T>` handles remember the heap active when they were
13//! created plus the target's heap allocation token. They upgrade only while
14//! that identity/token pair remains live. Handles to legacy uncollected boxes
15//! still upgrade forever, matching their process-lifetime allocation model.
16//!
17//! # Internal GC capability, not the embedding surface (issue #267)
18//!
19//! `GcRef` is a raw, `Copy`, guard-scoped GC capability: it carries no owner
20//! identity of its own, and its operations assume an active `HeapGuard` for the
21//! heap that owns the box. Holding a `GcRef` (or an issued `&T` from its
22//! `Deref`) across the owning heap's `drop_all`/`close`, or operating it under a
23//! *different* heap, is a use-after-free that safe code must not do. The
24//! guard-scoped operations here fail loudly on the one shape that is closable
25//! without reading the box — `downgrade`/`account_buffer` panic **deref-free**
26//! with no active guard (issue #267 F1/F3). The stale-*after-sweep* and
27//! foreign-heap shapes are **not** closed: validating them would mean reading a
28//! possibly-freed box, which *is* the use-after-free, so the raw type cannot
29//! make arbitrary handle lifetimes safe in release without slot-indexed handles
30//! / an API-shape change (spec option B).
31//!
32//! **`GcRef` is therefore an implementation-crate (`lua-types`/`lua-gc`)
33//! capability, not omniLua's public embedding surface.** Embedders never name
34//! it: the `omnilua` facade exposes only rooted handles (its `Value` /
35//! `RootedValue`, which own a `Lua`, root through a slab key, and return a
36//! stale-handle error on cross-state or post-teardown use) and deliberately does
37//! not re-export raw `Gc`/`GcRef`/`Heap`. Keep it that way — routing embedders
38//! through the rooted handle is what makes the "a handle must not outlive its
39//! heap" invariant real.
40
41use lua_gc::{Gc, HeapRef, Trace};
42
43/// A GC-managed pointer to a Lua collectable object. Newtype over
44/// `lua_gc::Gc<T>` so callers preserve `gc.0`-shape access while the
45/// backend swaps under them.
46#[derive(Debug)]
47pub struct GcRef<T: Trace + 'static>(pub Gc<T>);
48
49impl<T: Trace + 'static> GcRef<T> {
50 /// Allocate a new GC-tracked value on the active heap: joins the allgc
51 /// chain under a `HeapGuard` (set by `state.run()` / `pcall_k` /
52 /// `with_state`), or the heap's bootstrap list inside a bootstrap
53 /// window.
54 ///
55 /// Panics if no heap is active. There is no fallback: the old detached
56 /// arm allocated a box no heap ever freed (issue #249's leak class), and
57 /// since #253 moved host-side `LuaError` messages to owned bytes, no
58 /// legitimate guard-less allocation path remains — a panic here is
59 /// always a missing guard on the entry path, and the message says so.
60 pub fn new(value: T) -> Self {
61 let gc = lua_gc::with_current_heap(|heap| match heap {
62 Some(heap) => heap.allocate(value),
63 None => panic!(
64 "GcRef::new::<{}> with no active HeapGuard — a detached allocation \
65 would never be freed (issue #249 class); push a HeapGuard or \
66 bootstrap window on the entry path",
67 std::any::type_name::<T>()
68 ),
69 });
70 GcRef(gc)
71 }
72}
73
74impl<T: Trace + 'static> GcRef<T> {
75 /// Two `GcRef`s are identity-equal iff they point at the same box.
76 pub fn ptr_eq(a: &Self, b: &Self) -> bool {
77 Gc::ptr_eq(a.0, b.0)
78 }
79
80 /// Identity as a usize — used as a HashMap key for "same object" tests.
81 pub fn identity(&self) -> usize {
82 self.0.identity()
83 }
84
85 /// Number of strong references. `GcRef` is not reference-counted, so a live
86 /// handle reports one owning GC reachability handle.
87 pub fn strong_count(&self) -> usize {
88 1
89 }
90
91 /// Number of weak references. Weak handles are not counted.
92 pub fn weak_count(&self) -> usize {
93 0
94 }
95
96 /// Get a weak handle. If this allocation belongs to the currently-active
97 /// heap, the weak handle will stop upgrading once sweep removes that exact
98 /// heap allocation.
99 ///
100 /// # No-guard path is deref-free (issue #267 F1)
101 ///
102 /// With no active `HeapGuard` there is no heap to validate against, and the
103 /// handle **must not** read the box to decide — if its owning heap has been
104 /// dropped, the box is freed and that read is itself the use-after-free (the
105 /// old `is_heap_owned()` check was exactly this UAF-in-the-safety-check). So
106 /// this panics unconditionally, reading nothing, consistent with
107 /// [`GcRef::new`]'s guard-less panic and the always-on guard policy. The
108 /// legacy detached-box "always upgrades on a guard-less downgrade" path is
109 /// dropped with it.
110 ///
111 /// # Guarded path and residuals (issue #267)
112 ///
113 /// The guarded path mints a token against the active heap *without*
114 /// dereferencing the box — so a same-heap downgrade is validated by the
115 /// allocation token, and when the active guard is a *closed* heap the
116 /// closed-heap token refusal makes the resulting weak handle permanently
117 /// dead. The stale corners this cannot fix in release — a *foreign*-heap
118 /// downgrade (the box's owner is a different heap), a same-heap re-downgrade
119 /// of a box already swept while unrooted, and an already-issued `&T`
120 /// outliving a later `drop_all(&self)` — are **not** closed here: validating
121 /// them would require reading the (possibly freed) box, which *is* the
122 /// use-after-free. They are documented residuals that only slot-indexed
123 /// handles / an API-shape change (spec option B) close; see the spec and the
124 /// `stale_handle_kit`.
125 pub fn downgrade(&self) -> GcWeak<T> {
126 let identity = self.identity();
127 match lua_gc::with_current_heap(|heap| {
128 heap.map(|heap| {
129 let token = heap.register_allocation_token(identity);
130 (HeapRef::from_heap(heap), token)
131 })
132 }) {
133 Some((heap, allocation_token)) => GcWeak {
134 target: self.0,
135 identity,
136 allocation_token,
137 heap: Some(heap),
138 },
139 None => panic!(
140 "GcRef::downgrade::<{}> with no active HeapGuard — a GcRef \
141 operated outside its owning heap's guard cannot be validated \
142 against any heap, and reading the box to decide would be a \
143 use-after-free if the heap has been dropped; push a HeapGuard \
144 on the entry path",
145 std::any::type_name::<T>()
146 ),
147 }
148 }
149
150 /// Charge (`delta > 0`) or refund (`delta < 0`) bytes of this object's
151 /// owned heap buffers against the active heap's pacer, so collections
152 /// fire at honest memory pressure. No-op on `delta == 0` or when the
153 /// underlying box is uncollected (see [`lua_gc::Gc::account_buffer`]).
154 ///
155 /// # No-guard path is deref-free (issue #267 F3)
156 ///
157 /// With no active `HeapGuard` this panics unconditionally, reading nothing:
158 /// the charge would otherwise be silently dropped (pacer drift), and — as
159 /// with [`downgrade`](Self::downgrade) — reading the box to decide would be
160 /// a use-after-free if the heap has been dropped (the old `is_heap_owned()`
161 /// check was that UAF). The legacy detached no-op path is dropped with it.
162 ///
163 /// The guarded charge intrinsically reads the box (it mutates
164 /// `header.size`), so — unlike the no-guard path — it cannot be made
165 /// deref-free: a charge against a stale box under an active guard reads that
166 /// box, which is the same option-B residual as `downgrade`. Under quarantine
167 /// a same-heap swept target is caught by the `HDR_FREED` `debug_assert` in
168 /// `as_box`; in release it is a documented residual (see the spec).
169 pub fn account_buffer(&self, delta: isize) {
170 if delta == 0 {
171 return;
172 }
173 match lua_gc::with_current_heap(|h| {
174 h.map(|h| {
175 self.0.account_buffer(h, delta);
176 })
177 }) {
178 Some(()) => {}
179 None => panic!(
180 "GcRef::account_buffer::<{}>({delta}) with no active HeapGuard \
181 — the charge would be silently dropped and the pacer would \
182 drift from real memory, and reading the box to decide would be \
183 a use-after-free if the heap has been dropped; push a HeapGuard \
184 on the entry path",
185 std::any::type_name::<T>()
186 ),
187 }
188 }
189}
190
191/// A weak handle to a `GcRef<T>`.
192#[derive(Debug)]
193pub struct GcWeak<T: Trace + 'static> {
194 target: Gc<T>,
195 identity: usize,
196 allocation_token: usize,
197 heap: Option<HeapRef>,
198}
199
200impl<T: Trace + 'static> GcWeak<T> {
201 /// Try to promote to a strong reference.
202 pub fn upgrade(&self) -> Option<GcRef<T>> {
203 if let Some(heap) = &self.heap {
204 if !heap.contains_allocation(self.identity, self.allocation_token) {
205 return None;
206 }
207 }
208 Some(GcRef(self.target))
209 }
210
211 /// Strong reference count of the target from this weak handle's point of
212 /// view: one while it can still upgrade, zero after sweep.
213 pub fn strong_count(&self) -> usize {
214 usize::from(self.upgrade().is_some())
215 }
216
217 pub fn identity(&self) -> usize {
218 self.identity
219 }
220}
221
222impl<T: Trace + 'static> Clone for GcWeak<T> {
223 fn clone(&self) -> Self {
224 GcWeak {
225 target: self.target,
226 identity: self.identity,
227 allocation_token: self.allocation_token,
228 heap: self.heap.clone(),
229 }
230 }
231}
232
233impl<T: Trace + 'static> Clone for GcRef<T> {
234 fn clone(&self) -> Self {
235 GcRef(self.0)
236 }
237}
238
239impl<T: Trace + 'static> Copy for GcRef<T> {}
240
241impl<T: Trace + 'static> std::ops::Deref for GcRef<T> {
242 type Target = T;
243 fn deref(&self) -> &T {
244 &*self.0
245 }
246}
247
248impl<T: Trace + 'static> AsRef<T> for GcRef<T> {
249 fn as_ref(&self) -> &T {
250 &*self.0
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use lua_gc::Marker;
258
259 struct NoRoots;
260
261 impl Trace for NoRoots {
262 fn trace(&self, _m: &mut Marker) {}
263 }
264
265 #[derive(Debug)]
266 struct Cell0;
267
268 impl Trace for Cell0 {
269 fn trace(&self, _m: &mut Marker) {}
270 }
271
272 #[test]
273 fn heap_tracked_weak_refs_stop_upgrading_after_sweep() {
274 let heap = lua_gc::Heap::new();
275 heap.unpause();
276 let _guard = lua_gc::HeapGuard::push(&heap);
277
278 let strong = GcRef::new(Cell0);
279 let weak = strong.downgrade();
280 assert!(weak.upgrade().is_some());
281 assert_eq!(weak.strong_count(), 1);
282
283 heap.full_collect(&NoRoots);
284 assert!(weak.upgrade().is_none());
285 assert_eq!(weak.strong_count(), 0);
286 }
287
288 /// The detached fallback is gone (#253): a guard-less allocation is a
289 /// bug on the entry path, and it must fail loudly in every build, not
290 /// leak quietly for the life of the process.
291 #[test]
292 #[should_panic(expected = "no active HeapGuard")]
293 fn guardless_allocation_panics() {
294 let _ = GcRef::new(Cell0);
295 }
296
297 /// A guard-less `downgrade` of a heap-owned box mints a `GcWeak` with no
298 /// heap identity, so it upgrades forever — including after sweep frees the
299 /// target (use-after-free). That now panics unconditionally, not just
300 /// under a retired env flag.
301 #[test]
302 #[should_panic(expected = "no active HeapGuard")]
303 fn guardless_downgrade_of_heap_owned_box_panics() {
304 let heap = lua_gc::Heap::new();
305 let strong = {
306 let _guard = lua_gc::HeapGuard::push(&heap);
307 GcRef::new(Cell0)
308 };
309 let _ = strong.downgrade();
310 }
311
312 /// A guard-less `account_buffer` on a heap-owned box would silently drop
313 /// the pacer charge, drifting the collector from real memory. That now
314 /// panics unconditionally, not just under a retired env flag.
315 #[test]
316 #[should_panic(expected = "no active HeapGuard")]
317 fn guardless_account_buffer_on_heap_owned_box_panics() {
318 let heap = lua_gc::Heap::new();
319 let strong = {
320 let _guard = lua_gc::HeapGuard::push(&heap);
321 GcRef::new(Cell0)
322 };
323 strong.account_buffer(64);
324 }
325
326 /// Codex finding 2 on issue #260: downgrading a stale `GcRef` AFTER the
327 /// heap closed (its box already freed by `drop_all`) must yield a weak
328 /// handle that never upgrades. Before the closed-heap token refusal,
329 /// `downgrade` re-registered the freed box's address in the token map and
330 /// the weak handle upgraded into freed memory.
331 #[test]
332 fn downgrade_after_close_cannot_resurrect_freed_box() {
333 let heap = lua_gc::Heap::new();
334 heap.unpause();
335 let _guard = lua_gc::HeapGuard::push(&heap);
336
337 let stale = GcRef::new(Cell0);
338 heap.drop_all();
339
340 let weak = stale.downgrade();
341 assert!(
342 weak.upgrade().is_none(),
343 "a weak handle minted after close must never upgrade — the box \
344 is freed and the heap is closed"
345 );
346 }
347
348 /// Issue #267 regression: the same close-then-downgrade sequence must stay
349 /// sound under quarantine. A withdrawn revision of #267 added a "tripwire"
350 /// that read the target box header at `downgrade` time whenever the *active*
351 /// heap was quarantined — but the active heap's quarantine flag says nothing
352 /// about whether the target's owner already freed the box, so this deref'd
353 /// freed memory *inside* the safety check (codex found it here). `downgrade`
354 /// must never read the box: on a closed heap the token refusal alone makes
355 /// the weak permanently dead, with no header read.
356 #[test]
357 fn downgrade_after_close_under_quarantine_reads_no_box() {
358 let heap = lua_gc::Heap::new_quarantined();
359 heap.unpause();
360 let _guard = lua_gc::HeapGuard::push(&heap);
361
362 let stale = GcRef::new(Cell0);
363 heap.drop_all();
364
365 let weak = stale.downgrade();
366 assert!(weak.upgrade().is_none());
367 }
368}
369
370impl<T: PartialEq + Trace + 'static> PartialEq for GcRef<T> {
371 fn eq(&self, other: &Self) -> bool {
372 Gc::ptr_eq(self.0, other.0) || **self == **other
373 }
374}