Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/jsc/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@ static JSValue constructWebViewObject(VM& vm, JSObject* bunObject);

static JSValue constructEnvObject(VM& vm, JSObject* object)
{
return uncheckedDowncast<Zig::GlobalObject>(object->globalObject())->processEnvObject();
auto scope = DECLARE_THROW_SCOPE(vm);
// 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.
Comment thread
robobun marked this conversation as resolved.
JSObject* env = uncheckedDowncast<Zig::GlobalObject>(object->globalObject())->processEnvObject();
RETURN_IF_EXCEPTION(scope, {});
return env;
Comment thread
robobun marked this conversation as resolved.
}

JSC::EncodedJSValue flattenArrayOfBuffersIntoArrayBufferOrUint8Array(JSGlobalObject* lexicalGlobalObject, JSValue arrayValue, size_t maxLength, bool asUint8Array)
Expand Down
56 changes: 35 additions & 21 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "bun_dependency_versions.h"
#include <JavaScriptCore/InternalFieldTuple.h>
#include <JavaScriptCore/JSMicrotask.h>
#include <JavaScriptCore/MicrotaskQueue.h>
#include <JavaScriptCore/ObjectConstructor.h>
#include <JavaScriptCore/NumberPrototype.h>
#include "JSCommonJSModule.h"
Expand Down Expand Up @@ -1777,17 +1778,30 @@ static JSValue constructLoadEnvFile(VM& vm, JSObject* processObject)
return JSC::JSFunction::create(vm, globalObject, processObjectInternalsLoadEnvFileCodeGenerator(vm), globalObject);
}

// 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.
Comment thread
robobun marked this conversation as resolved.
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.
Comment thread
robobun marked this conversation as resolved.
static JSValue callLazyProcessBuilder(VM& vm, JSC::JSGlobalObject* globalObject, JSC::FunctionExecutable* (*generator)(VM&), const JSC::ArgList& args)
{
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
auto* function = JSC::JSFunction::create(vm, globalObject, generator(vm), 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;
Expand Down Expand Up @@ -2755,15 +2769,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).
Comment thread
robobun marked this conversation as resolved.
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);
Expand Down Expand Up @@ -2904,7 +2918,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();
}

Expand Down Expand Up @@ -2965,7 +2979,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;
Expand Down Expand Up @@ -3200,13 +3214,13 @@ static JSValue constructRevision(VM& vm, JSObject* processObject)
static JSValue constructEnv(VM& vm, JSObject* processObject)
{
auto* globalObject = uncheckedDowncast<Zig::GlobalObject>(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).
Comment thread
robobun marked this conversation as resolved.
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;
Expand Down Expand Up @@ -4253,13 +4267,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).
Comment thread
robobun marked this conversation as resolved.
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;
Expand Down Expand Up @@ -4388,23 +4402,23 @@ 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).
Comment thread
robobun marked this conversation as resolved.
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]] {
(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;
Expand All @@ -4428,13 +4442,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).
Comment thread
robobun marked this conversation as resolved.
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()) {
Expand Down
10 changes: 9 additions & 1 deletion src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2543,7 +2543,15 @@ void GlobalObject::finishCreation(VM& vm)

m_processEnvObject.initLater(
[](const JSC::LazyProperty<JSC::JSGlobalObject, JSC::JSObject>::Initializer& init) {
init.set(Bun::createEnvironmentVariablesMap(static_cast<Zig::GlobalObject*>(init.owner)).getObject());
auto scope = DECLARE_THROW_SCOPE(init.vm);
JSValue map = Bun::createEnvironmentVariablesMap(static_cast<Zig::GlobalObject*>(init.owner));
// The windowsEnv builtin can throw; a LazyProperty initializer
// must still set a value. Leave the exception pending.
Comment thread
robobun marked this conversation as resolved.
if (scope.exception() || !map || !map.isObject()) [[unlikely]] {
init.set(JSC::constructEmptyObject(init.owner));
return;
Comment thread
claude[bot] marked this conversation as resolved.
}
init.set(map.getObject());
});

m_processObject.initLater(
Expand Down
132 changes: 132 additions & 0 deletions test/js/node/process/process.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2384,3 +2384,135 @@ 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);
});

// 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("recovered:" + typeof Bun.env);
console.log("alive");`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// 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("");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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);
});
});