Skip to content

Seat StackCheck in both Formatter::new() constructors so deep non-Error values don't SIGSEGV the printer - #34884

Open
robobun wants to merge 6 commits into
mainfrom
farm/3f7b837f/fix-exception-printer-stackcheck
Open

Seat StackCheck in both Formatter::new() constructors so deep non-Error values don't SIGSEGV the printer#34884
robobun wants to merge 6 commits into
mainfrom
farm/3f7b837f/fix-exception-printer-stackcheck

Conversation

@robobun

@robobun robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Throwing or rejecting a deeply nested non-Error value (e.g. a 50k-deep nested array or Proxy chain) SIGSEGVs the process instead of printing the error and exiting 1. The same value SIGSEGVs the test runner when rendered by the snapshot/diff formatter (expect(a).toEqual([])).

Repro

let a = [];
for (let i = 0; i < 50000; i++) a = [a];
throw a;   // or Promise.reject(a), or expect(a).toEqual([]) in a test
[
  [
    [
...
Segmentation fault (exit 139)

3/3 on 1.4.0 and ASAN main. Node prints a truncated [ [ [ [Array] ] ] ] and exits 1.

Cause

  • console_object::Formatter::new() left stack_check at StackCheck::default(), whose cached_stack_end is 0 so is_safe_to_recurse() always returns true. format2 / Bun.inspect overwrote it with StackCheck::init(), but ~40 other callers (including VirtualMachine::print_exception) did not. The print_as_prelude recursion guard never fired for those, and print_array (which has no max_depth clamp of its own) walked the value until the native stack overflowed.
  • print_as_prelude also returned early at !can_circ before the stack check, so Tag::Proxy (not in can_have_circular_references()) bypassed the guard entirely via print_proxy -> self.format even where stack_check was seated.
  • test_runner::pretty_format::Formatter is a separate struct (used by toEqual/toMatchSnapshot diff rendering) with no stack_check field and no depth bound at all; its print_as recursed the same way.

Fix

  • console_object::Formatter::new() seats stack_check = StackCheck::init() itself. It is a single read of a cached thread-local, and every caller is already on a JS thread. On an unconfigured thread the FFI returns 0, same as the old default. The pre-existing explicit seatings in ConsoleObject.rs and the redundant .update() in JSValue::to_fmt are removed.
  • print_as_prelude checks is_safe_to_recurse() before the !can_circ early-return so the stack guard covers every recursive tag (array, Proxy, JSX, ...).
  • pretty_format::Formatter gains a stack_check field seated in new() and an is_safe_to_recurse() guard at the top of print_as.
  • can_throw_stack_overflow stays defaulted to false, so callers that don't opt in (the uncaught-exception printer, the diff formatter) truncate and set failed rather than throwing.

Verification

New tests in test/js/bun/util/reportError.test.ts spawn the 50k-deep repro via throw / Promise.reject (array) and throw (Proxy chain) and assert signalCode === null / exitCode === 1. New test in test/js/bun/test/pretty-format-overflow.test.ts spawns a bun test fixture that does expect(a).toEqual([]). All four SIGSEGV on the unfixed build and pass with the fix.

The tests use stderr: "ignore": on release builds the printer recurses ~20k-49k levels (8MB Linux stack, 128KB guard) writing ~K^2 bytes on the way down, so piping stderr would make the test itself the bottleneck.

Existing coverage (bun-inspect.test.ts 16k-deep Error chain, inspect.test.js, snapshot-tests/, expect.test.js, console/) all pass.

The pretty_format piece overlaps with #34885 (same guard, broader test matrix there); whichever lands second needs a trivial rebase.


[review] gate passed · iteration 1 · 6 files touched

fails on main (without fix)
ASAN without fix: 5 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/pretty-format-overflow.test.ts test/js/bun/util/reportError.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (676c1bd47)

test/js/bun/test/pretty-format-overflow.test.ts:
41 |     });
42 | 
43 |     const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
44 | 
45 |     // The test should fail due to assertion mismatch, but should NOT crash
46 |     expect(exitCode).toBe(1);
                          ^
error: expect(received).toBe(expected)

Expected: 1
Received: 139

      at <anonymous> (/workspace/bun/test/js/bun/test/pretty-format-overflow.test.ts:46:22)
(fail) pretty_format should handle deeply nested objects without crashing > deeply nested object with many properties [6449.42ms]
68 |       cwd: dir,
69 |       stdout: "ignore",
70 |       stderr: "ignore",
71 
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (11ce7219c)

test/js/bun/test/pretty-format-overflow.test.ts:
(pass) pretty_format should handle deeply nested objects without crashing > deeply nested object with many properties [45.39ms]
(pass) pretty_format should handle deeply nested objects without crashing > deeply nested array via toEqual [34.31ms]

test/js/bun/util/reportError.test.ts:
(pass) reportError [13.12ms]
(pass) native error printer handles lone surrogates in message and stack frame name as U+FFFD [12.50ms]
(pass) Proxy / new Proxy(a,{}) / throw a; > native error printer survives a deeply nested thrown value [25.73ms]
(pass) array / [a] / throw a; > native error printer survives a deeply nested thrown value [82.77ms]
(pass) array / [a] / Promise.reject(a); > native error printer survives a deeply nested thrown value [82.76ms]

 7 pass
 0 fail
 1 snapshots, 19 expect() calls
Ran 7 tests across 2 files. [352.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/pretty-format-overflow.test.ts test/js/bun/util/reportError.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (676c1bd47)

test/js/bun/test/pretty-format-overflow.test.ts:
(pass) pretty_format should handle deeply nested objects without crashing > deeply nested object with many properties [6643.80ms]
(pass) pretty_format should handle deeply nested objects without crashing > deeply nested array via toEqual [553.00ms]

test/js/bun/util/reportError.test.ts:
(pass) reportError [437.16ms]
(pass) native error printer handles lone surrogates in message and stack frame name as U+FFFD [434.87ms]
(pass) array / [a] / throw a; > native error printer survives a deeply nested thrown value [561.46ms]
(pass) array / [a] / Promise.reject(a); > native error printer survives a deeply nested thrown value [557
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 755ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/22] gen cpp.rs (cppbind)
[2/22] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 244 extern-C blocks audited
[3/22] gen JS modules (bundle-modules)
Preprocess modules (7686ms)
Bundle modules (86ms)
Postprocesss modules (463ms)
Bundle Functions (1138ms)
Generate Code (83ms)

[9.47s] Bundled "src/js" for production
  2042 kb
  165 internal modules
  13 native modules
  90 internal functions across 19 files
[3/9] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rus
... (truncated)
diff hotspot
src/jsc/ConsoleObject.rs                        | 16 ++++------------
 src/jsc/JSValue.rs                              |  1 -
 src/jsc/VirtualMachine.rs                       |  3 +--
 src/runtime/test_runner/pretty_format.rs        |  8 +++++++-
 test/js/bun/test/pretty-format-overflow.test.ts | 23 +++++++++++++++++++++++
 test/js/bun/util/reportError.test.ts            | 25 ++++++++++++++++++++++++-
 6 files changed, 59 insertions(+), 17 deletions(-)

gate history · 4 passed · 0 rejected · iteration 1

evidence per changed file
file                                             reads  edits  tests
src/jsc/ConsoleObject.rs                            10      6      0
src/jsc/JSValue.rs                                   2      3      0
src/jsc/VirtualMachine.rs                            7      6      0
src/runtime/test_runner/pretty_format.rs             4      3      0
test/js/bun/test/pretty-format-overflow.test.ts      1      1      0
test/js/bun/util/reportError.test.ts                 5      7      0

Throwing or rejecting a deeply nested non-Error value (e.g. a 50k-deep
array) SIGSEGVed the process instead of printing the error and exiting 1.

Formatter::new() leaves stack_check at StackCheck::default(), whose
cached_stack_end is 0 so is_safe_to_recurse() always passes. format2 /
Bun.inspect overwrite it with StackCheck::init(), but print_exception and
the non-Exception branch of the jsc_hooks print_exception hook did not,
so the print_as recursion guard never fired and print_array walked the
value until the native stack overflowed.

console.log of the same value correctly throws RangeError because its
formatter is seated; the uncaught-exception printer is now at least as
safe.
@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced with the repro in the PR body: stock bun and ASAN main both SIGSEGV (exit 139) at 50k depth; with the fix, 50k depth exits 1. Same crash in the test runner's pretty_format::Formatter (expect(50k-deep).toEqual([])) and for deep Proxy chains; both fixed.

Fix lives in both Formatter::new() constructors plus a reorder in print_as_prelude so the stack guard covers every recursive tag.

CI: the new tests in reportError.test.ts and pretty-format-overflow.test.ts pass on all lanes in build 76735. Remaining failures are unrelated: bun-server.test.ts websocket-idle-CPU threshold on darwin 14 aarch64 (red on two consecutive builds, diff untouched, handed to main-break triage) plus nine tests the CI helper marks flaky (repl EPIPE, require-cache RSS threshold, solc timeout, no-orphans TTY resume, proxy-stress, install, update-interactive, 20144, http-server-connections-leak). None touch formatting. Ready for review.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Formatter recursion checks are initialized centrally, unsafe recursion now stops formatting, and regression tests cover deeply nested assertion, thrown-value, and rejected-value scenarios.

Formatter recursion safety

Layer / File(s) Summary
Centralize formatter stack-check initialization
src/jsc/ConsoleObject.rs, src/jsc/JSValue.rs
Formatter::new uses StackCheck::init(), redundant call-site setup is removed, and JSValue::to_fmt resets remaining values.
Guard formatter recursion
src/jsc/ConsoleObject.rs, src/runtime/test_runner/pretty_format.rs, src/jsc/VirtualMachine.rs
Console formatting checks recursion safety before its early return, while pretty-formatting stops when recursion is unsafe.
Cover deeply nested values
test/js/bun/test/pretty-format-overflow.test.ts, test/js/bun/util/reportError.test.ts
Spawned regression cases verify deeply nested values do not terminate by signal and still exit with code 1.

Possibly related PRs

  • oven-sh/bun#34885: Both PRs modify pretty-format recursion safety and deep-nesting regression coverage.
  • oven-sh/bun#34892: Both PRs update formatter recursion-safety handling for deeply nested error values.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly matches the main change: initializing StackCheck in formatter constructors to prevent printer crashes on deep values.
Description check ✅ Passed The description covers the problem, fix, and verification, so it is mostly complete despite not using the exact template headings.

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

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:05 AM PT - Jul 21st, 2026

@robobun, your commit 676c1bd is building: #76735

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Crash through uncaught exception in a JS VM #24069 - Crash message is "Uncaught exception while handling uncaught exception", indicating the exception printer itself crashed while formatting a complex non-Error value (from happy-dom), which matches the uninitialised StackCheck bug this PR fixes.

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #24069

🤖 Generated with Claude Code

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Re the auto-linked #24069: that one is a different bug. Its stack trace shows the uncaughtException reentrancy guard panicking because a JS process.on('uncaughtException', ...) listener (happy-dom's BrowserExceptionObserver) threw a TypeError, and the second trip through VirtualMachine.uncaughtException hits panic: Uncaught exception while handling uncaught exception. This PR only touches the formatter's stack-overflow guard when printing a deeply nested thrown value; it doesn't change the listener-reentrancy path. Not adding Fixes #24069.

Comment thread src/jsc/VirtualMachine.rs Outdated
Addresses review: the invariant belongs in the constructor. Formatter::new()
takes a &JSGlobalObject so it is always on a JS thread where
Bun__StackCheck__getMaxStack() returns the configured bound (and degrades to
0 = always-passes on an unconfigured thread, same as the old default).

Drops the three per-call-site seatings added in the previous commit and the
four pre-existing now-redundant ones in ConsoleObject.rs.
@robobun robobun changed the title jsc: seat StackCheck on the uncaught-exception printer's Formatter console: seat StackCheck in Formatter::new() so every caller gets a live recursion bound Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@src/jsc/ConsoleObject.rs`:
- Around line 1729-1732: Prevent direct construction of Formatter from bypassing
StackCheck initialization by making Formatter and/or its fields private as
appropriate, or by exposing only constructor-based initialization. Ensure all
Formatter instances use Formatter::new and therefore initialize stack_check
through StackCheck::init, while preserving the existing formatting behavior.

In `@test/js/bun/util/reportError.test.ts`:
- Around line 147-149: Strengthen the assertions in the uncaught-error test
around the existing stderr and exitCode checks: separately verify that the
post-control uncaught-exception printer emits its expected marker or RangeError
output, rather than relying on the earlier console.log(a) catch output. Keep the
exit-code assertion and ensure the test fails if the uncaught printer exits with
code 1 without reporting the RangeError.
- Around line 126-128: Replace the regression comments near the
uncaught-exception printer and the corresponding lines around 145-146 with the
confirmed issue URL only, or remove them if no issue is tracked; do not retain
implementation history or behavioral explanation in those comments.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4e2ecd41-bac0-4fa9-b29a-ebadc4ecc699

📥 Commits

Reviewing files that changed from the base of the PR and between 2b90109 and bf63a9a.

📒 Files selected for processing (2)
  • src/jsc/ConsoleObject.rs
  • test/js/bun/util/reportError.test.ts

Comment thread src/jsc/ConsoleObject.rs Outdated
Comment thread test/js/bun/util/reportError.test.ts Outdated
Comment thread test/js/bun/util/reportError.test.ts Outdated
Comment thread src/jsc/ConsoleObject.rs Outdated
Comment thread test/js/bun/util/reportError.test.ts
Comment thread src/jsc/ConsoleObject.rs
pretty_format::Formatter (the test runner's snapshot/diff formatter) is a
separate struct from console_object::Formatter and had no recursion bound,
so expect(50k-deep-array).toEqual([]) / .toMatchSnapshot() SIGSEGVed via the
same mechanism. Add a stack_check field seated in new() and a
is_safe_to_recurse() guard at the top of print_as, mirroring ConsoleObject.

Test: use stderr: "ignore" and assert only signalCode/exitCode. On release
builds the printer recurses ~20k-49k levels (Linux 8MB stack, 128KB guard)
writing ~K^2 bytes before the guard fires; piping that into .text() crashed
the Windows aarch64 lane in the previous CI run.

Also drop the now-redundant stack_check.update() in JSValue::to_fmt (its only
caller) and the stale "seated by the caller" comment in VirtualMachine.rs.
@robobun robobun changed the title console: seat StackCheck in Formatter::new() so every caller gets a live recursion bound Seat StackCheck in both Formatter::new() constructors so deep non-Error values don't SIGSEGV the printer Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@test/js/bun/test/pretty-format-overflow.test.ts`:
- Around line 54-74: Strengthen both subprocess regressions by emitting a small
bounded-depth marker immediately before formatting or throwing, piping stdout or
another bounded stream so the marker remains observable, and asserting it while
continuing to ignore the potentially huge formatter diagnostics. In
test/js/bun/test/pretty-format-overflow.test.ts lines 54-74, verify the
50,000-level array was constructed before toEqual runs; in
test/js/bun/util/reportError.test.ts lines 126-141, verify each throw and
Promise.reject fixture reached the printer. Preserve the existing exit-status
and signal assertions.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9e66d5fa-2f29-45dd-8345-deec8e3b1aec

📥 Commits

Reviewing files that changed from the base of the PR and between bf63a9a and 2a99f15.

📒 Files selected for processing (5)
  • src/jsc/JSValue.rs
  • src/jsc/VirtualMachine.rs
  • src/runtime/test_runner/pretty_format.rs
  • test/js/bun/test/pretty-format-overflow.test.ts
  • test/js/bun/util/reportError.test.ts
💤 Files with no reviewable changes (1)
  • src/jsc/JSValue.rs

Comment thread test/js/bun/test/pretty-format-overflow.test.ts
@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

The pretty_format::Formatter piece overlaps with #34885 (opened ~7 minutes after this one by another run), which applies the same StackCheck guard to pretty_format.rs with a broader test matrix (deep object, toMatchSnapshot, JSX). This PR additionally fixes the console_object::Formatter side (the uncaught-exception printer, the original report) which #34885 does not touch. Whichever lands second will need a trivial rebase of pretty_format.rs.

Comment thread src/jsc/ConsoleObject.rs Outdated
Tag::Proxy is not in can_have_circular_references(), so the early return at
!can_circ skipped the is_safe_to_recurse() check and a 50k-deep Proxy chain
still SIGSEGVed via print_proxy -> self.format recursion. Stack safety is
orthogonal to circular-ref bookkeeping; move the check above the early return
so it covers every recursive tag.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@test/js/bun/util/reportError.test.ts`:
- Around line 126-130: Update the parameterized test title in the describe.each
block to include the stmt argument, so each case identifies whether it uses
throw or Promise.reject while preserving the existing wrap and type values.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c916b3aa-a140-4996-b486-d400c1a1732f

📥 Commits

Reviewing files that changed from the base of the PR and between 2a99f15 and 11ce721.

📒 Files selected for processing (2)
  • src/jsc/ConsoleObject.rs
  • test/js/bun/util/reportError.test.ts

Comment thread test/js/bun/util/reportError.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All prior feedback from this thread has been addressed in 2a99f15 and 11ce721 — I don't have anything new to flag. Deferring to a human for sign-off given this changes the recursion-guard invariant for every Formatter::new() caller across two subsystems, and the pretty_format piece needs merge-order coordination with #34885.

Extended reasoning...

Overview

Seats a live StackCheck in both console_object::Formatter::new() and test_runner::pretty_format::Formatter::new(), reorders print_as_prelude so the stack guard runs before the !can_circ early-return (covering Tag::Proxy), removes the four now-redundant per-call-site stack_check = StackCheck::init() assignments plus the redundant .update() in JSValue::to_fmt, and adds subprocess regression tests for deep array/Proxy via throw/Promise.reject/toEqual.

Security risks

None. The change strictly narrows a native-stack-overflow crash surface (SIGSEGV → truncate + set failed). can_throw_stack_overflow stays defaulted to false, so newly-covered callers truncate rather than throw.

Level of scrutiny

Moderate-to-high. The mechanism is a one-line constructor swap plus a guard reorder, and StackCheck::init() is a cheap thread-local read that degrades to the old always-pass default on unconfigured threads. But it implicitly changes behavior for every Formatter::new() caller (~40 sites across 18 files), reorders a check in a hot recursive path so it now runs on every tag (including primitives), and touches two separate formatter structs. shallow_clone() was verified to propagate the seated stack_check (ConsoleObject.rs:1764).

Other factors

  • All three prior review threads I raised (test output volume, stale comment/redundant .update(), pretty_format sibling, Proxy !can_circ bypass) were addressed in follow-up commits and are resolved.
  • The verifier agents examined and refuted the concern that stderr:"ignore" doesn't bound the child's own K² in-memory DiffFormatter buffer on release builds.
  • The PR description explicitly calls out overlap with #34885 (same pretty_format guard, broader test matrix) — whichever lands second needs a rebase, which is a maintainer coordination decision.
  • One trivial CodeRabbit nit (duplicate describe.each title string) was acknowledged as apply-at-merge; it doesn't affect test execution.
  • An earlier revision crashed the Windows aarch64 lane (build 76688) due to piped output volume; the current revision uses stderr:"ignore" — worth confirming CI is green across all lanes on 11ce721 before merge.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — all three prior rounds of feedback are addressed and the mechgate shows fail-before/pass-after on both ASAN and release.

What was reviewed:

  • StackCheck::init() seated in both Formatter::new() constructors; verified shallow_clone() copies it and all to_fmt callers pass freshly-constructed formatters, so the removed .update() was indeed dead.
  • print_as_prelude reorder: stack check now precedes the !can_circ early-return, so Proxy/JSX and other non-circular recursive tags are covered; the added per-leaf check is a pointer subtract + compare.
  • Tests use stderr: "ignore" to avoid the K² release-build output, assert signalCode === null before exitCode === 1, and the describe.each title now distinguishes throw vs. Promise.reject.
Extended reasoning...

Overview

Fixes a native-stack-overflow SIGSEGV when the console/test-runner formatter recurses into a deeply nested non-Error value (50k-deep array, Proxy chain) via three coordinated changes: (1) console_object::Formatter::new() seats stack_check: StackCheck::init() instead of default() and the four now-redundant per-call-site re-seats plus the to_fmt .update() are deleted; (2) print_as_prelude checks is_safe_to_recurse() before the !can_circ early-return so Tag::Proxy and other non-circular recursive tags hit the guard; (3) test_runner::pretty_format::Formatter gains a stack_check field and an is_safe_to_recurse() guard at the top of print_as. A stale comment in VirtualMachine.rs is trimmed. Four new subprocess tests cover throw/reject × array and throw × Proxy for the uncaught printer, plus expect(50k-deep).toEqual([]) for the diff formatter.

Security risks

None introduced. This closes a user-reachable native crash (DoS-class) — throwing a deep value would SIGSEGV the process. StackCheck::init() reads a cached thread-local; on unconfigured threads it returns 0 (identical to the old default), so no behavior change off the JS thread. can_throw_stack_overflow stays defaulted to false, so the uncaught-exception printer and diff formatter truncate + set failed rather than throwing into a context that can't catch.

Level of scrutiny

Medium-high — this is a hot-path native formatter used by every console.log, uncaught-exception print, and test-runner diff. The change itself is small and mechanical (a constructor default swap, a 6-line reorder, a mirrored field+guard in the sibling formatter). I reviewed this PR three times previously; each finding (missing pretty_format sibling, stale comment/dead .update(), K² stderr in the test, Proxy !can_circ bypass) was addressed in a follow-up commit. This run I re-verified: the reordered print_as_prelude at ConsoleObject.rs:3384-3393 is correct; shallow_clone() at :1764 copies stack_check from self; pretty_format::print_as at :1161-1164 has the guard; and a spot-check of to_fmt callers confirms they all pass freshly-constructed formatters, so removing the .update() cannot leave a stale bound.

Other factors

The PR body's mechgate evidence shows the new tests SIGSEGV on main (ASAN: 5 fail; release: Proxy variant fails) and pass with the fix on both profiles. CI build 76704 passed with three unrelated flakes (darwin websocket idle-CPU, two install-test flakes). All CodeRabbit findings are resolved/withdrawn. The overlap with #34885 is noted in the PR body — whichever lands second needs a trivial pretty_format.rs rebase. The bug-hunting system found nothing on this final revision.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant