Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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+exports.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once
// clang-format off

#include <JavaScriptCore/DeferTermination.h>

// --- Getters ---
#define FOR_EACH_GETTER(macro) \
macro(Archive) \
Expand Down Expand Up @@ -92,8 +94,12 @@ FOR_EACH_CALLBACK(DECLARE_ZIG_BUN_OBJECT_CALLBACK);
FOR_EACH_GETTER(DECLARE_ZIG_BUN_OBJECT_GETTER);
#undef DECLARE_ZIG_BUN_OBJECT_GETTER

// definition of the C++ wrapper to call the Rust function
// definition of the C++ wrapper to call the Rust function.
// Lazy property builder: exceptions must not propagate into reifyStaticProperty,
// which performs no exception check. A TerminationException cannot be cleared,
// so defer it across the getter.
#define DEFINE_ZIG_BUN_OBJECT_GETTER_WRAPPER(name) static JSC::JSValue BunObject_lazyPropCb_wrap_##name(JSC::VM &vm, JSC::JSObject *object) { \
JSC::DeferTerminationForAWhile deferTermination(vm); \
return JSC::JSValue::decode(BunObject_lazyPropCb_##name(object->globalObject(), object)); \
} \

Expand Down
13 changes: 13 additions & 0 deletions src/jsc/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <JavaScriptCore/JSPromise.h>
#include <JavaScriptCore/JSBase.h>
#include <JavaScriptCore/BuiltinNames.h>
#include <JavaScriptCore/DeferTermination.h>
#include "ScriptExecutionContext.h"
#include "WebCoreJSClientData.h"
#include <JavaScriptCore/JSFunction.h>
Expand Down Expand Up @@ -316,6 +317,10 @@ static JSValue constructPluginObject(VM& vm, JSObject* bunObject)

static JSValue defaultBunSQLObject(VM& vm, JSObject* bunObject)
{
// Lazy property builder: exceptions must not propagate into
// reifyStaticProperty, which performs no exception check. A
// TerminationException cannot be cleared, so defer it across the require.
JSC::DeferTerminationForAWhile deferTermination(vm);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(bunObject->globalObject());
JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql);
Expand All @@ -328,6 +333,10 @@ static JSValue defaultBunSQLObject(VM& vm, JSObject* bunObject)

static JSValue constructBunSQLObject(VM& vm, JSObject* bunObject)
{
// Lazy property builder: exceptions must not propagate into
// reifyStaticProperty, which performs no exception check. A
// TerminationException cannot be cleared, so defer it across the require.
JSC::DeferTerminationForAWhile deferTermination(vm);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(bunObject->globalObject());
JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql);
Expand Down Expand Up @@ -359,6 +368,10 @@ JSValue constructBunFetchObject(VM& vm, JSObject* bunObject)

static JSValue constructBunShell(VM& vm, JSObject* bunObject)
{
// Lazy property builder: exceptions must not propagate into
// reifyStaticProperty, which performs no exception check. A
// TerminationException cannot be cleared, so defer it across the call.
JSC::DeferTerminationForAWhile deferTermination(vm);
Comment thread
claude[bot] marked this conversation as resolved.
auto* globalObject = uncheckedDowncast<Zig::GlobalObject>(bunObject->globalObject());
JSFunction* createParsedShellScript = JSFunction::create(vm, bunObject->globalObject(), 2, "createParsedShellScript"_s, BunObject_callback_createParsedShellScript, ImplementationVisibility::Private, NoIntrinsic);
JSFunction* createShellInterpreterFunction = JSFunction::create(vm, bunObject->globalObject(), 1, "createShellInterpreter"_s, BunObject_callback_createShellInterpreter, ImplementationVisibility::Private, NoIntrinsic);
Expand Down
21 changes: 18 additions & 3 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -2652,6 +2653,10 @@ extern "C" void Bun__ForceFileSinkToBeSynchronousForProcessObjectStdio(JSC::JSGl
static JSValue constructStdioWriteStream(JSC::JSGlobalObject* globalObject, JSC::JSObject* processObject, int fd)
{
auto& vm = JSC::getVM(globalObject);
// Lazy property builder: exceptions must not propagate into
// reifyStaticProperty, which performs no exception check. A
// TerminationException cannot be cleared, so defer it across the call.
JSC::DeferTerminationForAWhile deferTermination(vm);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);

JSC::JSFunction* getStdioWriteStream = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetStdioWriteStreamCodeGenerator(vm), globalObject);
Expand Down Expand Up @@ -2715,6 +2720,10 @@ static JSValue constructStderr(VM& vm, JSObject* processObject)
static JSValue constructStdin(VM& vm, JSObject* processObject)
{
auto* globalObject = processObject->globalObject();
// Lazy property builder: exceptions must not propagate into
// reifyStaticProperty, which performs no exception check. A
// TerminationException cannot be cleared, so defer it across the call.
JSC::DeferTerminationForAWhile deferTermination(vm);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
JSC::JSFunction* getStdinStream = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetStdinStreamCodeGenerator(vm), globalObject);
JSC::MarkedArgumentBuffer args;
Expand Down Expand Up @@ -2783,7 +2792,9 @@ static JSValue constructProcessChannel(VM& vm, JSObject* processObject)
if (Bun__GlobalObject__hasIPC(globalObject)) {
auto& vm = JSC::getVM(globalObject);
// Lazy property builder: exceptions must not propagate into
// reifyStaticProperty, which performs no exception check.
// reifyStaticProperty, which performs no exception check. A
// TerminationException cannot be cleared, so defer it across the call.
JSC::DeferTerminationForAWhile deferTermination(vm);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);

JSC::JSFunction* getControl = JSC::JSFunction::create(vm, globalObject, processObjectInternalsGetChannelCodeGenerator(vm), globalObject);
Expand Down Expand Up @@ -3979,7 +3990,9 @@ extern "C" void Bun__Process__queueNextTick2(GlobalObject* globalObject, Encoded
static JSValue constructMainModuleProperty(VM& vm, JSObject* processObject)
{
// Lazy property builder: exceptions must not propagate into
// reifyStaticProperty, which performs no exception check.
// reifyStaticProperty, which performs no exception check. A
// TerminationException cannot be cleared, so defer it across the gets.
JSC::DeferTerminationForAWhile deferTermination(vm);
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
auto* globalObject = defaultGlobalObject(processObject->globalObject());
auto* bun = globalObject->bunObject();
Expand Down Expand Up @@ -4019,7 +4032,9 @@ JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObjec
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.
// reifyStaticProperty, which performs no exception check. A
// TerminationException cannot be cleared, so defer it across the call.
JSC::DeferTerminationForAWhile deferTermination(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]] {
Expand Down
79 changes: 79 additions & 0 deletions test/js/web/workers/worker-terminate-lifetime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,85 @@ test(
timeout,
);

// Regression: these properties are lazily built by JSC's reifyStaticProperty,
// which performs no exception check. A terminate() that landed while a builder
// was entering JS left a TerminationException pending (tryClearException()
// cannot clear one), so the builder either tripped the caller's
// `EXCEPTION_ASSERT(!scope.exception() || !hasSlot)` or handed the empty
// JSValue to putDirect (a null JSCell deref).
//
// Each worker blocks in Bun.sleepSync so terminate() is requested while the
// thread sits in native code with no JS safepoint ahead of the property read;
// the builder then runs with the termination trap armed. A blocking call is
// the point of the test, not a wait for a condition.
//
// One entry per builder shape: a process.* TopExceptionScope builder, a JS
// property get, an internal-module require, a JSC::call, and a Rust-backed
// getter behind the shared wrapper macro.
const lazyProperties = [
"process.nextTick",
"process.mainModule",
"process.stdin",
"Bun.$",
"Bun.sql",
"Bun.SQL",
"Bun.argv",
];
const blockMs = slow ? 600 : 200;

test(
"terminate() while a lazy property builder is entering JS does not abort",
async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const properties = ${JSON.stringify(lazyProperties)};
await Promise.all(
properties.map(property => {
const w = new Worker(
"data:text/javascript," +
encodeURIComponent(
'postMessage("go");' +
// Blocks the worker thread in native code; terminate() arms the
// termination trap while we are parked here.
"Bun.sleepSync(${blockMs});" +
// First touch of the lazy property: its builder enters JS and is
// terminated mid-call.
property + ";",
),
);
const { promise: closed, resolve, reject } = Promise.withResolvers();
w.addEventListener("close", resolve, { once: true });
// A worker that dies before reaching the property read would still
// fire close, and the test would pass having exercised nothing.
w.addEventListener("error", event => reject(event.error ?? new Error(property + ": " + event.message)), {
once: true,
});
w.addEventListener("message", () => w.terminate(), { once: true });
return closed;
}),
);
console.log("terminated " + properties.length);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stderr, stdout, exitCode, signalCode: proc.signalCode }).toEqual({
stderr: "",
stdout: `terminated ${lazyProperties.length}\n`,
exitCode: 0,
signalCode: null,
});
},
timeout,
);

// Regression: WebWorker__dispatchExit deref'd the C++ Worker on the worker
// thread; if that was the last ref, ~Worker → ~EventTarget ran there and
// EventListenerMap::releaseAssertOrSetThreadUID tripped because the listener
Expand Down
Loading