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");