Skip to content
Open
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
2 changes: 0 additions & 2 deletions src/jsc/bindings/NodeVMModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ JSValue NodeVMModule::evaluate(JSGlobalObject* globalObject, uint32_t timeout, b
// below, then convert it to ERR_SCRIPT_EXECUTION_*.
std::ignore = scope.exception();
if (vm.hasTerminationRequest() || vm.hasPendingTerminationException()) {
vm.drainMicrotasksForGlobalObject(nodeVmGlobalObject);
DECLARE_TOP_EXCEPTION_SCOPE(vm).clearException();
vm.clearHasTerminationRequest();
if (getSigintReceived()) {
Expand Down Expand Up @@ -245,7 +244,6 @@ JSValue NodeVMModule::evaluate(JSGlobalObject* globalObject, uint32_t timeout, b
// so the exception-check validator is satisfied before the TOP scope.
std::ignore = scope.exception();
if (vm.hasTerminationRequest() || vm.hasPendingTerminationException()) {
vm.drainMicrotasksForGlobalObject(nodeVmGlobalObject);
DECLARE_TOP_EXCEPTION_SCOPE(vm).clearException();
vm.clearHasTerminationRequest();
if (getSigintReceived()) {
Expand Down
1 change: 0 additions & 1 deletion src/jsc/bindings/NodeVMScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,6 @@ void NodeVMScript::destroy(JSCell* cell)
static bool checkForTermination(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, NodeVMScript* script, std::optional<double> timeout)
{
if (vm.hasTerminationRequest()) {
vm.drainMicrotasksForGlobalObject(globalObject);
// The termination may have fired inside an afterEvaluate microtask
// checkpoint, leaving the termination exception pending; clear it so
// the ERR_SCRIPT_EXECUTION_* error below replaces it.
Expand Down
105 changes: 105 additions & 0 deletions test/js/node/vm/vm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1177,3 +1177,108 @@ describe("node:vm SourceTextModule cyclic graph linking", () => {
expect(exitCode).toBe(0);
});
});

describe("timeout termination preserves already-queued promise reactions", () => {
// default microtask mode: jobs the guest queued before expiry share the host's queue
// and must run at the next host checkpoint, so a host `.then` on the guest's promise settles.
test.concurrent("runInContext", async () => {
const fixture = `
const vm = require("node:vm");
const c = vm.createContext({ o: [] });
let code;
try {
vm.runInContext(
"hostPromise = Promise.resolve().then(()=>{ o.push(1); return 'v' })" +
" .then((x)=>{ o.push(2); return x }); for(;;);",
c, { timeout: 80 });
} catch (e) { code = e.code; }
let hostSaw = "no";
c.hostPromise?.then(() => { hostSaw = "yes"; });
setTimeout(() => {
console.log("o=" + JSON.stringify(c.o) + " host=" + hostSaw + " code=" + code);
}, 0);
`;

await using proc = Bun.spawn({
cmd: [bunExe(), "-e", fixture],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({
stdout: "o=[1,2] host=yes code=ERR_SCRIPT_EXECUTION_TIMEOUT",
stderr: "",
exitCode: 0,
});
});

test.concurrent("runInThisContext", async () => {
const fixture = `
const vm = require("node:vm");
globalThis.o = [];
Promise.resolve().then(() => { o.push("host-before"); });
let code;
try {
vm.runInThisContext(
"globalThis.guestPromise = Promise.resolve().then(()=>{ o.push('guest'); }); for(;;);",
{ timeout: 80 });
} catch (e) { code = e.code; }
let hostSaw = "no";
globalThis.guestPromise?.then(() => { hostSaw = "yes"; });
setTimeout(() => {
console.log("o=" + JSON.stringify(o) + " host=" + hostSaw + " code=" + code);
}, 0);
`;

await using proc = Bun.spawn({
cmd: [bunExe(), "-e", fixture],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({
stdout: 'o=["host-before","guest"] host=yes code=ERR_SCRIPT_EXECUTION_TIMEOUT',
stderr: "",
exitCode: 0,
});
});

test.concurrent("SourceTextModule.evaluate", async () => {
const fixture = `
const vm = require("node:vm");
const c = vm.createContext({ o: [] });
const mod = new vm.SourceTextModule(
"globalThis.hostPromise = Promise.resolve().then(()=>{ o.push(1); }); for(;;);",
{ context: c });
await mod.link(() => { throw 0; });
let code;
try { await mod.evaluate({ timeout: 80 }); } catch (e) { code = e.code; }
let hostSaw = "no";
c.hostPromise?.then(() => { hostSaw = "yes"; });
setTimeout(() => {
console.log("o=" + JSON.stringify(c.o) + " host=" + hostSaw + " code=" + code);
}, 0);
`;

await using proc = Bun.spawn({
cmd: [bunExe(), "--input-type=module", "-e", fixture],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({
stdout: "o=[1] host=yes code=ERR_SCRIPT_EXECUTION_TIMEOUT",
stderr: "",
exitCode: 0,
});
});
});
Loading