From aee2ac85c559d707cff5371737c187bdeb8c4516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 13 Aug 2026 23:50:44 +0200 Subject: [PATCH 1/5] fix(gc): keep realm-owned runtime roots agent-local --- .../src/gc/tests/lazy_intrinsic_towers.rs | 130 ++++- .../src/object/global_this/generator.rs | 14 +- .../src/object/global_this/typed_array.rs | 5 +- .../src/object/iterator_prototypes.rs | 36 +- crates/perry-runtime/src/object/mod.rs | 472 ++++++++++++------ .../perry-runtime/src/object/native_module.rs | 2 + .../native_module/namespace_builders.rs | 22 +- crates/perry-runtime/src/web_storage.rs | 16 +- gc-handoff/REALM-GC-SUB-NOTES.md | 87 ++++ ...est_issue_8002_8003_thread_realm_caches.ts | 93 ++++ ...st_issue_8002_8003_thread_realm_caches.txt | 3 + 11 files changed, 666 insertions(+), 214 deletions(-) create mode 100644 gc-handoff/REALM-GC-SUB-NOTES.md create mode 100644 test-files/test_issue_8002_8003_thread_realm_caches.ts create mode 100644 test-parity/expected/test_issue_8002_8003_thread_realm_caches.txt diff --git a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs index 810a48f076..a72beda2d1 100644 --- a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs +++ b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs @@ -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 @@ -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, @@ -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) @@ -176,6 +170,102 @@ 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}; + + let bootstrap_gate = Arc::new(Mutex::new(())); + let both_alive = Arc::new(Barrier::new(2)); + let agent = |gate: Arc>, barrier: Arc| { + std::thread::Builder::new() + .stack_size(16 << 20) + .spawn(move || { + { + // 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(); + } + let snapshot = crate::object::test_realm_owned_root_snapshot(); + barrier.wait(); + 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(|| { diff --git a/crates/perry-runtime/src/object/global_this/generator.rs b/crates/perry-runtime/src/object/global_this/generator.rs index 3a8a9a121f..9aeb679524 100644 --- a/crates/perry-runtime/src/object/global_this/generator.rs +++ b/crates/perry-runtime/src/object/global_this/generator.rs @@ -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 @@ -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( diff --git a/crates/perry-runtime/src/object/global_this/typed_array.rs b/crates/perry-runtime/src/object/global_this/typed_array.rs index c573c2acb0..beeb82047d 100644 --- a/crates/perry-runtime/src/object/global_this/typed_array.rs +++ b/crates/perry-runtime/src/object/global_this/typed_array.rs @@ -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); diff --git a/crates/perry-runtime/src/object/iterator_prototypes.rs b/crates/perry-runtime/src/object/iterator_prototypes.rs index 862a612c79..e52ca22366 100644 --- a/crates/perry-runtime/src/object/iterator_prototypes.rs +++ b/crates/perry-runtime/src/object/iterator_prototypes.rs @@ -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(®EXP_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 @@ -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() { diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index 6af3a978cb..b74e7613c2 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -242,51 +242,127 @@ pub(crate) use this_binding::{ pub use to_string_tag::js_object_to_string; pub(crate) use to_string_tag::typed_array_to_string_tag_name; +/// An atomic GC root whose backing slot belongs to the calling Perry agent. +/// +/// The public handle stays process-global and contains no heap address. Every +/// load, store and scanner visit resolves through `perry_thread_local!` to the +/// current thread's real atomic. This preserves the explicit atomic API at the +/// call sites while making it impossible to publish one arena's raw pointer to +/// another realm (#8002/#8003). +pub(crate) struct RealmAtomicI64 { + slot: &'static crate::tls_hot::HotKey, +} + +impl RealmAtomicI64 { + const fn new(slot: &'static crate::tls_hot::HotKey) -> Self { + Self { slot } + } + + #[inline(always)] + pub(crate) fn load(&self, ordering: Ordering) -> i64 { + self.slot.with(|slot| slot.load(ordering)) + } + + #[inline(always)] + pub(crate) fn store(&self, value: i64, ordering: Ordering) { + self.slot.with(|slot| { + crate::gc::runtime_store_root_atomic_raw_i64(slot, value, ordering); + }); + } + + #[inline(always)] + pub(crate) fn with_slot(&self, f: impl FnOnce(&AtomicI64) -> R) -> R { + self.slot.with(f) + } + + #[cfg(test)] + pub(crate) fn test_slot_addr(&self) -> usize { + self.slot.with(|slot| slot as *const AtomicI64 as usize) + } +} + +/// `u64` twin of [`RealmAtomicI64`] for NaN-boxed root words. +pub(crate) struct RealmAtomicU64 { + slot: &'static crate::tls_hot::HotKey, +} + +impl RealmAtomicU64 { + const fn new(slot: &'static crate::tls_hot::HotKey) -> Self { + Self { slot } + } + + #[inline(always)] + pub(crate) fn load(&self, ordering: Ordering) -> u64 { + self.slot.with(|slot| slot.load(ordering)) + } + + #[inline(always)] + pub(crate) fn with_slot(&self, f: impl FnOnce(&AtomicU64) -> R) -> R { + self.slot.with(f) + } + + #[cfg(test)] + pub(crate) fn test_slot_addr(&self) -> usize { + self.slot.with(|slot| slot as *const AtomicU64 as usize) + } +} + +crate::perry_thread_local! { + static HTTP_METHODS_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; + static FS_CONSTANTS_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; + static OS_CONSTANTS_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; + static OS_CONSTANTS_SIGNALS_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; + static OS_CONSTANTS_ERRNO_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; + static OS_CONSTANTS_PRIORITY_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; + static OS_CONSTANTS_DLOPEN_CACHE_SLOT: AtomicU64 = const { AtomicU64::new(0) }; + static TYPED_ARRAY_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static TYPED_ARRAY_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static GENERATOR_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static GENERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static ASYNC_GENERATOR_INTRINSIC_PROTO_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static ASYNC_GENERATOR_PROTOTYPE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static LOCAL_STORAGE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; + static SESSION_STORAGE_PTR_SLOT: AtomicI64 = const { AtomicI64::new(0) }; +} + +static HTTP_METHODS_CACHE: RealmAtomicU64 = RealmAtomicU64::new(&HTTP_METHODS_CACHE_SLOT); +static FS_CONSTANTS_CACHE: RealmAtomicU64 = RealmAtomicU64::new(&FS_CONSTANTS_CACHE_SLOT); +static OS_CONSTANTS_CACHE: RealmAtomicU64 = RealmAtomicU64::new(&OS_CONSTANTS_CACHE_SLOT); +static OS_CONSTANTS_SIGNALS_CACHE: RealmAtomicU64 = + RealmAtomicU64::new(&OS_CONSTANTS_SIGNALS_CACHE_SLOT); +static OS_CONSTANTS_ERRNO_CACHE: RealmAtomicU64 = + RealmAtomicU64::new(&OS_CONSTANTS_ERRNO_CACHE_SLOT); +static OS_CONSTANTS_PRIORITY_CACHE: RealmAtomicU64 = + RealmAtomicU64::new(&OS_CONSTANTS_PRIORITY_CACHE_SLOT); +static OS_CONSTANTS_DLOPEN_CACHE: RealmAtomicU64 = + RealmAtomicU64::new(&OS_CONSTANTS_DLOPEN_CACHE_SLOT); + +pub(crate) static TYPED_ARRAY_INTRINSIC_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&TYPED_ARRAY_INTRINSIC_PTR_SLOT); +pub(crate) static TYPED_ARRAY_INTRINSIC_PROTO_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&TYPED_ARRAY_INTRINSIC_PROTO_PTR_SLOT); +pub(crate) static GENERATOR_FUNCTION_INTRINSIC_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT); +pub(crate) static GENERATOR_INTRINSIC_PROTO_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&GENERATOR_INTRINSIC_PROTO_PTR_SLOT); +pub(crate) static GENERATOR_PROTOTYPE_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&GENERATOR_PROTOTYPE_PTR_SLOT); +pub(crate) static ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR_SLOT); +pub(crate) static ASYNC_GENERATOR_INTRINSIC_PROTO_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&ASYNC_GENERATOR_INTRINSIC_PROTO_PTR_SLOT); +pub(crate) static ASYNC_GENERATOR_PROTOTYPE_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&ASYNC_GENERATOR_PROTOTYPE_PTR_SLOT); +pub(crate) static LOCAL_STORAGE_PTR: RealmAtomicI64 = RealmAtomicI64::new(&LOCAL_STORAGE_PTR_SLOT); +pub(crate) static SESSION_STORAGE_PTR: RealmAtomicI64 = + RealmAtomicI64::new(&SESSION_STORAGE_PTR_SLOT); + per_test_global! { - static HTTP_METHODS_CACHE: AtomicU64 = AtomicU64::new(0); - static FS_CONSTANTS_CACHE: AtomicU64 = AtomicU64::new(0); - static OS_CONSTANTS_CACHE: AtomicU64 = AtomicU64::new(0); - static OS_CONSTANTS_SIGNALS_CACHE: AtomicU64 = AtomicU64::new(0); - static OS_CONSTANTS_ERRNO_CACHE: AtomicU64 = AtomicU64::new(0); - static OS_CONSTANTS_PRIORITY_CACHE: AtomicU64 = AtomicU64::new(0); - static OS_CONSTANTS_DLOPEN_CACHE: AtomicU64 = AtomicU64::new(0); static GLOBAL_THIS_PTR: AtomicI64 = AtomicI64::new(0); static GLOBAL_THIS_READY: AtomicBool = AtomicBool::new(false); - // `%TypedArray%` intrinsic constructor/prototype roots used by per-kind - // typed array constructors and scanned by `scan_object_cache_roots_mut`. - pub(crate) static TYPED_ARRAY_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); - pub(crate) static TYPED_ARRAY_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); - // #3664: the generator / async-generator intrinsic prototype towers. - // `*_FUNCTION_INTRINSIC_PTR` = `%GeneratorFunction%` / `%AsyncGeneratorFunction%` - // (the constructor closures); `*_INTRINSIC_PROTO_PTR` = `%Generator%` / - // `%AsyncGenerator%` (a.k.a. `.prototype`), the object - // `Object.getPrototypeOf(function*(){})` resolves to; `*_PROTOTYPE_PTR` = - // `%Generator.prototype%` / `%AsyncGenerator.prototype%` (a.k.a. - // `.prototype.prototype`), carrying `next`/`return`/`throw`. All six are - // GC roots scanned by `scan_object_cache_roots_mut`. - // - // #7251: these six are `per_test_global!` (not a bare `static`) SPECIFICALLY - // so a test can observe "this tower has never been built" reliably. Before - // this they were plain process-global `AtomicI64`s, built exactly once per - // *process* — so whichever test happened to run first (order is - // libtest-nondeterministic) built them for every OTHER test on the same - // binary, and a gate trying to arm a collection around - // `ensure_generator_intrinsics()` / `ensure_typed_array_intrinsic()` found - // the tower already cached and measured nothing (see #7251's second and - // third failed gate attempts). `per_test_global!` gives each libtest THREAD - // — and libtest runs one thread per test — its own zeroed instance, so - // `crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs` sees a - // guaranteed-first-touch tower with no dependence on test execution order. - // Non-test builds expand to the identical plain `static` this replaced. - pub(crate) static GENERATOR_FUNCTION_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); - pub(crate) static GENERATOR_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); - pub(crate) static GENERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0); - pub(crate) static ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR: AtomicI64 = AtomicI64::new(0); - pub(crate) static ASYNC_GENERATOR_INTRINSIC_PROTO_PTR: AtomicI64 = AtomicI64::new(0); - pub(crate) static ASYNC_GENERATOR_PROTOTYPE_PTR: AtomicI64 = AtomicI64::new(0); -} -pub(crate) static LOCAL_STORAGE_PTR: AtomicI64 = AtomicI64::new(0); -pub(crate) static SESSION_STORAGE_PTR: AtomicI64 = AtomicI64::new(0); +} // Overflow field storage for objects that exceed their pre-allocated inline slot count. // Keyed by (obj_ptr as usize) -> Vec indexed by absolute field_index @@ -1120,74 +1196,39 @@ pub fn scan_object_cache_roots(mark: &mut dyn FnMut(f64)) { } pub fn scan_object_cache_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) { - visitor.visit_atomic_nanbox_u64_slot(&HTTP_METHODS_CACHE, Ordering::Relaxed, Ordering::Relaxed); - visitor.visit_atomic_nanbox_u64_slot(&FS_CONSTANTS_CACHE, Ordering::Relaxed, Ordering::Relaxed); - visitor.visit_atomic_nanbox_u64_slot(&OS_CONSTANTS_CACHE, Ordering::Relaxed, Ordering::Relaxed); - visitor.visit_atomic_nanbox_u64_slot( + for slot in [ + &HTTP_METHODS_CACHE, + &FS_CONSTANTS_CACHE, + &OS_CONSTANTS_CACHE, &OS_CONSTANTS_SIGNALS_CACHE, - Ordering::Relaxed, - Ordering::Relaxed, - ); - visitor.visit_atomic_nanbox_u64_slot( &OS_CONSTANTS_ERRNO_CACHE, - Ordering::Relaxed, - Ordering::Relaxed, - ); - visitor.visit_atomic_nanbox_u64_slot( &OS_CONSTANTS_PRIORITY_CACHE, - Ordering::Relaxed, - Ordering::Relaxed, - ); - visitor.visit_atomic_nanbox_u64_slot( &OS_CONSTANTS_DLOPEN_CACHE, - Ordering::Relaxed, - Ordering::Relaxed, - ); + ] { + slot.with_slot(|slot| { + visitor.visit_atomic_nanbox_u64_slot(slot, Ordering::Relaxed, Ordering::Relaxed); + }); + } visitor.visit_atomic_i64_slot(&GLOBAL_THIS_PTR, Ordering::Acquire, Ordering::Release); - visitor.visit_atomic_i64_slot( + // Realm intrinsic towers and Web Storage brands point into the calling + // thread's arena, so visit only this agent's backing atomics. + for slot in [ &TYPED_ARRAY_INTRINSIC_PTR, - Ordering::Acquire, - Ordering::Release, - ); - visitor.visit_atomic_i64_slot( &TYPED_ARRAY_INTRINSIC_PROTO_PTR, - Ordering::Acquire, - Ordering::Release, - ); - // #3664: generator / async-generator intrinsic tower roots. - visitor.visit_atomic_i64_slot( &GENERATOR_FUNCTION_INTRINSIC_PTR, - Ordering::Acquire, - Ordering::Release, - ); - visitor.visit_atomic_i64_slot( &GENERATOR_INTRINSIC_PROTO_PTR, - Ordering::Acquire, - Ordering::Release, - ); - visitor.visit_atomic_i64_slot( &GENERATOR_PROTOTYPE_PTR, - Ordering::Acquire, - Ordering::Release, - ); - visitor.visit_atomic_i64_slot( &ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR, - Ordering::Acquire, - Ordering::Release, - ); - visitor.visit_atomic_i64_slot( &ASYNC_GENERATOR_INTRINSIC_PROTO_PTR, - Ordering::Acquire, - Ordering::Release, - ); - visitor.visit_atomic_i64_slot( &ASYNC_GENERATOR_PROTOTYPE_PTR, - Ordering::Acquire, - Ordering::Release, - ); + &LOCAL_STORAGE_PTR, + &SESSION_STORAGE_PTR, + ] { + slot.with_slot(|slot| { + visitor.visit_atomic_i64_slot(slot, Ordering::Acquire, Ordering::Release); + }); + } async_generator_queue::scan_async_generator_queue_roots_mut(visitor); - visitor.visit_atomic_i64_slot(&LOCAL_STORAGE_PTR, Ordering::Acquire, Ordering::Release); - visitor.visit_atomic_i64_slot(&SESSION_STORAGE_PTR, Ordering::Acquire, Ordering::Release); // Shared `%IteratorPrototype%`-style singletons for Array/Map/Set/String // iterator objects. Each iterator instance's `[[Prototype]]` points here, so // these must stay live for the lifetime of any iterator. @@ -1199,7 +1240,9 @@ pub fn scan_object_cache_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<' &iterator_prototypes::STRING_ITERATOR_PROTOTYPE_PTR, &iterator_prototypes::REGEXP_STRING_ITERATOR_PROTOTYPE_PTR, ] { - visitor.visit_atomic_i64_slot(slot, Ordering::Acquire, Ordering::Release); + slot.with_slot(|slot| { + visitor.visit_atomic_i64_slot(slot, Ordering::Acquire, Ordering::Release); + }); } } @@ -1367,47 +1410,61 @@ pub(crate) fn test_keys_index_entry_exists(owner: usize) -> bool { #[cfg(test)] pub(crate) fn test_seed_object_cache_roots(object_cache_bits: [u64; 7], global_this_ptr: i64) { // GC_STORE_AUDIT(ROOT): test seed mirrors object cache roots scanned by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &HTTP_METHODS_CACHE, - object_cache_bits[0], - Ordering::Relaxed, - ); + HTTP_METHODS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64( + slot, + object_cache_bits[0], + Ordering::Relaxed, + ); + }); // GC_STORE_AUDIT(ROOT): test seed mirrors object cache roots scanned by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &FS_CONSTANTS_CACHE, - object_cache_bits[1], - Ordering::Relaxed, - ); + FS_CONSTANTS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64( + slot, + object_cache_bits[1], + Ordering::Relaxed, + ); + }); // GC_STORE_AUDIT(ROOT): test seed mirrors object cache roots scanned by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_CACHE, - object_cache_bits[2], - Ordering::Relaxed, - ); + OS_CONSTANTS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64( + slot, + object_cache_bits[2], + Ordering::Relaxed, + ); + }); // GC_STORE_AUDIT(ROOT): test seed mirrors object cache roots scanned by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_SIGNALS_CACHE, - object_cache_bits[3], - Ordering::Relaxed, - ); + OS_CONSTANTS_SIGNALS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64( + slot, + object_cache_bits[3], + Ordering::Relaxed, + ); + }); // GC_STORE_AUDIT(ROOT): test seed mirrors object cache roots scanned by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_ERRNO_CACHE, - object_cache_bits[4], - Ordering::Relaxed, - ); + OS_CONSTANTS_ERRNO_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64( + slot, + object_cache_bits[4], + Ordering::Relaxed, + ); + }); // GC_STORE_AUDIT(ROOT): test seed mirrors object cache roots scanned by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_PRIORITY_CACHE, - object_cache_bits[5], - Ordering::Relaxed, - ); + OS_CONSTANTS_PRIORITY_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64( + slot, + object_cache_bits[5], + Ordering::Relaxed, + ); + }); // GC_STORE_AUDIT(ROOT): test seed mirrors object cache roots scanned by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_DLOPEN_CACHE, - object_cache_bits[6], - Ordering::Relaxed, - ); + OS_CONSTANTS_DLOPEN_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64( + slot, + object_cache_bits[6], + Ordering::Relaxed, + ); + }); // GC_STORE_AUDIT(ROOT): test seed mirrors GLOBAL_THIS_PTR scanned by scan_object_cache_roots_mut. crate::gc::runtime_store_root_atomic_raw_i64( &GLOBAL_THIS_PTR, @@ -1433,38 +1490,143 @@ pub(crate) fn test_object_cache_roots() -> ([u64; 7], i64) { ) } +/// Materialize every #8002/#8003 realm-owned root on this agent. Kept as one +/// helper so the two-thread isolation gate below cannot accidentally exercise +/// only the backing TLS cells while all builders early-return. +#[cfg(test)] +pub(crate) fn test_materialize_realm_owned_roots() { + let global = js_get_global_this(); + assert_ne!( + crate::value::js_nanbox_get_pointer(global), + 0, + "globalThis bootstrap did not run" + ); + iterator_prototypes::ensure_iterator_prototypes(); + unsafe { + let _ = http_methods_array(); + let _ = create_fs_constants_object(); + } + for (name, cache) in [ + ("os.constants", &OS_CONSTANTS_CACHE), + ("os.constants.signals", &OS_CONSTANTS_SIGNALS_CACHE), + ("os.constants.errno", &OS_CONSTANTS_ERRNO_CACHE), + ("os.constants.priority", &OS_CONSTANTS_PRIORITY_CACHE), + ("os.constants.dlopen", &OS_CONSTANTS_DLOPEN_CACHE), + ] { + let _ = create_cached_sub_namespace(name, cache); + } +} + +/// `(name, backing-atomic address, rooted heap word)` for every root moved by +/// #8002/#8003. The backing address proves the storage is per-agent; the +/// nonzero heap word proves the corresponding builder actually populated it. +#[cfg(test)] +pub(crate) fn test_realm_owned_root_snapshot() -> Vec<(&'static str, usize, u64)> { + let mut roots = Vec::new(); + for (name, slot) in [ + ("HTTP_METHODS_CACHE", &HTTP_METHODS_CACHE), + ("FS_CONSTANTS_CACHE", &FS_CONSTANTS_CACHE), + ("OS_CONSTANTS_CACHE", &OS_CONSTANTS_CACHE), + ("OS_CONSTANTS_SIGNALS_CACHE", &OS_CONSTANTS_SIGNALS_CACHE), + ("OS_CONSTANTS_ERRNO_CACHE", &OS_CONSTANTS_ERRNO_CACHE), + ("OS_CONSTANTS_PRIORITY_CACHE", &OS_CONSTANTS_PRIORITY_CACHE), + ("OS_CONSTANTS_DLOPEN_CACHE", &OS_CONSTANTS_DLOPEN_CACHE), + ] { + roots.push((name, slot.test_slot_addr(), slot.load(Ordering::Acquire))); + } + for (name, slot) in [ + ("TYPED_ARRAY_INTRINSIC_PTR", &TYPED_ARRAY_INTRINSIC_PTR), + ( + "TYPED_ARRAY_INTRINSIC_PROTO_PTR", + &TYPED_ARRAY_INTRINSIC_PROTO_PTR, + ), + ( + "GENERATOR_FUNCTION_INTRINSIC_PTR", + &GENERATOR_FUNCTION_INTRINSIC_PTR, + ), + ( + "GENERATOR_INTRINSIC_PROTO_PTR", + &GENERATOR_INTRINSIC_PROTO_PTR, + ), + ("GENERATOR_PROTOTYPE_PTR", &GENERATOR_PROTOTYPE_PTR), + ( + "ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR", + &ASYNC_GENERATOR_FUNCTION_INTRINSIC_PTR, + ), + ( + "ASYNC_GENERATOR_INTRINSIC_PROTO_PTR", + &ASYNC_GENERATOR_INTRINSIC_PROTO_PTR, + ), + ( + "ASYNC_GENERATOR_PROTOTYPE_PTR", + &ASYNC_GENERATOR_PROTOTYPE_PTR, + ), + ("LOCAL_STORAGE_PTR", &LOCAL_STORAGE_PTR), + ("SESSION_STORAGE_PTR", &SESSION_STORAGE_PTR), + ( + "ITERATOR_PROTOTYPE_PTR", + &iterator_prototypes::ITERATOR_PROTOTYPE_PTR, + ), + ( + "ARRAY_ITERATOR_PROTOTYPE_PTR", + &iterator_prototypes::ARRAY_ITERATOR_PROTOTYPE_PTR, + ), + ( + "MAP_ITERATOR_PROTOTYPE_PTR", + &iterator_prototypes::MAP_ITERATOR_PROTOTYPE_PTR, + ), + ( + "SET_ITERATOR_PROTOTYPE_PTR", + &iterator_prototypes::SET_ITERATOR_PROTOTYPE_PTR, + ), + ( + "STRING_ITERATOR_PROTOTYPE_PTR", + &iterator_prototypes::STRING_ITERATOR_PROTOTYPE_PTR, + ), + ( + "REGEXP_STRING_ITERATOR_PROTOTYPE_PTR", + &iterator_prototypes::REGEXP_STRING_ITERATOR_PROTOTYPE_PTR, + ), + ] { + roots.push(( + name, + slot.test_slot_addr(), + slot.load(Ordering::Acquire) as u64, + )); + } + roots +} + #[cfg(test)] pub(crate) fn test_clear_object_cache_roots() { // GC_STORE_AUDIT(ROOT): test clear writes non-pointer sentinels into scanned object cache roots. - crate::gc::runtime_store_root_atomic_nanbox_u64(&HTTP_METHODS_CACHE, 0, Ordering::Relaxed); + HTTP_METHODS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, 0, Ordering::Relaxed); + }); // GC_STORE_AUDIT(ROOT): test clear writes non-pointer sentinels into scanned object cache roots. - crate::gc::runtime_store_root_atomic_nanbox_u64(&FS_CONSTANTS_CACHE, 0, Ordering::Relaxed); + FS_CONSTANTS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, 0, Ordering::Relaxed); + }); // GC_STORE_AUDIT(ROOT): test clear writes non-pointer sentinels into scanned object cache roots. - crate::gc::runtime_store_root_atomic_nanbox_u64(&OS_CONSTANTS_CACHE, 0, Ordering::Relaxed); + OS_CONSTANTS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, 0, Ordering::Relaxed); + }); // GC_STORE_AUDIT(ROOT): test clear writes non-pointer sentinels into scanned object cache roots. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_SIGNALS_CACHE, - 0, - Ordering::Relaxed, - ); + OS_CONSTANTS_SIGNALS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, 0, Ordering::Relaxed); + }); // GC_STORE_AUDIT(ROOT): test clear writes non-pointer sentinels into scanned object cache roots. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_ERRNO_CACHE, - 0, - Ordering::Relaxed, - ); + OS_CONSTANTS_ERRNO_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, 0, Ordering::Relaxed); + }); // GC_STORE_AUDIT(ROOT): test clear writes non-pointer sentinels into scanned object cache roots. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_PRIORITY_CACHE, - 0, - Ordering::Relaxed, - ); + OS_CONSTANTS_PRIORITY_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, 0, Ordering::Relaxed); + }); // GC_STORE_AUDIT(ROOT): test clear writes non-pointer sentinels into scanned object cache roots. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &OS_CONSTANTS_DLOPEN_CACHE, - 0, - Ordering::Relaxed, - ); + OS_CONSTANTS_DLOPEN_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, 0, Ordering::Relaxed); + }); // GC_STORE_AUDIT(ROOT): test clear writes non-pointer sentinel into scanned GLOBAL_THIS_PTR. crate::gc::runtime_store_root_atomic_raw_i64(&GLOBAL_THIS_PTR, 0, Ordering::Release); GLOBAL_THIS_READY.store(false, Ordering::Release); diff --git a/crates/perry-runtime/src/object/native_module.rs b/crates/perry-runtime/src/object/native_module.rs index 83e6937853..7235de9446 100644 --- a/crates/perry-runtime/src/object/native_module.rs +++ b/crates/perry-runtime/src/object/native_module.rs @@ -39,6 +39,8 @@ pub(crate) use callable_exports::{ }; pub(crate) use constants::get_native_module_constant; pub(crate) use module_keys::{native_module_enumerable_keys, native_module_has_enumerable_key}; +#[cfg(test)] +pub(crate) use namespace_builders::create_fs_constants_object; pub(crate) use namespace_builders::{ create_cached_sub_namespace, create_sub_namespace, http_global_agent_object, http_methods_array, http_status_codes_object, https_global_agent_object, diff --git a/crates/perry-runtime/src/object/native_module/namespace_builders.rs b/crates/perry-runtime/src/object/native_module/namespace_builders.rs index e03842a81e..0683c855d3 100644 --- a/crates/perry-runtime/src/object/native_module/namespace_builders.rs +++ b/crates/perry-runtime/src/object/native_module/namespace_builders.rs @@ -24,7 +24,7 @@ pub(crate) fn native_namespace_or_create(module_name: &str, namespace_obj: f64) js_create_native_module_namespace(module_name.as_ptr(), module_name.len()) } -pub(crate) fn create_cached_sub_namespace(name: &str, cache: &std::sync::atomic::AtomicU64) -> f64 { +pub(crate) fn create_cached_sub_namespace(name: &str, cache: &super::super::RealmAtomicU64) -> f64 { let cached = cache.load(Ordering::Relaxed); if cached != 0 { return f64::from_bits(cached); @@ -32,7 +32,9 @@ pub(crate) fn create_cached_sub_namespace(name: &str, cache: &std::sync::atomic: let result = create_sub_namespace(name); // GC_STORE_AUDIT(ROOT): os constants caches are mutable roots visited by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64(cache, result.to_bits(), Ordering::Relaxed); + cache.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, result.to_bits(), Ordering::Relaxed); + }); result } @@ -98,11 +100,9 @@ pub(crate) unsafe fn http_methods_array() -> f64 { } let value = crate::value::js_nanbox_pointer(arr as i64); // GC_STORE_AUDIT(ROOT): HTTP_METHODS_CACHE is a mutable root visited by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &crate::object::HTTP_METHODS_CACHE, - value.to_bits(), - Ordering::Relaxed, - ); + crate::object::HTTP_METHODS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, value.to_bits(), Ordering::Relaxed); + }); value } @@ -699,10 +699,8 @@ pub(crate) unsafe fn create_fs_constants_object() -> f64 { let result = crate::value::js_nanbox_pointer(obj as i64); // GC_STORE_AUDIT(ROOT): FS_CONSTANTS_CACHE is a mutable root visited by scan_object_cache_roots_mut. - crate::gc::runtime_store_root_atomic_nanbox_u64( - &crate::object::FS_CONSTANTS_CACHE, - result.to_bits(), - Ordering::Relaxed, - ); + crate::object::FS_CONSTANTS_CACHE.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_nanbox_u64(slot, result.to_bits(), Ordering::Relaxed); + }); result } diff --git a/crates/perry-runtime/src/web_storage.rs b/crates/perry-runtime/src/web_storage.rs index e4941a50d4..0abbdb0062 100644 --- a/crates/perry-runtime/src/web_storage.rs +++ b/crates/perry-runtime/src/web_storage.rs @@ -219,16 +219,12 @@ pub(crate) fn install_storage_globals( let local = make_storage_object(StorageKind::Local, storage_proto); let session = make_storage_object(StorageKind::Session, storage_proto); - crate::gc::runtime_store_root_atomic_raw_i64( - &crate::object::LOCAL_STORAGE_PTR, - local as i64, - Ordering::Release, - ); - crate::gc::runtime_store_root_atomic_raw_i64( - &crate::object::SESSION_STORAGE_PTR, - session as i64, - Ordering::Release, - ); + crate::object::LOCAL_STORAGE_PTR.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_raw_i64(slot, local as i64, Ordering::Release); + }); + crate::object::SESSION_STORAGE_PTR.with_slot(|slot| { + crate::gc::runtime_store_root_atomic_raw_i64(slot, session as i64, Ordering::Release); + }); set_global_storage_property(global, "localStorage", local); set_global_storage_property(global, "sessionStorage", session); diff --git a/gc-handoff/REALM-GC-SUB-NOTES.md b/gc-handoff/REALM-GC-SUB-NOTES.md new file mode 100644 index 0000000000..f975c7bcc5 --- /dev/null +++ b/gc-handoff/REALM-GC-SUB-NOTES.md @@ -0,0 +1,87 @@ +# Realm-local GC roots: #8002 / #8003 + +Worktree: `/Users/amlug/projects/perry/wt-realm-gc-sub` + +Target: `/Users/amlug/cargo-targets/realm-gc-sub` + +Base inspected: `origin/main` at `a9a99d8b7e8d3e2d3bd35d8725a34a4cab403f97` + +## Current-main audit + +- Both #8002 and #8003 are still open and have no issue comments. +- #8002 remains live in shipped builds. The iterator prototype slots are bare + `AtomicI64` statics. The typed-array and generator tower slots are declared + through `per_test_global!`, which is per-thread only under `cfg(test)` and + expands to the original process-global statics in production. +- #8003 is partly stale. PR #8024 / commit `a2ee0012b` converted + `FUNCTION_CLASS_IDS` and its companion class heap registries to + `perry_thread_local!`, so that subsection is already fixed on `main`. +- #8003's native-module caches remain production-global because they too use + `per_test_global!`. `HTTP_METHODS_CACHE` and `FS_CONSTANTS_CACHE` hold values + allocated in the calling thread's long-lived arena; the five OS namespace + caches hold realm-local namespace objects. +- #8003's `LOCAL_STORAGE_PTR` / `SESSION_STORAGE_PTR` remain bare process-global + atomics and are overwritten on every realm bootstrap. Brand checks still + compare against those shared cells. + +## Planned mechanism fix + +Use one small `RealmAtomicI64` / `RealmAtomicU64` adapter over +`perry_thread_local!` so existing load/store call sites remain explicit while +every shipped backing slot is per-agent. Mutable root scanning must borrow the +calling thread's backing atomic; behavioural and runtime tests must prove both +nonzero liveness and distinct addresses while two realm threads remain alive. + +## Implementation and focused validation + +- `RealmAtomicI64` / `RealmAtomicU64` are process-global *handles* containing + no heap pointer; their backing atomics are `perry_thread_local!` values. + Loads, barriered stores and mutable-root scanner visits all resolve through + the same current-agent slot. +- Converted 23 roots: six iterator prototypes, two `%TypedArray%` roots, six + generator/async-generator roots, seven native-module caches, and two Web + Storage brands. +- `build_iterator_prototypes` also gained a `GcSuppressScope`: it carried raw + shared/family pointers across allocating method and tag installation just as + the already-protected generator and TypedArray builders do. +- Added the two-live-agent runtime gate + `realm_owned_intrinsic_module_and_storage_roots_are_distinct`. It materializes + all 23 roots on both threads, proves every root word is nonzero, proves every + backing atomic differs, keeps both arenas alive at a barrier, and proves all + heap addresses differ. +- Added the positive-control no-move test + `iterator_prototype_tower_runs_in_a_no_move_window` and a behavioural + `perry/thread` probe covering iterator/generator/typed-array prototype + mutation isolation, native constants, Web Storage brands, and main-realm + preservation. + +Validated so far (all against this worktree/target): + +- `cargo check -p perry-runtime`: pass (pre-existing warnings only). +- all four `gc::tests::lazy_intrinsic_towers` tests: 4/4 pass. +- `object_cache_roots_survive_a_guard_clear_on_another_thread`: pass. +- `test_gc_init_mutable_scanner_families_rewrite_runtime_slots`: pass. +- `scripts/check_test_registration.py`, `scripts/check_gc_doc_claims.py`, + `scripts/gc_store_site_inventory.py`, and + `scripts/gc_runtime_root_holders.py`: pass. + +The required combined static-archive/compiler build passed for +`-p perry -p perry-runtime-static -p perry-stdlib-static`. The pinned artifacts +were rebuilt from this source and checked directly: + +- `perry-dev/perry`: 2026-08-13 23:44:05 +- `perry-dev/libperry_runtime.a`: 2026-08-13 23:43:44 +- `perry-dev/libperry_stdlib.a`: 2026-08-13 23:44:07 + +The registered behavioural test passes with those pinned artifacts. A direct +binary run was byte-identical to the expected output under both live arms: + +- seed 1, rate 1, moving loop polls, from-space protection depth 800: + 1,385 copying minors, 32,047 moved objects, 60,040 loop polls, and 1,385 + quarantined page-set retirements. +- seed 7, rate 1, moving loop polls, evacuation verification: 1,399 copying + minors, 32,047 moved objects, 60,040 loop polls, and 22,239 copied-object + events in the diagnostics. + +Sabotage/restore and the current-main landing-equivalent check are still +pending; do not treat this note as a merge claim yet. diff --git a/test-files/test_issue_8002_8003_thread_realm_caches.ts b/test-files/test_issue_8002_8003_thread_realm_caches.ts new file mode 100644 index 0000000000..7f41b84324 --- /dev/null +++ b/test-files/test_issue_8002_8003_thread_realm_caches.ts @@ -0,0 +1,93 @@ +// #8002/#8003: realm intrinsics, long-lived native-module values and Web +// Storage brand pointers must belong to the calling `perry/thread` agent. +// +// Warm every affected family on the main thread first. Before the fix that +// planted raw main-arena addresses in process-global caches; worker access then +// reused or overwrote them. The worker mutates the observable realm towers, +// drives for-of and a generator under forced collections, reads the cached +// native-module values, and uses both Storage brands. Its mutations must stay +// invisible to the main realm after it exits. +import { spawn } from "perry/thread"; + +declare function require(name: string): any; + +type Tagged = { realmOwner?: string }; + +function* syncGenerator(): Generator { + yield 4; + yield 5; + yield 6; +} + +function warmAndSummarize(tag: string): string { + const iteratorProto = Object.getPrototypeOf([1][Symbol.iterator]()) as Tagged; + const generatorProto = Object.getPrototypeOf(Object.getPrototypeOf(syncGenerator())) as Tagged; + const typedArrayProto = Object.getPrototypeOf(Uint8Array.prototype) as Tagged; + iteratorProto.realmOwner = tag + "-iterator"; + generatorProto.realmOwner = tag + "-generator"; + typedArrayProto.realmOwner = tag + "-typedarray"; + + let total = 0; + for (const value of [1, 2, 3]) total += value; + for (const value of syncGenerator()) total += value; + const typed = new Uint8Array([7, 8, 9]); + for (const value of typed) total += value; + + // Keep the affected roots live across enough short-lived allocations to + // force nursery turnover in both realms. The checksum makes this pressure + // observable, so neither the objects nor their reads can be discarded. + let pressure = 0; + for (let i = 0; i < 30000; i++) { + const row = { a: i, b: i + 1, c: i + 2 }; + const values = [row.a, row.b, row.c]; + pressure = (pressure + values[0] + values[1] + values[2]) % 1000003; + } + + const fs = require("node:fs"); + const os = require("node:os"); + const native = [ + typeof fs.constants.O_RDONLY === "number", + typeof os.constants.signals.SIGINT === "number", + typeof os.constants.errno.EACCES === "number", + typeof os.constants.priority.PRIORITY_NORMAL === "number", + typeof os.constants.dlopen.RTLD_LAZY === "number", + ].every(Boolean); + + localStorage.clear(); + sessionStorage.clear(); + localStorage.setItem("owner", tag + "-local"); + sessionStorage.setItem("owner", tag + "-session"); + const storage = localStorage.getItem("owner") + "/" + sessionStorage.getItem("owner"); + + return [ + iteratorProto.realmOwner, + generatorProto.realmOwner, + typedArrayProto.realmOwner, + String(total), + String(pressure), + String(native), + storage, + ].join("|"); +} + +const main = warmAndSummarize("main"); +console.log("main:", main); + +const worker = await spawn((): string => warmAndSummarize("worker")); +const expectedWorker = + "worker-iterator|worker-generator|worker-typedarray|45|40950|true|worker-local/worker-session"; +console.log("worker:", worker, "match:", worker === expectedWorker); + +const mainIteratorProto = Object.getPrototypeOf([1][Symbol.iterator]()) as Tagged; +const mainGeneratorProto = Object.getPrototypeOf(Object.getPrototypeOf(syncGenerator())) as Tagged; +const mainTypedArrayProto = Object.getPrototypeOf(Uint8Array.prototype) as Tagged; +console.log( + "main after:", + [ + mainIteratorProto.realmOwner, + mainGeneratorProto.realmOwner, + mainTypedArrayProto.realmOwner, + localStorage.getItem("owner"), + sessionStorage.getItem("owner"), + ].join("|"), +); diff --git a/test-parity/expected/test_issue_8002_8003_thread_realm_caches.txt b/test-parity/expected/test_issue_8002_8003_thread_realm_caches.txt new file mode 100644 index 0000000000..88cd95e546 --- /dev/null +++ b/test-parity/expected/test_issue_8002_8003_thread_realm_caches.txt @@ -0,0 +1,3 @@ +main: main-iterator|main-generator|main-typedarray|45|40950|true|main-local/main-session +worker: worker-iterator|worker-generator|worker-typedarray|45|40950|true|worker-local/worker-session match: true +main after: main-iterator|main-generator|main-typedarray|main-local|main-session From f31da6c336142fbd04c2062428e46ca17ba90631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 14 Aug 2026 00:04:55 +0200 Subject: [PATCH 2/5] docs(gc): record realm-root validation evidence --- gc-handoff/REALM-GC-SUB-NOTES.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/gc-handoff/REALM-GC-SUB-NOTES.md b/gc-handoff/REALM-GC-SUB-NOTES.md index f975c7bcc5..380a50481e 100644 --- a/gc-handoff/REALM-GC-SUB-NOTES.md +++ b/gc-handoff/REALM-GC-SUB-NOTES.md @@ -4,7 +4,10 @@ Worktree: `/Users/amlug/projects/perry/wt-realm-gc-sub` Target: `/Users/amlug/cargo-targets/realm-gc-sub` -Base inspected: `origin/main` at `a9a99d8b7e8d3e2d3bd35d8725a34a4cab403f97` +Initial base inspected: `origin/main` at +`a9a99d8b7e8d3e2d3bd35d8725a34a4cab403f97`. The healthy commit was then +rebased cleanly onto `fe0d4979204dfd6b8b166320e1ebdd2318f30518` +(#8044) before the landing-equivalent rerun. ## Current-main audit @@ -83,5 +86,24 @@ binary run was byte-identical to the expected output under both live arms: minors, 32,047 moved objects, 60,040 loop polls, and 22,239 copied-object events in the diagnostics. -Sabotage/restore and the current-main landing-equivalent check are still -pending; do not treat this note as a merge claim yet. +## Sabotage proof + +After committing healthy source, `RealmAtomicI64` / `RealmAtomicU64` were +temporarily changed to resolve through one process-global atomic per handle. +The exact two-agent gate failed on the first family: + +``` +HTTP_METHODS_CACHE resolved to one process-global atomic in both agents +left: 4353921112 +right: 4353921112 +``` + +The wrapper source was restored byte-for-byte to the healthy commit, rebuilt, +and the exact same gate passed. The combined compiler/runtime-static/ +stdlib-static build was then rerun from restored source; its log compiled +`perry-runtime` exactly once and finished successfully. Restored artifact +mtimes are 2026-08-14 00:04:01 (`perry`), 00:03:48 +(`libperry_runtime.a`), and 00:04:02 (`libperry_stdlib.a`). + +The current-main landing-equivalent rerun is still pending; do not treat this +note as a merge claim yet. From 67a8d33c6ba88d5beb5c07f7dc78481d564b9ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 14 Aug 2026 00:05:30 +0200 Subject: [PATCH 3/5] docs(changelog): note agent-local realm roots --- changelog.d/8055-agent-local-realm-roots.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8055-agent-local-realm-roots.md diff --git a/changelog.d/8055-agent-local-realm-roots.md b/changelog.d/8055-agent-local-realm-roots.md new file mode 100644 index 0000000000..1eca79b617 --- /dev/null +++ b/changelog.d/8055-agent-local-realm-roots.md @@ -0,0 +1 @@ +Keep iterator, generator, `%TypedArray%`, native-module, and Web Storage GC roots local to the Perry agent whose arena owns them. From 56d69fe8166e30151d4f190e8e318e1430c98574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 14 Aug 2026 00:20:20 +0200 Subject: [PATCH 4/5] test(gc): retire iterator root-holder exemptions --- gc-handoff/REALM-GC-SUB-NOTES.md | 28 +++++++++++++++---- scripts/gc_runtime_root_holders.json | 42 ---------------------------- 2 files changed, 23 insertions(+), 47 deletions(-) diff --git a/gc-handoff/REALM-GC-SUB-NOTES.md b/gc-handoff/REALM-GC-SUB-NOTES.md index 380a50481e..664aa93a2c 100644 --- a/gc-handoff/REALM-GC-SUB-NOTES.md +++ b/gc-handoff/REALM-GC-SUB-NOTES.md @@ -64,9 +64,14 @@ Validated so far (all against this worktree/target): - all four `gc::tests::lazy_intrinsic_towers` tests: 4/4 pass. - `object_cache_roots_survive_a_guard_clear_on_another_thread`: pass. - `test_gc_init_mutable_scanner_families_rewrite_runtime_slots`: pass. -- `scripts/check_test_registration.py`, `scripts/check_gc_doc_claims.py`, - `scripts/gc_store_site_inventory.py`, and - `scripts/gc_runtime_root_holders.py`: pass. +- `scripts/check_test_registration.py` and + `scripts/check_gc_doc_claims.py`: pass. +- `scripts/gc_runtime_root_holders.py`: pass after deleting the six stale + iterator-root exemptions; the new handle type is directly classified. +- `scripts/gc_store_site_inventory.py`: blocked on pre-existing main line + `crates/perry-codegen/src/expr/property_set.rs:1475` (introduced by + `5fcd94289`, untouched by this branch), which lacks a `GC_STORE_AUDIT` + marker. This branch does not claim that unrelated inventory gate as green. The required combined static-archive/compiler build passed for `-p perry -p perry-runtime-static -p perry-stdlib-static`. The pinned artifacts @@ -105,5 +110,18 @@ stdlib-static build was then rerun from restored source; its log compiled mtimes are 2026-08-14 00:04:01 (`perry`), 00:03:48 (`libperry_runtime.a`), and 00:04:02 (`libperry_stdlib.a`). -The current-main landing-equivalent rerun is still pending; do not treat this -note as a merge claim yet. +## Landing-equivalent result + +On the branch rebased onto `fe0d49792`, all four optimized +`gc::tests::lazy_intrinsic_towers` tests passed, including the two-agent and +three no-move gates. The exact combined static build recompiled all three +required packages and produced fresh artifacts at 00:12:44 (`perry`), 00:11:43 +(`libperry_runtime.a`), and 00:11:54 (`libperry_stdlib.a`). The registered +parity test passed against them. + +Fresh direct stress runs from those rebased artifacts remained byte-identical: + +- protect-fromspace: 1,406 copying minors, 32,047 moved objects, 60,040 loop + polls, and 1,406 quarantined page-set retirements. +- verify-evacuation: 1,400 copying minors, 32,047 moved objects, 60,040 loop + polls, and 22,239 copied-object events in diagnostics. diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index c4a4f510ee..ed32a5b654 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -86,48 +86,6 @@ "verdict": "not_a_gc_pointer", "why": "Inline-cache invalidation epoch. Epoch invalidation is the third rooting strategy this tree uses (see object/prop_plan.rs) \u2014 the cache is discarded on bump rather than scanned." }, - { - "file": "crates/perry-runtime/src/object/iterator_prototypes.rs", - "name": "ITERATOR_PROTOTYPE_PTR", - "verdict": "covered_elsewhere", - "scanner": "object::scan_object_cache_roots_mut (object/mod.rs:1141, the six-slot loop)", - "why": "All six %IteratorPrototype%-style singletons are visited by one loop in object/mod.rs." - }, - { - "file": "crates/perry-runtime/src/object/iterator_prototypes.rs", - "name": "ARRAY_ITERATOR_PROTOTYPE_PTR", - "verdict": "covered_elsewhere", - "scanner": "object::scan_object_cache_roots_mut (object/mod.rs:1141)", - "why": "One of the six %IteratorPrototype%-style singletons visited by a single loop in object/mod.rs. Every iterator instance's [[Prototype]] points at one of them, so all six must stay live for the lifetime of any iterator." - }, - { - "file": "crates/perry-runtime/src/object/iterator_prototypes.rs", - "name": "MAP_ITERATOR_PROTOTYPE_PTR", - "verdict": "covered_elsewhere", - "scanner": "object::scan_object_cache_roots_mut (object/mod.rs:1141)", - "why": "One of the six %IteratorPrototype%-style singletons visited by the same loop in object/mod.rs; see ITERATOR_PROTOTYPE_PTR for the full reasoning." - }, - { - "file": "crates/perry-runtime/src/object/iterator_prototypes.rs", - "name": "SET_ITERATOR_PROTOTYPE_PTR", - "verdict": "covered_elsewhere", - "scanner": "object::scan_object_cache_roots_mut (object/mod.rs:1141)", - "why": "One of the six %IteratorPrototype%-style singletons visited by the same loop in object/mod.rs; see ITERATOR_PROTOTYPE_PTR for the full reasoning." - }, - { - "file": "crates/perry-runtime/src/object/iterator_prototypes.rs", - "name": "STRING_ITERATOR_PROTOTYPE_PTR", - "verdict": "covered_elsewhere", - "scanner": "object::scan_object_cache_roots_mut (object/mod.rs:1141)", - "why": "One of the six %IteratorPrototype%-style singletons visited by the same loop in object/mod.rs; see ITERATOR_PROTOTYPE_PTR for the full reasoning." - }, - { - "file": "crates/perry-runtime/src/object/iterator_prototypes.rs", - "name": "REGEXP_STRING_ITERATOR_PROTOTYPE_PTR", - "verdict": "covered_elsewhere", - "scanner": "object::scan_object_cache_roots_mut (object/mod.rs:1141)", - "why": "One of the six %IteratorPrototype%-style singletons visited by the same loop in object/mod.rs; see ITERATOR_PROTOTYPE_PTR for the full reasoning." - }, { "file": "crates/perry-runtime/src/process.rs", "name": "MODULE_LOADER_NEXT_RESOLVE", From 977a19b8bb91988a098f25eeb66773c1825247f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 14 Aug 2026 00:46:47 +0200 Subject: [PATCH 5/5] test(gc): release realm-root peer on unwind --- .../src/gc/tests/lazy_intrinsic_towers.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs index a72beda2d1..258250bfa2 100644 --- a/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs +++ b/crates/perry-runtime/src/gc/tests/lazy_intrinsic_towers.rs @@ -218,12 +218,24 @@ fn iterator_prototype_tower_runs_in_a_no_move_window() { 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); + 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>, barrier: Arc| { 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 @@ -231,9 +243,7 @@ fn realm_owned_intrinsic_module_and_storage_roots_are_distinct() { let _bootstrap = gate.lock().expect("bootstrap gate"); crate::object::test_materialize_realm_owned_roots(); } - let snapshot = crate::object::test_realm_owned_root_snapshot(); - barrier.wait(); - snapshot + crate::object::test_realm_owned_root_snapshot() }) .expect("spawn realm agent") };