Prevent V8 re-entry after requested termination

Assisted-By: devx/e3d65847-fba6-490f-8cd2-6b88cf889c6f
This commit is contained in:
Scott Taylor committed 2026-08-03 23:03:17 -04:00
1 parent e143533987
commit 93ca18036b
3 files changed
+19 -21

No files matched your search

+4 -6
View File
@@ -544,13 +544,11 @@ pub fn dumpMemoryStats(self: *Env) void {
}
pub fn isExecutionTerminating(self: *const Env) bool {
return v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle);
return v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle) or self.terminatePending();
}
// Whether a forcible terminate has been requested (and not yet cleared by
// cancelTerminate). Unlike isExecutionTerminating, this is our own sticky
// flag, so it stays true after V8 consumes the terminate on the JSEntry
// unwind. Callers about to enter a fresh eval use it to refuse to run.
// Our sticky terminate flag remains true after V8 consumes the request while
// unwinding a JSEntry. isExecutionTerminating includes it to block fresh work.
pub fn terminatePending(self: *const Env) bool {
return self.terminate_requested.load(.acquire);
}
@@ -632,7 +630,7 @@ pub fn cancelTerminate(self: *Env) void {
pub fn performIsolateMicrotasks(self: *Env) void {
self.terminate_mutex.lockUncancelable(lp.io);
defer self.terminate_mutex.unlock(lp.io);
if (v8.v8__Isolate__IsExecutionTerminating(self.isolate.handle)) return;
if (self.isExecutionTerminating()) return;
v8.v8__Isolate__PerformMicrotaskCheckpoint(self.isolate.handle);
}
+12 -12
View File
@@ -66,7 +66,7 @@ pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Objec
}
// See _tryCallWithThis for why a pending termination blocks V8 entry.
if (v8.v8__Isolate__IsExecutionTerminating(local.isolate.handle)) {
if (local.ctx.env.isExecutionTerminating()) {
return error.ExecutionTerminated;
}
@@ -77,7 +77,7 @@ pub fn newInstance(self: *const Function, caught: *js.TryCatch.Caught) !js.Objec
// This creates a new instance using this Function as a constructor.
// const c_args = @as(?[*]const ?*c.Value, @ptrCast(&.{}));
const handle = v8.v8__Function__NewInstance(self.handle, local.handle, 0, null) orelse {
if (v8.v8__Isolate__IsExecutionTerminating(local.isolate.handle)) {
if (local.ctx.env.isExecutionTerminating()) {
return error.ExecutionTerminated;
}
caught.* = try_catch.caughtOrError(local.call_arena, error.Unknown);
@@ -158,7 +158,7 @@ fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args
// A pending termination (watchdog / CDP-disconnect kill) must not be
// followed by another V8 entry. Callers must treat ExecutionTerminated as
// stop running JS and unwind".
if (v8.v8__Isolate__IsExecutionTerminating(local.isolate.handle)) {
if (local.ctx.env.isExecutionTerminating()) {
return error.ExecutionTerminated;
}
@@ -212,7 +212,7 @@ fn _tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args
defer try_catch.deinit();
const handle = v8.v8__Function__Call(self.handle, local.handle, js_this.handle, @as(c_int, @intCast(js_args.len)), c_args) orelse {
if (v8.v8__Isolate__IsExecutionTerminating(local.isolate.handle)) {
if (local.ctx.env.isExecutionTerminating()) {
// Terminated mid-call, not a JS throw: no rethrow, no reporting.
return error.ExecutionTerminated;
}
@@ -265,7 +265,7 @@ pub fn persistWithThis(self: *const Function, value: anytype) !Global {
}
const testing = @import("../../testing.zig");
test "Function: termination is classified and blocks re-entry" {
test "Function: requested termination is classified and blocks re-entry" {
const frame = try testing.createFrame();
defer testing.test_session.closeAllPages();
@@ -286,24 +286,18 @@ test "Function: termination is classified and blocks re-entry" {
probe_err: ?anyerror = null,
fn kill(self: *@This()) void {
self.env.terminate();
self.env.requestTerminate();
}
fn probed(self: *@This()) void {
self.probe_ran = true;
}
// Runs at call depth >= 1: the killed inner call must leave the
// termination pending, and the follow-up call must refuse to enter V8
// (running it would silently clear the pending termination).
fn nested(self: *@This()) void {
var caught: js.TryCatch.Caught = undefined;
_ = self.f_kill.?.tryCall(void, .{}, &caught) catch |err| {
self.kill_err = err;
};
_ = self.f_probe.?.tryCall(void, .{}, &caught) catch |err| {
self.probe_err = err;
};
}
};
var state = State{ .env = env };
@@ -322,6 +316,12 @@ test "Function: termination is classified and blocks re-entry" {
var caught: js.TryCatch.Caught = undefined;
try testing.expectError(error.ExecutionTerminated, driver_fn.tryCall(void, .{nested_cb}, &caught));
try testing.expectEqual(error.ExecutionTerminated, state.kill_err.?);
try testing.expectEqual(true, env.terminatePending());
try testing.expectEqual(false, v8.v8__Isolate__IsExecutionTerminating(env.isolate.handle));
_ = state.f_probe.?.tryCall(void, .{}, &caught) catch |err| {
state.probe_err = err;
};
try testing.expectEqual(error.ExecutionTerminated, state.probe_err.?);
try testing.expectEqual(false, state.probe_ran);
+3 -3
View File
@@ -32,12 +32,12 @@ handle: *const v8.Script,
pub fn run(self: Script) !js.Value {
// See Function._tryCallWithThis for why a pending termination blocks V8
// entry and is distinct from a JS throw.
const isolate_handle = self.local.isolate.handle;
if (v8.v8__Isolate__IsExecutionTerminating(isolate_handle)) {
const env = self.local.ctx.env;
if (env.isExecutionTerminating()) {
return error.ExecutionTerminated;
}
const result = v8.v8__Script__Run(self.handle, self.local.handle) orelse {
if (v8.v8__Isolate__IsExecutionTerminating(isolate_handle)) {
if (env.isExecutionTerminating()) {
return error.ExecutionTerminated;
}
return error.JsException;