Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8055-agent-local-realm-roots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Keep iterator, generator, `%TypedArray%`, native-module, and Web Storage GC roots local to the Perry agent whose arena owns them.
140 changes: 120 additions & 20 deletions crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! #7251: the two lazy intrinsic-tower builders need #7217's NO-MOVE WINDOW,
//! and #7217 itself could not gate them.
//! #7251/#8002: lazy intrinsic-tower builders need #7217's NO-MOVE WINDOW,
//! and every realm must own the tower roots that point into its arena.
//!
//! `object::iterator_prototypes::build_iterator_prototypes`,
//! `object::global_this::generator::build_generator_tower` and
//! `object::global_this::typed_array::ensure_typed_array_intrinsic` have the
//! identical shape #7217 fixed for `populate_global_this_builtins`: each
//! builds an IMMORTAL object graph (hanging off a process-global intrinsic
//! slot for the life of the thread) by threading raw `*mut ObjectHeader` /
//! builds an IMMORTAL object graph (hanging off an agent-local intrinsic root
//! for the life of the thread) by threading raw `*mut ObjectHeader` /
//! `*mut ClosureHeader` locals across a dozen-plus allocating installs. A
//! relocating — or even a non-relocating, freeing — collection reached from
//! one of THEIR OWN allocations leaves the rest of the build writing through
Expand Down Expand Up @@ -42,24 +43,20 @@
//! allocation.
//!
//! 2. **Re-exec the test binary so the child is the first to touch a tower.**
//! The six intrinsic-tower statics were plain process-global `AtomicI64`s,
//! The intrinsic-tower statics were plain process-global `AtomicI64`s,
//! built exactly once per *process* — so whichever test happened to run
//! first on that binary (libtest's ordering is not something a single test
//! controls) built them for every other test, including a freshly
//! re-exec'd child, because OTHER tests in the SAME binary run before this
//! one and touch `globalThis`. FIXED HERE by converting the six statics
//! one and touch `globalThis`. FIXED HERE by converting the statics
//! (`TYPED_ARRAY_INTRINSIC_PTR`, `TYPED_ARRAY_INTRINSIC_PROTO_PTR`,
//! `GENERATOR_FUNCTION_INTRINSIC_PTR`, `GENERATOR_INTRINSIC_PROTO_PTR`,
//! `GENERATOR_PROTOTYPE_PTR`, `ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR`,
//! `ASYNC_GENERATOR_INTRINSIC_PROTO_PTR`, `ASYNC_GENERATOR_PROTOTYPE_PTR`)
//! in `object/mod.rs` from a bare `static` to `per_test_global!` — the
//! exact mechanism #7672 built for this class of hazard (see
//! `per_test_global.rs`'s module docs). In a test build each libtest
//! THREAD gets its own zeroed instance, and libtest runs one thread per
//! test, so "per thread" and "per test" coincide: this test's first read
//! of `GENERATOR_FUNCTION_INTRINSIC_PTR` is guaranteed `0` regardless of
//! what any other test in the binary did. Non-test builds expand to the
//! identical plain `static` — zero behavioural change in shipped binaries.
//! in `object/mod.rs` from bare process statics to `perry_thread_local!`
//! backing slots. Each libtest thread therefore gets a guaranteed-first
//! touch, and shipped `perry/thread` agents no longer reuse another realm's
//! raw arena pointers (#8002).
//!
//! 3. **Record `gc_is_suppressed()` from inside the builder under
//! `#[cfg(test)]`.** Reported *suppressed* even with the scope removed,
Expand Down Expand Up @@ -116,12 +113,9 @@ use super::super::*;
use super::support::*;
use std::sync::atomic::Ordering as StdOrdering;

/// Run `body` on a thread that has touched neither `globalThis` nor either
/// tower, mirroring `global_bootstrap.rs::on_a_fresh_thread`. Belt-and-braces
/// alongside the `per_test_global!` conversion above: libtest already gives
/// each `#[test]` its own thread (and therefore its own instance of every
/// `per_test_global!` table), but spawning explicitly keeps this test's
/// guarantee independent of that harness detail.
/// Run `body` on a thread that has touched neither `globalThis` nor any lazy
/// tower, mirroring `global_bootstrap.rs::on_a_fresh_thread`. The explicit
/// spawn keeps this test's guarantee independent of libtest's thread reuse.
fn on_a_fresh_thread(body: impl FnOnce() + Send + 'static) {
std::thread::Builder::new()
.stack_size(16 << 20)
Expand Down Expand Up @@ -176,6 +170,112 @@ fn armed_collection_is_serviced_by_the_next_allocation(collections_before: u64)
gc_collection_count() > collections_before
}

#[test]
fn iterator_prototype_tower_runs_in_a_no_move_window() {
on_a_fresh_thread(|| {
let _pacing = crate::gc::policy::force_legacy_gc_pacing();
crate::gc::ensure_gc_initialized();
assert_eq!(
crate::object::iterator_prototypes::ITERATOR_PROTOTYPE_PTR.load(StdOrdering::Acquire),
0,
"the iterator tower was already built before this thread's first touch"
);

arm_collection_reachable_by_next_allocation();
let collections_before = gc_collection_count();

crate::object::iterator_prototypes::ensure_iterator_prototypes();

assert_ne!(
crate::object::iterator_prototypes::ITERATOR_PROTOTYPE_PTR.load(StdOrdering::Acquire),
0,
"the iterator tower did not build, so the test measured nothing"
);
let collections_after = gc_collection_count();
assert_eq!(
collections_after, collections_before,
"a collection ran inside `build_iterator_prototypes` while its shared/family raw pointers were unrewritable"
);
assert!(
pending_collection_still_owed(),
"the window must defer the request, not drop it"
);
assert!(
!crate::gc::gc_is_suppressed(),
"the no-move window must close when the iterator tower builder returns"
);
assert!(
armed_collection_is_serviced_by_the_next_allocation(collections_after),
"the armed collection was never serviceable, so the no-collection assertion was vacuous"
);
});
}

/// #8002/#8003: every cached heap address must name the calling agent's live
/// arena. Both agents are held at the barrier so address inequality cannot be
/// earned by allocator reuse after the first thread exits.
#[test]
fn realm_owned_intrinsic_module_and_storage_roots_are_distinct() {
use std::sync::{Arc, Barrier, Mutex};

// The guard owns the sole wait path. It also runs while unwinding, so a
// panic during materialization or snapshot capture releases the peer
// instead of leaving it blocked forever. On success it keeps each agent
// (and therefore its arena) alive until both snapshots have been captured.
struct ReleasePeerOnDrop(Arc<Barrier>);
impl Drop for ReleasePeerOnDrop {
fn drop(&mut self) {
self.0.wait();
}
}

let bootstrap_gate = Arc::new(Mutex::new(()));
let both_alive = Arc::new(Barrier::new(2));
let agent = |gate: Arc<Mutex<()>>, barrier: Arc<Barrier>| {
std::thread::Builder::new()
.stack_size(16 << 20)
.spawn(move || {
let _release_peer = ReleasePeerOnDrop(barrier);
{
// GLOBAL_THIS_PTR is older process-global bootstrap state;
// serialize that unrelated initialization while auditing
// the roots moved by #8002/#8003.
let _bootstrap = gate.lock().expect("bootstrap gate");
crate::object::test_materialize_realm_owned_roots();
}
crate::object::test_realm_owned_root_snapshot()
})
.expect("spawn realm agent")
};

let a = agent(Arc::clone(&bootstrap_gate), Arc::clone(&both_alive));
let b = agent(bootstrap_gate, both_alive);
let a = a.join().expect("agent A panicked");
let b = b.join().expect("agent B panicked");

assert_eq!(a.len(), 23, "the gate must cover every #8002/#8003 root");
assert_eq!(a.len(), b.len());
for ((a_name, a_slot, a_root), (b_name, b_slot, b_root)) in a.iter().zip(&b) {
assert_eq!(a_name, b_name, "snapshot wiring diverged between agents");
assert_ne!(
*a_root, 0,
"agent A did not materialize {a_name}; distinctness would prove nothing"
);
assert_ne!(
*b_root, 0,
"agent B did not materialize {b_name}; distinctness would prove nothing"
);
assert_ne!(
a_slot, b_slot,
"{a_name} resolved to one process-global atomic in both agents"
);
assert_ne!(
a_root, b_root,
"{a_name} reused agent A's live heap address in agent B"
);
}
}

#[test]
fn generator_tower_runs_in_a_no_move_window() {
on_a_fresh_thread(|| {
Expand Down
14 changes: 7 additions & 7 deletions crates/perry-runtime/src/object/global_this/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,12 +573,12 @@ pub extern "C" fn js_generator_attach_closure_prototype(
/// ```
fn build_generator_tower(
is_async: bool,
ctor_slot: &std::sync::atomic::AtomicI64,
proto_slot: &std::sync::atomic::AtomicI64,
gen_proto_slot: &std::sync::atomic::AtomicI64,
ctor_slot: &crate::object::RealmAtomicI64,
proto_slot: &crate::object::RealmAtomicI64,
gen_proto_slot: &crate::object::RealmAtomicI64,
) {
// #7251: this builds an IMMORTAL object graph (the tower hangs off a
// process-global intrinsic slot for the life of the thread) by threading
// #7251: this builds an IMMORTAL object graph (the tower hangs off an
// agent-local intrinsic root for the life of the thread) by threading
// `ctor`/`proto`/`gen_proto` as raw `*mut ObjectHeader` / `*mut
// ClosureHeader` locals across a dozen-plus allocating installs below.
// None of those locals is a slot the collector rewrites, so a relocating
Expand Down Expand Up @@ -711,8 +711,8 @@ fn build_generator_tower(
gen_proto_slot.store(gen_proto as i64, Ordering::Release);
}

/// Build both generator intrinsic towers. Idempotent; called once from
/// `populate_global_this_builtins` under the globalThis singleton CAS. (#3664)
/// Build both generator intrinsic towers. Idempotent within the current
/// agent; called during its global bootstrap or by the lazy accessors. (#3664)
pub(crate) fn ensure_generator_intrinsics() {
if crate::object::GENERATOR_FUNCTION_INTRINSIC_PTR.load(Ordering::Acquire) == 0 {
build_generator_tower(
Expand Down
5 changes: 2 additions & 3 deletions crates/perry-runtime/src/object/global_this/typed_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,8 @@ fn install_typed_array_iterator_symbol(proto_obj: *mut ObjectHeader) {
/// walks read `null.prototype` and the constructor's `__proto__` returned the
/// `0.0` no-value placeholder (`typeof Int8Array.__proto__ === "number"`).
///
/// Idempotent: subsequent calls return the cached pointer. Called from
/// `populate_global_this_builtins` (single-threaded under the singleton CAS),
/// so the AtomicI64 stores don't need to race-resolve.
/// Idempotent within the current agent: subsequent calls return its cached
/// pointer. The owning agent is the only thread that accesses these roots.
pub(crate) fn ensure_typed_array_intrinsic(
) -> (*mut crate::closure::ClosureHeader, *mut ObjectHeader) {
let existing_ctor = crate::object::TYPED_ARRAY_INTRINSIC_PTR.load(Ordering::Acquire);
Expand Down
36 changes: 29 additions & 7 deletions crates/perry-runtime/src/object/iterator_prototypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,30 @@ use super::{
use crate::value::JSValue;
use std::sync::atomic::{AtomicI64, Ordering};

// GC-rooted singleton slots. Scanned in `object/mod.rs::scan_object_cache_roots_mut`.
pub(crate) static ITERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0);
pub(crate) static ARRAY_ITERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0);
pub(crate) static MAP_ITERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0);
pub(crate) static SET_ITERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0);
pub(crate) static STRING_ITERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0);
pub(crate) static REGEXP_STRING_ITERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0);
// GC-rooted singleton slots. Each realm builds its own tower in its own arena;
// the process-global handles resolve to per-agent atomics and are scanned in
// `object/mod.rs::scan_object_cache_roots_mut` (#8002).
crate::perry_thread_local! {
static ITERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) };
static ARRAY_ITERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) };
static MAP_ITERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) };
static SET_ITERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) };
static STRING_ITERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) };
static REGEXP_STRING_ITERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) };
}

pub(crate) static ITERATOR_PROTOTYPE_PTR: super::RealmAtomicI64 =
super::RealmAtomicI64::new(&ITERATOR_PROTOTYPE_PTR_SLOT);
pub(crate) static ARRAY_ITERATOR_PROTOTYPE_PTR: super::RealmAtomicI64 =
super::RealmAtomicI64::new(&ARRAY_ITERATOR_PROTOTYPE_PTR_SLOT);
pub(crate) static MAP_ITERATOR_PROTOTYPE_PTR: super::RealmAtomicI64 =
super::RealmAtomicI64::new(&MAP_ITERATOR_PROTOTYPE_PTR_SLOT);
pub(crate) static SET_ITERATOR_PROTOTYPE_PTR: super::RealmAtomicI64 =
super::RealmAtomicI64::new(&SET_ITERATOR_PROTOTYPE_PTR_SLOT);
pub(crate) static STRING_ITERATOR_PROTOTYPE_PTR: super::RealmAtomicI64 =
super::RealmAtomicI64::new(&STRING_ITERATOR_PROTOTYPE_PTR_SLOT);
pub(crate) static REGEXP_STRING_ITERATOR_PROTOTYPE_PTR: super::RealmAtomicI64 =
super::RealmAtomicI64::new(&REGEXP_STRING_ITERATOR_PROTOTYPE_PTR_SLOT);

/// Dispatch `method` on the implicit-`this` iterator instance, routing by class
/// id to the matching existing iterator dispatcher. Shared by the per-family
Expand Down Expand Up @@ -156,6 +173,11 @@ fn chain_to(child: *mut ObjectHeader, parent: *mut ObjectHeader) {
/// Build the shared `%IteratorPrototype%` and the four family prototypes,
/// storing them in the GC-rooted slots. Idempotent.
fn build_iterator_prototypes() {
// The tower is reachable lazily from the first iterator allocation, not
// only from globalThis bootstrap. Keep its raw locals stable across the
// allocating method/tag installs, just like the generator and TypedArray
// intrinsic builders (#7251).
let _no_move = crate::gc::GcSuppressScope::new();
// Shared %IteratorPrototype% — carries [Symbol.iterator] returning `this`.
let shared = js_object_alloc(0, 0);
if shared.is_null() {
Expand Down
Loading
Loading