From 396238df1f2e42e32607f1b061d2b9f291a7e9ec Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:17:05 +0000 Subject: [PATCH] process: don't hand an empty JSValue to reifyStaticProperty on exception Six lazy PropertyCallback builders (constructVersions, constructProcessReleaseObject, constructProcessReportObject, constructProcessConfigObject, Process_stubEmptySet, constructFeatures) declared a TopExceptionScope but used RETURN_IF_EXCEPTION(scope, {}) to bail on failure. reifyStaticProperty stores the callback's result via putDirect with no exception check, so an empty JSValue there trips ASSERT(value) in putDirectInternal, and the exception escapes unchecked. Replace each RETURN_IF_EXCEPTION with a clearAndReportLazyPropertyException helper that clears and reports the exception and returns jsUndefined(), matching the existing pattern in Process_stubEmptyArray. Migrate the nine other in-file callers of that inline pattern to the same helper. --- src/jsc/bindings/BunProcess.cpp | 92 +++++++++++++--------------- test/js/node/process/process.test.js | 50 +++++++++++++++ 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 81f7487465e0..d70134b7aa54 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -207,12 +207,24 @@ static JSValue constructPlatform(VM& vm, JSObject* processObject) #endif } +// A LazyPropertyCallback's result is stored via putDirect with no exception +// check (see reifyStaticProperty), so the builder must never return the empty +// JSValue and must not let an exception escape. +static JSValue clearAndReportLazyPropertyException(JSC::TopExceptionScope& scope, JSC::JSGlobalObject* globalObject) +{ + auto* exception = scope.exception(); + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + return JSC::jsUndefined(); +} + 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 (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); 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))); @@ -277,7 +289,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 (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return release; } @@ -2515,7 +2528,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 (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return report; } @@ -2551,11 +2565,8 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) 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); - return JSC::jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); variables->putDirect(vm, JSC::Identifier::fromString(vm, "v8_enable_i8n_support"_s), JSC::jsNumber(1), 0); variables->putDirect(vm, JSC::Identifier::fromString(vm, "enable_lto"_s), JSC::jsBoolean(false), 0); // Node 26's common.gypi evaluates enable_thin_lto/lto_jobs conditions; gyp @@ -2649,7 +2660,8 @@ static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) #endif config->freeze(vm); - RETURN_IF_EXCEPTION(scope, {}); + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return config; } @@ -2688,11 +2700,8 @@ static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC: JSC::CallData callData = JSC::getCallData(getStdioWriteStream); 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); - return jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); ASSERT_WITH_MESSAGE(JSC::isJSArray(result), "Expected an array from getStdioWriteStream"); JSC::JSArray* resultObject = uncheckedDowncast(result); @@ -2749,11 +2758,8 @@ static JSValue constructStdin(VM& vm, JSObject* processObject) JSC::CallData callData = JSC::getCallData(getStdinStream); auto result = JSC::profiledCall(globalObject, ProfilingReason::API, getStdinStream, callData, globalObject, args); - if (auto* exception = scope.exception()) { - (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); - return jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return result; } @@ -2814,11 +2820,8 @@ static JSValue constructProcessChannel(VM& vm, JSObject* processObject) JSC::CallData callData = JSC::getCallData(getControl); 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); - return jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return result; } else { return jsUndefined(); @@ -3016,11 +3019,8 @@ static JSValue constructEnv(VM& vm, JSObject* processObject) // reifyStaticProperty, which performs no exception check. 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); - return JSC::jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return env; } @@ -3885,11 +3885,8 @@ static JSValue Process_stubEmptyArray(VM& vm, JSObject* processObject) // reifyStaticProperty, which performs no exception check. 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); - return JSC::jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, processObject->globalObject()); return array; } @@ -3898,7 +3895,8 @@ 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 (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return result; } @@ -4023,18 +4021,12 @@ static JSValue constructMainModuleProperty(VM& vm, JSObject* processObject) auto* bun = globalObject->bunObject(); 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); - return JSC::jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); auto* requireMap = globalObject->requireMap(); JSValue mainModule = requireMap->get(globalObject, mainValue); - if (auto* exception = scope.exception()) [[unlikely]] { - (void)scope.tryClearException(); - Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); - return JSC::jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return mainModule; } @@ -4060,11 +4052,8 @@ JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObjec // reifyStaticProperty, which performs no exception check. 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); - return JSC::jsUndefined(); - } + if (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); if (nextTickFunction && nextTickFunction.isObject()) { this->m_nextTickFunction.set(vm, this, nextTickFunction.getObject()); } @@ -4126,7 +4115,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 (scope.exception()) [[unlikely]] + return clearAndReportLazyPropertyException(scope, globalObject); return object; } diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index 5d45a4567ef5..114e0e0fd64f 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -1596,6 +1596,56 @@ it.each(["stdin", "stdout", "stderr"])("%s stream accessor should handle excepti ); }); +// JSC's reifyStaticProperty stores a PropertyCallback's return value via +// putDirect with no exception check, so a builder must never return the empty +// JSValue (putDirectInternal asserts on it) or let an exception escape. The +// allocation paths in these builders only fail on OOM, so this is a scope- +// discipline guard rather than a deterministic crash repro. +it("lazy process properties reify under JSC exception-scope validation", async () => { + const properties = [ + "versions", + "release", + "report", + "config", + "allowedNodeEnvironmentFlags", + "features", + "_preload_modules", + "env", + "mainModule", + ]; + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const names = ${JSON.stringify(properties)}; + const types = {}; + for (const name of names) types[name] = typeof process[name]; + void process.report.getReport(); + process.stdout.write(JSON.stringify(types)); + `, + ], + env: { ...bunEnv, BUN_JSC_validateExceptionChecks: "1" }, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stderr, types: JSON.parse(stdout || "null"), exitCode }).toEqual({ + stderr: "", + types: { + versions: "object", + release: "object", + report: "object", + config: "object", + allowedNodeEnvironmentFlags: "object", + features: "object", + _preload_modules: "object", + env: "object", + mainModule: "undefined", + }, + exitCode: 0, + }); +}); + it("process.versions", () => { expect(process.versions.node).toEqual("26.3.0"); expect(process.versions.v8).toEqual("14.6.202.34-node.20");