Skip to content

fix(v8): allow non-ErrorInstance objects in Error.prepareStackTrace#27712

Open
hasesho05 wants to merge 3 commits intooven-sh:mainfrom
hasesho05:fix-27708
Open

fix(v8): allow non-ErrorInstance objects in Error.prepareStackTrace#27712
hasesho05 wants to merge 3 commits intooven-sh:mainfrom
hasesho05:fix-27708

Conversation

@hasesho05
Copy link

Summary

Root Cause

jsFunctionDefaultErrorPrepareStackTrace in FormatStackTraceForJS.cpp used jsDynamicCast<ErrorInstance*> to validate its first argument, throwing TypeError for any non-ErrorInstance object. This caused a critical hang through this chain:

  1. @babel/core replaces Error.prepareStackTrace with a stackTraceRewriter function (in rewrite-stack-trace.js) that chains to the original default prepareStackTrace
  2. @xmldom/xmldom uses an ES5-style Error subclass (ParseError) with Object.create(Error.prototype) — no super() call, so JSC doesn't recognize it as an ErrorInstance
  3. When Error.captureStackTrace(this, ParseError) is called inside the ParseError constructor, bun calls the chained prepareStackTrace, which calls the default implementation
  4. The default implementation throws TypeError: First argument must be an Error object
  5. This prevents xmldom's ParseError from being properly thrown during malformed SVG parsing
  6. xmldom's SAX parser enters an error recovery path that triggers an infinite loop in the position() function's linePattern regex (/\r\n?|\n|$/g)

Fix

Changed the check from jsDynamicCast<ErrorInstance*> to isObject() + getObject(), accepting any JavaScript object. This matches V8's behavior — Error.prepareStackTrace(error, structuredStackTrace) should work with any error-like object, not just native Error instances.

- auto errorObject = jsDynamicCast<JSC::ErrorInstance*>(callFrame->argument(0));
- if (!errorObject) {
-     throwTypeError(lexicalGlobalObject, scope, "First argument must be an Error object"_s);
+ JSC::JSValue errorArg = callFrame->argument(0);
+ if (!errorArg.isObject()) {
+     throwTypeError(lexicalGlobalObject, scope, "First argument must be an object"_s);
      return {};
  }
+ auto* errorObject = errorArg.getObject();

Test plan

  • Verified fix with reproduction repository (https://github.com/hasesho05/bun-test-hang-repro) — 8 test files, 39 tests pass in 645ms (previously hung indefinitely)
  • Added unit tests in test/js/node/v8/capture-stack-trace.test.js:
    • ES5-style Error subclass with Error.captureStackTrace + custom prepareStackTrace
    • Plain object with Error.captureStackTrace + custom prepareStackTrace
  • Added end-to-end regression test in test/regression/issue/027708.test.ts

🤖 Generated with Claude Code

Error.prepareStackTrace's default implementation
(jsFunctionDefaultErrorPrepareStackTrace) required its first argument
to be a JSC ErrorInstance via jsDynamicCast<ErrorInstance*>. This threw
TypeError for ES5-style Error subclasses that use
Object.create(Error.prototype) without calling super()/Error.call(this).

This caused a critical hang when:
1. @babel/core replaces Error.prepareStackTrace with a "stackTraceRewriter"
   that chains to the original default prepareStackTrace
2. @xmldom/xmldom's ES5-style ParseError calls Error.captureStackTrace(this)
3. The chained call to the default prepareStackTrace throws TypeError
4. xmldom's SAX parser enters error recovery, leading to an infinite loop
   in the position() function's linePattern regex (/\r\n?|\n|$/g)

The fix relaxes the check from jsDynamicCast<ErrorInstance*> to
isObject()+getObject(), matching V8's behavior which accepts any object
as the first argument to Error.prepareStackTrace.

Fixes oven-sh#27708

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Walkthrough

Relaxed first-argument validation in the default JS stack-trace formatter to accept generic objects, added regression tests for Error.captureStackTrace behavior with ES5-style subclasses and plain objects when prepareStackTrace is overridden, and added a multi-file regression suite (with a new SVG validation helper) reproducing the xmldom+svgr hang.

Changes

Cohort / File(s) Summary
Stack Trace Error Handling
src/bun.js/bindings/FormatStackTraceForJS.cpp
Replaced direct cast to ErrorInstance* with a generic object check (isObject() / getObject()); changed the invalid-argument message from requiring an Error to requiring an object. Control flow otherwise preserved.
Error.captureStackTrace Tests
test/js/node/v8/capture-stack-trace.test.js
Added regression tests ensuring Error.captureStackTrace works with ES5-style Error subclasses and with plain objects when Error.prepareStackTrace is overridden.
Issue 27708 regression suite & helpers
test/regression/issue/027708.test.ts, tests/helpers.ts, tests/...
Added a multi-file regression test setup reproducing the hang involving @xmldom/xmldom and @svgr/core; introduced validateSvg(svg: string): ValidationResult in tests/helpers.ts and generated parse and SVGR test files used by the suite.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive The regression test in test/regression/issue/027708.test.ts appears to create a multi-file test scenario and SVG validation helper (validateSvg) beyond minimal reproduction, making it unclear if all scope is justified for the core bug fix. Clarify whether the SVG validation helper and multi-file test generation in 027708.test.ts are necessary for the core fix or represent additional scope; consider splitting if excessive.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main code change: allowing non-ErrorInstance objects in Error.prepareStackTrace, which aligns with the primary modification in FormatStackTraceForJS.cpp.
Description check ✅ Passed The PR description comprehensively covers the required template sections with detailed context: what the PR does, root cause analysis, the fix implementation, and verification steps.
Linked Issues check ✅ Passed The code changes fully address issue #27708 by fixing Error.prepareStackTrace to accept any object, preventing the TypeError that broke xmldom's ES5-style Error subclass handling and the downstream infinite loop in the SAX parser.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/027708.test.ts`:
- Around line 170-193: The test can hang because stdout/stderr are piped and
never consumed and the child process isn't scoped with resource cleanup; change
the spawn usage to use resource-scoped syntax (await using const proc =
Bun.spawn(...)) and ensure both proc.stdout and proc.stderr are actively drained
(e.g., pipe them to a no-op/writable sink or consume via read loop) so pipe
backpressure cannot block the child; replace the raw setTimeout-based Promise
with Bun.sleep or an awaited race like Promise.race([proc.exited,
Bun.sleep(timeout)]) to avoid unscoped timers, and keep the existing kill+await
proc.exited logic so cleanup runs under the await using scope.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6c7e972 and 80dfce7.

📒 Files selected for processing (3)
  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • test/js/node/v8/capture-stack-trace.test.js
  • test/regression/issue/027708.test.ts

Comment on lines +170 to +193
const proc = Bun.spawn({
cmd: [bunExe(), "test"],
cwd: dir,
env: bunEnv,
stderr: "pipe",
stdout: "pipe",
});

const timeout = 30_000;
const raceResult = await Promise.race([
proc.exited,
new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), timeout)),
]);

if (raceResult === "timeout") {
proc.kill();
await proc.exited;
throw new Error(
`bun test hung for ${timeout / 1000}s — prepareStackTrace + ES5 Error subclass issue not fixed (issue #27708)`,
);
}

// Verify tests actually passed
expect(proc.exitCode).toBe(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Process lifecycle here can self-induce hangs (undrained pipes) and violates test harness rules (setTimeout, missing await using).

stdout/stderr are piped but never consumed, which can block the child on pipe backpressure and mimic a hang. Also, this test uses setTimeout and does not scope Bun.spawn with await using.

🔧 Suggested adjustment
-  const proc = Bun.spawn({
+  await using proc = Bun.spawn({
     cmd: [bunExe(), "test"],
     cwd: dir,
     env: bunEnv,
-    stderr: "pipe",
-    stdout: "pipe",
+    stderr: "ignore",
+    stdout: "ignore",
   });

-  const timeout = 30_000;
-  const raceResult = await Promise.race([
-    proc.exited,
-    new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), timeout)),
-  ]);
-
-  if (raceResult === "timeout") {
-    proc.kill();
-    await proc.exited;
-    throw new Error(
-      `bun test hung for ${timeout / 1000}s — prepareStackTrace + ES5 Error subclass issue not fixed (issue `#27708`)`,
-    );
-  }
-
-  // Verify tests actually passed
-  expect(proc.exitCode).toBe(0);
+  // Non-hang is enforced by this test's timeout.
+  const exitCode = await proc.exited;
+  expect(exitCode).toBe(0);

As per coding guidelines: "Use await using or using to ensure proper resource cleanup for APIs like Bun.spawn" and "Do not use setTimeout in tests; instead await the condition to be met."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/027708.test.ts` around lines 170 - 193, The test can
hang because stdout/stderr are piped and never consumed and the child process
isn't scoped with resource cleanup; change the spawn usage to use
resource-scoped syntax (await using const proc = Bun.spawn(...)) and ensure both
proc.stdout and proc.stderr are actively drained (e.g., pipe them to a
no-op/writable sink or consume via read loop) so pipe backpressure cannot block
the child; replace the raw setTimeout-based Promise with Bun.sleep or an awaited
race like Promise.race([proc.exited, Bun.sleep(timeout)]) to avoid unscoped
timers, and keep the existing kill+await proc.exited logic so cleanup runs under
the await using scope.

Address review feedback:
- Use `await using` for proper resource cleanup via Symbol.asyncDispose
- Set stdout/stderr to "ignore" to prevent pipe backpressure hangs
- Remove manual setTimeout-based timeout in favor of test's 60s timeout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/027708.test.ts`:
- Line 65: The regex in the generated sanitizer (HELPERS_TS) is over-escaped in
the template literal: occurrences like "\\\\s" and "\\\\[" produce incorrect
literal escapes in the emitted source; update the template so those sequences
are one level less escaped (use "\\s" and "\\[" in the template) so the final
generated regex source contains the proper \s and \[ escapes used by the RegExp,
e.g. adjust the template literal that produces the pattern used in the sanitized
assignment (the code that generates the /<!DOCTYPE.../ and /<!ENTITY.../
patterns) to remove one layer of backslashes.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 80dfce7 and 2ca9602.

📒 Files selected for processing (1)
  • test/regression/issue/027708.test.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/027708.test.ts`:
- Around line 142-145: The package.json fixture currently uses caret ranges for
dependencies ("@svgr/core", "@svgr/plugin-jsx", "@xmldom/xmldom"); update those
entries to pin exact versions by removing the leading '^' so the values become
"8.1.0" and "0.9.8" respectively, ensuring the generated fixture uses
deterministic, exact dependency versions.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2ca9602 and a73d81f.

📒 Files selected for processing (1)
  • test/regression/issue/027708.test.ts

Comment on lines +142 to +145
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@xmldom/xmldom": "^0.9.8",
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n test/regression/issue/027708.test.ts | sed -n '135,155p'

Repository: oven-sh/bun

Length of output: 852


🏁 Script executed:

# Check the complete devDependencies section to understand the context
rg -A 20 "devDependencies:" test/regression/issue/027708.test.ts

Repository: oven-sh/bun

Length of output: 622


🏁 Script executed:

# Check if there are other test fixtures and how they specify dependencies
fd "\.test\.ts" test/regression/issue/ -x basename {} | head -5

Repository: oven-sh/bun

Length of output: 146


🏁 Script executed:

# Look for patterns in other test files with package.json generation
rg -l "devDependencies" test/regression/ --type ts

Repository: oven-sh/bun

Length of output: 260


🏁 Script executed:

# Check how other regression tests specify dependencies
rg -B 3 -A 5 "devDependencies" test/regression/issue/11806.test.ts test/regression/issue/07740.test.ts

Repository: oven-sh/bun

Length of output: 1158


🏁 Script executed:

# Look at the issue this regression test is for - what problem was it solving?
# Check if there are comments in the test explaining the purpose
rg -B 5 "27708\|hang\|xmldom\|svgr" test/regression/issue/027708.test.ts | head -50

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Check git history or comments about why caret ranges might be used
git log --oneline -n 20 -- test/regression/issue/027708.test.ts 2>/dev/null || echo "No git history available"

Repository: oven-sh/bun

Length of output: 133


🏁 Script executed:

# Check if there are any guidelines about version pinning in test fixtures
rg "version|pin|exact|caret" test/CLAUDE.md CLAUDE.md test/regression/README.md 2>/dev/null | head -20

Repository: oven-sh/bun

Length of output: 120


🏁 Script executed:

# Get more context about the test and the issue it addresses
rg -B 10 "bun test should not hang with 7\+" test/regression/issue/027708.test.ts

Repository: oven-sh/bun

Length of output: 448


🏁 Script executed:

# Check what the test is actually verifying - does it check for a hang/timeout?
rg -A 30 "bun test should not hang" test/regression/issue/027708.test.ts | head -50

Repository: oven-sh/bun

Length of output: 964


🏁 Script executed:

# Look for the GitHub issue number reference or comments about why these specific versions
rg "27708\|issue" test/regression/issue/027708.test.ts | head -20

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Check if other tests that use exact versions had a specific reason (like reproducing a bug)
rg -B 5 -A 5 "1\.0\.0" test/regression/issue/07740.test.ts

Repository: oven-sh/bun

Length of output: 465


🏁 Script executed:

# Check what the specific issue `#27708` was about - search for any related commits or PRs
git log --all --oneline --grep="27708\|hang.*svgr\|hang.*xmldom" 2>/dev/null | head -10 || echo "No git log available"

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Examine the complete test to see if it validates against the behavior at these specific versions
rg -A 80 "bun test should not hang with 7\+" test/regression/issue/027708.test.ts | tail -60

Repository: oven-sh/bun

Length of output: 1327


🏁 Script executed:

# Check if there are any version constraints or lock files in the test fixture approach
rg "bun\.lockb|yarn\.lock|package-lock\.json" test/regression/issue/027708.test.ts

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Check the purpose: does the test verify behavior at THESE specific versions, or general behavior?
rg -B 2 -A 10 "timeout\|hang\|expect" test/regression/issue/027708.test.ts | grep -A 5 "const result"

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Verify the complete test flow and check if there are assertions about specific package behavior
rg -A 120 "bun test should not hang with 7\+" test/regression/issue/027708.test.ts | tail -50

Repository: oven-sh/bun

Length of output: 1327


🏁 Script executed:

# Search for any documentation about regression test practices regarding dependency versions
find test -name "README*" -o -name "CLAUDE*" | xargs grep -l "regression\|version" 2>/dev/null | head -5

Repository: oven-sh/bun

Length of output: 221


🏁 Script executed:

# Check if the versions in the caret ranges have known breaking changes
web_search
npm registry `@svgr/core` versions after 8.1.0

Repository: oven-sh/bun

Length of output: 206


🌐 Web query:

@svgr/core 8.1.0 package versions npm

💡 Result:

  • Package: @svgr/core (npm)
  • Latest (as of March 3, 2026): 8.1.0 (npmjs.com)
  • Published date for 8.1.0: Aug 15, 2023 (security.snyk.io)
  • Recent versions (newest → older): 8.1.0, 8.0.0, 7.0.0, 6.5.1, 6.5.0, 6.4.0, 6.3.1, … (security.snyk.io)

To list all versions from npm locally:

npm view `@svgr/core` versions --json

Citations:


🌐 Web query:

@xmldom/xmldom 0.9.8 package breaking changes

💡 Result:

Are there breaking changes in 0.9.8 itself?

No. The 0.9.8 changelog entry lists fixes/performance/chore changes, but no “BREAKING CHANGE” notes for that release. [1]

Breaking changes you’re likely hitting when upgrading to 0.9.8 (from 0.8.x / earlier 0.9.x)

From the project changelog, the notable breaking changes in the 0.9.x line (that still apply when you land on 0.9.8) include:

  • DOMParser.parseFromString now requires mimeType as the 2nd argument. [2]
  • HTML parsing rules now require the passed mimeType to match 'text/html' (not just namespace-driven behavior). [3]
  • Type change: DOMParser.parseFromString return type corrected to Document | undefined (you may need to handle undefined in TS). [3]
  • DOMParser.options instance property removed; use the individual readonly option properties instead; also locator option is now just a boolean. [3]
  • Removed non-spec boolean toggle for “HTML rules” on:
    • XMLSerializer.serializeToString
    • Node.toString
    • Document.toString [4]
  • Attribute/NamedNodeMap behavior aligned with DOM spec:
    • iteration over attributes now in the “right” order
    • non-existing attributes return null instead of undefined
    • similar null behavior for Attr.namespaceURI and Attr.prefix [5]

If you tell me what version you’re upgrading from (e.g., 0.8.11 vs 0.9.7) and what breaks (error message / failing test), I can narrow it to the exact change.


🏁 Script executed:

# Check test/CLAUDE.md for any guidance on regression test practices
cat test/CLAUDE.md | grep -A 10 -B 5 "regression\|version"

Repository: oven-sh/bun

Length of output: 780


🏁 Script executed:

# Check if there are similar test patterns with dynamic vs pinned versions
rg "devDependencies.*\^|devDependencies.*[0-9]+" test/regression/issue/*.test.ts -A 3 | head -40

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

# Verify the exact claim: check if there are any newer versions of these packages currently
npm view `@svgr/core`@"^8.1.0" version 2>/dev/null || echo "Checking npm would resolve to:"

Repository: oven-sh/bun

Length of output: 60


🏁 Script executed:

# Check if other test fixtures in the repo use exact vs caret versions consistently
rg "devDependencies.*{" test/regression/issue/*.test.ts -A 10 | grep -E "^\s+\"[^\"]+\":\s+[\"\^]" | sort | uniq -c

Repository: oven-sh/bun

Length of output: 154


🏁 Script executed:

# Check version patterns in regression test fixtures
rg "devDependencies" test/regression/issue/*.test.ts -A 10 | grep -E '\"[^\"]+\":\s+' | head -30

Repository: oven-sh/bun

Length of output: 1348


🏁 Script executed:

# Check what the actual resolved versions would be if someone ran bun install today
python3 <<'PYEOF'
import re
from pathlib import Path

# Read the test file
content = Path("test/regression/issue/027708.test.ts").read_text()

# Find devDependencies block
match = re.search(r"devDependencies:\s*\{([^}]+)\}", content, re.DOTALL)
if match:
    deps = match.group(1)
    print("Current dependencies in test/regression/issue/027708.test.ts:")
    for line in deps.split('\n'):
        if line.strip():
            print(f"  {line.strip()}")
PYEOF

Repository: oven-sh/bun

Length of output: 204


🏁 Script executed:

# Check if there's a pattern in the codebase about regression test fixture versions
rg "@svgr|@xmldom" test/regression/ -B 2 -A 2

Repository: oven-sh/bun

Length of output: 2500


Use exact versions for the fixture dependencies to ensure regression test determinism.

Lines 142–145 use caret ranges in the generated package.json. While these currently resolve to the latest versions, pinning exact versions (remove the ^ prefix) makes the regression test's conditions explicit and protects against any future patch or minor releases that could affect test behavior. This aligns with best practices for regression test stability.

🔧 Proposed fix
       devDependencies: {
-        "@svgr/core": "^8.1.0",
-        "@svgr/plugin-jsx": "^8.1.0",
-        "@xmldom/xmldom": "^0.9.8",
+        "@svgr/core": "8.1.0",
+        "@svgr/plugin-jsx": "8.1.0",
+        "@xmldom/xmldom": "0.9.8",
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/027708.test.ts` around lines 142 - 145, The
package.json fixture currently uses caret ranges for dependencies ("@svgr/core",
"@svgr/plugin-jsx", "@xmldom/xmldom"); update those entries to pin exact
versions by removing the leading '^' so the values become "8.1.0" and "0.9.8"
respectively, ensuring the generated fixture uses deterministic, exact
dependency versions.

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.

bun test hangs when running 7+ test files with @xmldom/xmldom + @svgr/core

1 participant