Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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