fix(runtime): root promises tracked for unhandled rejection (UAF) (#6077)#6209
Conversation
) The unhandled-rejection set (`UNHANDLED_REJECTIONS` in `promise/then.rs`) stores raw promise addresses, but no GC root scanner referenced it. A regular promise is allocated in the MOVABLE arena (`js_promise_new_with_parent_impl`), so a tracked promise could be swept or evacuated before the program-end report — at which point `js_promise_report_unhandled_rejections` dereferences `(*pr).state` / `(*pr).reason` through a stale/forwarded pointer. Arena pages stay mapped, so this reads a recycled promise (silent misreport / wrong "Uncaught (in promise)" reason) more often than it hard-faults, but it is a use-after-free either way. Register `scan_unhandled_rejection_roots_mut` as a mutable GC root scanner: it visits each tracked address with `visit_usize_slot`, which marks the promise live AND rewrites the stored pointer on evacuation. Entries are already removed from the set the moment a reaction is wired (`mark_rejection_handled`) or the promise settles handled, so this never keeps a genuinely-dead promise alive past its own report. Validated: under `PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1` (which panics if a live slot still points at a forwarded object), a distinctive unhandled rejection driven through heavy allocation reports its exact reason with no panic and no corruption; 25 promise/async gap tests stay green; normal promise resolve/reject/await behavior is unchanged. Scope note: this fixes the memory-safety half of #6077. The end-of-turn reporting timing (so a late `.catch` no longer suppresses) and the `process.on('unhandledRejection'/'rejectionHandled')` emitter are a separate behavior change (blast radius on rejection semantics) left as a follow-up.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR adds a GC root scanner for unhandled promise rejections. A new function scans ChangesUnhandled Rejection GC Root Scanner
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Summary
The unhandled-rejection tracking set (
UNHANDLED_REJECTIONS,promise/then.rs) stores raw promise addresses but had no GC root scanner. A regular promise is allocated in the movable arena, so a tracked promise could be swept or evacuated before the program-end report — at which pointjs_promise_report_unhandled_rejectionsdereferences(*pr).state/(*pr).reasonthrough a stale/forwarded pointer (a use-after-free). Arena pages stay mapped, so it reads a recycled promise (silent misreport / wrong reason) more often than it hard-faults.Addresses the memory-safety half of #6077 (the "latent UAF" the issue flags).
Fix
Register
scan_unhandled_rejection_roots_mutas a mutable GC root scanner (mirroring the ~55 existing side-table scanners). It visits each tracked address withvisit_usize_slot, which marks the promise live and rewrites the stored pointer on evacuation. Entries are already removed the moment a reaction is wired (mark_rejection_handled) or the promise settles handled, so this never keeps a dead promise alive past its own report.Validation
PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1(panics if a live slot still points at a forwarded object): a distinctive unhandled rejection driven through heavy allocation reports its exact reason, no panic, no corruption. Multi-rejection stress likewise clean.resolve/reject/awaitbehavior unchanged.cargo fmt+ GC store-site inventory clean.Scope note
This is the memory-safety half. The other two parts of #6077 — end-of-turn reporting timing (so a late
.catchno longer suppresses) and theprocess.on('unhandledRejection'/'rejectionHandled')emitter — are a behavior change with real blast radius on rejection semantics (default-crash-on-unhandled), so they're left as a documented follow-up rather than bundled into this UAF fix.Code-only per contributor convention (no version/CHANGELOG bump).
Summary by CodeRabbit