From 2b56d3c21609e08ee1549a35265b53b3558120db Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Wed, 11 Mar 2026 15:37:24 +0530 Subject: [PATCH 1/7] mark_sweep: run destructors/finalizers on collector drop --- .../mark_sweep/internals/ephemeron.rs | 2 +- .../collectors/mark_sweep/internals/gc_box.rs | 6 +- .../collectors/mark_sweep/internals/mod.rs | 2 +- .../collectors/mark_sweep/internals/vtable.rs | 14 ++ oscars/src/collectors/mark_sweep/mod.rs | 36 +++- oscars/src/collectors/mark_sweep/tests.rs | 180 ++++++++++++++++++ 6 files changed, 227 insertions(+), 13 deletions(-) diff --git a/oscars/src/collectors/mark_sweep/internals/ephemeron.rs b/oscars/src/collectors/mark_sweep/internals/ephemeron.rs index 85b22cd..91f513f 100644 --- a/oscars/src/collectors/mark_sweep/internals/ephemeron.rs +++ b/oscars/src/collectors/mark_sweep/internals/ephemeron.rs @@ -142,7 +142,7 @@ pub(crate) const fn vtable_of() -> &'sta }, finalize_fn: |this| unsafe { let ephemeron = this.cast::>>().as_ref().value(); - Finalize::finalize(ephemeron); + Trace::run_finalizer(ephemeron); }, _key_type_id: TypeId::of::(), _key_size: size_of::>(), diff --git a/oscars/src/collectors/mark_sweep/internals/gc_box.rs b/oscars/src/collectors/mark_sweep/internals/gc_box.rs index edf0d34..42a8ac2 100644 --- a/oscars/src/collectors/mark_sweep/internals/gc_box.rs +++ b/oscars/src/collectors/mark_sweep/internals/gc_box.rs @@ -6,7 +6,7 @@ use crate::collectors::mark_sweep::Finalize; use crate::collectors::mark_sweep::internals::gc_header::{GcHeader, HeaderColor}; use crate::collectors::mark_sweep::{Trace, TraceColor}; -use super::{DropFn, TraceFn, VTable, vtable_of}; +use super::{DropFn, FinalizeFn, TraceFn, VTable, vtable_of}; pub struct NonTraceable(()); @@ -166,6 +166,10 @@ impl GcBox { self.vtable.drop_fn() } + pub(crate) fn finalize_fn(&self) -> FinalizeFn { + self.vtable.finalize_fn() + } + pub(crate) fn size(&self) -> usize { self.vtable.size() } diff --git a/oscars/src/collectors/mark_sweep/internals/mod.rs b/oscars/src/collectors/mark_sweep/internals/mod.rs index 63f896b..e87ffd1 100644 --- a/oscars/src/collectors/mark_sweep/internals/mod.rs +++ b/oscars/src/collectors/mark_sweep/internals/mod.rs @@ -5,6 +5,6 @@ mod vtable; pub(crate) use ephemeron::Ephemeron; pub(crate) use gc_header::{GcHeader, HeaderColor}; -pub(crate) use vtable::{DropFn, TraceFn, VTable, vtable_of}; +pub(crate) use vtable::{DropFn, FinalizeFn, TraceFn, VTable, vtable_of}; pub use self::gc_box::{GcBox, NonTraceable, WeakGcBox}; diff --git a/oscars/src/collectors/mark_sweep/internals/vtable.rs b/oscars/src/collectors/mark_sweep/internals/vtable.rs index b8eb643..93404a1 100644 --- a/oscars/src/collectors/mark_sweep/internals/vtable.rs +++ b/oscars/src/collectors/mark_sweep/internals/vtable.rs @@ -27,12 +27,20 @@ pub(crate) const fn vtable_of() -> &'static VTable { // SAFETY: The caller must ensure the erased pointer is not dropped or deallocated. unsafe { core::ptr::drop_in_place(this.as_mut()) }; } + + // SAFETY: The caller must ensure that the passed erased pointer is `GcBox`. + unsafe fn finalize_fn(this: GcErasedPointer) { + // SAFETY: The caller must ensure that the passed erased pointer is `GcBox`. + let value = unsafe { this.cast::>>().as_ref().value() }; + Trace::run_finalizer(value); + } } impl HasVTable for T { const VTABLE: &'static VTable = &VTable { trace_fn: T::trace_fn, drop_fn: T::drop_fn, + finalize_fn: T::finalize_fn, type_id: TypeId::of::(), size: size_of::>(), }; @@ -43,11 +51,13 @@ pub(crate) const fn vtable_of() -> &'static VTable { pub(crate) type TraceFn = unsafe fn(this: GcErasedPointer, color: TraceColor); pub(crate) type DropFn = unsafe fn(this: GcErasedPointer); +pub(crate) type FinalizeFn = unsafe fn(this: GcErasedPointer); #[derive(Debug)] pub(crate) struct VTable { trace_fn: TraceFn, drop_fn: DropFn, + finalize_fn: FinalizeFn, type_id: TypeId, size: usize, } @@ -61,6 +71,10 @@ impl VTable { self.drop_fn } + pub(crate) fn finalize_fn(&self) -> FinalizeFn { + self.finalize_fn + } + pub(crate) const fn type_id(&self) -> TypeId { self.type_id } diff --git a/oscars/src/collectors/mark_sweep/mod.rs b/oscars/src/collectors/mark_sweep/mod.rs index 9bef614..bd8bb43 100644 --- a/oscars/src/collectors/mark_sweep/mod.rs +++ b/oscars/src/collectors/mark_sweep/mod.rs @@ -124,11 +124,19 @@ impl Drop for MarkSweepGarbageCollector { // SAFETY: // `Gc` pointers act as if they live forever (`'static`). - // if the GC drops while they exist, we leak the memory to prevent a UAF - if self.pools_len() > 0 - && (!self.root_queue.borrow().is_empty() - || !self.pending_root_queue.borrow().is_empty()) - { + // if the GC drops while rooted values still exist, we leak memory to prevent UAF. + let has_rooted_values = self + .root_queue + .borrow() + .iter() + .any(|node| unsafe { node.as_ref().value().is_rooted() }) + || self + .pending_root_queue + .borrow() + .iter() + .any(|node| unsafe { node.as_ref().value().is_rooted() }); + + if self.pools_len() > 0 && has_rooted_values { // Unrooted items are NOT swept here so they intentionally leak // instead of triggering a Use-After-Free. // The underlying arena pools WILL be dropped (and OS memory reclaimed) @@ -179,7 +187,9 @@ impl MarkSweepGarbageCollector { let ephemerons = core::mem::take(&mut *self.ephemeron_queue.borrow_mut()); for ephemeron in ephemerons { let ephemeron_ref = unsafe { ephemeron.as_ref() }; - unsafe { ephemeron_ref.value().drop_fn()(ephemeron) }; + let vtable = ephemeron_ref.value(); + unsafe { vtable.finalize_fn()(ephemeron) }; + unsafe { vtable.drop_fn()(ephemeron) }; self.allocator .borrow_mut() .free_slot(ephemeron.cast::()); @@ -188,14 +198,18 @@ impl MarkSweepGarbageCollector { let roots = core::mem::take(&mut *self.root_queue.borrow_mut()); for node in roots { let node_ref = unsafe { node.as_ref() }; - unsafe { node_ref.value().drop_fn()(node) }; + let gc_box = node_ref.value(); + unsafe { gc_box.finalize_fn()(node) }; + unsafe { gc_box.drop_fn()(node) }; self.allocator.borrow_mut().free_slot(node.cast::()); } let pending_e = core::mem::take(&mut *self.pending_ephemeron_queue.borrow_mut()); for ephemeron in pending_e { let ephemeron_ref = unsafe { ephemeron.as_ref() }; - unsafe { ephemeron_ref.value().drop_fn()(ephemeron) }; + let vtable = ephemeron_ref.value(); + unsafe { vtable.finalize_fn()(ephemeron) }; + unsafe { vtable.drop_fn()(ephemeron) }; self.allocator .borrow_mut() .free_slot(ephemeron.cast::()); @@ -204,7 +218,9 @@ impl MarkSweepGarbageCollector { let pending_r = core::mem::take(&mut *self.pending_root_queue.borrow_mut()); for node in pending_r { let node_ref = unsafe { node.as_ref() }; - unsafe { node_ref.value().drop_fn()(node) }; + let gc_box = node_ref.value(); + unsafe { gc_box.finalize_fn()(node) }; + unsafe { gc_box.drop_fn()(node) }; self.allocator.borrow_mut().free_slot(node.cast::()); } } @@ -294,7 +310,7 @@ impl MarkSweepGarbageCollector { // Check if the value is not reachable, i.e. dead. if !gc_box.is_reachable(color) { // Finalize the dead item - gc_box.finalize(); + unsafe { gc_box.finalize_fn()(*node) }; // Recheck if the value is now rooted again after finalization. if gc_box.is_rooted() { unsafe { gc_box.trace_fn()(*node, color) }; diff --git a/oscars/src/collectors/mark_sweep/tests.rs b/oscars/src/collectors/mark_sweep/tests.rs index 1ee6f04..100feb6 100644 --- a/oscars/src/collectors/mark_sweep/tests.rs +++ b/oscars/src/collectors/mark_sweep/tests.rs @@ -741,3 +741,183 @@ mod thin_vec_trace { collector.collect(); } } + +#[test] +fn collector_drop_runs_destructors_for_live_gc_values() { + use core::cell::Cell; + use rust_alloc::rc::Rc; + + struct DropSpy { + drops: Rc>, + } + + impl Drop for DropSpy { + fn drop(&mut self) { + self.drops.set(self.drops.get() + 1); + } + } + + impl Finalize for DropSpy {} + + // SAFETY: `DropSpy` has no traceable children. + unsafe impl Trace for DropSpy { + crate::empty_trace!(); + } + + let drops = Rc::new(Cell::new(0)); + { + let collector = &mut MarkSweepGarbageCollector::default() + .with_page_size(128) + .with_heap_threshold(256); + + let _gc = Gc::new_in( + DropSpy { + drops: Rc::clone(&drops), + }, + collector, + ); + + assert_eq!(drops.get(), 0); + } + + assert_eq!(drops.get(), 1, "collector drop should run value destructor"); +} + +#[test] +fn collector_drop_runs_ephemeron_value_destructors_for_live_values() { + use core::cell::Cell; + use rust_alloc::rc::Rc; + + struct DropSpy { + drops: Rc>, + } + + impl Drop for DropSpy { + fn drop(&mut self) { + self.drops.set(self.drops.get() + 1); + } + } + + impl Finalize for DropSpy {} + + // SAFETY: `DropSpy` has no traceable children. + unsafe impl Trace for DropSpy { + crate::empty_trace!(); + } + + let drops = Rc::new(Cell::new(0)); + { + let collector = &mut MarkSweepGarbageCollector::default() + .with_page_size(128) + .with_heap_threshold(256); + + let mut map = WeakMap::new(collector); + let key = Gc::new_in(1u64, collector); + + map.insert( + &key, + DropSpy { + drops: Rc::clone(&drops), + }, + collector, + ); + + assert_eq!(drops.get(), 0); + } + + assert_eq!( + drops.get(), + 1, + "collector drop should run ephemeron value destructor" + ); +} + +#[test] +fn collector_drop_runs_finalizers_for_live_gc_values() { + use core::cell::Cell; + use rust_alloc::rc::Rc; + + struct FinalizeSpy { + finalized: Rc>, + } + + impl Finalize for FinalizeSpy { + fn finalize(&self) { + self.finalized.set(self.finalized.get() + 1); + } + } + + // SAFETY: `FinalizeSpy` has no traceable children. + unsafe impl Trace for FinalizeSpy { + crate::empty_trace!(); + } + + let finalized = Rc::new(Cell::new(0)); + { + let collector = &mut MarkSweepGarbageCollector::default() + .with_page_size(128) + .with_heap_threshold(256); + + let _gc = Gc::new_in( + FinalizeSpy { + finalized: Rc::clone(&finalized), + }, + collector, + ); + + assert_eq!(finalized.get(), 0); + } + + assert_eq!( + finalized.get(), + 1, + "collector drop should run value finalizer" + ); +} + +#[test] +fn collector_drop_runs_ephemeron_value_finalizers_for_live_values() { + use core::cell::Cell; + use rust_alloc::rc::Rc; + + struct FinalizeSpy { + finalized: Rc>, + } + + impl Finalize for FinalizeSpy { + fn finalize(&self) { + self.finalized.set(self.finalized.get() + 1); + } + } + + // SAFETY: `FinalizeSpy` has no traceable children. + unsafe impl Trace for FinalizeSpy { + crate::empty_trace!(); + } + + let finalized = Rc::new(Cell::new(0)); + { + let collector = &mut MarkSweepGarbageCollector::default() + .with_page_size(128) + .with_heap_threshold(256); + + let mut map = WeakMap::new(collector); + let key = Gc::new_in(1u64, collector); + + map.insert( + &key, + FinalizeSpy { + finalized: Rc::clone(&finalized), + }, + collector, + ); + + assert_eq!(finalized.get(), 0); + } + + assert_eq!( + finalized.get(), + 1, + "collector drop should run ephemeron value finalizer" + ); +} From fc89c4509d4a23b1b96b288e7f07fd0ea8ac6416 Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Wed, 11 Mar 2026 15:41:14 +0530 Subject: [PATCH 2/7] mark_sweep: avoid dropping revived values at teardown --- oscars/src/collectors/mark_sweep/mod.rs | 48 +++++++++++++++++++++---- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/oscars/src/collectors/mark_sweep/mod.rs b/oscars/src/collectors/mark_sweep/mod.rs index bd8bb43..a180218 100644 --- a/oscars/src/collectors/mark_sweep/mod.rs +++ b/oscars/src/collectors/mark_sweep/mod.rs @@ -185,41 +185,75 @@ impl MarkSweepGarbageCollector { // arena2 uses a bitmap (`mark_dropped`) and reclaims automatically fn sweep_all_queues(&self) { let ephemerons = core::mem::take(&mut *self.ephemeron_queue.borrow_mut()); - for ephemeron in ephemerons { + let roots = core::mem::take(&mut *self.root_queue.borrow_mut()); + let pending_e = core::mem::take(&mut *self.pending_ephemeron_queue.borrow_mut()); + let pending_r = core::mem::take(&mut *self.pending_root_queue.borrow_mut()); + + // Phase 1: finalize everything without dropping/freeing. + for ephemeron in ephemerons.iter().copied() { + let ephemeron_ref = unsafe { ephemeron.as_ref() }; + let vtable = ephemeron_ref.value(); + unsafe { vtable.finalize_fn()(ephemeron) }; + } + + for node in roots.iter().copied() { + let node_ref = unsafe { node.as_ref() }; + let gc_box = node_ref.value(); + unsafe { gc_box.finalize_fn()(node) }; + } + + for ephemeron in pending_e.iter().copied() { let ephemeron_ref = unsafe { ephemeron.as_ref() }; let vtable = ephemeron_ref.value(); unsafe { vtable.finalize_fn()(ephemeron) }; + } + + for node in pending_r.iter().copied() { + let node_ref = unsafe { node.as_ref() }; + let gc_box = node_ref.value(); + unsafe { gc_box.finalize_fn()(node) }; + } + + // Phase 2: if finalization revived any roots, do not drop/free now. + // Dropping immediately after revival would be unsound. + let revived_rooted = roots + .iter() + .chain(pending_r.iter()) + .any(|node| unsafe { node.as_ref().value().is_rooted() }); + + if revived_rooted { + return; + } + + // Phase 3: no revival happened, so it's safe to drop and reclaim slots. + for ephemeron in ephemerons { + let ephemeron_ref = unsafe { ephemeron.as_ref() }; + let vtable = ephemeron_ref.value(); unsafe { vtable.drop_fn()(ephemeron) }; self.allocator .borrow_mut() .free_slot(ephemeron.cast::()); } - let roots = core::mem::take(&mut *self.root_queue.borrow_mut()); for node in roots { let node_ref = unsafe { node.as_ref() }; let gc_box = node_ref.value(); - unsafe { gc_box.finalize_fn()(node) }; unsafe { gc_box.drop_fn()(node) }; self.allocator.borrow_mut().free_slot(node.cast::()); } - let pending_e = core::mem::take(&mut *self.pending_ephemeron_queue.borrow_mut()); for ephemeron in pending_e { let ephemeron_ref = unsafe { ephemeron.as_ref() }; let vtable = ephemeron_ref.value(); - unsafe { vtable.finalize_fn()(ephemeron) }; unsafe { vtable.drop_fn()(ephemeron) }; self.allocator .borrow_mut() .free_slot(ephemeron.cast::()); } - let pending_r = core::mem::take(&mut *self.pending_root_queue.borrow_mut()); for node in pending_r { let node_ref = unsafe { node.as_ref() }; let gc_box = node_ref.value(); - unsafe { gc_box.finalize_fn()(node) }; unsafe { gc_box.drop_fn()(node) }; self.allocator.borrow_mut().free_slot(node.cast::()); } From 332485e5985aa7754f00f99110b44a86d95907a8 Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Wed, 11 Mar 2026 19:38:29 +0530 Subject: [PATCH 3/7] mark_sweep: defer weak map reclaim until safe teardown --- oscars/src/collectors/mark_sweep/mod.rs | 42 +++++++++++++++++-------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/oscars/src/collectors/mark_sweep/mod.rs b/oscars/src/collectors/mark_sweep/mod.rs index a180218..bfc5ea0 100644 --- a/oscars/src/collectors/mark_sweep/mod.rs +++ b/oscars/src/collectors/mark_sweep/mod.rs @@ -114,14 +114,6 @@ impl Drop for MarkSweepGarbageCollector { } } - // Reclaim all collector-owned weak maps. - // Single-threaded, so this is safe. - for &map_ptr in self.weak_maps.borrow().iter() { - unsafe { - let _ = rust_alloc::boxed::Box::from_raw(map_ptr.as_ptr()); - } - } - // SAFETY: // `Gc` pointers act as if they live forever (`'static`). // if the GC drops while rooted values still exist, we leak memory to prevent UAF. @@ -144,7 +136,9 @@ impl Drop for MarkSweepGarbageCollector { } else { // No rooted items are alive. Sweep and clean up the remaining // cycles and loose allocations before the allocator natively drops. - self.sweep_all_queues(); + if self.sweep_all_queues() { + self.reclaim_dead_weak_maps(); + } } } } @@ -177,13 +171,16 @@ impl MarkSweepGarbageCollector { self.allocator.borrow_mut().drop_empty_pools(); } - // Force drops all elements in the internal tracking queues and clears - // them without regard for reachability. + // Force-finalizes all tracked items and, when safe, drops/frees all queue + // allocations without regard for reachability. // // NOTE: This intentionally differs from arena2's sweep_all_queues. // arena3 uses`free_slot` calls to reclaim memory. // arena2 uses a bitmap (`mark_dropped`) and reclaims automatically - fn sweep_all_queues(&self) { + // + // Returns `false` if finalization revived roots, because dropping/freeing + // immediately after revival is unsound. + fn sweep_all_queues(&self) -> bool { let ephemerons = core::mem::take(&mut *self.ephemeron_queue.borrow_mut()); let roots = core::mem::take(&mut *self.root_queue.borrow_mut()); let pending_e = core::mem::take(&mut *self.pending_ephemeron_queue.borrow_mut()); @@ -222,7 +219,7 @@ impl MarkSweepGarbageCollector { .any(|node| unsafe { node.as_ref().value().is_rooted() }); if revived_rooted { - return; + return false; } // Phase 3: no revival happened, so it's safe to drop and reclaim slots. @@ -257,6 +254,25 @@ impl MarkSweepGarbageCollector { unsafe { gc_box.drop_fn()(node) }; self.allocator.borrow_mut().free_slot(node.cast::()); } + + true + } + + fn reclaim_dead_weak_maps(&self) { + // During collector teardown, reclaim only maps that have already been + // marked dead by `WeakMap::drop` to avoid freeing inners that are still + // observable through live/revived wrappers. + self.weak_maps.borrow_mut().retain(|&map_ptr| { + let map = unsafe { map_ptr.as_ref() }; + if map.is_alive() { + true + } else { + unsafe { + let _ = rust_alloc::boxed::Box::from_raw(map_ptr.as_ptr()); + } + false + } + }); } // Extracts and sweeps items that are considered dead (different trace color). From 2e241232249430cfba6db88b8b4d8db6100db8d0 Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Thu, 12 Mar 2026 00:33:39 +0530 Subject: [PATCH 4/7] mark_sweep: use mark-finalize-mark-drop at teardown --- oscars/src/collectors/mark_sweep/mod.rs | 146 ++++++++++++++-------- oscars/src/collectors/mark_sweep/tests.rs | 78 ++++++++++++ 2 files changed, 169 insertions(+), 55 deletions(-) diff --git a/oscars/src/collectors/mark_sweep/mod.rs b/oscars/src/collectors/mark_sweep/mod.rs index bfc5ea0..e5c1d8e 100644 --- a/oscars/src/collectors/mark_sweep/mod.rs +++ b/oscars/src/collectors/mark_sweep/mod.rs @@ -171,88 +171,124 @@ impl MarkSweepGarbageCollector { self.allocator.borrow_mut().drop_empty_pools(); } - // Force-finalizes all tracked items and, when safe, drops/frees all queue - // allocations without regard for reachability. + // Force-collect all tracked items in collector teardown. + // + // Phases: + // 1. mark + // 2. finalize unreachable + // 3. mark again (finalizers can resurrect) + // 4. drop + free still-unreachable // // NOTE: This intentionally differs from arena2's sweep_all_queues. // arena3 uses`free_slot` calls to reclaim memory. // arena2 uses a bitmap (`mark_dropped`) and reclaims automatically - // - // Returns `false` if finalization revived roots, because dropping/freeing - // immediately after revival is unsound. fn sweep_all_queues(&self) -> bool { let ephemerons = core::mem::take(&mut *self.ephemeron_queue.borrow_mut()); let roots = core::mem::take(&mut *self.root_queue.borrow_mut()); let pending_e = core::mem::take(&mut *self.pending_ephemeron_queue.borrow_mut()); let pending_r = core::mem::take(&mut *self.pending_root_queue.borrow_mut()); + let color = self.trace_color.get(); - // Phase 1: finalize everything without dropping/freeing. - for ephemeron in ephemerons.iter().copied() { - let ephemeron_ref = unsafe { ephemeron.as_ref() }; - let vtable = ephemeron_ref.value(); - unsafe { vtable.finalize_fn()(ephemeron) }; - } + let mark_pass = || { + // Mark rooted roots first. + for node in roots.iter().chain(pending_r.iter()).copied() { + let node_ref = unsafe { node.as_ref() }; + let gc_box = node_ref.value(); + if gc_box.is_rooted() { + unsafe { gc_box.trace_fn()(node, color) }; + } + } - for node in roots.iter().copied() { + // Then mark ephemeron values with live keys. + for ephemeron in ephemerons.iter().chain(pending_e.iter()).copied() { + let ephemeron_ref = unsafe { ephemeron.as_ref() }; + let vtable = ephemeron_ref.value(); + if unsafe { vtable.is_reachable_fn()(ephemeron, color) } { + unsafe { vtable.trace_fn()(ephemeron, color) }; + } + } + }; + + // Phase 1: mark. + mark_pass(); + + // Phase 2: finalize unreachable only. + for node in roots.iter().chain(pending_r.iter()).copied() { let node_ref = unsafe { node.as_ref() }; let gc_box = node_ref.value(); - unsafe { gc_box.finalize_fn()(node) }; + if !gc_box.is_reachable(color) { + unsafe { gc_box.finalize_fn()(node) }; + } } - for ephemeron in pending_e.iter().copied() { + for ephemeron in ephemerons.iter().chain(pending_e.iter()).copied() { let ephemeron_ref = unsafe { ephemeron.as_ref() }; let vtable = ephemeron_ref.value(); - unsafe { vtable.finalize_fn()(ephemeron) }; - } - - for node in pending_r.iter().copied() { - let node_ref = unsafe { node.as_ref() }; - let gc_box = node_ref.value(); - unsafe { gc_box.finalize_fn()(node) }; + if !unsafe { vtable.is_reachable_fn()(ephemeron, color) } { + unsafe { vtable.finalize_fn()(ephemeron) }; + } } - // Phase 2: if finalization revived any roots, do not drop/free now. - // Dropping immediately after revival would be unsound. - let revived_rooted = roots - .iter() - .chain(pending_r.iter()) - .any(|node| unsafe { node.as_ref().value().is_rooted() }); + // Phase 3: mark again after finalization. + mark_pass(); - if revived_rooted { - return false; + // Phase 4: drop and free only values that are still unreachable. + for node in roots { + let (is_reachable, drop_fn) = { + let node_ref = unsafe { node.as_ref() }; + let gc_box = node_ref.value(); + (gc_box.is_reachable(color), gc_box.drop_fn()) + }; + if !is_reachable { + unsafe { drop_fn(node) }; + self.allocator.borrow_mut().free_slot(node.cast::()); + } } - // Phase 3: no revival happened, so it's safe to drop and reclaim slots. - for ephemeron in ephemerons { - let ephemeron_ref = unsafe { ephemeron.as_ref() }; - let vtable = ephemeron_ref.value(); - unsafe { vtable.drop_fn()(ephemeron) }; - self.allocator - .borrow_mut() - .free_slot(ephemeron.cast::()); + for node in pending_r { + let (is_reachable, drop_fn) = { + let node_ref = unsafe { node.as_ref() }; + let gc_box = node_ref.value(); + (gc_box.is_reachable(color), gc_box.drop_fn()) + }; + if !is_reachable { + unsafe { drop_fn(node) }; + self.allocator.borrow_mut().free_slot(node.cast::()); + } } - for node in roots { - let node_ref = unsafe { node.as_ref() }; - let gc_box = node_ref.value(); - unsafe { gc_box.drop_fn()(node) }; - self.allocator.borrow_mut().free_slot(node.cast::()); + for ephemeron in ephemerons { + let (is_reachable, drop_fn) = { + let ephemeron_ref = unsafe { ephemeron.as_ref() }; + let vtable = ephemeron_ref.value(); + ( + unsafe { vtable.is_reachable_fn()(ephemeron, color) }, + vtable.drop_fn(), + ) + }; + if !is_reachable { + unsafe { drop_fn(ephemeron) }; + self.allocator + .borrow_mut() + .free_slot(ephemeron.cast::()); + } } for ephemeron in pending_e { - let ephemeron_ref = unsafe { ephemeron.as_ref() }; - let vtable = ephemeron_ref.value(); - unsafe { vtable.drop_fn()(ephemeron) }; - self.allocator - .borrow_mut() - .free_slot(ephemeron.cast::()); - } - - for node in pending_r { - let node_ref = unsafe { node.as_ref() }; - let gc_box = node_ref.value(); - unsafe { gc_box.drop_fn()(node) }; - self.allocator.borrow_mut().free_slot(node.cast::()); + let (is_reachable, drop_fn) = { + let ephemeron_ref = unsafe { ephemeron.as_ref() }; + let vtable = ephemeron_ref.value(); + ( + unsafe { vtable.is_reachable_fn()(ephemeron, color) }, + vtable.drop_fn(), + ) + }; + if !is_reachable { + unsafe { drop_fn(ephemeron) }; + self.allocator + .borrow_mut() + .free_slot(ephemeron.cast::()); + } } true diff --git a/oscars/src/collectors/mark_sweep/tests.rs b/oscars/src/collectors/mark_sweep/tests.rs index 100feb6..fd03925 100644 --- a/oscars/src/collectors/mark_sweep/tests.rs +++ b/oscars/src/collectors/mark_sweep/tests.rs @@ -921,3 +921,81 @@ fn collector_drop_runs_ephemeron_value_finalizers_for_live_values() { "collector drop should run ephemeron value finalizer" ); } + +#[test] +fn collector_drop_does_not_drop_values_revived_by_finalizer() { + use crate::collectors::mark_sweep::internals::{GcBox, NonTraceable}; + use core::ptr::NonNull; + use core::sync::atomic::{AtomicU32, Ordering}; + + static TARGET_DROPS: AtomicU32 = AtomicU32::new(0); + static REVIVER_DROPS: AtomicU32 = AtomicU32::new(0); + + TARGET_DROPS.store(0, Ordering::SeqCst); + REVIVER_DROPS.store(0, Ordering::SeqCst); + + struct DropSpy; + + impl Drop for DropSpy { + fn drop(&mut self) { + TARGET_DROPS.fetch_add(1, Ordering::SeqCst); + } + } + + impl Finalize for DropSpy {} + + // SAFETY: `DropSpy` has no traceable children. + unsafe impl Trace for DropSpy { + crate::empty_trace!(); + } + + struct Reviver { + target_ptr: NonNull>, + } + + impl Drop for Reviver { + fn drop(&mut self) { + REVIVER_DROPS.fetch_add(1, Ordering::SeqCst); + } + } + + impl Finalize for Reviver { + fn finalize(&self) { + // Resurrect `target` by incrementing its root count during finalization. + unsafe { self.target_ptr.as_ref().inc_roots() }; + } + } + + // SAFETY: `Reviver` contains no traceable fields. + unsafe impl Trace for Reviver { + crate::empty_trace!(); + } + { + let collector = &mut MarkSweepGarbageCollector::default() + .with_page_size(128) + .with_heap_threshold(256); + + let target = Gc::new_in(DropSpy, collector); + + let reviver = Gc::new_in( + Reviver { + target_ptr: target.as_sized_inner_ptr(), + }, + collector, + ); + + drop(target); + drop(reviver); + } + + assert_eq!( + TARGET_DROPS.load(Ordering::SeqCst), + 0, + "collector drop must not drop values revived during finalization" + ); + assert_eq!( + REVIVER_DROPS.load(Ordering::SeqCst), + 1, + "collector drop should still drop unreachable values after second mark" + ); +} From 5ca8967ed49d2502f9c8681a26ec5923423dfccf Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Thu, 12 Mar 2026 20:15:45 +0530 Subject: [PATCH 5/7] mark_sweep: simplify collector-drop teardown phases --- oscars/src/collectors/mark_sweep/mod.rs | 126 ++++++---------------- oscars/src/collectors/mark_sweep/tests.rs | 78 -------------- 2 files changed, 33 insertions(+), 171 deletions(-) diff --git a/oscars/src/collectors/mark_sweep/mod.rs b/oscars/src/collectors/mark_sweep/mod.rs index e5c1d8e..cd1f2a1 100644 --- a/oscars/src/collectors/mark_sweep/mod.rs +++ b/oscars/src/collectors/mark_sweep/mod.rs @@ -136,9 +136,8 @@ impl Drop for MarkSweepGarbageCollector { } else { // No rooted items are alive. Sweep and clean up the remaining // cycles and loose allocations before the allocator natively drops. - if self.sweep_all_queues() { - self.reclaim_dead_weak_maps(); - } + self.sweep_all_queues(); + self.reclaim_dead_weak_maps(); } } } @@ -174,130 +173,71 @@ impl MarkSweepGarbageCollector { // Force-collect all tracked items in collector teardown. // // Phases: - // 1. mark - // 2. finalize unreachable - // 3. mark again (finalizers can resurrect) - // 4. drop + free still-unreachable + // 1. finalize everything + // 2. drop + free everything + // + // Since this runs only during collector drop (not a normal collection + // cycle), we don't need reachability marking here. // // NOTE: This intentionally differs from arena2's sweep_all_queues. // arena3 uses`free_slot` calls to reclaim memory. // arena2 uses a bitmap (`mark_dropped`) and reclaims automatically - fn sweep_all_queues(&self) -> bool { + fn sweep_all_queues(&self) { let ephemerons = core::mem::take(&mut *self.ephemeron_queue.borrow_mut()); let roots = core::mem::take(&mut *self.root_queue.borrow_mut()); let pending_e = core::mem::take(&mut *self.pending_ephemeron_queue.borrow_mut()); let pending_r = core::mem::take(&mut *self.pending_root_queue.borrow_mut()); - let color = self.trace_color.get(); - - let mark_pass = || { - // Mark rooted roots first. - for node in roots.iter().chain(pending_r.iter()).copied() { - let node_ref = unsafe { node.as_ref() }; - let gc_box = node_ref.value(); - if gc_box.is_rooted() { - unsafe { gc_box.trace_fn()(node, color) }; - } - } - - // Then mark ephemeron values with live keys. - for ephemeron in ephemerons.iter().chain(pending_e.iter()).copied() { - let ephemeron_ref = unsafe { ephemeron.as_ref() }; - let vtable = ephemeron_ref.value(); - if unsafe { vtable.is_reachable_fn()(ephemeron, color) } { - unsafe { vtable.trace_fn()(ephemeron, color) }; - } - } - }; - - // Phase 1: mark. - mark_pass(); - // Phase 2: finalize unreachable only. + // Phase 1: finalize everything while all allocations are still alive. for node in roots.iter().chain(pending_r.iter()).copied() { let node_ref = unsafe { node.as_ref() }; let gc_box = node_ref.value(); - if !gc_box.is_reachable(color) { - unsafe { gc_box.finalize_fn()(node) }; - } + unsafe { gc_box.finalize_fn()(node) }; } for ephemeron in ephemerons.iter().chain(pending_e.iter()).copied() { let ephemeron_ref = unsafe { ephemeron.as_ref() }; let vtable = ephemeron_ref.value(); - if !unsafe { vtable.is_reachable_fn()(ephemeron, color) } { - unsafe { vtable.finalize_fn()(ephemeron) }; - } + unsafe { vtable.finalize_fn()(ephemeron) }; } - // Phase 3: mark again after finalization. - mark_pass(); - - // Phase 4: drop and free only values that are still unreachable. + // Phase 2: drop and free all tracked values. for node in roots { - let (is_reachable, drop_fn) = { - let node_ref = unsafe { node.as_ref() }; - let gc_box = node_ref.value(); - (gc_box.is_reachable(color), gc_box.drop_fn()) - }; - if !is_reachable { - unsafe { drop_fn(node) }; - self.allocator.borrow_mut().free_slot(node.cast::()); - } + let node_ref = unsafe { node.as_ref() }; + let gc_box = node_ref.value(); + unsafe { gc_box.drop_fn()(node) }; + self.allocator.borrow_mut().free_slot(node.cast::()); } for node in pending_r { - let (is_reachable, drop_fn) = { - let node_ref = unsafe { node.as_ref() }; - let gc_box = node_ref.value(); - (gc_box.is_reachable(color), gc_box.drop_fn()) - }; - if !is_reachable { - unsafe { drop_fn(node) }; - self.allocator.borrow_mut().free_slot(node.cast::()); - } + let node_ref = unsafe { node.as_ref() }; + let gc_box = node_ref.value(); + unsafe { gc_box.drop_fn()(node) }; + self.allocator.borrow_mut().free_slot(node.cast::()); } for ephemeron in ephemerons { - let (is_reachable, drop_fn) = { - let ephemeron_ref = unsafe { ephemeron.as_ref() }; - let vtable = ephemeron_ref.value(); - ( - unsafe { vtable.is_reachable_fn()(ephemeron, color) }, - vtable.drop_fn(), - ) - }; - if !is_reachable { - unsafe { drop_fn(ephemeron) }; - self.allocator - .borrow_mut() - .free_slot(ephemeron.cast::()); - } + let ephemeron_ref = unsafe { ephemeron.as_ref() }; + let vtable = ephemeron_ref.value(); + unsafe { vtable.drop_fn()(ephemeron) }; + self.allocator + .borrow_mut() + .free_slot(ephemeron.cast::()); } for ephemeron in pending_e { - let (is_reachable, drop_fn) = { - let ephemeron_ref = unsafe { ephemeron.as_ref() }; - let vtable = ephemeron_ref.value(); - ( - unsafe { vtable.is_reachable_fn()(ephemeron, color) }, - vtable.drop_fn(), - ) - }; - if !is_reachable { - unsafe { drop_fn(ephemeron) }; - self.allocator - .borrow_mut() - .free_slot(ephemeron.cast::()); - } + let ephemeron_ref = unsafe { ephemeron.as_ref() }; + let vtable = ephemeron_ref.value(); + unsafe { vtable.drop_fn()(ephemeron) }; + self.allocator + .borrow_mut() + .free_slot(ephemeron.cast::()); } - - true } fn reclaim_dead_weak_maps(&self) { // During collector teardown, reclaim only maps that have already been - // marked dead by `WeakMap::drop` to avoid freeing inners that are still - // observable through live/revived wrappers. + // marked dead by `WeakMap::drop`. self.weak_maps.borrow_mut().retain(|&map_ptr| { let map = unsafe { map_ptr.as_ref() }; if map.is_alive() { diff --git a/oscars/src/collectors/mark_sweep/tests.rs b/oscars/src/collectors/mark_sweep/tests.rs index fd03925..100feb6 100644 --- a/oscars/src/collectors/mark_sweep/tests.rs +++ b/oscars/src/collectors/mark_sweep/tests.rs @@ -921,81 +921,3 @@ fn collector_drop_runs_ephemeron_value_finalizers_for_live_values() { "collector drop should run ephemeron value finalizer" ); } - -#[test] -fn collector_drop_does_not_drop_values_revived_by_finalizer() { - use crate::collectors::mark_sweep::internals::{GcBox, NonTraceable}; - use core::ptr::NonNull; - use core::sync::atomic::{AtomicU32, Ordering}; - - static TARGET_DROPS: AtomicU32 = AtomicU32::new(0); - static REVIVER_DROPS: AtomicU32 = AtomicU32::new(0); - - TARGET_DROPS.store(0, Ordering::SeqCst); - REVIVER_DROPS.store(0, Ordering::SeqCst); - - struct DropSpy; - - impl Drop for DropSpy { - fn drop(&mut self) { - TARGET_DROPS.fetch_add(1, Ordering::SeqCst); - } - } - - impl Finalize for DropSpy {} - - // SAFETY: `DropSpy` has no traceable children. - unsafe impl Trace for DropSpy { - crate::empty_trace!(); - } - - struct Reviver { - target_ptr: NonNull>, - } - - impl Drop for Reviver { - fn drop(&mut self) { - REVIVER_DROPS.fetch_add(1, Ordering::SeqCst); - } - } - - impl Finalize for Reviver { - fn finalize(&self) { - // Resurrect `target` by incrementing its root count during finalization. - unsafe { self.target_ptr.as_ref().inc_roots() }; - } - } - - // SAFETY: `Reviver` contains no traceable fields. - unsafe impl Trace for Reviver { - crate::empty_trace!(); - } - { - let collector = &mut MarkSweepGarbageCollector::default() - .with_page_size(128) - .with_heap_threshold(256); - - let target = Gc::new_in(DropSpy, collector); - - let reviver = Gc::new_in( - Reviver { - target_ptr: target.as_sized_inner_ptr(), - }, - collector, - ); - - drop(target); - drop(reviver); - } - - assert_eq!( - TARGET_DROPS.load(Ordering::SeqCst), - 0, - "collector drop must not drop values revived during finalization" - ); - assert_eq!( - REVIVER_DROPS.load(Ordering::SeqCst), - 1, - "collector drop should still drop unreachable values after second mark" - ); -} From 2452a85f2cd0e8cab9afc89b76894c8459d5c204 Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Thu, 12 Mar 2026 20:54:29 +0530 Subject: [PATCH 6/7] tests: make gc_alloc_vec_survives_collection miri-stable --- oscars/src/collectors/mark_sweep/gc_collections.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/oscars/src/collectors/mark_sweep/gc_collections.rs b/oscars/src/collectors/mark_sweep/gc_collections.rs index dbdac01..15ae612 100644 --- a/oscars/src/collectors/mark_sweep/gc_collections.rs +++ b/oscars/src/collectors/mark_sweep/gc_collections.rs @@ -221,12 +221,12 @@ mod tests { #[test] fn gc_alloc_vec_survives_collection() { - let collector = &mut MarkSweepGarbageCollector::default() + let collector = MarkSweepGarbageCollector::default() .with_page_size(256) .with_heap_threshold(512); - let vec = GcAllocVec::with_capacity(100, collector); - let gc_vec = Gc::new_in(GcRefCell::new(vec), collector); + let vec = GcAllocVec::with_capacity(100, &collector); + let gc_vec = Gc::new_in(GcRefCell::new(vec), &collector); for i in 0..100u64 { gc_vec.borrow_mut().push(i); @@ -236,6 +236,11 @@ mod tests { assert_eq!(gc_vec.borrow().len(), 100); assert_eq!(gc_vec.borrow()[50], 50); + + // Drop the handle and run a normal collection cycle so cleanup happens + // through regular sweep logic instead of collector-drop teardown. + drop(gc_vec); + collector.collect(); } #[test] From 763465e544001aef0e11ecbdddb5b920e1f3949c Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Fri, 13 Mar 2026 07:15:48 +0530 Subject: [PATCH 7/7] mark_sweep: clarify weak_map reclaim safety comments --- oscars/src/collectors/mark_sweep/mod.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/oscars/src/collectors/mark_sweep/mod.rs b/oscars/src/collectors/mark_sweep/mod.rs index cd1f2a1..37c0584 100644 --- a/oscars/src/collectors/mark_sweep/mod.rs +++ b/oscars/src/collectors/mark_sweep/mod.rs @@ -239,15 +239,18 @@ impl MarkSweepGarbageCollector { // During collector teardown, reclaim only maps that have already been // marked dead by `WeakMap::drop`. self.weak_maps.borrow_mut().retain(|&map_ptr| { + // SAFETY: the pointer is valid as long as it's in this list. let map = unsafe { map_ptr.as_ref() }; - if map.is_alive() { - true - } else { + let is_map_alive = map.is_alive(); + if !is_map_alive { + // SAFETY: `map_ptr` came from `rust_alloc::boxed::Box::into_raw` when + // the weak map was registered, so it must be reclaimed with + // `rust_alloc::boxed::Box::from_raw` (not allocator-api2 `Box`). unsafe { let _ = rust_alloc::boxed::Box::from_raw(map_ptr.as_ptr()); } - false } + is_map_alive }); } @@ -258,17 +261,21 @@ impl MarkSweepGarbageCollector { self.weak_maps.borrow_mut().retain(|&map_ptr| { // SAFETY: the pointer is valid as long as it's in this list. let map = unsafe { map_ptr.as_ref() }; - if map.is_alive() { + let is_map_alive = map.is_alive(); + if is_map_alive { // We need mut access to prune. unsafe { (&mut *map_ptr.as_ptr()).prune_dead_entries(sweep_color) }; - true } else { // WeakMap was dropped, reclaim the inner allocation. + // + // SAFETY: `map_ptr` came from `rust_alloc::boxed::Box::into_raw` when + // the weak map was registered, so it must be reclaimed with + // `rust_alloc::boxed::Box::from_raw` (not allocator-api2 `Box`). unsafe { let _ = rust_alloc::boxed::Box::from_raw(map_ptr.as_ptr()); } - false } + is_map_alive }); self.run_sweep_phase();