Skip to content

fix(runtime): JSX and raw-JSON shared a class id; add a scanning gate (#7587) - #7589

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7587-class-id-collision
Aug 7, 2026
Merged

fix(runtime): JSX and raw-JSON shared a class id; add a scanning gate (#7587)#7589
proggeramlug merged 2 commits into
mainfrom
fix/7587-class-id-collision

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

JSX_NODE_CLASS_ID and RAW_JSON_CLASS_ID were both 0xFFFF_00A0 — the second class-id collision found in a week, after #7576's ITERATOR_HELPER_CLASS_ID / STRING_ITERATOR_CLASS_ID pair took out the entire TC39 iterator-helpers surface.

It was not inert

value/to_string.rs:1144 discriminates on class_id alone — no second condition — and returns field 0 as the object's string. A raw-JSON wrapper is allocated with one field and keeps its text in field 0, so String(JSON.rawJSON("123")) matched the JSX arm and stringified through it.

It returned a plausible answer purely by coincidence of layout — both types happen to store their payload in the same slot. That is the more instructive failure: a collision can produce right-looking output for exactly as long as the two types agree structurally, and break the day either layout changes.

RAW_JSON_CLASS_ID moves to the free 0xFFFF_00A1. Neither id is referenced by perry-codegen/perry-hir or persisted anywhere, so the move is internal.

The gate is the more important half

#7576 added iterator_class_ids_are_pairwise_distinct, a Rust test enumerating seven iterator ids. It is good and it stays. It could not have caught this — different family, not in the list.

That is the structural problem with an enumerated list: it covers only the constants somebody remembered to add, and the failure being guarded against is forgetting that a constant exists. A gate whose coverage depends on the same attention the bug depends on is not a gate.

scripts/class_id_collisions.py scans every crate instead, so a new constant is covered the moment it is written, with no list to update. Wired into lint next to the addr-class audit.

It detects two shapes, and the second one found itself. Different names on one value is the collision. The same name on one value is a deliberate cross-crate mirror — perry-ext-events restates the runtime's ABORT_SIGNAL_CLASS_ID so it can recognise runtime AbortSignal objects — which is correct and must not be reported, or the gate is permanently red. But one name carrying different values is mirror drift, and that is worse than a collision: each crate stays internally consistent, so nothing looks wrong, while the type silently stops being recognised across the boundary. The gate catches both.

Sabotage-verified, with real exit codes

Captured without a pipe, because $? after | head is head's status — a trap that has bitten this repo:

sabotage exit
reintroduce the 0xFFFF_00A0 collision 1
drift the ABORT_SIGNAL_CLASS_ID mirror 1
raise MIN_CONSTANTS (stale-scan floor) 2
clean 0

The floor exists because a scan that silently matches nothing prints "no collisions" and means nothing — same discipline as gc_root_dominance_corpus.sh's MIN_COMPILED.

Validation

JSON.rawJSON byte-identical to node 26.5.1 across isRawJSON, stringify, the rawJSON own property, and a nested/mixed array; JSX rendering unchanged as a control. cargo test -p perry-runtime --lib 1838 passed / 0 failed. addr_class_inventory, raw_handle_debt (998), check_file_size.sh, cargo fmt --check all clean.

CI has a deep backlog and may not report; this is local validation.

…#7587)

JSX_NODE_CLASS_ID and RAW_JSON_CLASS_ID were both 0xFFFF_00A0 -- the second
such collision in a week after #7576. Not inert: value/to_string.rs
discriminates on class_id ALONE and returns field 0, and both types keep their
payload in field 0, so String(JSON.rawJSON("123")) took the JSX arm and looked
correct purely by coincidence of layout.

RAW_JSON_CLASS_ID moves to the free 0xFFFF_00A1. Neither id is baked into
codegen or persisted, so the move is internal.

The more important half is the gate. #7576 added a Rust test enumerating seven
iterator ids; it is good and stays, but it could not have caught this -- a
different family, not in the list. A gate whose coverage depends on the same
attention the bug depends on is not a gate. scripts/class_id_collisions.py
SCANS every crate instead, so a new constant is covered the moment it is
written.

It detects two shapes. Different names on one value is the collision. The same
name on one value is a deliberate cross-crate mirror (perry-ext-events restates
the runtime's ABORT_SIGNAL_CLASS_ID) and is allowed -- but one name carrying
DIFFERENT values is mirror drift, which is worse than a collision because each
crate stays self-consistent while the type stops being recognised across the
boundary.

Sabotage-verified with real exit codes: reintroducing the collision exits 1,
drifting the AbortSignal mirror exits 1, raising the stale-scan floor exits 2,
clean exits 0.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@proggeramlug, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b7925d4-483a-4050-af81-63c909c0243f

📥 Commits

Reviewing files that changed from the base of the PR and between d2dca58 and 721a41c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • .github/workflows/test.yml
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/7589-class-id-collision-gate.md
  • crates/perry-runtime/src/json/raw_json.rs
  • scripts/class_id_collisions.py

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.

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