From 4b9d1fe1c16b41caff60e1f4ff40a3e16026ed10 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:02:47 +0000 Subject: [PATCH 1/6] process: don't run the uncaught-exception machinery from inside a lazy 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. --- src/jsc/bindings/BunProcess.cpp | 61 +++++++++++++++--------- src/jsc/bindings/ZigGlobalObject.cpp | 12 ++++- test/js/node/process/process.test.js | 69 ++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 22 deletions(-) diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index ce39ea85cff8..4c3f06c4eedf 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -10,6 +10,7 @@ #include "bun_dependency_versions.h" #include #include +#include #include #include #include "JSCommonJSModule.h" @@ -1777,9 +1778,27 @@ static JSValue constructLoadEnvFile(VM& vm, JSObject* processObject) return JSC::JSFunction::create(vm, globalObject, processObjectInternalsLoadEnvFileCodeGenerator(vm), globalObject); } +JSC_DECLARE_HOST_FUNCTION(jsFunctionReportUncaughtException); + +// Lazy PropertyCallback builders run while setUpStaticFunctionSlot / +// reifyAllStaticProperties is reifying the property, i.e. in the middle of a +// property lookup whose walk (JSObject::getPropertySlot) holds the object's +// Structure*. Running the uncaught-exception machinery there re-enters JS, +// which reifies further 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. +static void reportLazyPropertyBuilderException(JSC::JSGlobalObject* globalObject, JSC::Exception* exception) +{ + auto& vm = JSC::getVM(globalObject); + auto* report = JSC::JSFunction::create(vm, globalObject, 1, String(), jsFunctionReportUncaughtException, JSC::ImplementationVisibility::Private); + JSC::QueuedTask task { nullptr, JSC::InternalMicrotask::BunInvokeJobWithArguments, 0, globalObject, report, JSC::JSValue(exception) }; + vm.queueMicrotask(WTF::move(task)); +} + // Lazy PropertyCallback builders that enter JS. reifyAllStaticProperties wraps these in -// DeferTerminationForAWhile; a non-termination throw is cleared+reported so the worker's -// reifyAllStaticProperties (node:worker_threads preload) doesn't leave a pending exception. +// DeferTerminationForAWhile; a non-termination throw is cleared (and reported through +// reportLazyPropertyBuilderException) so the worker's reifyAllStaticProperties +// (node:worker_threads preload) doesn't leave a pending exception. static JSValue callLazyProcessBuilder(VM& vm, JSC::JSGlobalObject* globalObject, JSC::FunctionExecutable* (*generator)(VM&), const JSC::ArgList& args) { auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); @@ -1787,7 +1806,7 @@ static JSValue callLazyProcessBuilder(VM& vm, JSC::JSGlobalObject* globalObject, auto result = JSC::profiledCall(globalObject, ProfilingReason::API, function, JSC::getCallData(function), globalObject->globalThis(), args); if (auto* exception = scope.exception()) [[unlikely]] { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + reportLazyPropertyBuilderException(globalObject, exception); return jsUndefined(); } return result; @@ -2755,15 +2774,15 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) // v8_use_snapshot: 1 // } // } - // Lazy property builder: exceptions must not propagate into - // reifyStaticProperty, which performs no exception check. + // Lazy property builder: a throw is cleared, not propagated + // (see callLazyProcessBuilder). auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSC::JSObject* config = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); JSC::JSObject* variables = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2); JSC::JSArray* shareableBuiltins = JSC::constructEmptyArray(globalObject, nullptr); if (auto* exception = scope.exception()) [[unlikely]] { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + reportLazyPropertyBuilderException(globalObject, exception); return JSC::jsUndefined(); } variables->putDirect(vm, JSC::Identifier::fromString(vm, "v8_enable_i18n_support"_s), JSC::jsNumber(1), 0); @@ -2904,7 +2923,7 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC: auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdioWriteStream, callData, globalObject->globalThis(), args); if (auto* exception = scope.exception()) { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + reportLazyPropertyBuilderException(globalObject, exception); return jsUndefined(); } @@ -2965,7 +2984,7 @@ static JSValue constructStdin(VM& vm, JSObject* processObject) auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdinStream, callData, globalObject, args); if (auto* exception = scope.exception()) { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + reportLazyPropertyBuilderException(globalObject, exception); return jsUndefined(); } return result; @@ -3200,13 +3219,13 @@ static JSValue constructRevision(VM& vm, JSObject* processObject) static JSValue constructEnv(VM& vm, JSObject* processObject) { auto* globalObject = uncheckedDowncast(processObject->globalObject()); - // Lazy property builder: exceptions must not propagate into - // reifyStaticProperty, which performs no exception check. + // Lazy property builder: a throw is cleared, not propagated + // (see callLazyProcessBuilder). auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSValue env = globalObject->processEnvObject(); if (auto* exception = scope.exception()) [[unlikely]] { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + reportLazyPropertyBuilderException(globalObject, exception); return JSC::jsUndefined(); } return env; @@ -4253,13 +4272,13 @@ JSC_DEFINE_HOST_FUNCTION(Process_stubFunctionReturningArray, (JSGlobalObject * g static JSValue Process_stubEmptyArray(VM& vm, JSObject* processObject) { - // Lazy property builder: exceptions must not propagate into - // reifyStaticProperty, which performs no exception check. + // Lazy property builder: a throw is cleared, not propagated + // (see callLazyProcessBuilder). auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSC::JSArray* array = JSC::constructEmptyArray(processObject->globalObject(), nullptr); if (auto* exception = scope.exception()) [[unlikely]] { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(processObject->globalObject(), exception); + reportLazyPropertyBuilderException(processObject->globalObject(), exception); return JSC::jsUndefined(); } return array; @@ -4388,8 +4407,8 @@ extern "C" void Bun__Process__queueNextTick2(GlobalObject* globalObject, Encoded // return require.cache.get(Bun.main) static JSValue constructMainModuleProperty(VM& vm, JSObject* processObject) { - // Lazy property builder: exceptions must not propagate into - // reifyStaticProperty, which performs no exception check. + // Lazy property builder: a throw is cleared, not propagated + // (see callLazyProcessBuilder). auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); auto* globalObject = defaultGlobalObject(processObject->globalObject()); auto* bun = globalObject->bunObject(); @@ -4397,14 +4416,14 @@ static JSValue constructMainModuleProperty(VM& vm, JSObject* processObject) JSValue mainValue = bun->get(globalObject, builtinNames.mainPublicName()); if (auto* exception = scope.exception()) [[unlikely]] { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + reportLazyPropertyBuilderException(globalObject, exception); return JSC::jsUndefined(); } auto* requireMap = globalObject->requireMap(); JSValue mainModule = requireMap->get(globalObject, mainValue); if (auto* exception = scope.exception()) [[unlikely]] { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + reportLazyPropertyBuilderException(globalObject, exception); return JSC::jsUndefined(); } return mainModule; @@ -4428,13 +4447,13 @@ JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObjec args.append(JSC::JSFunction::create(vm, globalObject, 1, String(), jsFunctionDrainMicrotaskQueue, ImplementationVisibility::Private)); args.append(JSC::JSFunction::create(vm, globalObject, 1, String(), jsFunctionReportUncaughtException, ImplementationVisibility::Private)); - // Lazy property builder: exceptions must not propagate into - // reifyStaticProperty, which performs no exception check. + // Lazy property builder: a throw is cleared, not propagated + // (see callLazyProcessBuilder). auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSValue nextTickFunction = JSC::profiledCall(globalObject, ProfilingReason::API, initializer, JSC::getCallData(initializer), globalObject->globalThis(), args); if (auto* exception = scope.exception()) [[unlikely]] { (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + reportLazyPropertyBuilderException(globalObject, exception); return JSC::jsUndefined(); } if (nextTickFunction && nextTickFunction.isObject()) { diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 3022c09cafe3..f8331a7be17f 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -2543,7 +2543,17 @@ void GlobalObject::finishCreation(VM& vm) m_processEnvObject.initLater( [](const JSC::LazyProperty::Initializer& init) { - init.set(Bun::createEnvironmentVariablesMap(static_cast(init.owner)).getObject()); + auto scope = DECLARE_THROW_SCOPE(init.vm); + JSValue map = Bun::createEnvironmentVariablesMap(static_cast(init.owner)); + // On Windows the map is built by the windowsEnv builtin, which can + // throw when user code clobbered a global it depends on. A + // LazyProperty initializer must still set a value; leave the + // exception pending for the caller. + if (scope.exception() || !map || !map.isObject()) [[unlikely]] { + init.set(JSC::constructEmptyObject(init.owner)); + return; + } + init.set(map.getObject()); }); m_processObject.initLater( diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 3f4e0c0a2cb9..362d2b982778 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -2384,3 +2384,72 @@ it("no socket close handler runs after the 'exit' event", async () => { expect(stdout).toBe("exit\n"); expect(exitCode).toBe(0); }); + +describe.concurrent("lazy property builders", () => { + // Lazy process properties are reified in the middle of a property lookup. + // When the builder throws (user code clobbered a global it depends on), the + // error must not be reported from inside the lookup: the uncaughtException + // machinery runs arbitrary JS, which reifies more static properties and + // transitions object structures under the in-progress prototype-chain walk + // (stale-Structure assert in debug builds). The report is deferred to a + // microtask, so the handler observes it after the statement completes. + it("defers a builder failure report until after the property lookup", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `globalThis.Set = 123; + const order = []; + process.on("uncaughtException", e => order.push("uncaught:" + e.constructor.name)); + order.push("before"); + order.push("value:" + String(process.allowedNodeEnvironmentFlags)); + order.push("after"); + process.on("exit", () => console.log(order.join(",")));`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toBe("before,value:undefined,after,uncaught:TypeError\n"); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + }); + + // On Windows process.env is built by a JS builtin, so a clobbered global can + // fail it while another lazy property (Bun.$ evaluates shell.ts, which reads + // process.env) is being reified. The failed builder must leave a usable + // process.env behind, and the deferred report keeps the uncaughtException + // handler (which reifies more Bun properties here) from running while the + // Bun.$ prototype-chain walk is still on the stack. + it("survives a clobbered global breaking the env builder mid-walk", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `Bun.inspect; + process.on("uncaughtException", e => { + Bun.gc; + Bun.SHA1; + console.log("uncaught:" + e.constructor.name); + }); + globalThis.Proxy = 123; + globalThis.Symbol = 123; + try { + Bun.$; + } catch (e) { + console.log("caught:" + e.constructor.name); + } + console.log("alive");`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const expected = isWindows ? "caught:TypeError\nalive\nuncaught:TypeError\n" : "caught:TypeError\nalive\n"; + expect(stdout).toBe(expected); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + }); +}); From d44e6a6acb76a8755ee0990b43f332a4415b3110 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:21:38 +0000 Subject: [PATCH 2/6] Bun.env: return empty from the builder when env construction fails 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. --- src/jsc/bindings/BunObject.cpp | 9 +++++++- test/js/node/process/process.test.js | 33 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 8a022228eb4c..7bf0ffdffac9 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -107,7 +107,14 @@ static JSValue constructWebViewObject(VM& vm, JSObject* bunObject); static JSValue constructEnvObject(VM& vm, JSObject* object) { - return uncheckedDowncast(object->globalObject())->processEnvObject(); + auto scope = DECLARE_THROW_SCOPE(vm); + // On Windows the env map is built by a JS builtin that can throw when user + // code clobbered a global it depends on. Return empty so reifyStaticProperty + // doesn't putDirect a value while the exception is pending (the lookup would + // report not-found after the structure already transitioned). + JSObject* env = uncheckedDowncast(object->globalObject())->processEnvObject(); + RETURN_IF_EXCEPTION(scope, {}); + return env; } JSC::EncodedJSValue flattenArrayOfBuffersIntoArrayBufferOrUint8Array(JSGlobalObject* lexicalGlobalObject, JSValue arrayValue, size_t maxLength, bool asUint8Array) diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 362d2b982778..51bf146465c0 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -2452,4 +2452,37 @@ describe.concurrent("lazy property builders", () => { expect(stderr).toBe(""); expect(exitCode).toBe(0); }); + + // Bun.env shares the lazy env map. When its construction fails, the builder + // must return empty with the exception pending (like Bun.$) instead of + // handing reifyStaticProperty a value: putDirect would transition the Bun + // object's structure while the failed lookup is reported as not-found, and + // the in-progress walk would advance through a stale Structure*. + // Bun.inspect is pre-reified because the windowsEnv builtin reads + // Bun.inspect.custom while building the map, which transitions the Bun + // object mid-walk on its own; that separate bug is fixed in #37175. + it("Bun.env access with a broken env builder throws instead of crashing", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `Bun.inspect; + globalThis.Proxy = 123; + try { + console.log("typeof:" + typeof Bun.env); + } catch (e) { + console.log("caught:" + e.constructor.name); + } + console.log("alive");`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const expected = isWindows ? "caught:TypeError\nalive\n" : "typeof:object\nalive\n"; + expect(stdout).toBe(expected); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + }); }); From d5b79ab7c8f852d9bf9036ed676869c9d9014400 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:23:35 +0000 Subject: [PATCH 3/6] Tighten lazy-builder comments --- src/jsc/bindings/BunObject.cpp | 7 +++---- src/jsc/bindings/BunProcess.cpp | 11 ++++------- src/jsc/bindings/ZigGlobalObject.cpp | 6 ++---- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 7bf0ffdffac9..d05b1d744f1d 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -108,10 +108,9 @@ static JSValue constructWebViewObject(VM& vm, JSObject* bunObject); static JSValue constructEnvObject(VM& vm, JSObject* object) { auto scope = DECLARE_THROW_SCOPE(vm); - // On Windows the env map is built by a JS builtin that can throw when user - // code clobbered a global it depends on. Return empty so reifyStaticProperty - // doesn't putDirect a value while the exception is pending (the lookup would - // report not-found after the structure already transitioned). + // The env map build can throw (Windows builds it in JS). Return empty like + // Bun.$: a non-empty value would be reified while the lookup reports + // not-found, staling the in-progress walk. JSObject* env = uncheckedDowncast(object->globalObject())->processEnvObject(); RETURN_IF_EXCEPTION(scope, {}); return env; diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 4c3f06c4eedf..f525654dedb2 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -1780,13 +1780,10 @@ static JSValue constructLoadEnvFile(VM& vm, JSObject* processObject) JSC_DECLARE_HOST_FUNCTION(jsFunctionReportUncaughtException); -// Lazy PropertyCallback builders run while setUpStaticFunctionSlot / -// reifyAllStaticProperties is reifying the property, i.e. in the middle of a -// property lookup whose walk (JSObject::getPropertySlot) holds the object's -// Structure*. Running the uncaught-exception machinery there re-enters JS, -// which reifies further 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. +// Builders run inside a property lookup whose walk caches the object's +// Structure*. Reporting synchronously runs the uncaught-exception machinery +// (arbitrary JS) right there, transitioning structures under the walk, so +// queue the report as a microtask instead. static void reportLazyPropertyBuilderException(JSC::JSGlobalObject* globalObject, JSC::Exception* exception) { auto& vm = JSC::getVM(globalObject); diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index f8331a7be17f..7c27290f42c6 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -2545,10 +2545,8 @@ void GlobalObject::finishCreation(VM& vm) [](const JSC::LazyProperty::Initializer& init) { auto scope = DECLARE_THROW_SCOPE(init.vm); JSValue map = Bun::createEnvironmentVariablesMap(static_cast(init.owner)); - // On Windows the map is built by the windowsEnv builtin, which can - // throw when user code clobbered a global it depends on. A - // LazyProperty initializer must still set a value; leave the - // exception pending for the caller. + // The windowsEnv builtin can throw; a LazyProperty initializer + // must still set a value. Leave the exception pending. if (scope.exception() || !map || !map.isObject()) [[unlikely]] { init.set(JSC::constructEmptyObject(init.owner)); return; From 99b139dd477581dc045a25cd232d36e5a7c8f7d7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:45:18 +0000 Subject: [PATCH 4/6] Drop redundant local declaration of jsFunctionReportUncaughtException --- src/jsc/bindings/BunProcess.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index f525654dedb2..3a67768cb47d 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -1778,8 +1778,6 @@ static JSValue constructLoadEnvFile(VM& vm, JSObject* processObject) return JSC::JSFunction::create(vm, globalObject, processObjectInternalsLoadEnvFileCodeGenerator(vm), globalObject); } -JSC_DECLARE_HOST_FUNCTION(jsFunctionReportUncaughtException); - // Builders run inside a property lookup whose walk caches the object's // Structure*. Reporting synchronously runs the uncaught-exception machinery // (arbitrary JS) right there, transitioning structures under the walk, so From 8b244c7240cb4726a750459415fcaf22bea395f7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:50:35 +0000 Subject: [PATCH 5/6] Cover stdio builder deferred reports and Bun.env recovery in tests --- test/js/node/process/process.test.js | 32 +++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 51bf146465c0..6e4787c4614a 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -2473,6 +2473,7 @@ describe.concurrent("lazy property builders", () => { } catch (e) { console.log("caught:" + e.constructor.name); } + console.log("recovered:" + typeof Bun.env); console.log("alive");`, ], env: bunEnv, @@ -2480,9 +2481,38 @@ describe.concurrent("lazy property builders", () => { stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - const expected = isWindows ? "caught:TypeError\nalive\n" : "typeof:object\nalive\n"; + // The second access returns the cached fallback object instead of throwing + // again or crashing. + const expected = isWindows + ? "caught:TypeError\nrecovered:object\nalive\n" + : "typeof:object\nrecovered:object\nalive\n"; expect(stdout).toBe(expected); expect(stderr).toBe(""); expect(exitCode).toBe(0); }); + + // constructStdioWriteStream and constructStdin have their own clear+report + // blocks (they don't go through callLazyProcessBuilder), so pin their + // deferred ordering too. + it("defers stdio builder failure reports until after the lookup", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `globalThis.Symbol = 123; + const order = []; + process.on("uncaughtException", e => order.push("uncaught:" + e.constructor.name)); + order.push("stdout:" + String(process.stdout)); + order.push("stdin:" + String(process.stdin)); + process.on("exit", () => console.log(order.join(",")));`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout).toBe("stdout:undefined,stdin:undefined,uncaught:TypeError,uncaught:TypeError\n"); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + }); }); From ccbda6a4bc7690467c163f24ed3ce281c1ea4040 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:55:25 +0000 Subject: [PATCH 6/6] ci: retrigger