diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index e201bcf23d..782207c5ff 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -11,7 +11,7 @@ use crate::expr::FnCtx; use crate::module::LlModule; use crate::stmt; use crate::strings::StringPool; -use crate::types::{LlvmType, DOUBLE, I1, I32, I64}; +use crate::types::{LlvmType, DOUBLE, I1, I32, I64, PTR}; use super::opts::CrossModuleCtx; use super::typed_abi::{ @@ -568,6 +568,25 @@ pub(super) fn compile_closure( if typed_public_trampoline.is_some() { lf.linkage = "internal".to_string(); } + + // gh #6206 / #6081: closures/arrows compiled WITHOUT a shadow frame left + // their pointer-typed params/locals invisible to the exact-roots copying + // minor (production skips the conservative native-stack scan), so an + // evacuating GC fired mid-body swept values reachable only from the + // closure's own frame — the referrer then read freed-and-reused memory. + // Emit the same frame the top-level function path gets (function.rs). + let shadow_slot_map = if super::helpers::shadow_stack_enabled() { + let flat_const_ids: std::collections::HashSet = + cross_module.flat_const_arrays.keys().copied().collect(); + let m = crate::collectors::collect_pointer_typed_locals(params, body, &flat_const_ids); + lf.enable_shadow_frame(m.len() as u32); + m + } else { + std::collections::HashMap::new() + }; + let shadow_slot_clears_after_stmt = + crate::collectors::collect_shadow_slot_clear_points(body, &shadow_slot_map); + let _ = lf.create_block("entry"); let mut closure_boxed_vars = module_boxed_vars.clone(); @@ -581,6 +600,12 @@ pub(super) fn compile_closure( for p in params { let arg_name = format!("%arg{}", p.id); let slot = super::arguments::store_param_slot(blk, p, &closure_boxed_vars, &arg_name); + if let Some(slot_idx) = shadow_slot_map.get(&p.id).copied() { + blk.call_void( + "js_shadow_slot_bind", + &[(I32, &slot_idx.to_string()), (PTR, &slot)], + ); + } map.insert(p.id, slot); } map @@ -802,8 +827,8 @@ pub(super) fn compile_closure( pending_declares: Vec::new(), integer_locals: native_facts.integer_locals(), unsigned_i32_locals: native_facts.unsigned_i32_locals(), - shadow_slot_map: std::collections::HashMap::new(), - shadow_slot_clears_after_stmt: std::collections::HashMap::new(), + shadow_slot_map, + shadow_slot_clears_after_stmt, arena_state_slot: None, class_keys_slots: HashMap::new(), cached_lengths: HashMap::new(), diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index ee82ad2dae..e0167a8dce 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -10,7 +10,7 @@ use crate::expr::FnCtx; use crate::module::LlModule; use crate::stmt; use crate::strings::StringPool; -use crate::types::{LlvmType, DOUBLE, I1, I32, I64}; +use crate::types::{LlvmType, DOUBLE, I1, I32, I64, PTR}; use super::helpers::scoped_static_method_name; use super::opts::CrossModuleCtx; @@ -291,6 +291,28 @@ pub(super) fn compile_method( if typed_public_trampoline.is_some() || force_generic_body { lf.linkage = "internal".to_string(); } + + // gh #6206 / #6081: methods were compiled WITHOUT a shadow frame — same + // exact-roots liveness hole as closures (see compile_closure). One extra + // slot roots the receiver (`this` is a pointer value reachable from + // nothing else when the caller holds it only in a register temp). + let shadow_slot_map = if super::helpers::shadow_stack_enabled() { + let flat_const_ids: std::collections::HashSet = + cross_module.flat_const_arrays.keys().copied().collect(); + let m = crate::collectors::collect_pointer_typed_locals( + &method.params, + &method.body, + &flat_const_ids, + ); + lf.enable_shadow_frame(m.len() as u32 + 1); + m + } else { + std::collections::HashMap::new() + }; + let this_shadow_slot_idx = shadow_slot_map.len() as u32; + let shadow_slot_clears_after_stmt = + crate::collectors::collect_shadow_slot_clear_points(&method.body, &shadow_slot_map); + let _ = lf.create_block("entry"); let mut method_boxed_vars = module_boxed_vars.clone(); @@ -302,10 +324,22 @@ pub(super) fn compile_method( let blk = lf.block_mut(0).unwrap(); let this_slot = blk.alloca(DOUBLE); blk.store(DOUBLE, "%this_arg", &this_slot); + if super::helpers::shadow_stack_enabled() { + blk.call_void( + "js_shadow_slot_bind", + &[(I32, &this_shadow_slot_idx.to_string()), (PTR, &this_slot)], + ); + } let mut map = HashMap::new(); for p in &method.params { let arg_name = format!("%arg{}", p.id); let slot = super::arguments::store_param_slot(blk, p, &method_boxed_vars, &arg_name); + if let Some(slot_idx) = shadow_slot_map.get(&p.id).copied() { + blk.call_void( + "js_shadow_slot_bind", + &[(I32, &slot_idx.to_string()), (PTR, &slot)], + ); + } map.insert(p.id, slot); } (this_slot, map) @@ -422,8 +456,8 @@ pub(super) fn compile_method( pending_declares: Vec::new(), integer_locals: native_facts.integer_locals(), unsigned_i32_locals: native_facts.unsigned_i32_locals(), - shadow_slot_map: std::collections::HashMap::new(), - shadow_slot_clears_after_stmt: std::collections::HashMap::new(), + shadow_slot_map, + shadow_slot_clears_after_stmt, arena_state_slot: None, class_keys_slots: HashMap::new(), cached_lengths: HashMap::new(), @@ -1147,6 +1181,27 @@ pub(super) fn compile_static_method( let ic_base = llmod.ic_counter; let buffer_alias_base = llmod.buffer_alias_counter; let lf = llmod.define_function(&llvm_name, DOUBLE, params); + + // gh #6206 / #6081: same shadow-frame emission as compile_method — static + // method bodies were equally invisible to the exact-roots copying minor. + // One extra slot roots the resolved receiver: static `this` is usually + // the non-pointer INT32 class-ref, but `js_static_this_resolve` returns a + // REAL heap receiver for `C.m.call(x)` / `.apply(x)` / inherited `D.m()` + // dynamic dispatch, and that object may be reachable only from this slot. + let shadow_slot_map = if super::helpers::shadow_stack_enabled() { + let flat_const_ids: std::collections::HashSet = + cross_module.flat_const_arrays.keys().copied().collect(); + let m = + crate::collectors::collect_pointer_typed_locals(&f.params, &f.body, &flat_const_ids); + lf.enable_shadow_frame(m.len() as u32 + 1); + m + } else { + std::collections::HashMap::new() + }; + let this_shadow_slot_idx = shadow_slot_map.len() as u32; + let shadow_slot_clears_after_stmt = + crate::collectors::collect_shadow_slot_clear_points(&f.body, &shadow_slot_map); + let _ = lf.create_block("entry"); let mut static_boxed_vars = module_boxed_vars.clone(); @@ -1179,10 +1234,22 @@ pub(super) fn compile_static_method( &[(DOUBLE, &class_ref_lit)], ); blk.store(DOUBLE, &resolved_this, &this_slot); + if super::helpers::shadow_stack_enabled() { + blk.call_void( + "js_shadow_slot_bind", + &[(I32, &this_shadow_slot_idx.to_string()), (PTR, &this_slot)], + ); + } let mut map = HashMap::new(); for p in &f.params { let arg_name = format!("%arg{}", p.id); let slot = super::arguments::store_param_slot(blk, p, &static_boxed_vars, &arg_name); + if let Some(slot_idx) = shadow_slot_map.get(&p.id).copied() { + blk.call_void( + "js_shadow_slot_bind", + &[(I32, &slot_idx.to_string()), (PTR, &slot)], + ); + } map.insert(p.id, slot); } (this_slot, map) @@ -1298,8 +1365,8 @@ pub(super) fn compile_static_method( pending_declares: Vec::new(), integer_locals: native_facts.integer_locals(), unsigned_i32_locals: native_facts.unsigned_i32_locals(), - shadow_slot_map: std::collections::HashMap::new(), - shadow_slot_clears_after_stmt: std::collections::HashMap::new(), + shadow_slot_map, + shadow_slot_clears_after_stmt, arena_state_slot: None, class_keys_slots: HashMap::new(), cached_lengths: HashMap::new(), diff --git a/crates/perry-runtime/src/array/iter_methods.rs b/crates/perry-runtime/src/array/iter_methods.rs index 9f1b401760..39f9895b9e 100644 --- a/crates/perry-runtime/src/array/iter_methods.rs +++ b/crates/perry-runtime/src/array/iter_methods.rs @@ -170,6 +170,15 @@ pub extern "C" fn js_array_map( let length = (*arr).length; let scope = crate::gc::RuntimeHandleScope::new(); let rooted = RootedIterArray::new(&scope, arr); + // Root the callback closure across the iteration. A callback allocated + // by a frameless caller (arrow/method — #6081) is reachable ONLY via + // this raw param + the native stack, which an evacuating minor does NOT + // scan (copied-minor eligibility requires no conservative stack scan). + // Closures are non-movable, so an unrooted one is swept in place mid- + // loop → the next dispatch calls freed memory ("object is not a + // function" / wild-pointer crash). Masked by PERRY_GEN_GC_EVACUATE=0, + // whose non-moving minor DOES run the conservative scan. See gh #6206. + let cb_handle = scope.root_raw_const_ptr(callback); let _tg = DenseThisGuard::bind_undefined(); // ECMA-262 §23.1.3.20 step 5: ArraySpeciesCreate(O, len) runs BEFORE @@ -204,6 +213,7 @@ pub extern "C" fn js_array_map( } }; // JS .map() callback receives (element, index, array). + let callback = cb_handle.get_raw_const_ptr::(); let mapped = js_closure_call3(callback, element, i as f64, rooted.receiver()); if is_plain { let result = result_arr(&result_rooted); @@ -285,6 +295,8 @@ pub extern "C" fn js_array_filter( let length = (*arr).length; let scope = crate::gc::RuntimeHandleScope::new(); let rooted = RootedIterArray::new(&scope, arr); + // Root the callback across the loop — see js_array_map / gh #6206. + let cb_handle = scope.root_raw_const_ptr(callback); let _tg = DenseThisGuard::bind_undefined(); // ECMA-262 §23.1.3.7 step 5: ArraySpeciesCreate(O, 0) runs before the @@ -312,6 +324,7 @@ pub extern "C" fn js_array_filter( None => continue, } }; + let callback = cb_handle.get_raw_const_ptr::(); let keep = js_closure_call3(callback, element, i as f64, rooted.receiver()); // Proper truthy check: handles NaN-boxed booleans (TAG_FALSE != 0.0 but is falsy) if crate::value::js_is_truthy(keep) != 0 { diff --git a/crates/perry-runtime/src/gc/oldgen.rs b/crates/perry-runtime/src/gc/oldgen.rs index e0bad6bb20..d5441b0fbe 100644 --- a/crates/perry-runtime/src/gc/oldgen.rs +++ b/crates/perry-runtime/src/gc/oldgen.rs @@ -197,7 +197,65 @@ pub(super) fn select_old_page_defrag_pages_from_snapshot( selection } +/// gh #6206 test hook: the defrag machinery's unit tests exercise the +/// selection/copy/re-remember mechanics directly and must bypass the +/// production off-gate below. Thread-local so parallel tests don't race. +#[cfg(test)] +thread_local! { + pub(crate) static OLD_DEFRAG_TEST_OVERRIDE: std::cell::Cell> = + const { std::cell::Cell::new(None) }; +} + +/// RAII enable for the defrag unit tests: forces the off-gate open on this +/// thread for the guard's lifetime. +#[cfg(test)] +pub(crate) struct OldDefragTestEnable; + +#[cfg(test)] +impl OldDefragTestEnable { + pub(crate) fn new() -> Self { + OLD_DEFRAG_TEST_OVERRIDE.with(|c| c.set(Some(true))); + OldDefragTestEnable + } +} + +#[cfg(test)] +impl Drop for OldDefragTestEnable { + fn drop(&mut self) { + OLD_DEFRAG_TEST_OVERRIDE.with(|c| c.set(None)); + } +} + +fn old_page_defrag_enabled() -> bool { + #[cfg(test)] + if let Some(v) = OLD_DEFRAG_TEST_OVERRIDE.with(|c| c.get()) { + return v; + } + use std::sync::OnceLock; + static OPT_IN: OnceLock = OnceLock::new(); + *OPT_IN.get_or_init(|| { + matches!( + std::env::var("PERRY_GC_OLD_DEFRAG").as_deref(), + Ok("1") | Ok("on") | Ok("true") + ) + }) +} + pub(super) fn select_old_page_defrag_pages(force: bool) -> OldPageDefragSelection { + // gh #6206: old-page defrag evacuation is OFF pending a rewrite-contract + // fix. With defrag active, a reader can observe a pre-move address of a + // defrag-moved old object long after the cycle (wild-pointer crash / + // silently corrupt cached value); the reproducer corrupts 6/6 with defrag + // enabled and is clean 6/6 with it disabled, on the same binary, while + // every heap-payload slot (arrays in-length, object fields, Map entries) + // verifies as correctly rewritten — the stale reference lives on a + // non-heap path (address-keyed cache / IC / side table) the defrag + // rewrite doesn't reach. Nursery evacuation and tenured promotion (the + // reclaim-critical moving paths) are unaffected. Re-enable for + // debugging/bisection with PERRY_GC_OLD_DEFRAG=1. + if !old_page_defrag_enabled() { + return OldPageDefragSelection::default(); + } let snapshot = crate::arena::old_page_meta_snapshot(); select_old_page_defrag_pages_from_snapshot(&snapshot, force) } diff --git a/crates/perry-runtime/src/gc/telemetry.rs b/crates/perry-runtime/src/gc/telemetry.rs index 717a4370b6..58358e2aa6 100644 --- a/crates/perry-runtime/src/gc/telemetry.rs +++ b/crates/perry-runtime/src/gc/telemetry.rs @@ -70,6 +70,11 @@ pub(super) struct OldYoungEdgeMissing { pub(super) parent: usize, pub(super) slot: usize, pub(super) child: usize, + // gh #6206: edge-type diagnostics for the verifier panic. + pub(super) parent_obj_type: u8, + pub(super) child_obj_type: u8, + pub(super) parent_is_old_arena: bool, + pub(super) parent_marked: bool, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -79,17 +84,49 @@ pub(super) struct OldYoungEdgeVerifyStats { pub(super) checked_old_to_young_edges: usize, pub(super) missing_edges: usize, pub(super) first_missing: Option, + // gh #6206: per-type histograms of missing edges + pub(super) missing_by_parent_type: [u32; 32], + pub(super) missing_by_child_type: [u32; 32], + pub(super) missing_parent_malloc: u32, + pub(super) missing_parent_unmarked: u32, } impl OldYoungEdgeVerifyStats { #[inline] pub(super) fn record_missing(&mut self, parent: usize, slot: usize, child: usize) { + self.record_missing_diag(parent, slot, child, 0, 0, false, false); + } + + #[inline] + #[allow(clippy::too_many_arguments)] + pub(super) fn record_missing_diag( + &mut self, + parent: usize, + slot: usize, + child: usize, + parent_obj_type: u8, + child_obj_type: u8, + parent_is_old_arena: bool, + parent_marked: bool, + ) { self.missing_edges = self.missing_edges.saturating_add(1); + self.missing_by_parent_type[(parent_obj_type as usize) & 31] += 1; + self.missing_by_child_type[(child_obj_type as usize) & 31] += 1; + if !parent_is_old_arena { + self.missing_parent_malloc += 1; + } + if !parent_marked { + self.missing_parent_unmarked += 1; + } if self.first_missing.is_none() { self.first_missing = Some(OldYoungEdgeMissing { parent, slot, child, + parent_obj_type, + child_obj_type, + parent_is_old_arena, + parent_marked, }); } } diff --git a/crates/perry-runtime/src/gc/tests/barrier.rs b/crates/perry-runtime/src/gc/tests/barrier.rs index 3588408935..83daee0808 100644 --- a/crates/perry-runtime/src/gc/tests/barrier.rs +++ b/crates/perry-runtime/src/gc/tests/barrier.rs @@ -646,7 +646,9 @@ fn test_old_young_edge_verifier_trace_json_shape() { parent: 0x1111, slot: 0x2222, child: 0x3333, + ..Default::default() }), + ..Default::default() }; trace.record_phase("old_young_edge_verify", std::time::Duration::from_micros(7)); diff --git a/crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs b/crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs index 030ab96150..f4bcf6d7a2 100644 --- a/crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs +++ b/crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs @@ -28,6 +28,7 @@ fn test_copying_minor_promotes_survivor_on_fourth_survival() { #[test] fn test_copying_minor_preserves_old_page_accounting_for_defrag_policy() { + let _defrag = OldDefragTestEnable::new(); struct ResetGcTestState { pinned_header: *mut GcHeader, } diff --git a/crates/perry-runtime/src/gc/tests/cycle_state.rs b/crates/perry-runtime/src/gc/tests/cycle_state.rs index b355622978..186cacbb2d 100644 --- a/crates/perry-runtime/src/gc/tests/cycle_state.rs +++ b/crates/perry-runtime/src/gc/tests/cycle_state.rs @@ -842,6 +842,7 @@ fn bounded_minor_fallback_preserves_age_and_trace_fields() { #[test] fn budgeted_minor_fallback_ignores_forced_evacuation_and_stays_non_moving() { + let _defrag = OldDefragTestEnable::new(); let _guard = CopyingNurseryTestGuard::new(2); let _trigger_guard = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); let _force = EnvVarGuard::set("PERRY_GC_FORCE_EVACUATE", "1"); diff --git a/crates/perry-runtime/src/gc/tests/oldgen.rs b/crates/perry-runtime/src/gc/tests/oldgen.rs index 814e20ea31..e1dc0ca47a 100644 --- a/crates/perry-runtime/src/gc/tests/oldgen.rs +++ b/crates/perry-runtime/src/gc/tests/oldgen.rs @@ -689,6 +689,7 @@ fn test_old_page_defrag_skips_non_movable_buffer_and_typed_array() { #[test] fn test_old_page_defrag_re_remembers_young_child_after_collection_clear() { + let _defrag = OldDefragTestEnable::new(); struct ResetGcTestState; impl Drop for ResetGcTestState { diff --git a/crates/perry-runtime/src/gc/verify.rs b/crates/perry-runtime/src/gc/verify.rs index d453fb5ae2..72d59e49e8 100644 --- a/crates/perry-runtime/src/gc/verify.rs +++ b/crates/perry-runtime/src/gc/verify.rs @@ -470,7 +470,28 @@ pub(super) unsafe fn verify_old_young_slot_covered( stats.checked_old_to_young_edges = stats.checked_old_to_young_edges.saturating_add(1); let parent_addr = parent_header as usize; if !old_young_slot_covered(snapshot, parent_addr, slot) { - stats.record_missing(parent_addr, slot as usize, child_addr); + // gh #6206: record parent/child GC types so the panic below can + // print per-type histograms of the dropped edges. + let parent_obj_type = (*parent_header).obj_type; + let parent_marked = (*parent_header).gc_flags & (GC_FLAG_MARKED | GC_FLAG_PINNED) != 0; + let parent_user = (parent_header as *mut u8).add(GC_HEADER_SIZE) as usize; + let parent_is_old_arena = matches!( + crate::arena::classify_heap_generation(parent_user), + crate::arena::HeapGeneration::Old + ); + let child_obj_type = { + let ch = (child_addr as *const u8).sub(GC_HEADER_SIZE) as *const GcHeader; + (*ch).obj_type + }; + stats.record_missing_diag( + parent_addr, + slot as usize, + child_addr, + parent_obj_type, + child_obj_type, + parent_is_old_arena, + parent_marked, + ); } } @@ -495,19 +516,43 @@ pub(super) unsafe fn verify_old_young_parent_slots_covered( #[cold] pub(super) fn panic_old_young_edge_verifier_failed(stats: OldYoungEdgeVerifyStats) -> ! { let missing = stats.first_missing.unwrap_or_default(); + // gh #6206: readable per-type histograms of the missing edges. + let type_name = |t: u8| -> &'static str { gc_type_info(t).map_or("?", |i| i.name) }; + let mut parent_hist = String::new(); + let mut child_hist = String::new(); + for t in 0u8..32 { + let p = stats.missing_by_parent_type[t as usize]; + if p != 0 { + parent_hist.push_str(&format!(" {}({})={}", type_name(t), t, p)); + } + let c = stats.missing_by_child_type[t as usize]; + if c != 0 { + child_hist.push_str(&format!(" {}({})={}", type_name(t), t, c)); + } + } panic!( - "old-young-edge-verifier failed: checked_old_objects={} checked_remembered_pages={} checked_old_to_young_edges={} missing_edges={} first_missing_parent=0x{:x} first_missing_slot=0x{:x} first_missing_child=0x{:x}", + "old-young-edge-verifier failed: checked_old_objects={} checked_remembered_pages={} checked_old_to_young_edges={} missing_edges={} malloc_parents={} unmarked_parents={}\n first_missing: parent=0x{:x} type={}({}) old_arena={} marked={} slot=0x{:x} child=0x{:x} child_type={}({})\n missing_by_parent_type:{}\n missing_by_child_type:{}", stats.checked_old_objects, stats.checked_remembered_pages, stats.checked_old_to_young_edges, stats.missing_edges, + stats.missing_parent_malloc, + stats.missing_parent_unmarked, missing.parent, + type_name(missing.parent_obj_type), + missing.parent_obj_type, + missing.parent_is_old_arena, + missing.parent_marked, missing.slot, - missing.child + missing.child, + type_name(missing.child_obj_type), + missing.child_obj_type, + parent_hist, + child_hist, ); } -pub(super) fn verify_old_to_young_edges_covered() -> OldYoungEdgeVerifyStats { +pub(super) fn verify_old_to_young_edges_collect() -> OldYoungEdgeVerifyStats { let snapshot = remembered_dirty_snapshot(); let mut stats = OldYoungEdgeVerifyStats { checked_remembered_pages: snapshot.dirty_pages.len(), @@ -524,8 +569,27 @@ pub(super) fn verify_old_to_young_edges_covered() -> OldYoungEdgeVerifyStats { } } }); + stats +} + +pub(super) fn verify_old_to_young_edges_covered() -> OldYoungEdgeVerifyStats { + let stats = verify_old_to_young_edges_collect(); if stats.missing_edges != 0 { - panic_old_young_edge_verifier_failed(stats); + // gh #6206: this check runs at AtomicFinalize, AFTER the mark phase + // consumed the dirty logs, so its "missing" edges can be a + // measurement artifact. PERRY_GC_VERIFY_RS_NONFATAL=1 demotes it to + // a warning so the stale-forwarded-refs verifier (which runs later + // in the same cycle) can be reached. + use std::sync::OnceLock; + static NONFATAL: OnceLock = OnceLock::new(); + if *NONFATAL.get_or_init(|| std::env::var_os("PERRY_GC_VERIFY_RS_NONFATAL").is_some()) { + eprintln!( + "[gc-verify] old-young-edge-verifier (non-fatal): missing_edges={}", + stats.missing_edges + ); + } else { + panic_old_young_edge_verifier_failed(stats); + } } stats } diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index eebb1f7a20..10027afe86 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -947,7 +947,15 @@ unsafe fn map_set_string_key_value( return map; } + // See js_map_set: ensure_capacity can fire a MOVING minor; root the key + // (a heap string, which CAN move) and value across it and re-derive the + // `*const StringHeader` before boxing. gh #6206. + let scope = crate::gc::RuntimeHandleScope::new(); + let key_handle = scope.root_string_ptr(key); + let value_handle = scope.root_nanbox_f64(value); let grew = ensure_capacity(map); + let key = key_handle.get_raw_const_ptr::(); + let value = value_handle.get_nanbox_f64(); let size = (*map).size; let entries = entries_ptr_mut(map); if grew && size > 0 { @@ -1014,8 +1022,18 @@ pub extern "C" fn js_map_set(map: *mut MapHeader, key: f64, value: f64) -> *mut return map; } - // Key doesn't exist, append a new entry + // Key doesn't exist, append a new entry. `ensure_capacity` can fire a + // MOVING minor (via gc_note_external_side_alloc) — its "conservative + + // non-moving" comment is false under evacuation. `key`/`value` are held + // only in these native-stack params (a freshly-built, not-yet-inserted + // object is reachable via nothing else), which an evacuating minor does + // not scan, so root them across the grow and re-derive after. gh #6206. + let scope = crate::gc::RuntimeHandleScope::new(); + let key_handle = scope.root_nanbox_f64(key); + let value_handle = scope.root_nanbox_f64(value); let grew = ensure_capacity(map); + let key = key_handle.get_nanbox_f64(); + let value = value_handle.get_nanbox_f64(); let size = (*map).size; let entries = entries_ptr_mut(map); if grew && size > 0 {