Skip to content
Merged
18 changes: 18 additions & 0 deletions changelog.d/11664-gc-pinned-objects-are-roots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
**GC: a pinned object is a root, marked and traced like any other object.**
Every mark entry used to treat `GC_FLAG_PINNED` as "already marked", so a
pinned object was kept but never traced, and a child reachable only through it
was freed. `js_promise_new_cross_thread` (bcrypt, sharp, container compose,
worker_threads, thread spawn) lost its `then`/`await` reaction closure after
one full collection: a use-after-free when the native side resolved.

- A pin now means only "don't move, don't sweep". The mark entries in
`gc/trace.rs` and `gc/roots.rs`, the copying minor's mark of a malloc or
long-lived object (`gc/copying.rs`), the incremental mark barrier and the
budgeted cycle's block persistence no longer short-circuit on it. The copying
minor one meant a pinned `gc_malloc` parent's young children were neither
forwarded nor kept: a malloc parent is never remembered by the write barrier,
so being traced from its root was its only cover.
- Pinned objects are found as roots through the header bit plus a per-block
`pinned_summary` (arena) and a malloc-registry summary, both set only by the
pin setters in `gc/pin.rs`. Leaf objects need no root.
- The full trace's block persistence no longer counts a pinned header as live.
10 changes: 10 additions & 0 deletions crates/perry-runtime/src/arena/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ fn try_alloc_block(min_size: usize, injectable: bool) -> Option<ArenaBlock> {
object_starts: new_object_start_bitmap(size),
dead_cycles: 0,
old_free_holes: false,
pinned_summary: false,
});
}
let data = unsafe { alloc(layout) };
Expand All @@ -357,6 +358,7 @@ fn try_alloc_block(min_size: usize, injectable: bool) -> Option<ArenaBlock> {
object_starts: new_object_start_bitmap(size),
dead_cycles: 0,
old_free_holes: false,
pinned_summary: false,
})
}

Expand Down Expand Up @@ -435,6 +437,13 @@ pub(crate) struct ArenaBlock {
/// lets every other reset skip the walk over all chains. Taking a hole
/// leaves it set — it may over-approximate, never under-approximate.
pub(crate) old_free_holes: bool,
/// Some header in this block may carry `GC_FLAG_PINNED`. Set by the pin
/// setters (`gc::pin_object` / `pin_object_non_young`, through
/// [`note_pinned_arena_header`]); cleared by the pinned-root walk when a
/// walk of the block finds no pinned header. The header bit is the
/// authority: this only says which blocks the walk must visit, so it may
/// over-approximate and must never under-approximate.
pub(crate) pinned_summary: bool,
}

impl ArenaBlock {
Expand Down Expand Up @@ -644,6 +653,7 @@ impl Arena {
object_starts: Box::new([]),
dead_cycles: 0,
old_free_holes: false,
pinned_summary: false,
}],
current: 0,
generation,
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/arena/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub(crate) use construction::ConstructionBatch;
mod inline;
mod map_allocations;
mod page_meta;
mod pinned;
pub(crate) use pinned::{collect_pinned_arena_headers, note_pinned_arena_header};
/// #7742: whole-block in-place promotion of a (near-)fully-live young
/// generation, in place of object-by-object evacuation.
mod promote;
Expand Down
78 changes: 78 additions & 0 deletions crates/perry-runtime/src/arena/pinned.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Which arena blocks may hold a pinned object.
//!
//! A pinned object is a GC root: something outside the heap the collector can
//! see (a native completion, a cross-thread queue, an AppKit string return)
//! holds its address. `GC_FLAG_PINNED` in the header is the authority on
//! whether an object is pinned. [`ArenaBlock::pinned_summary`] only tells the
//! root scan which blocks to walk for such headers, so a cycle never walks the
//! whole heap to find a handful of pins.
//!
//! The summary is set by the pin setters in `gc/pin.rs` (the only sanctioned
//! writers of the header bit, enforced by `scripts/gc_pin_sites.py`) and
//! cleared by [`collect_pinned_arena_headers`] when it walks a block and finds
//! no pinned header left in it. A pinned object is never swept, so a block
//! holding one is never reset; a set summary on a reset block only costs one
//! wasted walk.

use super::*;
use crate::gc::GcHeader;

/// Record that the arena object whose header is at `header_addr` is pinned.
/// Returns `false` when the address is in none of this thread's arena blocks.
pub(crate) fn note_pinned_arena_header(header_addr: usize) -> bool {
let note = |arena: &mut Arena| -> bool {
for block in arena.blocks.iter_mut() {
let base = block.data as usize;
if header_addr >= base && header_addr < base + block.size {
block.pinned_summary = true;
return true;
}
}
false
};
ARENA.with(|a| note(unsafe { &mut *a.get() }))
|| SURVIVOR_ARENA_0.with(|a| note(unsafe { &mut *a.get() }))
|| SURVIVOR_ARENA_1.with(|a| note(unsafe { &mut *a.get() }))
|| LONGLIVED_ARENA.with(|a| note(unsafe { &mut *a.get() }))
|| OLD_ARENA.with(|a| note(unsafe { &mut *a.get() }))
}

/// Push every header carrying `GC_FLAG_PINNED` in a block whose summary is
/// set onto `out`, and set each walked block's summary to whether it holds one.
///
/// `include_tenured` walks `Longlived` and `Old` blocks as well. A pass that
/// cannot act on a tenured object (a minor: old objects are black leaves and
/// their young children are remembered by the barrier) leaves it `false`.
/// `place_tenured` walks EVERY tenured block, summary or not: a tenured pin
/// made through `gc::pin_object_non_young` is not placed in its block at pin
/// time (see `gc/pin.rs`), so this walk is what places it.
pub(crate) fn collect_pinned_arena_headers(
include_tenured: bool,
place_tenured: bool,
out: &mut Vec<*mut GcHeader>,
) {
sync_inline_arena_state();
let walk = |arena: &mut Arena, every_block: bool, out: &mut Vec<*mut GcHeader>| {
for block in arena.blocks.iter_mut() {
if !every_block && !block.pinned_summary {
continue;
}
let mut found = false;
super::walk::for_each_block_header(block, |header| {
// SAFETY: the walker yields parseable headers of this block.
if unsafe { (*header).gc_flags } & crate::gc::GC_FLAG_PINNED != 0 {
found = true;
out.push(header);
}
});
block.pinned_summary = found;
}
};
ARENA.with(|a| walk(unsafe { &mut *a.get() }, false, out));
SURVIVOR_ARENA_0.with(|a| walk(unsafe { &mut *a.get() }, false, out));
SURVIVOR_ARENA_1.with(|a| walk(unsafe { &mut *a.get() }, false, out));
if include_tenured {
LONGLIVED_ARENA.with(|a| walk(unsafe { &mut *a.get() }, place_tenured, out));
OLD_ARENA.with(|a| walk(unsafe { &mut *a.get() }, place_tenured, out));
}
}
1 change: 1 addition & 0 deletions crates/perry-runtime/src/arena/promote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ fn take_block(block: PromotedBlock) -> Option<ArenaBlock> {
object_starts: Box::new([]),
dead_cycles: 0,
old_free_holes: false,
pinned_summary: false,
},
))
};
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-runtime/src/arena/quarantine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ pub(crate) fn copying_quarantine_from_spaces_and_flip() -> ArenaResetStats {
object_starts: new_object_start_bitmap(block.size),
dead_cycles: 0,
old_free_holes: false,
pinned_summary: false,
});
}
ensure_usable_current_block(arena);
Expand Down Expand Up @@ -1136,6 +1137,7 @@ mod tombstone_tests {
object_starts: Box::new([]),
dead_cycles: 0,
old_free_holes: false,
pinned_summary: false,
}
}

Expand Down Expand Up @@ -1172,6 +1174,7 @@ mod tombstone_tests {
object_starts: new_object_start_bitmap(SIZE),
dead_cycles: 0,
old_free_holes: false,
pinned_summary: false,
}],
current: 0,
generation: HeapGeneration::Nursery,
Expand Down
75 changes: 31 additions & 44 deletions crates/perry-runtime/src/arena/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,8 +749,6 @@ pub fn old_arena_walk_objects(mut callback: impl FnMut(*mut u8)) {
/// for the general arena, `general_block_count()..arena_block_count()`
/// for the longlived arena (issue #179).
pub fn arena_walk_objects_with_block_index(mut callback: impl FnMut(*mut u8, usize)) {
use crate::gc::GcHeader;

sync_inline_arena_state();

let general_n = ARENA.with(|a| unsafe { (*a.get()).blocks.len() });
Expand All @@ -759,26 +757,7 @@ pub fn arena_walk_objects_with_block_index(mut callback: impl FnMut(*mut u8, usi
let mut walk_region = |blocks: &[ArenaBlock], base: usize| {
for (i, block) in blocks.iter().enumerate() {
let block_idx = base + i;
let mut offset = 0usize;
while offset < block.offset {
let aligned = (offset + 7) & !7;
if aligned >= block.offset {
break;
}
let header_ptr = unsafe { block.data.add(aligned) };
let header = header_ptr as *const GcHeader;
unsafe {
let total_size = (*header).size as usize;
if total_size == 0 || total_size > block.size {
break;
}
let obj_type = (*header).obj_type;
if crate::gc::gc_type_is_arena_walkable(obj_type) {
callback(header_ptr, block_idx);
}
offset = aligned + total_size;
}
}
for_each_block_header(block, |header| callback(header.cast(), block_idx));
}
};

Expand Down Expand Up @@ -818,12 +797,39 @@ pub fn arena_walk_objects_with_block_index(mut callback: impl FnMut(*mut u8, usi
/// it already knows have no live objects (issue #64 follow-up).
///
/// Block indices are global (general arena first, longlived after).
/// Every arena-walkable object header in `block`'s bump-allocated prefix, in
/// address order: the linear block iteration the walkers share. A header
/// address comes from `block.data` plus the sizes of the headers before it,
/// never from a value, and the walk stops at the first header whose size is 0
/// or exceeds the block.
pub(crate) fn for_each_block_header(
block: &ArenaBlock,
mut callback: impl FnMut(*mut crate::gc::GcHeader),
) {
let mut offset = 0usize;
while offset < block.offset {
let aligned = (offset + 7) & !7;
if aligned >= block.offset {
break;
}
// SAFETY: `aligned < block.offset`, so this is a parseable header
// inside the block's bump-allocated prefix.
let header = unsafe { block.data.add(aligned) } as *mut crate::gc::GcHeader;
let (total_size, obj_type) = unsafe { ((*header).size as usize, (*header).obj_type) };
if total_size == 0 || total_size > block.size {
break;
}
if crate::gc::gc_type_is_arena_walkable(obj_type) {
callback(header);
}
offset = aligned + total_size;
}
}

pub fn arena_walk_objects_filtered(
mut block_filter: impl FnMut(usize) -> bool,
mut callback: impl FnMut(*mut u8, usize),
) {
use crate::gc::GcHeader;

sync_inline_arena_state();

let general_n = ARENA.with(|a| unsafe { (*a.get()).blocks.len() });
Expand All @@ -838,26 +844,7 @@ pub fn arena_walk_objects_filtered(
if !block_filter(block_idx) {
continue;
}
let mut offset = 0usize;
while offset < block.offset {
let aligned = (offset + 7) & !7;
if aligned >= block.offset {
break;
}
let header_ptr = unsafe { block.data.add(aligned) };
let header = header_ptr as *const GcHeader;
unsafe {
let total_size = (*header).size as usize;
if total_size == 0 || total_size > block.size {
break;
}
let obj_type = (*header).obj_type;
if crate::gc::gc_type_is_arena_walkable(obj_type) {
callback(header_ptr, block_idx);
}
offset = aligned + total_size;
}
}
for_each_block_header(block, |header| callback(header.cast(), block_idx));
}
};

Expand Down
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/gc/barrier/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,9 @@ fn incremental_mark_barrier_value_with_valid_ptrs(
}
unsafe {
let flags = (*header).gc_flags;
if flags & (GC_FLAG_MARKED | GC_FLAG_PINNED | GC_FLAG_FORWARDED) != 0 {
if flags & (GC_FLAG_MARKED | GC_FLAG_FORWARDED) != 0
|| crate::gc::pin::pinned_counts_as_marked(flags)
{
return false;
}
(*header).gc_flags = flags | GC_FLAG_MARKED;
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/gc/copying.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ impl CopyingNurseryCollector {
CopyingPointerKind::Longlived | CopyingPointerKind::Malloc => {
unsafe {
let flags = (*ptr.header).gc_flags;
if flags & (GC_FLAG_MARKED | GC_FLAG_PINNED) == 0 {
if flags & GC_FLAG_MARKED == 0 && !super::pin::pinned_counts_as_marked(flags) {
(*ptr.header).gc_flags = flags | GC_FLAG_MARKED;
self.worklist.push(ptr.header);
self.survival_push();
Expand Down
5 changes: 3 additions & 2 deletions crates/perry-runtime/src/gc/cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ impl BlockPersistCycleState {
}
let header = header_ptr as *mut GcHeader;
unsafe {
if (*header).gc_flags & (GC_FLAG_MARKED | GC_FLAG_PINNED) != 0 {
if (*header).gc_flags & GC_FLAG_MARKED != 0 {
self.block_has_live[block_idx] = true;
}
}
Expand Down Expand Up @@ -272,7 +272,8 @@ impl BlockPersistCycleState {
}
let header = header_ptr as *mut GcHeader;
unsafe {
if (*header).gc_flags & (GC_FLAG_MARKED | GC_FLAG_PINNED) == 0 {
let flags = (*header).gc_flags;
if flags & GC_FLAG_MARKED == 0 && !super::pin::pinned_counts_as_marked(flags) {
(*header).gc_flags |= GC_FLAG_MARKED;
self.worklist.push(header);
self.newly_marked = self.newly_marked.saturating_add(1);
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,10 @@ pub fn gc_init() {
reg_scanner!(crate::intl::segmenter::scan_segment_record_keys_roots_mut);
reg_scanner!(small_int_cache_mutable_root_scanner);
reg_scanner!(concat_memo_mutable_root_scanner);
// A pinned object is a root: its holder is an external reference the
// collector cannot see. Found through the block / malloc-registry pin
// summaries the pin setters maintain (gc/pin.rs, arena/pinned.rs).
reg_scanner!(pin::scan_pinned_object_roots_mut);
reg_scanner!(crate::string::trim_cache::scan_trim_cache_roots_mut);
reg_scanner!(crate::builtins::scan_console_log_singleton_roots_mut);
reg_scanner!(crate::builtins::scan_structured_clone_memo_roots_mut);
Expand Down
Loading
Loading