From 31238c605b2ea12519d813bbab46cc21c6c5ab99 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 11 Jul 2026 04:24:23 +0000 Subject: [PATCH 1/9] process: fully clear exceptions in lazy PropertyCallback builders A worker.terminate() that lands while a lazy process.* PropertyCallback builder (constructStdout/constructStdin/constructNextTickFn/...) is entering JS left the TerminationException pending: the builders called scope.tryClearException(), which refuses to clear a termination. JSC's reifyStaticProperty/setUpStaticFunctionSlot don't check for exceptions, so getOwnPropertySlot returned true with the exception still pending and tripped EXCEPTION_ASSERT in JSValue::get / getOwnPropertyDescriptor. Since #31216, every node:worker_threads Worker preloads the module and runs setupWorkerStdio(), which does Object.defineProperty(process, "stdout", ...) and triggers constructStdioWriteStream via the getOwnPropertyDescriptor path on every worker bootstrap. That turned a rare flake into test-worker-message-port-transfer-terminate.js aborting on the x64-asan lane. Replace the tryClearException()/RETURN_IF_EXCEPTION({}) pattern in every process PropertyCallback builder with a shared helper that fully clears the exception (TopExceptionScope::clearException, which the VM trap re-arms at the next safepoint) and skips reportUncaughtExceptionAtEventLoop for the termination case. --- src/jsc/bindings/BunProcess.cpp | 61 +++++++++++-------- .../worker_threads/worker_threads.test.ts | 36 ++++++++++- 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 498fdcf9278f..643627013856 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -206,12 +206,27 @@ static JSValue constructPlatform(VM& vm, JSObject* processObject) #endif } +// LazyPropertyCallback builders run inside getOwnPropertySlot, which performs no +// exception check; tryClearException() won't clear a TerminationException, so +// clear unconditionally (the VM trap re-throws it at the next safepoint). +static void clearLazyPropertyCallbackException(JSC::VM& vm, JSC::TopExceptionScope& scope, JSC::JSGlobalObject* globalObject, JSC::Exception* exception) +{ + scope.clearException(); + if (!vm.isTerminationException(exception)) { + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + scope.clearException(); + } +} + static JSValue constructVersions(VM& vm, JSObject* processObject) { auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); auto* globalObject = processObject->globalObject(); JSC::JSObject* object = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 24); - RETURN_IF_EXCEPTION(scope, {}); + if (auto* exception = scope.exception()) [[unlikely]] { + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + return JSC::jsUndefined(); + } object->putDirect(vm, JSC::Identifier::fromString(vm, "node"_s), JSC::jsOwnedString(vm, makeAtomString(ASCIILiteral::fromLiteralUnsafe(REPORTED_NODEJS_VERSION)))); object->putDirect(vm, JSC::Identifier::fromString(vm, "bun"_s), JSC::jsOwnedString(vm, String(ASCIILiteral::fromLiteralUnsafe(Bun__version)).substring(1))); @@ -275,7 +290,8 @@ static JSValue constructProcessReleaseObject(VM& vm, JSObject* processObject) release->putDirect(vm, Identifier::fromString(vm, "sourceUrl"_s), jsOwnedString(vm, WTF::String(std::span { Bun__githubURL, strlen(Bun__githubURL) })), 0); release->putDirect(vm, Identifier::fromString(vm, "headersUrl"_s), jsOwnedString(vm, String("https://nodejs.org/download/release/v" REPORTED_NODEJS_VERSION "/node-v" REPORTED_NODEJS_VERSION "-headers.tar.gz"_s)), 0); - RETURN_IF_EXCEPTION(scope, {}); + if (auto* exception = scope.exception()) [[unlikely]] + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return release; } @@ -2513,7 +2529,8 @@ static JSValue constructProcessReportObject(VM& vm, JSObject* processObject) report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsBoolean(false), 0); report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsString(vm, String("SIGUSR2"_s)), 0); report->putDirect(vm, JSC::Identifier::fromString(vm, "writeReport"_s), JSC::JSFunction::create(vm, globalObject, 1, String("writeReport"_s), Process_functionWriteReport, ImplementationVisibility::Public), 0); - RETURN_IF_EXCEPTION(scope, {}); + if (auto* exception = scope.exception()) [[unlikely]] + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return report; } @@ -2550,8 +2567,7 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) 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); + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return JSC::jsUndefined(); } variables->putDirect(vm, JSC::Identifier::fromString(vm, "v8_enable_i8n_support"_s), JSC::jsNumber(1), 0); @@ -2647,7 +2663,8 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) #endif config->freeze(vm); - RETURN_IF_EXCEPTION(scope, {}); + if (auto* exception = scope.exception()) [[unlikely]] + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return config; } @@ -2687,8 +2704,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); + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return jsUndefined(); } @@ -2748,8 +2764,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); + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return jsUndefined(); } return result; @@ -2813,8 +2828,7 @@ static JSValue constructProcessChannel(VM& vm, JSObject* processObject) auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getControl, callData, globalObject->globalThis(), args); if (auto* exception = scope.exception()) [[unlikely]] { - (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return jsUndefined(); } return result; @@ -3015,8 +3029,7 @@ static JSValue constructEnv(VM& vm, JSObject* processObject) 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); + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return JSC::jsUndefined(); } return env; @@ -3884,8 +3897,7 @@ static JSValue Process_stubEmptyArray(VM& vm, JSObject* processObject) 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); + clearLazyPropertyCallbackException(vm, scope, processObject->globalObject(), exception); return JSC::jsUndefined(); } return array; @@ -3896,7 +3908,10 @@ static JSValue Process_stubEmptySet(VM& vm, JSObject* processObject) auto* globalObject = processObject->globalObject(); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSSet* result = JSSet::create(vm, globalObject->setStructure()); - RETURN_IF_EXCEPTION(scope, {}); + if (auto* exception = scope.exception()) [[unlikely]] { + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + return JSC::jsUndefined(); + } return result; } @@ -4022,15 +4037,13 @@ static JSValue constructMainModuleProperty(VM& vm, JSObject* processObject) auto& builtinNames = Bun::builtinNames(vm); JSValue mainValue = bun->get(globalObject, builtinNames.mainPublicName()); if (auto* exception = scope.exception()) [[unlikely]] { - (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + clearLazyPropertyCallbackException(vm, scope, 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); + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return JSC::jsUndefined(); } return mainModule; @@ -4059,8 +4072,7 @@ JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObjec 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); + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return JSC::jsUndefined(); } if (nextTickFunction && nextTickFunction.isObject()) { @@ -4124,7 +4136,8 @@ static JSValue constructFeatures(VM& vm, JSObject* processObject) object->putDirect(vm, Identifier::fromString(vm, "require_module"_s), jsBoolean(true)); object->putDirect(vm, Identifier::fromString(vm, "typescript"_s), jsString(vm, String("transform"_s))); - RETURN_IF_EXCEPTION(scope, {}); + if (auto* exception = scope.exception()) [[unlikely]] + clearLazyPropertyCallbackException(vm, scope, globalObject, exception); return object; } diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 21a5a8f72a66..f74fdc06aae4 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1,4 +1,4 @@ -import { bunEnv, bunExe, tmpdirSync } from "harness"; +import { bunEnv, bunExe, isASAN, isDebug, tmpdirSync } from "harness"; import { once } from "node:events"; import fs from "node:fs"; import { join, relative, resolve } from "node:path"; @@ -1305,6 +1305,40 @@ test("close(cb) interleaves with other close listeners in registration order", a expect(order2).toEqual(["B", "C"]); }); +// terminate() during the worker's node:worker_threads preload used to trip +// EXCEPTION_ASSERT in JSValue::get / JSObject::getOwnPropertyDescriptor: the +// lazy process.stdout/stderr/stdin PropertyCallbacks enter JS inside +// getOwnPropertySlot, and tryClearException() won't clear a termination +// exception, so getOwnPropertySlot returned true with it still pending. +// The assertion is debug-build only; amplified from the upstream Node +// test-worker-message-port-transfer-terminate.js (10 workers → 60). +test.skipIf(!isASAN && !isDebug)("terminate() during worker bootstrap doesn't trip getOwnPropertySlot assert", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const { Worker } = require("worker_threads"); + const N = 60; + let done = 0; + for (let i = 0; i < N; ++i) { + const w = new Worker("require('worker_threads').parentPort.on('message', () => {})", { eval: true }); + setImmediate(() => { + w.terminate().then(() => { if (++done === N) console.log("ok"); }); + }); + }`, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout: stdout.trim(), stderr, exitCode, signalCode: proc.signalCode }).toEqual({ + stdout: "ok", + stderr: "", + exitCode: 0, + signalCode: null, + }); +}); + test("getHeapStatistics settles when terminated mid-request", async () => { const w = new Worker("setInterval(() => {}, 1e6)", { eval: true }); await once(w, "online"); From 10cae2e3dbf24cac78c3c25ae90957187cd963d7 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 04:26:25 +0000 Subject: [PATCH 2/9] [autofix.ci] apply automated fixes --- .../worker_threads/worker_threads.test.ts | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index f74fdc06aae4..450192ffe946 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1312,12 +1312,14 @@ test("close(cb) interleaves with other close listeners in registration order", a // exception, so getOwnPropertySlot returned true with it still pending. // The assertion is debug-build only; amplified from the upstream Node // test-worker-message-port-transfer-terminate.js (10 workers → 60). -test.skipIf(!isASAN && !isDebug)("terminate() during worker bootstrap doesn't trip getOwnPropertySlot assert", async () => { - await using proc = Bun.spawn({ - cmd: [ - bunExe(), - "-e", - `const { Worker } = require("worker_threads"); +test.skipIf(!isASAN && !isDebug)( + "terminate() during worker bootstrap doesn't trip getOwnPropertySlot assert", + async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const { Worker } = require("worker_threads"); const N = 60; let done = 0; for (let i = 0; i < N; ++i) { @@ -1326,18 +1328,19 @@ test.skipIf(!isASAN && !isDebug)("terminate() during worker bootstrap doesn't tr w.terminate().then(() => { if (++done === N) console.log("ok"); }); }); }`, - ], - env: bunEnv, - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout: stdout.trim(), stderr, exitCode, signalCode: proc.signalCode }).toEqual({ - stdout: "ok", - stderr: "", - exitCode: 0, - signalCode: null, - }); -}); + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout: stdout.trim(), stderr, exitCode, signalCode: proc.signalCode }).toEqual({ + stdout: "ok", + stderr: "", + exitCode: 0, + signalCode: null, + }); + }, +); test("getHeapStatistics settles when terminated mid-request", async () => { const w = new Worker("setInterval(() => {}, 1e6)", { eval: true }); From bb6ade6c2743565aa29e5ce272595498232cde01 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 11 Jul 2026 04:29:12 +0000 Subject: [PATCH 3/9] test: trim regression comment to three lines --- test/js/node/worker_threads/worker_threads.test.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 450192ffe946..14423fbaedf7 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1305,13 +1305,9 @@ test("close(cb) interleaves with other close listeners in registration order", a expect(order2).toEqual(["B", "C"]); }); -// terminate() during the worker's node:worker_threads preload used to trip -// EXCEPTION_ASSERT in JSValue::get / JSObject::getOwnPropertyDescriptor: the -// lazy process.stdout/stderr/stdin PropertyCallbacks enter JS inside -// getOwnPropertySlot, and tryClearException() won't clear a termination -// exception, so getOwnPropertySlot returned true with it still pending. -// The assertion is debug-build only; amplified from the upstream Node -// test-worker-message-port-transfer-terminate.js (10 workers → 60). +// Amplified test-worker-message-port-transfer-terminate.js: terminate() during the +// worker_threads preload reifies lazy process.stdout/stdin inside getOwnPropertySlot, +// which asserts (debug only) if the builder returns with a termination pending. test.skipIf(!isASAN && !isDebug)( "terminate() during worker bootstrap doesn't trip getOwnPropertySlot assert", async () => { From 5a3f7669f0ca0940ebaefcd47e156a530d908e79 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 11 Jul 2026 04:33:24 +0000 Subject: [PATCH 4/9] test: make stderr diagnostic-only (shown on failure, not asserted empty) --- test/js/node/worker_threads/worker_threads.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 14423fbaedf7..d6396a35a2c7 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1329,7 +1329,8 @@ test.skipIf(!isASAN && !isDebug)( stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect({ stdout: stdout.trim(), stderr, exitCode, signalCode: proc.signalCode }).toEqual({ + // stderr is diagnostic-only (ASAN/debug can emit benign warnings on success). + expect({ stdout: stdout.trim(), stderr: exitCode === 0 ? "" : stderr, exitCode, signalCode: proc.signalCode }).toEqual({ stdout: "ok", stderr: "", exitCode: 0, From 21df2e17660b9579df82255c7786108d14cfb387 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 04:35:25 +0000 Subject: [PATCH 5/9] [autofix.ci] apply automated fixes --- test/js/node/worker_threads/worker_threads.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index d6396a35a2c7..7d100d933905 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1330,7 +1330,12 @@ test.skipIf(!isASAN && !isDebug)( }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); // stderr is diagnostic-only (ASAN/debug can emit benign warnings on success). - expect({ stdout: stdout.trim(), stderr: exitCode === 0 ? "" : stderr, exitCode, signalCode: proc.signalCode }).toEqual({ + expect({ + stdout: stdout.trim(), + stderr: exitCode === 0 ? "" : stderr, + exitCode, + signalCode: proc.signalCode, + }).toEqual({ stdout: "ok", stderr: "", exitCode: 0, From 872748f62f7eab84c9d7cd8d4ac57f3352eb515a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 11 Jul 2026 05:18:37 +0000 Subject: [PATCH 6/9] Switch to DeferTerminationForAWhile; make the regression test deterministic Per review: use JSC::DeferTerminationForAWhile (matching LazyProperty::callFunc) instead of clearing the termination exception. The builder runs to completion and the trap re-fires on scope exit. Regression test switched from a 60-worker stress to a deterministic sleepSync + property read per builder, which reliably fails before the fix and doesn't depend on termination landing in a timing window. --- src/jsc/bindings/BunProcess.cpp | 76 ++++++++++--------- .../worker_threads/worker_threads.test.ts | 26 +++---- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 643627013856..d4bdf8b58157 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -16,6 +16,7 @@ #include "ErrorCode+List.h" #include "JavaScriptCore/ArgList.h" #include "JavaScriptCore/CallData.h" +#include "JavaScriptCore/DeferTermination.h" #include "JavaScriptCore/TopExceptionScope.h" #include "JavaScriptCore/JSCJSValue.h" #include "JavaScriptCore/JSCast.h" @@ -207,26 +208,17 @@ static JSValue constructPlatform(VM& vm, JSObject* processObject) } // LazyPropertyCallback builders run inside getOwnPropertySlot, which performs no -// exception check; tryClearException() won't clear a TerminationException, so -// clear unconditionally (the VM trap re-throws it at the next safepoint). -static void clearLazyPropertyCallbackException(JSC::VM& vm, JSC::TopExceptionScope& scope, JSC::JSGlobalObject* globalObject, JSC::Exception* exception) -{ - scope.clearException(); - if (!vm.isTerminationException(exception)) { - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); - scope.clearException(); - } -} +// exception check; defer termination (as JSC's own LazyProperty::callFunc does) +// so a worker.terminate() mid-builder can't leave it pending for the caller. +#define DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm__) JSC::DeferTerminationForAWhile deferScopeForLazyProperty(vm__) static JSValue constructVersions(VM& vm, JSObject* processObject) { + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); auto* globalObject = processObject->globalObject(); JSC::JSObject* object = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 24); - if (auto* exception = scope.exception()) [[unlikely]] { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); - return JSC::jsUndefined(); - } + RETURN_IF_EXCEPTION(scope, {}); object->putDirect(vm, JSC::Identifier::fromString(vm, "node"_s), JSC::jsOwnedString(vm, makeAtomString(ASCIILiteral::fromLiteralUnsafe(REPORTED_NODEJS_VERSION)))); object->putDirect(vm, JSC::Identifier::fromString(vm, "bun"_s), JSC::jsOwnedString(vm, String(ASCIILiteral::fromLiteralUnsafe(Bun__version)).substring(1))); @@ -283,6 +275,7 @@ static JSValue constructVersions(VM& vm, JSObject* processObject) static JSValue constructProcessReleaseObject(VM& vm, JSObject* processObject) { auto* globalObject = processObject->globalObject(); + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); auto* release = JSC::constructEmptyObject(globalObject); @@ -290,8 +283,7 @@ static JSValue constructProcessReleaseObject(VM& vm, JSObject* processObject) release->putDirect(vm, Identifier::fromString(vm, "sourceUrl"_s), jsOwnedString(vm, WTF::String(std::span { Bun__githubURL, strlen(Bun__githubURL) })), 0); release->putDirect(vm, Identifier::fromString(vm, "headersUrl"_s), jsOwnedString(vm, String("https://nodejs.org/download/release/v" REPORTED_NODEJS_VERSION "/node-v" REPORTED_NODEJS_VERSION "-headers.tar.gz"_s)), 0); - if (auto* exception = scope.exception()) [[unlikely]] - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + RETURN_IF_EXCEPTION(scope, {}); return release; } @@ -2517,6 +2509,7 @@ static JSValue constructProcessReportObject(VM& vm, JSObject* processObject) auto* globalObject = processObject->globalObject(); auto process = uncheckedDowncast(processObject); + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); auto* report = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 10); report->putDirect(vm, JSC::Identifier::fromString(vm, "compact"_s), JSC::jsBoolean(false), 0); @@ -2529,8 +2522,7 @@ static JSValue constructProcessReportObject(VM& vm, JSObject* processObject) report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsBoolean(false), 0); report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsString(vm, String("SIGUSR2"_s)), 0); report->putDirect(vm, JSC::Identifier::fromString(vm, "writeReport"_s), JSC::JSFunction::create(vm, globalObject, 1, String("writeReport"_s), Process_functionWriteReport, ImplementationVisibility::Public), 0); - if (auto* exception = scope.exception()) [[unlikely]] - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + RETURN_IF_EXCEPTION(scope, {}); return report; } @@ -2562,12 +2554,14 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) // } // Lazy property builder: exceptions must not propagate into // reifyStaticProperty, which performs no exception check. + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); 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]] { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); return JSC::jsUndefined(); } variables->putDirect(vm, JSC::Identifier::fromString(vm, "v8_enable_i8n_support"_s), JSC::jsNumber(1), 0); @@ -2663,8 +2657,7 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) #endif config->freeze(vm); - if (auto* exception = scope.exception()) [[unlikely]] - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + RETURN_IF_EXCEPTION(scope, {}); return config; } @@ -2690,6 +2683,7 @@ extern "C" void Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio(JSC::JSGl static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC::JSObject* processObject, int fd) { auto& vm = JSC::getVM(globalObject); + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSC::JSFunction* getStdioWriteStream = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetStdioWriteStreamCodeGenerator(vm), globalObject); @@ -2704,7 +2698,8 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC: auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdioWriteStream, callData, globalObject->globalThis(), args); if (auto* exception = scope.exception()) { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); return jsUndefined(); } @@ -2752,6 +2747,7 @@ static JSValue constructStderr(VM& vm, JSObject* processObject) static JSValue constructStdin(VM& vm, JSObject* processObject) { auto* globalObject = processObject->globalObject(); + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSC::JSFunction* getStdinStream = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetStdinStreamCodeGenerator(vm), globalObject); JSC::MarkedArgumentBuffer args; @@ -2764,7 +2760,8 @@ static JSValue constructStdin(VM& vm, JSObject* processObject) auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdinStream, callData, globalObject, args); if (auto* exception = scope.exception()) { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); return jsUndefined(); } return result; @@ -2820,6 +2817,7 @@ static JSValue constructProcessChannel(VM& vm, JSObject* processObject) auto& vm = JSC::getVM(globalObject); // Lazy property builder: exceptions must not propagate into // reifyStaticProperty, which performs no exception check. + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSC::JSFunction* getControl = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetChannelCodeGenerator(vm), globalObject); @@ -2828,7 +2826,8 @@ static JSValue constructProcessChannel(VM& vm, JSObject* processObject) auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getControl, callData, globalObject->globalThis(), args); if (auto* exception = scope.exception()) [[unlikely]] { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); return jsUndefined(); } return result; @@ -3026,10 +3025,12 @@ 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. + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSValue env = globalObject->processEnvObject(); if (auto* exception = scope.exception()) [[unlikely]] { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); return JSC::jsUndefined(); } return env; @@ -3894,10 +3895,12 @@ static JSValue Process_stubEmptyArray(VM& vm, JSObject* processObject) { // Lazy property builder: exceptions must not propagate into // reifyStaticProperty, which performs no exception check. + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSC::JSArray* array = JSC::constructEmptyArray(processObject->globalObject(), nullptr); if (auto* exception = scope.exception()) [[unlikely]] { - clearLazyPropertyCallbackException(vm, scope, processObject->globalObject(), exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(processObject->globalObject(), exception); return JSC::jsUndefined(); } return array; @@ -3906,12 +3909,10 @@ static JSValue Process_stubEmptyArray(VM& vm, JSObject* processObject) static JSValue Process_stubEmptySet(VM& vm, JSObject* processObject) { auto* globalObject = processObject->globalObject(); + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); JSSet* result = JSSet::create(vm, globalObject->setStructure()); - if (auto* exception = scope.exception()) [[unlikely]] { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); - return JSC::jsUndefined(); - } + RETURN_IF_EXCEPTION(scope, {}); return result; } @@ -4031,19 +4032,22 @@ static JSValue constructMainModuleProperty(VM& vm, JSObject* processObject) { // Lazy property builder: exceptions must not propagate into // reifyStaticProperty, which performs no exception check. + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); auto* globalObject = defaultGlobalObject(processObject->globalObject()); auto* bun = globalObject->bunObject(); auto& builtinNames = Bun::builtinNames(vm); JSValue mainValue = bun->get(globalObject, builtinNames.mainPublicName()); if (auto* exception = scope.exception()) [[unlikely]] { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); return JSC::jsUndefined(); } auto* requireMap = globalObject->requireMap(); JSValue mainModule = requireMap->get(globalObject, mainValue); if (auto* exception = scope.exception()) [[unlikely]] { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); return JSC::jsUndefined(); } return mainModule; @@ -4069,10 +4073,12 @@ JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObjec // Lazy property builder: exceptions must not propagate into // reifyStaticProperty, which performs no exception check. + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); 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]] { - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); return JSC::jsUndefined(); } if (nextTickFunction && nextTickFunction.isObject()) { @@ -4114,6 +4120,7 @@ static JSValue constructFeatures(VM& vm, JSObject* processObject) // cached_builtins: [Getter] // } auto* globalObject = processObject->globalObject(); + DEFER_TERMINATION_FOR_LAZY_PROPERTY(vm); auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); auto* object = constructEmptyObject(globalObject); @@ -4136,8 +4143,7 @@ static JSValue constructFeatures(VM& vm, JSObject* processObject) object->putDirect(vm, Identifier::fromString(vm, "require_module"_s), jsBoolean(true)); object->putDirect(vm, Identifier::fromString(vm, "typescript"_s), jsString(vm, String("transform"_s))); - if (auto* exception = scope.exception()) [[unlikely]] - clearLazyPropertyCallbackException(vm, scope, globalObject, exception); + RETURN_IF_EXCEPTION(scope, {}); return object; } diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 7d100d933905..dc3d2d7204f1 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1305,25 +1305,25 @@ test("close(cb) interleaves with other close listeners in registration order", a expect(order2).toEqual(["B", "C"]); }); -// Amplified test-worker-message-port-transfer-terminate.js: terminate() during the -// worker_threads preload reifies lazy process.stdout/stdin inside getOwnPropertySlot, -// which asserts (debug only) if the builder returns with a termination pending. +// terminate() armed while a lazy process.* PropertyCallback builder enters JS +// used to leave the termination pending inside getOwnPropertySlot and trip its +// EXCEPTION_ASSERT (debug only); the worker_threads preload hits this via stdout. test.skipIf(!isASAN && !isDebug)( - "terminate() during worker bootstrap doesn't trip getOwnPropertySlot assert", + "terminate() during a lazy process.* builder doesn't trip getOwnPropertySlot assert", async () => { await using proc = Bun.spawn({ cmd: [ bunExe(), "-e", - `const { Worker } = require("worker_threads"); - const N = 60; - let done = 0; - for (let i = 0; i < N; ++i) { - const w = new Worker("require('worker_threads').parentPort.on('message', () => {})", { eval: true }); - setImmediate(() => { - w.terminate().then(() => { if (++done === N) console.log("ok"); }); - }); - }`, + `const props = ["stdout", "stderr", "stdin", "nextTick", "mainModule"]; + Promise.all(props.map(p => new Promise((resolve, reject) => { + const w = new Worker("data:text/javascript," + encodeURIComponent( + 'postMessage("go"); Bun.sleepSync(300); process[' + JSON.stringify(p) + '];' + )); + w.addEventListener("message", () => w.terminate()); + w.addEventListener("close", resolve, { once: true }); + w.addEventListener("error", e => reject(new Error(p + ": " + (e.error?.message || e.message))), { once: true }); + }))).then(() => console.log("ok"), e => { console.error(e); process.exit(1); });`, ], env: bunEnv, stderr: "pipe", From 21e3bb0e3db30c34426b12974ff90f2336a5399f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 11 Jul 2026 05:34:55 +0000 Subject: [PATCH 7/9] Move regression test to worker-terminate-lifetime.test.ts worker_threads.test.ts has several pre-existing 5s timeouts under debug+ASAN that are unrelated to this fix. --- .../worker_threads/worker_threads.test.ts | 41 +------------------ .../workers/worker-terminate-lifetime.test.ts | 35 ++++++++++++++++ 2 files changed, 36 insertions(+), 40 deletions(-) diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index dc3d2d7204f1..21a5a8f72a66 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1,4 +1,4 @@ -import { bunEnv, bunExe, isASAN, isDebug, tmpdirSync } from "harness"; +import { bunEnv, bunExe, tmpdirSync } from "harness"; import { once } from "node:events"; import fs from "node:fs"; import { join, relative, resolve } from "node:path"; @@ -1305,45 +1305,6 @@ test("close(cb) interleaves with other close listeners in registration order", a expect(order2).toEqual(["B", "C"]); }); -// terminate() armed while a lazy process.* PropertyCallback builder enters JS -// used to leave the termination pending inside getOwnPropertySlot and trip its -// EXCEPTION_ASSERT (debug only); the worker_threads preload hits this via stdout. -test.skipIf(!isASAN && !isDebug)( - "terminate() during a lazy process.* builder doesn't trip getOwnPropertySlot assert", - async () => { - await using proc = Bun.spawn({ - cmd: [ - bunExe(), - "-e", - `const props = ["stdout", "stderr", "stdin", "nextTick", "mainModule"]; - Promise.all(props.map(p => new Promise((resolve, reject) => { - const w = new Worker("data:text/javascript," + encodeURIComponent( - 'postMessage("go"); Bun.sleepSync(300); process[' + JSON.stringify(p) + '];' - )); - w.addEventListener("message", () => w.terminate()); - w.addEventListener("close", resolve, { once: true }); - w.addEventListener("error", e => reject(new Error(p + ": " + (e.error?.message || e.message))), { once: true }); - }))).then(() => console.log("ok"), e => { console.error(e); process.exit(1); });`, - ], - env: bunEnv, - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - // stderr is diagnostic-only (ASAN/debug can emit benign warnings on success). - expect({ - stdout: stdout.trim(), - stderr: exitCode === 0 ? "" : stderr, - exitCode, - signalCode: proc.signalCode, - }).toEqual({ - stdout: "ok", - stderr: "", - exitCode: 0, - signalCode: null, - }); - }, -); - test("getHeapStatistics settles when terminated mid-request", async () => { const w = new Worker("setInterval(() => {}, 1e6)", { eval: true }); await once(w, "online"); diff --git a/test/js/web/workers/worker-terminate-lifetime.test.ts b/test/js/web/workers/worker-terminate-lifetime.test.ts index b938d02fc470..c431e2d353b8 100644 --- a/test/js/web/workers/worker-terminate-lifetime.test.ts +++ b/test/js/web/workers/worker-terminate-lifetime.test.ts @@ -119,3 +119,38 @@ test( }, timeout, ); + +// terminate() armed while a lazy process.* PropertyCallback builder enters JS +// used to leave the termination pending inside getOwnPropertySlot and trip its +// EXCEPTION_ASSERT (debug only); the worker_threads preload hits this via stdout. +test.skipIf(!slow)( + "terminate() during a lazy process.* builder doesn't trip getOwnPropertySlot assert", + async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const props = ["stdout", "stderr", "stdin", "nextTick", "mainModule"]; + Promise.all(props.map(p => new Promise((resolve, reject) => { + const w = new Worker("data:text/javascript," + encodeURIComponent( + 'postMessage("go"); Bun.sleepSync(300); process[' + JSON.stringify(p) + '];' + )); + w.addEventListener("message", () => w.terminate()); + w.addEventListener("close", resolve, { once: true }); + w.addEventListener("error", e => reject(new Error(p + ": " + (e.error?.message || e.message))), { once: true }); + }))).then(() => console.log("ok"), e => { console.error(e); process.exit(1); });`, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // stderr is diagnostic-only (ASAN/debug can emit benign warnings on success). + expect({ stdout: stdout.trim(), stderr: exitCode === 0 ? "" : stderr, exitCode, signalCode: proc.signalCode }).toEqual({ + stdout: "ok", + stderr: "", + exitCode: 0, + signalCode: null, + }); + }, + timeout, +); From 4fc5126c2c49587b7e17fb473e77de695f7345c0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 05:36:59 +0000 Subject: [PATCH 8/9] [autofix.ci] apply automated fixes --- test/js/web/workers/worker-terminate-lifetime.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/js/web/workers/worker-terminate-lifetime.test.ts b/test/js/web/workers/worker-terminate-lifetime.test.ts index c431e2d353b8..d7bfbbc58aa8 100644 --- a/test/js/web/workers/worker-terminate-lifetime.test.ts +++ b/test/js/web/workers/worker-terminate-lifetime.test.ts @@ -145,7 +145,12 @@ test.skipIf(!slow)( }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); // stderr is diagnostic-only (ASAN/debug can emit benign warnings on success). - expect({ stdout: stdout.trim(), stderr: exitCode === 0 ? "" : stderr, exitCode, signalCode: proc.signalCode }).toEqual({ + expect({ + stdout: stdout.trim(), + stderr: exitCode === 0 ? "" : stderr, + exitCode, + signalCode: proc.signalCode, + }).toEqual({ stdout: "ok", stderr: "", exitCode: 0, From b36f4ed9d715f2b8fd06a73e4d1c385233e34fb2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:10:48 +0000 Subject: [PATCH 9/9] ci: retrigger