From 93ca18036b2f3062a6c60d35e56eee65682ea007 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Mon, 3 Aug 2026 23:03:17 -0400 Subject: [PATCH] Prevent V8 re-entry after requested termination Assisted-By: devx/e3d65847-fba6-490f-8cd2-6b88cf889c6f --- src/browser/js/Env.zig | 10 ++++------ src/browser/js/Function.zig | 24 ++++++++++++------------ src/browser/js/Script.zig | 6 +++--- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index d56a2d8a8..c240cb7de 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -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); } diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index c0d8c7b1d..f9067922e 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -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); diff --git a/src/browser/js/Script.zig b/src/browser/js/Script.zig index 94404851e..08b28ef95 100644 --- a/src/browser/js/Script.zig +++ b/src/browser/js/Script.zig @@ -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;