Skip to content

process: don't run the uncaught-exception machinery from inside a lazy property lookup - #37258

Open
robobun wants to merge 6 commits into
mainfrom
farm/5615d83c/defer-lazy-builder-report
Open

process: don't run the uncaught-exception machinery from inside a lazy property lookup#37258
robobun wants to merge 6 commits into
mainfrom
farm/5615d83c/defer-lazy-builder-report

Conversation

@robobun

@robobun robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes two crashes, found by fuzzing, in the lazy builders behind process.* properties when user code clobbers a global the builder depends on.

1. The builder failure report re-entered JS from inside the property lookup.

Lazy process properties (env, stdout, stdin, nextTick, config, allowedNodeEnvironmentFlags, finalization, mainModule) are reified by setUpStaticFunctionSlot / reifyAllStaticProperties in the middle of JSObject::getPropertySlot, whose prototype-chain walk holds the object's Structure*. When a builder threw, BunProcess.cpp cleared the exception and called reportUncaughtExceptionAtEventLoop synchronously. That runs the uncaught-exception machinery (process._fatalException lookup, uncaughtException handlers, error printing), i.e. arbitrary JS, which reifies more static properties and transitions object structures while the walk is still on the stack. On assertion-enabled builds this trips:

ASSERTION FAILED: isCompilationThread() || Thread::mayBeGCThread() || object->structure() == this
JSC::Structure::storedPrototype
JSC::JSObject::getPropertySlot<0,1>
JSC::LLInt::performLLIntGetByID

The report is now queued as a microtask, so it runs after the lookup completes. Clearing (rather than propagating) stays: node:worker_threads preload deletes main-only process keys, which bulk-reifies every builder through reifyAllStaticProperties, and a pending exception there aborted workers in stress tests (see the comment on callLazyProcessBuilder; the stale "reifyStaticProperty performs no exception check" comments are updated to point there). Observable change: an uncaughtException handler now sees the builder failure after the statement that triggered reification, not in the middle of it.

2. The process.env initializer violated LazyProperty's contract on failure.

On Windows process.env is built by the windowsEnv JS builtin. When it throws (e.g. globalThis.Proxy clobbered), createEnvironmentVariablesMap returns with the exception pending and the m_processEnvObject initializer called init.set(...getObject()) on a non-object:

ASSERTION FAILED: value
JSC::LazyProperty<JSC::JSGlobalObject,JSC::JSObject>::set
Zig::GlobalObject::finishCreation::<lambda>  (m_processEnvObject)
Zig::GlobalObject::processEnvObject
Bun::constructEnv
JSC::reifyStaticProperty

A LazyProperty initializer that returns without init.set aborts release builds too. The initializer now falls back to an empty object and leaves the exception pending, the same pattern #37175 and #37213 use for the util.inspect initializers (those PRs cover the utilInspect sites and the windowsEnv read of Bun.inspect; this PR covers the process builders and the env initializer, with no overlapping hunks).

Both need deliberate global tampering to hit, but fuzzers reach them and crash 2 is a hard crash on release builds.

How did you verify your code works?

Two tests in test/js/node/process/process.test.js:

  • defers a builder failure report until after the property lookup: fails on the unfixed build on every platform (the handler observes before,uncaught:TypeError,value:undefined,after; fixed order is before,value:undefined,after,uncaught:TypeError), passes with the fix.
  • survives a clobbered global breaking the env builder mid-walk: on an unfixed Windows debug build this aborts with the LazyProperty::set assertion above; with only the env-initializer fix applied it aborts with the storedPrototype assertion (the deferred report is what removes the mid-walk JS); with both fixes it exits cleanly. Verified on Windows x64 (debug) and Linux x64 (debug ASAN).

Also ran test/js/node/process/process.test.js, process-nexttick, process-stdio, process-stdin, run-process-env, worker-transfer-terminate-stress (the scenario that requires builder exceptions to be cleared), and worker-terminate-lifetime on the debug build: no new failures (the pre-existing USER-unset and DNS-teardown failures in this container reproduce identically without this diff).


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/process/process.test.js

…y property lookup

Lazy process properties (env, stdout, stdin, nextTick, config,
allowedNodeEnvironmentFlags, finalization, mainModule) are reified in
the middle of a property lookup. When a builder threw, it cleared the
exception and called reportUncaughtExceptionAtEventLoop synchronously,
re-entering JS (process._fatalException lookup, uncaughtException
handlers, error printing) while JSObject::getPropertySlot still holds
the object's Structure*. That JS reifies more static properties and
transitions structures under the walk, tripping the stale-Structure
assert in Structure::storedPrototype. Queue the report as a microtask
so it runs after the lookup completes.

Also make the processEnvObject LazyProperty initializer set a value when
createEnvironmentVariablesMap fails (on Windows process.env is built by
a JS builtin that user code can break by clobbering globals): returning
without init.set violates LazyProperty's contract and aborts the
process.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The PR defers uncaught-exception reporting from process lazy-property builders through microtasks. Environment construction now preserves pending exceptions and uses empty-object fallbacks. Tests cover asynchronous reporting, nested reification failures, and platform-specific Bun.env behavior.

Lazy property safety

Layer / File(s) Summary
Deferred lazy-builder reporting
src/jsc/bindings/BunProcess.cpp
Adds microtask-based exception reporting for process configuration, environment, streams, arrays, main-module lookup, and next-tick initialization.
Environment construction fallbacks
src/jsc/bindings/BunObject.cpp, src/jsc/bindings/ZigGlobalObject.cpp
Checks environment-construction exceptions and invalid results. Returns or installs empty fallback objects while preserving pending exceptions.
Lazy failure regression coverage
test/js/node/process/process.test.js
Tests deferred reporting, environment-builder failures during Bun.$ reification, safe Bun.env failures, and stream-builder failures across platforms.

Possibly related PRs

  • oven-sh/bun#31831: Modifies lazy process and environment construction in the same binding files.
  • oven-sh/bun#37160: Addresses exception handling during lazy environment-object initialization.
  • oven-sh/bun#37256: Addresses pending exceptions during JavaScriptCore lazy property initialization.

Suggested reviewers: jarred-sumner, cirospaciari

🚥 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 and concisely describes the primary change: deferring uncaught-exception handling during lazy property lookup.
Description check ✅ Passed The description follows the template and clearly explains the changes, failure modes, affected platforms, and verification performed.

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

@github-actions github-actions Bot added the claude label Aug 9, 2026
Comment thread src/jsc/bindings/ZigGlobalObject.cpp
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix stale-structure abort when a lazy Bun property builder throws mid-lookup #37001 - Fixes the same Structure::storedPrototype stale-structure assertion caused by a throwing lazy property builder, including removing the same inline reportUncaughtExceptionAtEventLoop call out of the builder.
  2. Fix crashes when inspecting objects whose property enumeration throws #37175 - Fixes the same two defects in the same two files: the uncaught-exception report re-entering JS from a lazy static-table builder, and a ZigGlobalObject::finishCreation initLater initializer aborting instead of calling init.set.
  3. Clear pending exceptions from lazy property getters during property enumeration #37213 - Also removes the inline reportUncaughtExceptionAtEventLoop from the lazy builders and makes the ZigGlobalObject.cpp initLater initializers always init.set a fallback rather than abort.

🤖 Generated with Claude Code

reifyStaticProperty putDirects any non-empty value even when the builder
left an exception pending, and setUpStaticFunctionSlot then reports the
slot as not-found, so the in-progress prototype-chain walk advances
through the pre-putDirect Structure* and trips the storedPrototype
assert. Return empty like the other Bun object builders so the failure
path never transitions the structure, and the access throws a catchable
error instead.
Comment thread src/jsc/bindings/BunObject.cpp Outdated
Comment thread src/jsc/bindings/BunProcess.cpp Outdated
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/ZigGlobalObject.cpp Outdated
Comment thread src/jsc/bindings/BunObject.cpp
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/ZigGlobalObject.cpp
@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Same crash family, different sites; checked each candidate's diff for overlap:

No overlapping hunks with any of the three, so they can land in any order. One of this PR's test fixtures pre-reifies Bun.inspect specifically because #37175 owns that bug.

Comment thread src/jsc/bindings/BunObject.cpp
Comment thread src/jsc/bindings/BunProcess.cpp Outdated
@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:47 AM PT - Aug 9th, 2026

@robobun, your commit ccbda6a has 1 failures in Build #90985 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37258

That installs a local version of the PR into your bun-37258 executable, so you can run:

bun-37258 --bun

@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/node/process/process.test.js`:
- Around line 2471-2485: Extend the subprocess script in the Bun.env
error-handling test so that, after the catch block, it accesses Bun.env again
and logs or asserts that the recovered value is an object. Update the Windows
expected stdout in the existing process test to include this recovery assertion
while preserving the current non-Windows output and alive check.
🪄 Autofix

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: 9d4fa17f-fa04-4994-9016-3c4068f25264

📥 Commits

Reviewing files that changed from the base of the PR and between 9008ae7 and 99b139d.

📒 Files selected for processing (4)
  • src/jsc/bindings/BunObject.cpp
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/ZigGlobalObject.cpp
  • test/js/node/process/process.test.js

Comment thread test/js/node/process/process.test.js

@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.

I re-reviewed after the fixes in d44e6a6, d5b79ab, 99b139d, and 8b244c7 — everything I flagged earlier is addressed and no new issues were found. Given this touches JSC exception-scope discipline, the LazyProperty contract, and observable uncaughtException timing, and needs to land alongside three overlapping open PRs (#37001/#37175/#37213), a maintainer sign-off is still warranted.

What was reviewed

  • reportLazyPropertyBuilderException: the QueuedTask/BunInvokeJobWithArguments pattern matches existing use at ZigGlobalObject.cpp:3609 and WebStreamsMisc.cpp:167; the header declaration in BunProcess.h covers the symbol so the dropped local declaration is correct.
  • m_processEnvObject initializer: fallback to constructEmptyObject on throw satisfies the LazyProperty must-set contract; both PropertyCallback callers (constructEnv, constructEnvObject) now handle the pending exception.
  • Confirmed the two remaining #if BUN_DEBUG reportUncaughtExceptionAtEventLoop sites in BunObject.cpp are deliberately left for the sibling PRs — no overlapping hunks.
Extended reasoning...

Overview

The PR modifies four files: BunProcess.cpp (adds reportLazyPropertyBuilderException helper that queues the report as a microtask instead of calling reportUncaughtExceptionAtEventLoop synchronously; rewires eight lazy-builder call sites), ZigGlobalObject.cpp (m_processEnvObject initializer now sets an empty-object fallback and leaves the exception pending instead of asserting on init.set(nullptr)), BunObject.cpp (constructEnvObject for Bun.env now opens a throw scope and returns empty on exception), and test/js/node/process/process.test.js (four new subprocess tests).

Security risks

None identified. The change is purely about exception-handling ordering and crash avoidance under deliberately hostile global tampering (fuzzer inputs). No auth, crypto, network, or filesystem surface is touched.

Level of scrutiny

High. This is JSC-bindings code that manipulates exception scopes (DECLARE_TOP_EXCEPTION_SCOPE, tryClearException), the LazyProperty contract, and microtask queuing — all areas where subtle mistakes cause use-after-free or assertion crashes rather than test failures. The observable-behavior change (uncaughtException handler now fires after the triggering statement rather than during it) is defensible but is a semantic shift a maintainer should ratify. The Windows-only processEnvObject fallback path can only be fully verified in Windows CI.

Other factors

All three of my prior findings on this PR were addressed (the Bun.env sibling caller, the redundant forward declaration, and the comment-length cleanup). The author gave a reasoned response for leaving the two #if BUN_DEBUG SQL-builder blocks to the three overlapping open PRs, which is a coordination call a human reviewer should confirm. The QueuedTask construction matches two existing in-tree call sites, so the pattern is established. Test coverage is good: four subprocess tests pin the deferred ordering, the env-builder fallback, the Bun.env throw path, and the stdio builder path, with per-platform expected output. CI (#90963) is still building at the time of this review.

@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.

I re-reviewed after the follow-up commits (d44e6a6, d5b79ab, 99b139d, 8b244c7) and didn't find any new issues — the earlier Bun.env sibling and the redundant forward-declaration are addressed, and the added tests cover the recovery path. Given this touches JSC exception handling inside lazy property builders and sits alongside three overlapping open PRs (#37001/#37175/#37213), a maintainer sign-off on the merge order is still worthwhile.

What was reviewed

  • reportLazyPropertyBuilderException follows the established QueuedTask/vm.queueMicrotask pattern used in WebStreamsMisc.cpp and JSReadRequest.cpp, so the queued Exception* is GC-rooted the same way.
  • The m_processEnvObject initializer's fallback always calls init.set and leaves the exception pending; both PropertyCallback callers (constructEnv, constructEnvObject) now handle it.
  • The two remaining #if BUN_DEBUG synchronous-report sites in defaultBunSQLObject/constructBunSQLObject are intentionally left for the other open PRs per the author's response.
Extended reasoning...

Overview

The PR fixes two fuzzer-found crashes in the lazy process.* property builders: (1) a synchronous reportUncaughtExceptionAtEventLoop call from inside getPropertySlot's prototype-chain walk, which re-enters JS and transitions structures under a cached Structure* (debug-build assertion); and (2) the m_processEnvObject LazyProperty initializer calling init.set(nullptr) when the Windows windowsEnv builtin throws (release-build abort). The fix defers the report via a microtask (QueuedTask + vm.queueMicrotask) and gives the initializer an empty-object fallback with the exception left pending. A follow-up commit extended the same handling to the Bun.env builder (constructEnvObject) after my earlier comment. Four subprocess tests pin the deferred ordering and the recovery behaviour on both Windows and POSIX.

Security risks

None. The trigger requires deliberate global tampering (globalThis.Proxy = 123, etc.) in the user's own process; no untrusted input is parsed and no privilege boundary is crossed.

Level of scrutiny

High. This is C++ JSC binding code touching exception scopes, LazyProperty initializer contracts, and microtask queuing — an area where the repo review guide explicitly calls out re-entrancy and GC-rooting hazards. I verified the new reportLazyPropertyBuilderException helper matches the established QueuedTask/BunInvokeJobWithArguments pattern already used at WebStreamsMisc.cpp:167 and JSReadRequest.cpp:56 for passing heap JSValues through the microtask queue, so the Exception* is rooted the same way those call sites' values are.

Other factors

All three of my earlier inline findings are resolved (the Bun.env sibling in d44e6a6, comment trimming in d5b79ab, and the redundant declaration in 99b139d), and CodeRabbit's test-recovery suggestion landed in 8b244c7. The one intentionally-declined item — the two #if BUN_DEBUG blocks in defaultBunSQLObject/constructBunSQLObject — is covered by open PRs #37001/#37160/#37213 per the author, which is a reasonable scoping call but does mean a maintainer should confirm the intended landing order. That merge-order coordination, plus the general sensitivity of JSC re-entrancy changes, is why I'm deferring rather than approving.

@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: 190/196 jobs green on the latest run (build 90985). The one red job is the darwin 14 aarch64 test-bun step failing outside the runner with exit 255, which also fails the same way on main (it was red on both runs of this PR and is tagged pre-existing by the CI tooling); the two warning entries are known-flaky tests that passed when retried alone. No failures touch this PR's changes. The diff is ready for review.

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