inspect, pretty_format, ipc, yaml: harden recursive walkers against stack overflow and dangling state#34889
inspect, pretty_format, ipc, yaml: harden recursive walkers against stack overflow and dangling state#34889robobun wants to merge 4 commits into
Conversation
…ending exceptions ConsoleObject.rs / pretty_format.rs: - Formatter::new() now seats StackCheck::init() instead of a no-op default, so every caller (uncaught-exception printer, Bun__inspect, all expect matchers) gets a real stack bound without having to remember to set one. - print_as_prelude checks the stack before the can_have_circular_references gate, so tags that recurse but were not in that list (Proxy, JSX, Promise, iterators) no longer overflow the native stack. - JSX added to can_have_circular_references in both formatters so a self-referencing element prints [Circular]. - JestPrettyFormat's Formatter gains a stack_check and print_as checks it, so a toEqual diff of a deeply nested value truncates instead of SEGV. ipc.rs: when advanced-mode payload deserialization throws, clear the pending TypeError and report InvalidFormat (matching the JSON-mode path), so a child writing a malformed frame closes the channel instead of surfacing as the parent's uncaught exception. YAMLObject.rs: AnchorAliasName::PropValue owns its prop_name (OwnedString) instead of borrowing the scan iterator's PropertyNameArray entry, which is freed before the emit pass reads it when a getter GCs in between.
|
Warning Review limit reached
Next review available in: 17 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (9)
Comment |
|
Updated 1:11 AM PT - Jul 21st, 2026
✅ @robobun, your commit 520f3b51e479504b39027a9b72ebe8437c74b316 passed in 🧪 To try this PR locally: bunx bun-pr 34889That installs a local version of the PR into your bun-34889 --bun |
|
Status: all four changed test files fail before the The YAML test is Windows aarch64 CI found that on release lanes the error printer writes ~2.5GB to stderr before the stack check fires when the thrown value is a ~60k-deep array (release frames are small, and Not in this PR: the cross-realm |
|
Found 4 issues this PR may fix:
🤖 Generated with Claude Code |
|
On the suggested issues:
|
|
Overlap with concurrently-opened PRs (all from the same audit run):
Unique to this PR and not in any of the above: moving the Happy to reduce this to just the |
print_array does not check max_depth, so on release lanes the error printer writes O(N^2) bytes (N bounded only by the stack) before the stack check fires. On Windows aarch64 release that is ~2.5GB, which proc.stderr.text() cannot materialise. Discard stderr and assert on signalCode/exitCode; a regression would be signalCode SIGSEGV (POSIX) or a non-1 NTSTATUS exitCode (Windows) rather than exit 1.
There was a problem hiding this comment.
Thanks — 520f3b5 addresses the last stderr-volume nit; all prior feedback is now resolved and I found nothing new this pass. Deferring to a human because this bundles five crash-class fixes across four subsystems (formatter stack-check seating/reordering, pretty_format StackCheck, IPC pending-exception clear, YAML anchor-name UAF) and there's an open coordination question against #34883/#34884/#34885/#34887/#29709 that a maintainer should decide.
What was reviewed:
print_as_preludereorder: stack check now runs for every tag;!can_circearly-return still preserved after the check, so non-recursive tags skip the visited-map work as before.Formatter::new()seatingStackCheck::init(): checked thatformat2/Bun.inspectreseats are still present (harmless), and thatinit()is a cheap SP capture so per-console.logcost is negligible.- IPC advanced-mode: the
clear_exception()+InvalidFormatmapping matches the existing JSON-mode path at ipc.rs:522, andon_data2already handlesInvalidFormatby closing the socket. - YAML
OwnedString:dupe_ref()takes a WTF ref andOwnedStringreleases on Drop, so the anchor map's lifetime now owns the name;append_string(prop_name.get())and thecan_use_prop_name_as_anchor(&BunString)deref both still see the right bytes.
Extended reasoning...
Overview
Five targeted hardening changes across src/jsc/ConsoleObject.rs, src/jsc/VirtualMachine.rs (comment-only), src/jsc/ipc.rs, src/runtime/api/YAMLObject.rs, and src/runtime/test_runner/pretty_format.rs, plus four test files. Each source change is a few lines: (1) seat a live StackCheck in Formatter::new(); (2) move the stack-check in print_as_prelude above the can_have_circular_references gate and add Tag::JSX to that set (both files); (3) add a StackCheck field/guard to the jest diff formatter; (4) clear the pending JSC exception when advanced-mode IPC deserialize throws and map to InvalidFormat; (5) own the YAML anchor property-name string via OwnedString::new(prop_name.dupe_ref()).
Security risks
None introduced. All five are defensive: they remove a DoS surface (native stack overflow reachable from console.log/throw/expect().toEqual() on adversarial input), close a peer-controlled dangling-exception path in IPC, and fix a GC-triggerable UAF. No new parsing, no auth/crypto, no user-facing API surface.
Level of scrutiny
High — REVIEW.md flags native memory safety as the most-blocked category, and this touches recursion bounds, exception-scope discipline, and WTF string lifetime in one PR. The individual diffs are small and each has a fail-before/pass-after test, but the combination spans four subsystems and the author has explicitly left open whether to land this combined or split across the four sibling PRs (#34883–#34887). That coordination decision belongs to a maintainer.
Other factors
- All three prior review rounds (undrained stdout, exact-empty-stderr asserts, O(N²) piped output for deep-JSX and then for the throw/reject siblings) were addressed promptly in 63e92e6 / 968388a / 520f3b5; the current diff reflects every suggestion.
- The bug-hunting system found nothing this run.
- The
StackCheck::init()-in-new()change alters behaviour for everyFormatter::new()caller; I checked that the callers named in the PR description (error printer,Bun__inspect, expect matchers) previously relied on the always-true default and now get a real bound, and that the paths that already reseated (format2) are unaffected. - Tests follow harness conventions after the fix-ups (pipes drained or ignored, no exact-empty stderr, ASAN gate on the UAF repro with
Malloc=1, signalCode-based crash assertion for the stderr-heavy cases). - CI was still building at the time of the last timeline entry; the IPC test is noted as platform-deferred.
A pattern audit of structured-clone / serialization walkers found five crash-class bugs in bun's own value walkers (the WebKit SerializedScriptValue core itself is well-hardened). This PR fixes the five memory-safety ones; the cross-realm inspect output bug will follow separately since it is output-correctness rather than a crash.
console.log / Bun.inspect stack overflow on JSX and Proxy
print_as_preluderan thestack_check.is_safe_to_recurse()guard only aftercan_have_circular_references()returned true, andTag::JSX/Tag::Proxy(plusPromise, the iterator tags, andCustomFormattedObject) were not in that set. Any of them could recurse straight past both the visited map and the stack check. The stack check now runs before the gate so every recursive tag is bounded, andJSXjoins the circular-reference set so a self-referencing element prints[Circular]instead of hitting the depth limit.Uncaught exception / unhandled rejection printer stack overflow
Formatter::new()leftstack_checkatStackCheck::default()(a no-op whoseis_safe_to_recurse()always returns true).format2(the normalconsole.logpath) reseated it withStackCheck::init(), butprint_exception,Bun__inspect, and every expect-matcher call site did not, so their recursion guard never fired.Formatter::new()now seats a liveStackCheck; the redundant reseats informat2etc. are left in place as harmless.bun testdiff formatter stack overflowJestPrettyFormat::Formatterhad no stack or depth check at all. Added aStackCheckfield and a check at the top ofprint_asthat truncates with...and setsfailed.IPC advanced-mode malformed frame surfaces as the parent's uncaught TypeError
SerializedScriptValue::deserializethrowsTypeError: Unable to deserialize data.for an invalid wire version. The advanced-mode decoder propagated that asIPCDecodeError::JSErrorand the receive loop closed the socket without clearing the pending exception, so the parent saw an uncaught error on its next turn (an abort in debug). The decoder now clears the exception and returnsInvalidFormat, matching the JSON-mode path.YAML.stringify anchor name use-after-free
AnchorAliasName::PropValue.prop_nameborrowed the scan pass'sJSPropertyIteratorPropertyNameArrayentry as a barebun_core::String(no ref). For a Proxy whoseownKeysreturns fresh strings, nothing else holds thatStringImpl; once the inner iterator drops and a later getter GCs, the emit pass reads freed bytes.prop_nameis now anOwnedStringbuilt fromdupe_ref().ASAN report before the fix
Verification
All four test files fail before the
src/change and pass after:Supersedes #29709 (which added the
Tag::JSXcircular-reference entry; that change is included here along with the broader stack-check fix).no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/spawn/spawn.ipc.test.ts