Skip to content

fix(lint): clear the GC store-site and address-classification gates (#7258, #7259) - #7273

Merged
proggeramlug merged 3 commits into
mainfrom
fix/7258-7259-lint-gates
Aug 2, 2026
Merged

fix(lint): clear the GC store-site and address-classification gates (#7258, #7259)#7273
proggeramlug merged 3 commits into
mainfrom
fix/7258-7259-lint-gates

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Clears steps 12 and 13 of the lint job. Both scripts exit 0 locally; both exit 1 on origin/main.

Fixes #7258. Fixes #7259.

lint runs its steps in order and stops at the first failure, and step 9 (public benchmark evidence freshness, #7257) fails first — so steps 12 and 13 have never actually executed in CI and neither failure appears in any Actions log. Both were reproduced by running the scripts directly.

Triage

Each finding was placed in one of three buckets before being touched.

#7258 — GC store-site inventory (19 findings)

5 were phantom — a rule defect. classify_rust_store searched the atomic-store regexes against call_window (a 6-line forward window) without anchoring, so one cache.store(...) was re-reported from each of the five lines above it. The reported "store sites" included continue; and a bare }. anchored_search now requires the match to begin inside the head line, which keeps the split-call coverage the window exists for (CACHE.store();) and reports each site exactly once, at the line that contains it.

3 were legitimate new runtime sites needing an audit class. All arrived after the gate was written:

site origin class why it is safe
array/alloc.rs:55 #7138 INIT TAG_HOLE fill of a just-allocated array not yet reachable from any root; a non-pointer sentinel, so no old value to remember and no edge to record
array/push_pop.rs:101 #7138 INIT same, for the freshly grown array's added [old_capacity, new_capacity) slack — storage this allocation has never published
array/indexing.rs:111 #7071 ROOT this is the collector's root-rewrite of a registered side-table slot, mutator stopped; visit_usize_slot returns true only when it relocated the object, and the value written is the visitor's own to-space address

11 were codegen stores. Four (index.rs ×3, index_set.rs) already carried a GC_STORE_AUDIT(POINTER_FREE) marker — #6915's value_is_canonical_raw_f64 branch split pushed the else-branch store 9–10 lines below it, outside the ±6-line window. The other seven had prose rationale but not the canonical marker form:

  • instance_misc1.rs, property_set.rs — reached only when emit_plain_finite_number_check proved the value's exponent is not all-ones. Every NaN-box tag (INT32/STRING/POINTER/BIGINT) has an all-ones exponent, so the stored value is a genuine unboxed double and cannot be a GC pointer.
  • proven_view_access.rs ×5 — all five arms store into the typed array's backing store (view.data_slot), whose elements are raw numeric bytes. This is the codegen-side counterpart of the runtime carve-out the script already makes for the typedarray/typedarray_view/buffer modules (is_pointer_free_module).

No generated code and no runtime behaviour changed — the runtime/codegen half of that commit is comments only.

#7259 — address-classification audit

2 ratchet regressions: real defects, fixed properly. Three hand-rolled floors landed on 2026-07-30 (8fed172cf child_process, 7a4a86677 fs) without going through value::addr_class. js_get_string_pointer_unified forwards a POINTER_TAG payload verbatim and JSValue::as_pointer does the same — those payloads carry registry handle ids as well as heap pointers. A 0x1000/0x10000 floor sits an order of magnitude below HANDLE_BAND_MAX, so a fetch (0x40000..0xE0000), zlib (0xE0000..0xF0000) or proxy (0xF0000..0x100000) id sailed straight into a StringHeader/ObjectHeader dereference — the Linux-only fault class of #1843/#4004/#6271 that macOS's high allocation base hides.

Every handle-floor site in both files was converted, including the two grandfathered ones (same defect, same one-line fix), so both baseline entries drop to zero rather than being re-pinned at a lower number. is_handle_band also subsumes the redundant is_null() checks that followed. cp_raw_slot_is_heap_ptr keeps an explicit raw > 0 so a negative i64 slot isn't turned into a huge "above the band" value by as usize.

1 stale allowlist substring. constants.rs spells the same POSIX flag two ways; the entry matched only one:

("O_SYMLINK", (0x200000) as f64),   <- failed the gate
"O_SYMLINK" => Some(0x200000),      <- suppressed

Re-keyed on the constant name. The entry is still path-prefixed to constants.rs and still requires the O_SYMLINK token, so any other band literal in that file still fails the gate. The pre-split object/native_module.rs entry was dropped — that file now contains zero O_SYMLINK mentions and zero band literals.

Ratchet baseline: 566 → 544 sites, 256 → 249 entries. Verified zero increases and zero new keys by diffing the (rule, path) → count maps, so this is purely slack that accumulated while lint was red. Only 3 of the 22 are this PR; the stale-entry warnings the gate printed after every pass are now gone.

Gates assert their own subject

Per CLAUDE.md, both new regression tests were confirmed to fail without the fix, not merely to pass with it:

  • the proto_cache_scan case fails on the pre-anchoring logic with exactly the five phantom lines;
  • the O_SYMLINK case fails against the pre-fix allowlist with exactly the uncovered spelling.

Finding-set diff vs origin/main

Resolved, not reshaped — every removal is a finding that no longer exists or is now classified, and nothing new was suppressed. origin/main numbers come from a clean detached worktree.

origin/main this branch
gc_store_site_inventory.py exit 1, 19 findings exit 0 — 1255 files, 269 audited sites, 77 allowlisted
addr_class_inventory.py exit 1, 2 ratchet regressions + 1 finding exit 0 — 846 files, 264 allowlisted, 544 ratcheted

The anchoring change was measured in isolation first: it removes exactly the five phantom array/indexing.rs lines and nothing else, leaving the real store at line 111 to be classified on its merits.

#7271 and #7265 landed mid-work; both gates' finding sets at defa4d601 are byte-identical to those at a25077491, so neither affects this.

Left open, deliberately

addr_class_inventory.py's SCAN_ROOTS covers only perry-runtime and perry-stdlib, while its sibling gc_store_site_inventory.py also globs crates/perry-ext-*. When #6826 moved perry-stdlib/src/http.rs into the HTTP extension, its 11 handle-floor sites moved out of the gate's field of view rather than being fixed — the "gate runs but its subject never did" pattern. 18 sites across the ext crates are unaudited, 8 of them band-literal (not ratcheted, so they would hard-fail until each is individually justified). That per-site audit is its own piece of work and is not folded in here: tracked as #7272.

This PR does not by itself turn lint green — step 9 (#7257, blocked on #7264) still fails ahead of these two.

Verification

  • python3 scripts/gc_store_site_inventory.py --self-test && python3 scripts/gc_store_site_inventory.py → 0
  • python3 scripts/addr_class_inventory.py --self-test && python3 scripts/addr_class_inventory.py → 0
  • cargo fmt --all -- --check clean
  • scripts/check_file_size.sh clean
  • cargo build --profile perry-dev -p perry -p perry-runtime-static -p perry-stdlib-static clean (the one perry-runtime warning, gc/mod.rs:70 barrier_arming::*, is pre-existing on main from perf(gc): stop maintaining a remembered set nothing has read yet (#7187 Phase A) #7250 and untouched here)

Not verified: no gap/parity sweep was run. The #7258 half cannot affect behaviour (comments only). The #7259 half changes three runtime address guards in a strictly more rejecting direction — the gate's own rationale notes this conversion is safety-monotonic ("it can only reject MORE addresses, never dereference more") — but the child_process spawn/fork option-slot path and fs.readdir(withFileTypes) were not exercised end-to-end, and the defect being fixed is Linux-only, so a macOS run would not have demonstrated it either way.

Summary by CodeRabbit

  • Bug Fixes

    • Improved pointer validation to prevent unsafe dereferences of invalid handle-band addresses.
    • Corrected address classification and allowlist handling for platform constants.
    • Improved detection of garbage-collection store sites, reducing duplicate or false-positive findings.
  • Documentation

    • Added safety annotations explaining initialization and write-barrier decisions across runtime and code generation paths.
  • Tests

    • Added regression coverage for split store operations, address validation rules, and classification exceptions.
    • Updated lint baselines; one remaining lint step continues to report failures.

Ralph Küpper added 2 commits August 2, 2026 21:11
Comment-only in the runtime/codegen, plus one rule-precision fix in the
scanner. No generated code and no runtime behaviour changes.

Triage of the 19:

* 5 were PHANTOM. `classify_rust_store` searched the atomic-store regexes
  against `call_window` (6 lines forward) unanchored, so one
  `cache.store(...)` was re-reported from each of the five lines above it
  — including `continue;` and a bare `}`. Anchoring the match to the head
  line keeps split-call coverage and reports the site once.

* 3 were genuinely new runtime sites needing a class:
  - array/alloc.rs, array/push_pop.rs (#7138): TAG_HOLE fill of freshly
    allocated, not-yet-published capacity -> INIT.
  - array/indexing.rs (#7071): the collector's own rewrite of a registered
    side-table root slot, mutator stopped -> ROOT.

* 11 were codegen stores. Four already carried a POINTER_FREE marker that
  the #6915 `value_is_canonical_raw_f64` branch split pushed out of the
  +/-6-line window; the other seven (finite-check-guarded field stores,
  typed-array backing-store element stores) had prose rationale but not the
  canonical marker form.

Self-tests: the new `proto_cache_scan` case fails on the pre-fix
unanchored logic with exactly the five phantom lines, so the regression
guard is live rather than merely non-throwing.
Three hand-rolled address floors landed on 2026-07-30 without going through
`value::addr_class`, tripping the ratchet:

  child_process/value_util.rs  `> 0x10000` / `<= 0x10000`  (8fed172)
  fs/dirent.rs                 `< 0x1000`                  (7a4a866)

Both are real, not bookkeeping. `js_get_string_pointer_unified` forwards a
POINTER_TAG payload verbatim, and `JSValue::as_pointer` does the same — those
payloads carry registry handle ids as well as heap pointers. A `0x1000` /
`0x10000` floor sits an order of magnitude below `HANDLE_BAND_MAX`, so a fetch
(0x40000..0xE0000), zlib (0xE0000..0xF0000) or proxy (0xF0000..0x100000) id
sailed through into a `StringHeader`/`ObjectHeader` dereference — the
Linux-only fault class of #1843/#4004/#6271 that macOS's high allocation base
hides.

Converted every handle-floor site in both files (including the two
grandfathered ones, same defect, same fix) to `is_handle_band` /
`is_above_handle_band`, which also subsumes the redundant null checks that
followed. `cp_raw_slot_is_heap_ptr` keeps an explicit `raw > 0` so a negative
i64 slot is not turned into a huge "above the band" value by `as usize`.

Separately, the `O_SYMLINK` allowlist entry was keyed on ONE of the two
spellings in constants.rs, so `("O_SYMLINK", (0x200000) as f64),` failed the
gate while `"O_SYMLINK" => Some(0x200000),` was suppressed. Re-keyed on the
constant name; a self-test now asserts BOTH spellings are covered and fails
against the old entry.

Ratchet baseline regenerated: 566 -> 544 sites, 249 entries. Zero increases
and zero new keys (verified by diffing the key/count maps) — purely the slack
that had accumulated while lint was red. 3 of the 22 are this change; 11 are
`perry-stdlib/src/http.rs`, whose sites moved into `crates/perry-ext-http/`
when #6826 merged it into the HTTP extension. `SCAN_ROOTS` covers only
perry-runtime and perry-stdlib, so those sites are no longer audited at all —
tracked separately, not silently absorbed here.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change audits GC store sites, anchors multiline store detection, replaces hand-rolled address floors with handle-band predicates, updates the address-class allowlist and baseline, and adds regression tests.

Changes

GC store audit

Layer / File(s) Summary
Numeric store audit
crates/perry-codegen/src/expr/index.rs, index_set.rs, instance_misc1.rs, property_set.rs, proven_view_access.rs
Codegen comments and audit markers document pointer-free numeric and typed-array stores.
Runtime array store audit
crates/perry-runtime/src/array/alloc.rs, indexing.rs, push_pop.rs
Runtime comments classify fresh-array initialization, growth slack, and root rewrites as non-barriered operations.
Store inventory matching and tests
scripts/gc_store_site_inventory.py, changelog.d/7273-lint-gc-store-and-addr-class-gates.md
Store matching is anchored to call-opening lines. Regression tests cover split atomic calls and duplicate window findings.

Handle-band validation

Layer / File(s) Summary
Runtime handle-band pointer validation
crates/perry-runtime/src/child_process/value_util.rs, crates/perry-runtime/src/fs/dirent.rs
Raw pointer validation rejects handle-band values before dereference and uses shared predicates for option and argument pointers.
Address-class tooling updates
scripts/addr_class_allowlist.txt, scripts/addr_class_inventory.py, scripts/addr_class_ratchet_baseline.txt, changelog.d/7273-lint-gc-store-and-addr-class-gates.md
Allowlist matching, self-tests, baseline entries, and documented lint results reflect the updated address classification.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • PerryTS/perry#7258 — The PR audits the reported GC store sites and fixes multiline inventory detection.
  • PerryTS/perry#7259 — The PR updates the same runtime pointer validation and address-class tooling areas.
  • PerryTS/perry#7272 — The changelog records the remaining unscanned perry-ext-* scope described by this issue.

Possibly related PRs

  • PerryTS/perry#6636 — Both PRs update handle-band validation, allowlists, and ratchet tooling.
  • PerryTS/perry#6713 — Both PRs use centralized addr_class predicates for handle-band pointer validation.
  • PerryTS/perry#6810 — Both PRs modify address validation and numeric-array store audit paths.

Suggested reviewers: thehypnoo, andrewtdiz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary changes: fixing the GC store-site and address-classification lint gates.
Description check ✅ Passed The description provides detailed change context, issue references, test results, limitations, and deliberately excluded work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7258-7259-lint-gates

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/fs/dirent.rs`:
- Around line 132-140: Update the filesystem option-field access around
options_value and js_object_get_field_by_name to use
options_bool_field(options_value, b"withFileTypes") instead of constructing a
field key and dereferencing obj_ptr directly. Ensure the helper roots and
reloads options_value across any allocating operation, while preserving the
existing withFileTypes behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ddfb04e3-664d-4256-b0d3-77c117b1520c

📥 Commits

Reviewing files that changed from the base of the PR and between defa4d6 and 5c0b30f.

📒 Files selected for processing (15)
  • changelog.d/7273-lint-gc-store-and-addr-class-gates.md
  • crates/perry-codegen/src/expr/index.rs
  • crates/perry-codegen/src/expr/index_set.rs
  • crates/perry-codegen/src/expr/instance_misc1.rs
  • crates/perry-codegen/src/expr/property_set.rs
  • crates/perry-codegen/src/expr/proven_view_access.rs
  • crates/perry-runtime/src/array/alloc.rs
  • crates/perry-runtime/src/array/indexing.rs
  • crates/perry-runtime/src/array/push_pop.rs
  • crates/perry-runtime/src/child_process/value_util.rs
  • crates/perry-runtime/src/fs/dirent.rs
  • scripts/addr_class_allowlist.txt
  • scripts/addr_class_inventory.py
  • scripts/addr_class_ratchet_baseline.txt
  • scripts/gc_store_site_inventory.py

Comment on lines +132 to 140
// #7259: a POINTER_TAG payload can be a registry handle id rather than a
// heap address, and `< 0x1000` sits an order of magnitude below
// `HANDLE_BAND_MAX` — fetch/zlib/proxy ids passed it and were dereferenced
// as an ObjectHeader (the Linux-only fault class of #1843/#4004/#6271).
// `is_handle_band` also subsumes the null check that used to follow.
if crate::value::addr_class::is_handle_band(raw_ptr) {
return false;
}
let obj_ptr = raw_ptr as *const crate::object::ObjectHeader;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Root options_value before constructing the field key.

js_string_from_bytes at Line 141 can collect. obj_ptr can then be stale when Line 142 calls js_object_get_field_by_name.

Replace this duplicated read with options_bool_field(options_value, b"withFileTypes"). That helper roots and reloads options_value.

As per coding guidelines, GC-managed pointers must remain rooted across every operation that can collect. Based on learnings, options_field_value provides the required GC-safe filesystem option-field access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/fs/dirent.rs` around lines 132 - 140, Update the
filesystem option-field access around options_value and
js_object_get_field_by_name to use options_bool_field(options_value,
b"withFileTypes") instead of constructing a field key and dereferencing obj_ptr
directly. Ensure the helper roots and reloads options_value across any
allocating operation, while preserving the existing withFileTypes behavior.

Sources: Coding guidelines, Learnings

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Good catch, and the underlying hazard is real — but it is pre-existing and out of scope here, so I have filed it as #7274 rather than folding it in.

To be precise about what is wrong: the problem is not the direct field access versus options_bool_field. It is that js_string_from_bytes on the next line allocates, and obj_ptr was decoded before it. If that allocation triggers an evacuating collection, obj_ptr names a swept from-space address. Re-deriving it from options_value would not help either — options_value is a plain Rust f64 local, not a GC root, so it is stale by the same argument.

The correct fix is the one the sibling options_field_value already uses 30 lines below in this same file: a RuntimeHandleScope, scope.root_nanbox_f64(options_value), and a re-read through the handle after the allocating call. Same signature, no caller churn — so it is a small fix, just not this PR's.

Keeping it separate is deliberate. #7273 is a lint-gate fix whose runtime half is six one-line address-guard swaps in a strictly more-rejecting direction; a GC-rooting change has a different risk profile and deserves its own reviewable and revertable commit. The two lines this PR touches in options_with_file_types (the < 0x1000 floor becoming addr_class::is_handle_band) neither introduce nor worsen the rooting hazard.

@proggeramlug
proggeramlug merged commit 61410a2 into main Aug 2, 2026
8 of 9 checks passed
@proggeramlug
proggeramlug deleted the fix/7258-7259-lint-gates branch August 2, 2026 19:22
@proggeramlug

Copy link
Copy Markdown
Contributor Author

CI triage: both red checks are pre-existing on main, neither is from this PR

Warnings (product) — filed as #7277. Two denied warnings, both in gc/barrier_arming, a module this PR does not touch:

error: glob import doesn't reexport anything with visibility `pub(crate)`
70 | pub(crate) use barrier_arming::*;
error: function `remembered_reconstruct_census` is never used
   --> crates/perry-runtime/src/gc/barrier_arming.rs:118:15

It has never been caught because no main run has compiled that module under the warnings gate: main's last Tests run is c9cd73ba5 (2026-08-02 07:41) and barrier_arming.rs landed in c7893c4ac (#7250, 12:58), so git merge-base --is-ancestor c7893c4ac c9cd73ba5 is false. Every PR opened after #7250 inherits this.

gc-ratchet — already tracked as #7255. Every row reports minor_cycles 0, copied_objects 0, freed_bytes 0 — the collector ran no cycles at all, which is #7255's "four of the six PR-gating arms run ZERO copying minors corpus-wide". It has failed on six consecutive main runs, including #7256, #7263, #7270 and #7265, all of which predate this branch.

This PR cannot be the cause of either. Its entire footprint in perry-runtime/src/array/ is comments — verified mechanically:

$ git diff origin/main...HEAD -- crates/perry-runtime/src/array/ \
    | grep -E "^[+-]" | grep -vE "^(\+\+\+|---)" \
    | grep -vE "^[+-]\s*//" | grep -vE "^[+-]\s*$"
NONE — comment-only confirmed

and the only non-comment runtime changes are six address-guard swaps in child_process/value_util.rs and fs/dirent.rs, on spawn/fork option-slot and fs.readdir paths that the GC benchmark corpus does not exercise.

Per the repo's own guidance I have not touched gc/ or the matrix scripts — #7187 and #7255 are live there.

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

Labels

None yet

Projects

None yet

1 participant