diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 6935f0cbbce..a58ad6fdb35 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -3913,6 +3913,8 @@ static JSValue constructMainModuleProperty(VM& vm, JSObject* processObject) JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObject) { + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + JSNextTickQueue* nextTickQueueObject; if (!globalObject->m_nextTickQueue) { nextTickQueueObject = JSNextTickQueue::create(globalObject); @@ -3930,6 +3932,11 @@ JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObjec args.append(JSC::JSFunction::create(vm, globalObject, 1, String(), jsFunctionReportUncaughtException, ImplementationVisibility::Private)); JSValue nextTickFunction = JSC::profiledCall(globalObject, ProfilingReason::API, initializer, JSC::getCallData(initializer), globalObject->globalThis(), args); + if (auto* exception = scope.exception()) { + (void)scope.tryClearException(); + Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception); + return jsUndefined(); + } if (nextTickFunction && nextTickFunction.isObject()) { this->m_nextTickFunction.set(vm, this, nextTickFunction.getObject()); } diff --git a/test/js/node/process/process-nexttick-stack-overflow.test.ts b/test/js/node/process/process-nexttick-stack-overflow.test.ts new file mode 100644 index 00000000000..8e0f0a5f7f8 --- /dev/null +++ b/test/js/node/process/process-nexttick-stack-overflow.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// When the stack is nearly exhausted, lazily initializing process.nextTick +// runs JS that can throw a stack overflow. The lazy property callback must +// not leave an exception pending (assertion in JSObject::get) or reify the +// property as an internal Exception object. +test("process.nextTick lazy init does not leak Exception object on stack overflow", async () => { + const src = ` + const { writeFileSync } = require("fs"); + let done = false; + function recurse() { + try { recurse(); } catch {} + if (done) return; + done = true; + try { process.nextTick(() => {}); } catch {} + } + recurse(); + writeFileSync(1, "typeof=" + typeof process.nextTick + "\\n"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", src], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout.trim()).not.toBe("typeof=object"); + expect(["typeof=function", "typeof=undefined"]).toContain(stdout.trim()); + expect(proc.signalCode).toBeNull(); +});