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
10 changes: 8 additions & 2 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3921,12 +3921,18 @@ 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));

JSValue nextTickFunction = JSC::profiledCall(globalObject, ProfilingReason::API, initializer, JSC::getCallData(initializer), globalObject->globalThis(), args);
// Lazy PropertyCallback: must not throw, and must not leak the
// JSC::Exception cell as the cached property value on failure.
NakedPtr<JSC::Exception> returnedException;
JSValue nextTickFunction = JSC::profiledCall(globalObject, ProfilingReason::API, initializer, JSC::getCallData(initializer), globalObject->globalThis(), args, returnedException);
if (returnedException) [[unlikely]]
return jsUndefined();
if (nextTickFunction && nextTickFunction.isObject()) {
this->m_nextTickFunction.set(vm, this, nextTickFunction.getObject());
return nextTickFunction;
}

return nextTickFunction;
return jsUndefined();
}

static JSValue constructProcessNextTickFn(VM& vm, JSObject* processObject)
Expand Down
42 changes: 42 additions & 0 deletions test/js/node/process/process-nexttick-stack-overflow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

// When process.nextTick is accessed for the first time while the stack is
// already exhausted, the lazy initializer fails. Previously this cached the
// raw JSC::Exception cell as the value of process.nextTick, which then
// tripped a debug assertion in JSCell::toStringSlowCase (and threw a bogus
// "Cannot convert a symbol to a string" in release) when JS later tried to
// call it and build the "is not a function" error message.
test("process.nextTick first accessed at max stack depth does not crash", async () => {
const src = `
let done = false;
function F0() {
if (done) return;
try { F0(); } catch (e) {
done = true;
try { process.nextTick; } catch (_) {}
}
}
F0();
const nt = process.nextTick;
if (nt !== undefined && typeof nt !== "function")
throw new Error("process.nextTick leaked as a non-function value (typeof " + typeof nt + ")");
try { process.nextTick(); } catch (e) {
if (!(e instanceof Error)) throw new Error("unexpected throw " + e);
}
console.log("ok", typeof nt);
`;
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(stderr).toBe("");
expect(stdout.trim()).toMatch(/^ok (undefined|function)$/);
expect(proc.signalCode).toBeNull();
expect(exitCode).toBe(0);
});
Loading