From d795c8954f2b115997fadb7ba3b5c30e78b789f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 14 Aug 2026 21:35:22 +0200 Subject: [PATCH 1/2] fix(ci): restore the lint gate on main Three lint steps failed on a pristine `main` checkout, so a required status context could not distinguish a good pull request from a bad one and every merge went through admin bypass. - timer.rs had reached 2010 lines against the 2000-line cap. Its #[cfg(test)] scanner seeding/snapshot helpers move to timer/test_scanner_support.rs, re-exported by name because a glob does not propagate to the `crate::timer::...` call sites in the GC root-scanner tests. timer.rs is now 1866 lines. - property_set.rs already had a GC_STORE_AUDIT(POINTER_FREE) marker for its guarded raw-f64 class-field store, but a multi-line canonicalize_raw_f64_numeric_store_value call had pushed it 8 lines above the store, outside the inventory's +/-6 window. The marker moves against the store it describes; nothing about the store changes. - object/spill.rs (5 bare reads, ceiling 3) and json_tape.rs (22, ceiling 21) exceeded their raw-handle ceilings. Three reads convert to RuntimeHandle::across_mut rather than raising a ceiling, which is what raw_handle_debt_files.txt asks for: across object_meta_ensure and js_array_alloc_with_length_exact in spill.rs, and across the LazyArrayRooted safepoint in json_tape.rs. All three already reloaded the pointer afterwards, so this is the same discipline in the form the ratchet can count, and the pre-call address is no longer nameable. Baseline drops 998 -> 993. --update also tightened three ceilings carrying pre-existing slack (define_property.rs 3->2, reflect_support.rs 4->3, string/split.rs 10->7); no open pull request touches those files. Refs #8092 --- crates/perry-codegen/src/expr/property_set.rs | 6 +- crates/perry-runtime/src/json_tape.rs | 5 +- crates/perry-runtime/src/object/spill.rs | 9 +- crates/perry-runtime/src/timer.rs | 160 +----------------- .../src/timer/test_scanner_support.rs | 151 +++++++++++++++++ scripts/raw_handle_debt_baseline.txt | 2 +- scripts/raw_handle_debt_files.txt | 12 +- 7 files changed, 177 insertions(+), 168 deletions(-) create mode 100644 crates/perry-runtime/src/timer/test_scanner_support.rs diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index de87b5590d..8a62c5ffc1 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -1464,14 +1464,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { // Guarded raw-f64 slots are pointer-free by typed // shape descriptor; non-number writes miss the // guard and use the boxed setter fallback. - // GC_STORE_AUDIT(POINTER_FREE): typed raw-f64 class - // slots contain numbers only. let blk = ctx.block(); let numeric_value = canonicalize_raw_f64_numeric_store_value( blk, &val_double, ); + // GC_STORE_AUDIT(POINTER_FREE): typed raw-f64 class + // slots contain numbers only. Kept adjacent to the + // store: the inventory only scans +/-6 lines, and the + // multi-line canonicalize call pushed it out of range. blk.store(DOUBLE, &numeric_value, &field_ptr); Some(numeric_value) } else { diff --git a/crates/perry-runtime/src/json_tape.rs b/crates/perry-runtime/src/json_tape.rs index 1c0c1ef7a0..3e4bc0bcfd 100644 --- a/crates/perry-runtime/src/json_tape.rs +++ b/crates/perry-runtime/src/json_tape.rs @@ -1271,8 +1271,9 @@ pub unsafe fn alloc_lazy_array( (*hdr).cumulative_walk_steps = 0; (*hdr).sequential_streak = 0; let hdr_handle = scope.root_raw_mut_ptr(hdr); - json_tape_safepoint(JsonTapeSafepoint::LazyArrayRooted, hdr as usize); - let hdr = hdr_handle.get_raw_mut_ptr::(); + let (_, hdr) = hdr_handle.across_mut::(|| { + json_tape_safepoint(JsonTapeSafepoint::LazyArrayRooted, hdr as usize) + }); (*hdr).blob_str = blob_handle.get_raw_const_ptr::(); note_lazy_raw_slot( hdr, diff --git a/crates/perry-runtime/src/object/spill.rs b/crates/perry-runtime/src/object/spill.rs index 03bed6666c..884f388e77 100644 --- a/crates/perry-runtime/src/object/spill.rs +++ b/crates/perry-runtime/src/object/spill.rs @@ -180,16 +180,15 @@ pub(crate) fn reserve_object_spill(obj_ptr: usize, field_count: u32) { let scope = crate::gc::RuntimeHandleScope::new(); let obj_handle = scope.root_raw_mut_ptr(obj); - object_meta_ensure(obj); - - let obj = obj_handle.get_raw_mut_ptr::(); + let (_, obj) = obj_handle.across_mut::(|| object_meta_ensure(obj)); let meta = (*obj).meta; if (*meta).spill != 0 { return; } - let spill = crate::array::js_array_alloc_with_length_exact(field_count); - let obj = obj_handle.get_raw_mut_ptr::(); + let (spill, obj) = obj_handle.across_mut::(|| { + crate::array::js_array_alloc_with_length_exact(field_count) + }); let meta = (*obj).meta; if (*meta).spill == 0 { (*meta).spill = spill as u64; diff --git a/crates/perry-runtime/src/timer.rs b/crates/perry-runtime/src/timer.rs index b58601a5fd..694718ae76 100644 --- a/crates/perry-runtime/src/timer.rs +++ b/crates/perry-runtime/src/timer.rs @@ -390,6 +390,14 @@ mod ownership; mod ref_states; #[cfg(test)] // #7680: not re-exported; reach via `crate::timer::test_shared_queues::` pub(crate) mod test_shared_queues; +#[cfg(test)] +mod test_scanner_support; +#[cfg(test)] +pub(crate) use test_scanner_support::{ + test_callback_timer_snapshot, test_clear_all_timer_scanner_roots, + test_clear_timer_scanner_roots, test_seed_many_timeout_roots, + test_seed_timer_scanner_roots, test_timer_scanner_snapshot, +}; pub(crate) use ownership::purge_agent_timers; use ownership::{has_refed_callback_timer, has_refed_interval_timer, has_refed_promise_timer}; @@ -1742,158 +1750,6 @@ impl TimerRootScanState { } } -#[cfg(test)] -const TEST_CALLBACK_TIMER_ID: i64 = i64::MIN + 101; -#[cfg(test)] -const TEST_INTERVAL_TIMER_ID: i64 = i64::MIN + 102; - -#[cfg(test)] -#[derive(Debug, Default)] -pub(crate) struct TestTimerScannerSnapshot { - pub timeout_promise_ptr: usize, - pub timeout_value_bits: u64, - pub callback_ptr: usize, - pub callback_arg_bits: u64, - pub callback_context_store_bits: u64, - pub interval_callback_ptr: usize, - pub interval_context_store_bits: u64, -} - -#[cfg(test)] -pub(crate) fn test_seed_timer_scanner_roots( - promise: *mut Promise, - value: f64, - callback: i64, - arg: f64, - context_store: f64, -) { - let context = crate::async_context::test_snapshot_with_store(context_store); - let deadline = Instant::now() + Duration::from_secs(86_400); - TIMER_QUEUE.lock().unwrap().push(Timer { - // #6185: test scaffolding runs on the primary agent. - owner: crate::agent::current_agent(), - deadline, - promise, - value, - has_ref: true, - }); - CALLBACK_TIMERS.lock().unwrap().push(CallbackTimer { - // #6185: test scaffolding runs on the primary agent. - owner: crate::agent::current_agent(), - id: TEST_CALLBACK_TIMER_ID, - kind: CallbackTimerKind::Timeout, - deadline, - delay_ms: 86_400_000, - callback, - args: vec![arg], - context: context.clone(), - async_id: 0, - trigger_async_id: 0, - cleared: false, - }); - INTERVAL_TIMERS.lock().unwrap().push(IntervalTimer { - // #6185: test scaffolding runs on the primary agent. - owner: crate::agent::current_agent(), - id: TEST_INTERVAL_TIMER_ID, - callback, - interval_ms: 86_400_000, - next_deadline: deadline, - args: Vec::new(), - context, - cleared: false, - }); -} - -#[cfg(test)] -pub(crate) fn test_seed_many_timeout_roots(values: &[f64]) { - let deadline = Instant::now() + Duration::from_secs(86_400); - let mut q = TIMER_QUEUE.lock().unwrap(); - q.clear(); - for &value in values { - q.push(Timer { - // #6185: test scaffolding runs on the primary agent. - owner: crate::agent::current_agent(), - deadline, - promise: std::ptr::null_mut(), - value, - has_ref: true, - }); - } -} - -#[cfg(test)] -pub(crate) fn test_clear_all_timer_scanner_roots() { - TIMER_QUEUE.lock().unwrap().clear(); - CALLBACK_TIMERS.lock().unwrap().clear(); - INTERVAL_TIMERS.lock().unwrap().clear(); -} - -#[cfg(test)] -pub(crate) fn test_timer_scanner_snapshot() -> TestTimerScannerSnapshot { - let mut snapshot = TestTimerScannerSnapshot::default(); - if let Some(timer) = TIMER_QUEUE.lock().unwrap().last() { - snapshot.timeout_promise_ptr = timer.promise as usize; - snapshot.timeout_value_bits = timer.value.to_bits(); - } - if let Some(timer) = CALLBACK_TIMERS - .lock() - .unwrap() - .iter() - .find(|timer| timer.id == TEST_CALLBACK_TIMER_ID) - { - snapshot.callback_ptr = timer.callback as usize; - snapshot.callback_arg_bits = timer.args.first().copied().map(f64::to_bits).unwrap_or(0); - snapshot.callback_context_store_bits = - crate::async_context::test_snapshot_first_store(&timer.context) - .map(f64::to_bits) - .unwrap_or(0); - } - if let Some(timer) = INTERVAL_TIMERS - .lock() - .unwrap() - .iter() - .find(|timer| timer.id == TEST_INTERVAL_TIMER_ID) - { - snapshot.interval_callback_ptr = timer.callback as usize; - snapshot.interval_context_store_bits = - crate::async_context::test_snapshot_first_store(&timer.context) - .map(f64::to_bits) - .unwrap_or(0); - } - snapshot -} - -#[cfg(test)] -pub(crate) fn test_callback_timer_snapshot(timer_id: i64) -> Option<(usize, u64)> { - CALLBACK_TIMERS - .lock() - .unwrap() - .iter() - .find(|timer| timer.id == timer_id) - .map(|timer| { - ( - timer.callback as usize, - timer.args.first().copied().map(f64::to_bits).unwrap_or(0), - ) - }) -} - -#[cfg(test)] -pub(crate) fn test_clear_timer_scanner_roots(promise_before: usize, promise_after: usize) { - TIMER_QUEUE.lock().unwrap().retain(|timer| { - let promise = timer.promise as usize; - promise != promise_before && promise != promise_after - }); - CALLBACK_TIMERS - .lock() - .unwrap() - .retain(|timer| timer.id != TEST_CALLBACK_TIMER_ID); - INTERVAL_TIMERS - .lock() - .unwrap() - .retain(|timer| timer.id != TEST_INTERVAL_TIMER_ID); -} - #[cfg(test)] mod drain_expired_tests { use super::drain_expired_timers; diff --git a/crates/perry-runtime/src/timer/test_scanner_support.rs b/crates/perry-runtime/src/timer/test_scanner_support.rs new file mode 100644 index 0000000000..a2db305c3d --- /dev/null +++ b/crates/perry-runtime/src/timer/test_scanner_support.rs @@ -0,0 +1,151 @@ +//! Test-only seeding and snapshot helpers for the timer root scanners. +//! +//! Split out of `timer.rs` to keep that file under the 2000-line cap +//! (`scripts/check_file_size.sh`). These are `#[cfg(test)]` support routines +//! reached as `crate::timer::` from the GC root-scanner tests; the +//! parent re-exports each one by name. + +use super::*; + +const TEST_CALLBACK_TIMER_ID: i64 = i64::MIN + 101; +const TEST_INTERVAL_TIMER_ID: i64 = i64::MIN + 102; + +#[derive(Debug, Default)] +pub(crate) struct TestTimerScannerSnapshot { + pub timeout_promise_ptr: usize, + pub timeout_value_bits: u64, + pub callback_ptr: usize, + pub callback_arg_bits: u64, + pub callback_context_store_bits: u64, + pub interval_callback_ptr: usize, + pub interval_context_store_bits: u64, +} + +pub(crate) fn test_seed_timer_scanner_roots( + promise: *mut Promise, + value: f64, + callback: i64, + arg: f64, + context_store: f64, +) { + let context = crate::async_context::test_snapshot_with_store(context_store); + let deadline = Instant::now() + Duration::from_secs(86_400); + TIMER_QUEUE.lock().unwrap().push(Timer { + // #6185: test scaffolding runs on the primary agent. + owner: crate::agent::current_agent(), + deadline, + promise, + value, + has_ref: true, + }); + CALLBACK_TIMERS.lock().unwrap().push(CallbackTimer { + // #6185: test scaffolding runs on the primary agent. + owner: crate::agent::current_agent(), + id: TEST_CALLBACK_TIMER_ID, + kind: CallbackTimerKind::Timeout, + deadline, + delay_ms: 86_400_000, + callback, + args: vec![arg], + context: context.clone(), + async_id: 0, + trigger_async_id: 0, + cleared: false, + }); + INTERVAL_TIMERS.lock().unwrap().push(IntervalTimer { + // #6185: test scaffolding runs on the primary agent. + owner: crate::agent::current_agent(), + id: TEST_INTERVAL_TIMER_ID, + callback, + interval_ms: 86_400_000, + next_deadline: deadline, + args: Vec::new(), + context, + cleared: false, + }); +} + +pub(crate) fn test_seed_many_timeout_roots(values: &[f64]) { + let deadline = Instant::now() + Duration::from_secs(86_400); + let mut q = TIMER_QUEUE.lock().unwrap(); + q.clear(); + for &value in values { + q.push(Timer { + // #6185: test scaffolding runs on the primary agent. + owner: crate::agent::current_agent(), + deadline, + promise: std::ptr::null_mut(), + value, + has_ref: true, + }); + } +} + +pub(crate) fn test_clear_all_timer_scanner_roots() { + TIMER_QUEUE.lock().unwrap().clear(); + CALLBACK_TIMERS.lock().unwrap().clear(); + INTERVAL_TIMERS.lock().unwrap().clear(); +} + +pub(crate) fn test_timer_scanner_snapshot() -> TestTimerScannerSnapshot { + let mut snapshot = TestTimerScannerSnapshot::default(); + if let Some(timer) = TIMER_QUEUE.lock().unwrap().last() { + snapshot.timeout_promise_ptr = timer.promise as usize; + snapshot.timeout_value_bits = timer.value.to_bits(); + } + if let Some(timer) = CALLBACK_TIMERS + .lock() + .unwrap() + .iter() + .find(|timer| timer.id == TEST_CALLBACK_TIMER_ID) + { + snapshot.callback_ptr = timer.callback as usize; + snapshot.callback_arg_bits = timer.args.first().copied().map(f64::to_bits).unwrap_or(0); + snapshot.callback_context_store_bits = + crate::async_context::test_snapshot_first_store(&timer.context) + .map(f64::to_bits) + .unwrap_or(0); + } + if let Some(timer) = INTERVAL_TIMERS + .lock() + .unwrap() + .iter() + .find(|timer| timer.id == TEST_INTERVAL_TIMER_ID) + { + snapshot.interval_callback_ptr = timer.callback as usize; + snapshot.interval_context_store_bits = + crate::async_context::test_snapshot_first_store(&timer.context) + .map(f64::to_bits) + .unwrap_or(0); + } + snapshot +} + +pub(crate) fn test_callback_timer_snapshot(timer_id: i64) -> Option<(usize, u64)> { + CALLBACK_TIMERS + .lock() + .unwrap() + .iter() + .find(|timer| timer.id == timer_id) + .map(|timer| { + ( + timer.callback as usize, + timer.args.first().copied().map(f64::to_bits).unwrap_or(0), + ) + }) +} + +pub(crate) fn test_clear_timer_scanner_roots(promise_before: usize, promise_after: usize) { + TIMER_QUEUE.lock().unwrap().retain(|timer| { + let promise = timer.promise as usize; + promise != promise_before && promise != promise_after + }); + CALLBACK_TIMERS + .lock() + .unwrap() + .retain(|timer| timer.id != TEST_CALLBACK_TIMER_ID); + INTERVAL_TIMERS + .lock() + .unwrap() + .retain(|timer| timer.id != TEST_INTERVAL_TIMER_ID); +} diff --git a/scripts/raw_handle_debt_baseline.txt b/scripts/raw_handle_debt_baseline.txt index 806adbfbe9..783646df5f 100644 --- a/scripts/raw_handle_debt_baseline.txt +++ b/scripts/raw_handle_debt_baseline.txt @@ -1 +1 @@ -998 +993 diff --git a/scripts/raw_handle_debt_files.txt b/scripts/raw_handle_debt_files.txt index 99fa343bc1..65b162873a 100644 --- a/scripts/raw_handle_debt_files.txt +++ b/scripts/raw_handle_debt_files.txt @@ -64,13 +64,16 @@ 3 crates/perry-runtime/src/gc/tests/copying.rs 4 crates/perry-runtime/src/gc/tests/helper_stores.rs 47 crates/perry-runtime/src/gc/tests/runtime_roots/callback_scanners.rs +4 crates/perry-runtime/src/gc/tests/runtime_roots/fs_options_object.rs 3 crates/perry-runtime/src/gc/tests/runtime_roots/hook_dispatch_handles.rs +3 crates/perry-runtime/src/gc/tests/runtime_roots/json_shape_template.rs 1 crates/perry-runtime/src/gc/tests/runtime_roots/string_slice.rs 10 crates/perry-runtime/src/gc/tests/runtime_roots/transient_handles.rs 11 crates/perry-runtime/src/json/replacer.rs 26 crates/perry-runtime/src/json/reviver.rs 3 crates/perry-runtime/src/json/stringify.rs 1 crates/perry-runtime/src/json/stringify_scalars.rs +2 crates/perry-runtime/src/json/stringify_shape_template.rs 21 crates/perry-runtime/src/json_tape.rs 26 crates/perry-runtime/src/map.rs 2 crates/perry-runtime/src/module_require.rs @@ -103,12 +106,12 @@ 19 crates/perry-runtime/src/object/native_module/async_hooks_exports.rs 26 crates/perry-runtime/src/object/native_module/callable_exports.rs 6 crates/perry-runtime/src/object/object_literal_ops.rs -3 crates/perry-runtime/src/object/object_ops/define_property.rs +2 crates/perry-runtime/src/object/object_ops/define_property.rs 3 crates/perry-runtime/src/object/object_ops/descriptor_helpers.rs 4 crates/perry-runtime/src/object/object_ops/from_entries.rs 5 crates/perry-runtime/src/object/object_ops/keys_array.rs 6 crates/perry-runtime/src/object/polymorphic_index.rs -4 crates/perry-runtime/src/object/reflect_support.rs +3 crates/perry-runtime/src/object/reflect_support.rs 3 crates/perry-runtime/src/object/spill.rs 2 crates/perry-runtime/src/object/typed_array_define.rs 3 crates/perry-runtime/src/os.rs @@ -132,7 +135,7 @@ 2 crates/perry-runtime/src/string/append.rs 7 crates/perry-runtime/src/string/concat.rs 1 crates/perry-runtime/src/string/mod.rs -10 crates/perry-runtime/src/string/split.rs +7 crates/perry-runtime/src/string/split.rs 2 crates/perry-runtime/src/symbol/iterator.rs 27 crates/perry-runtime/src/thread.rs 7 crates/perry-runtime/src/timer.rs @@ -151,6 +154,3 @@ 19 crates/perry-runtime/src/wasi.rs 6 crates/perry-runtime/src/weakref.rs 23 crates/perry-runtime/src/webassembly.rs -3 crates/perry-runtime/src/gc/tests/runtime_roots/json_shape_template.rs -2 crates/perry-runtime/src/json/stringify_shape_template.rs -4 crates/perry-runtime/src/gc/tests/runtime_roots/fs_options_object.rs From 6b722cee939acec9c85f23233ff312b562ca7194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 14 Aug 2026 21:44:30 +0200 Subject: [PATCH 2/2] chore(changelog): add fragment for #8093 --- changelog.d/8093-restore-lint-on-main.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 changelog.d/8093-restore-lint-on-main.md diff --git a/changelog.d/8093-restore-lint-on-main.md b/changelog.d/8093-restore-lint-on-main.md new file mode 100644 index 0000000000..897307e034 --- /dev/null +++ b/changelog.d/8093-restore-lint-on-main.md @@ -0,0 +1,8 @@ +### Fixed + +- Restore the `lint` gate on `main`. Three of its steps were failing on a pristine checkout, so the required context could not distinguish a good pull request from a bad one and every merge went through admin bypass (#8092, #8093). + - `crates/perry-runtime/src/timer.rs` had reached 2010 lines against the 2000-line cap. Its `#[cfg(test)]` scanner seeding/snapshot helpers move to `timer/test_scanner_support.rs`, re-exported by name (a glob would not propagate to the `crate::timer::…` call sites in the GC root-scanner tests). `timer.rs` is now 1866 lines. + - `crates/perry-codegen/src/expr/property_set.rs` had a `GC_STORE_AUDIT(POINTER_FREE)` marker for its guarded raw-f64 class-field store, but a multi-line `canonicalize_raw_f64_numeric_store_value` call had pushed the marker 8 lines above the store — outside the inventory's ±6-line window. The marker moves to sit against the store it describes. No change to what is stored or barriered. + - `object/spill.rs` (5 bare reads against a ceiling of 3) and `json_tape.rs` (22 against 21) exceeded their raw-handle ceilings. Three reads convert to `RuntimeHandle::across_mut`, which is the conversion `raw_handle_debt_files.txt` asks for rather than a raised ceiling: in `spill.rs` across `object_meta_ensure` and `js_array_alloc_with_length_exact`, and in `json_tape.rs` across the `LazyArrayRooted` safepoint. Each already reloaded the pointer afterwards, so this is the same discipline expressed in the form the ratchet can count; the combinator additionally makes the pre-call address unnameable. + +The raw-handle baseline drops 998 → 993. `--update` also tightened three ceilings that were carrying pre-existing slack (`object_ops/define_property.rs` 3→2, `reflect_support.rs` 4→3, `string/split.rs` 10→7); none of those files is touched by an open pull request.