Skip to content

fix(runtime): clear the gates that #12109 left red - #12121

Draft
steinybot wants to merge 27 commits into
PerryTS:mainfrom
steinybot:steiny/ci2-main-fallout
Draft

steinybot wants to merge 27 commits into
PerryTS:mainfrom
steinybot:steiny/ci2-main-fallout

Conversation

@steinybot

@steinybot steinybot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Merge in this order:

  1. fix(docs): the my-bindings fixture reads buffers inside a no-GC scope #12112
  2. fix(ext): ext-crate tests root values through RuntimeHandleScope #12113
  3. fix(runtime): audit the new raw stores and classify addresses with addr_class #12114
  4. ci: register the dark GC witnesses and isolate the new test counters #12115
  5. refactor: split five files back under the 2000-line cap #12116
  6. fix(runtime): root raw handles across calls and correct the handle ratchets #12117
  7. fix(runtime): instanceof never mints a class object for a builtin id #12118
  8. fix(gc_effects): exempt aHash's random-source init from the indirect-call taint #12119
  9. docs: regenerate the API reference #12120
  10. fix(runtime): clear the gates that #12109 left red #12121 (this PR)
  11. fix(runtime): match node on the gap-suite regressions #12149

Problem

CI on main is red again after #12025. Commits since then broke check, warnings, cargo-test, gc-call-effects, self-test-checkers, gap-suite and fourteen lint steps, so no PR can show a green build. Each later failure in a job was hidden behind an earlier one.

Solution

Fix each breakage in its own PR. Each PR targets main and holds every fix above it in the order list, so the last PR is the one that can run fully green. Merge in order: each diff then shrinks to its own fix.

In this PR

Fixes the checks that #12109 (node:sqlite on native payloads) broke after the rest of this chain was written, the thread-local ratchet in self-test-checkers, the generated tables that only CI can build, and the flaky z8 stream test.

Changes

  1. See one-line per_test_global! statics in the holder census (diff). The census matched a static only at the start of a line, so the macro form was invisible although it is a real static outside tests. The thread-exit audit shares the census and now sees the timer STORE map, whose entry names its exit hook.

    r"^\s*(?:(?:crate::|\$crate::)?per_test_global!\s*\(\s*)?"
    r"(?:#\[[^\]]*\]\s*)*(?:pub(?:\([^)]*\))?\s+)?static\s+(?:ref\s+)?(?:mut\s+)?"

  2. Move the hidden JS-state group out of native_payload.rs (diff). It brings the file under the 2000-line cap. The public paths are re-exported.

  3. Let the ledger floor follow the ledger (diff). node:sqlite on native payloads; cheaper native callback path; statements own their sqlite3_stmt #12109 removed three sqlite handle tables, so the census fell below the fixed floor of 200. The census may not drop below 90% of the recorded totals or below 150.

    # The census floor guards against a scan that silently stops matching, not
    # against conversions: #11919 removes tables and producers on purpose, so the
    # floor follows the reviewed ledger rather than a fixed count. A fall of more
    # than a tenth below the recorded totals needs a reviewed --update first. The
    # absolute minimum stops an --update run by a broken scan from lowering the
    # ledger, and with it the relative floor, to nothing.
    CENSUS_LEDGER_FRACTION = (9, 10)
    CENSUS_MINIMUM = (150, 150)
    def census_too_small(census: tuple[int, int], recorded: tuple[int, int]) -> bool:
    num, den = CENSUS_LEDGER_FRACTION
    return any(
    actual < minimum or actual * den < ledger * num
    for actual, ledger, minimum in zip(census, recorded, CENSUS_MINIMUM)
    )

  4. Classify four holders that lost accidental coverage (diff). They were covered only through the deleted sqlite scanner's bare-name path. Each is a flag, a counter or a weak address-keyed table.

  5. Root the native-payload test handles (diff). The raw handles pass through with_mut_ptr and with_const_ptr. keys_array.rs guards with is_handle_band, and the warnings fixes drop an unsafe block and two unused imports.

  6. Move the new thread-locals onto the hot cache (diff). Fourteen raw thread_local! declarations become perry_thread_local!. The two in alloc_census.rs stay raw and are recorded: the allocator hook reads them before the runtime's TLS exists.

  7. Regenerate the wasm32 ABI table (diff). It lacked the scoped byte API and native-payload stream entry points.

  8. Take CI's fresh Windows call-effects table (diff). The Windows archives cross-build with cargo-xwin, so the table comes from the CI job's output.

  9. Bound what z8 retains after a collection, not RSS (diff). z8 failed on main too: mimalloc commits and purges in 5 to 11 MB steps, and the test bounded RSS at 4 MiB. It now bounds the thread's retained heap at 64 KiB above the warm batch, and a new retain_chunk sabotage proves it still fails on a leak.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7

steinybot and others added 2 commits October 7, 2026 10:39
read_buffer_bytes takes a NoGc scope since PerryTS#12103, so the fixture no
longer compiled, and the warnings and check jobs stopped there. The
fixture's tests also call runtime-backed perry-ffi functions, so they
take perry-ffi with runtime-link as a dev-dependency, like the ext
crates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
The runtime does not export the shadow-stack entry points on native
stack-map targets. The http, net, streams and ws test modules called
them, so they no longer compiled and the warnings job failed.

On those targets the guards' shadow frames rooted nothing. The net and
http tests now hold the values that must survive a collection in a
RuntimeHandleScope. The guards call gc_init(), which registers the
runtime-handle scanner that program startup would register.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
@coderabbitai

coderabbitai Bot commented Oct 6, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

steinybot and others added 15 commits October 7, 2026 11:42
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
…dr_class

The GC store-site, address-classification and SSO inventories failed
on main.

- Store sites: the new raw writes get audit markers. None of them
  stores a GC reference in a heap slot, so no barrier was missing.
- Address classes: the new GcHeader casts use the gc and addr_class
  helpers, and the clone writer's handle floors use is_handle_band.
- SSO: the stream emitter's shape fast paths read a key's header
  through heap_string_header, which declines a short string.

The clone writer now returns an empty string for a STRING_TAG payload
in the handle band, where it read memory before. The method-site
helpers now decline a misaligned or out-of-range address before they
read a header. Both baselines go down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Four lint steps failed on main.

- Local binding audit: receiver_kind reads a typed-array hint. The
  emitted guard checks the receiver's kind at run time, so a wrong
  hint only loses the fast path. The allowlist records it as
  runtime-validated.
- Test registration: the checker now accepts a #[path] declaration
  in a sibling file. native_payload_lifecycle.rs was declared that way
  and its tests already ran.
- Test registration: three test_gap_gc fixtures join the GC corpus.
  The fixture-env parser accepts PERRY_GC_PROTECT_OLD_SWEEP, which
  two of them set. protected_poll_reclaim gets the parity-env line
  that its header describes, so its collections now run.
- Per-test global sinks: the new test counters use per_test_global!.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Each split moves whole functions or inline test modules into a child
file. Module paths and test names do not change.

- dynamic_dispatch.rs: build_direct_method_args moves to
  dynamic_dispatch_args.rs.
- node_stream_readwrite.rs: the chunk and byte collectors move to
  node_stream_chunk_collect.rs.
- method_site.rs: prime_inherited and the constfn tests move to child
  files.
- delete_rest.rs and read_holder.rs: their inline test modules move
  to child files.

The buffer-layout and address-class baselines follow the moved
node_stream lines. Their totals do not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
…tchets

Six ratchet steps failed on main.

- Raw-handle debt: each flagged site now uses with_*_ptr for a call
  that cannot collect, or RuntimeHandle::across_* where an allocating
  call comes before a re-read. The baseline falls from 861 to 848.
- iterator_push_pending read a raw promise and iterator after
  js_array_alloc and js_array_push_f64. It now roots both values and
  re-reads them after each call.
- Native-handle ledger: the zlib Statics rule required a field that
  no longer exists, so it counted 0 tables. It now counts the streams
  and listeners maps, which are keyed by i64 handles.
- Unrooted locals: the collection-point matcher matched "gc(" inside
  no_gc(. Names now match only at the start of an identifier. The
  self-test plants both cases.
- Thread-exit globals and registry lifetime: delete the entries for
  INLINE_OWNING_U32_CACHE and VERDICT_CLASSES, which no longer exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
The perry-runtime test binary aborted in the stream instanceof test, so
cargo-test stopped before most suites ran.

- instanceof: live_chain_answer and the native-subclass static arm
  passed builtin class ids, such as PassThrough's, to
  class_constructor_ref_value. Only a compiled class has a class
  function object. Both now check is_compiled_class_id and fall back
  to the declared chain walk. A release build no longer mints a bogus
  pinned closure for a builtin id.
- Regex program cells: init_program_cell wrote None through a
  temporary after it zeroed the payload, which copied stack bytes into
  the cell. It writes a field only if the field does not already read
  as None.
- The residual-prototype relocation test registers the object-model
  scanners. Its setPrototypeOf calls fill the canonical-keys trie, and
  without a scanner a copying minor left stale key cells. CI crashed
  on this intermittently.
- Stale tests: the thread-exit closure and symbol side-table tests
  follow PerryTS#11931, and the macOS length test follows PerryTS#10694.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
The guarded writes in init_program_cell sit outside the window of the
existing POINTER_FREE marker, so the store-site inventory flagged
them. They fill a fresh, unpublished cell whose fields hold no GC
pointer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
…call taint

Since PerryTS#11992, NATIVE_CALLABLE_EXPORTS builds an ahash::RandomState in
its thread-local initializer, and every HotKey slow path reaches that
initializer. aHash's RAND_SOURCE OnceBox init makes one indirect call.
The call drops the losing Box<dyn RandomSource>, which is always a
DefaultRandomSource with no drop glue. The classifier treated the call
as unknown, so about 740 macOS symbols became Reenters, js_arena_alloc
among them.

A new indirect rule exempts that one instantiation. A forbid rule on
set_random_source keeps the exemption sound. The thread-creation rule
also matches Arc's new alloc::rcs::arc path, which the nightly bump
renamed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
The table is regenerated from this branch's release archives with
scripts/gc_call_effects/regen.sh macos-aarch64. With the aHash rule,
js_arena_alloc stays AllocOnly and 809 symbols stay Leaf. The changed
rows come from runtime code that changed after the last regeneration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
On linux-x86_64 the NATIVE_CALLABLE_EXPORTS taint reaches a different
aHash initializer: OnceBox<[[u64; 4]; 2]>::init for the fixed seeds.
get_fixed_seeds fills that table from getrandom, and the register call
in init is that getrandom or dealloc path. CI's fresh Linux table
still had AllocOnly at 3 and js_arena_alloc as Reenters without this
rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
steinybot and others added 7 commits October 7, 2026 12:09
docs/src/api/reference.md is empty on main, so the check job's API docs
drift step fails. scripts/regen_api_docs.sh output restores it from the
manifest. docs/api/perry.d.ts does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
The holder census matched a static only when the line started with
`static`. A one-line `per_test_global!(static X: T = ...)` was
invisible, although it expands to a real static outside tests. That
made the iterator_close CALLS entry read as stale. DECL now accepts
the macro prefix, and the self-test plants that form.

The thread-exit audit shares the census, so it now sees the timer
STORE map. Its entry names purge_agent_timers, which retire_agent
calls to drop the exiting agent's partition.

GC_INIT_DONE's frontier pin is deleted. The ext-http test guard now
calls gc_init, so the bare-name graph reaches it. The holder is a
Cell<bool> and never needed coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
PerryTS#12109 (node:sqlite on native payloads) failed six checks on main.

- File size: native_payload.rs's hidden JS-state group moves to the
  native_payload_state.rs child. The public paths are re-exported.
- Address classes: install_own_builtin_accessor guards with
  is_handle_band. The guard also rejects 0x10000..0x100000, where no
  heap object lives.
- Raw-handle debt: the native-payload tests and native_payload_birth
  pass handles through with_mut_ptr and with_const_ptr.
- Native-handle ledger: PerryTS#12109 removed three sqlite handle tables, so
  the census fell below the fixed floor of 200. The floor now follows
  the ledger: the census may not drop below 90% of the recorded totals
  or below 150. The self-test plants a collapse and a 20% drop.
- GC holders: four holders lost their accidental coverage with the
  sqlite scanner. Each gets a researched not_a_gc_pointer verdict.
- Warnings: drop an unneeded unsafe block and two unused imports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
…12121)

The thread-local policy ratchet in self-test-checkers failed on main.
Fourteen raw thread_local! declarations become perry_thread_local!, so
their address comes from the per-thread hot cache. The two
alloc_census.rs declarations stay raw and are recorded: the allocator
hook reads them before the runtime's TLS exists, and a HotKey's first
touch would allocate and re-enter that hook.

The async_context.rs and builtin_closure_metadata.rs counts are
re-recorded lower. The PASS1_MARKED holder pin follows the edited
allocation_pacing.rs after a re-audit of its window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
The table lacked the js_perry_bytes_* and js_perry_stream_* entry
points that the scoped byte API and native-payload streams added, so
the lint job's ABI consistency step failed. This is the output of
scripts/runtime_abi_check.py --emit-wasm-abi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
…2121)

The table is CI's fresh output from the gc-call-effects
(windows-x86_64) job on this chain. That job cross-builds the archives
with cargo-xwin, which this branch's author cannot run locally. The
committed table had 13 symbols in a weaker class than the archives
prove.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7
@steinybot
steinybot force-pushed the steiny/ci2-main-fallout branch from 3c278fd to e9db03f Compare October 6, 2026 23:13
steinybot and others added 2 commits October 7, 2026 12:32
…TS#12121)

z8 failed on main and on this chain. It bounded anonymous RSS on a
process that had run about 5000 tests. mimalloc commits and purges
memory in 5 to 11 MB steps on its own schedule. CI showed a
one-time 6.4 MB step, then a fall. On macOS the RSS probe read 0, so
the test could not fail there.

The test now bounds the thread's retained heap after a full
collection: arena bytes, external bytes and payload-reported native
bytes. The peak may sit less than 64 KiB above the warm batch, so a
leak of 44 bytes per stream fails. A new retain_chunk sabotage keeps
every finished chunk alive while every counter stays green, and the
bound turns the test red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V5MSmMLJm6sMdSS5r64ds7

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant