From 4dcb2c997e01e4367ca6118629fb4ac712f9692c Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 8 Apr 2026 16:54:58 +0800 Subject: [PATCH] Better handle v8 callback with no valid context In https://github.com/lightpanda-io/browser/pull/1885 we added fallback to the incumbent context when the current context had be released (by us, but not by v8). This now handles the case where there is no incumbent context. It's not clear exactly why this can happen, but we do see it in some WPT tests (e.g. /html/browsers/the-window-object/named-access-on-the-window-object/navigated-named-objects.window.html) --- src/browser/js/Caller.zig | 26 +++++++++++++++++++++----- src/browser/js/Context.zig | 12 ++++++++---- src/browser/js/Env.zig | 2 +- src/browser/js/bridge.zig | 32 ++++++++++++++++++++++++-------- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index cd4d25d3..ed4a4119 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -39,9 +39,22 @@ prev_local: ?*const js.Local, prev_context: *Context, // Takes the raw v8 isolate and extracts the context from it. -pub fn init(self: *Caller, v8_isolate: *v8.Isolate) void { - const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate }); +// Returns false if the context has been destroyed (e.g., navigated-away iframe), +// in which case a JS exception has been thrown and the caller should return immediately. +pub fn init(self: *Caller, v8_isolate: *v8.Isolate) bool { + const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate }) orelse { + throwDetachedError(v8_isolate); + return false; + }; initWithContext(self, ctx, v8_context); + return true; +} + +fn throwDetachedError(isolate: *v8.Isolate) void { + const message = "Cannot execute in detached context (e.g., navigated-away iframe)"; + const v8_message = v8.v8__String__NewFromUtf8(isolate, message.ptr, v8.kNormal, @intCast(message.len)); + const js_exception = v8.v8__Exception__Error(v8_message); + _ = v8.v8__Isolate__ThrowException(isolate, js_exception); } fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void { @@ -60,9 +73,9 @@ fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) ctx.local = &self.local; } -pub fn initFromHandle(self: *Caller, handle: ?*const v8.FunctionCallbackInfo) void { +pub fn initFromHandle(self: *Caller, handle: ?*const v8.FunctionCallbackInfo) bool { const isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; - self.init(isolate); + return self.init(isolate); } pub fn deinit(self: *Caller) void { @@ -538,7 +551,10 @@ pub const Function = struct { pub fn call(comptime T: type, info_handle: *const v8.FunctionCallbackInfo, func: anytype, comptime opts: Opts) void { const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(info_handle).?; - const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate }); + const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate }) orelse { + throwDetachedError(v8_isolate); + return; + }; const info = FunctionCallbackInfo{ .handle = info_handle }; var hs: js.HandleScope = undefined; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index beec0625..b691af95 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -138,7 +138,8 @@ pub fn fromC(c_context: *const v8.Context) ?*Context { /// Returns the Context and v8::Context for the given isolate. /// If the current context is from a destroyed Context (e.g., navigated-away iframe), /// falls back to the incumbent context (the calling context). -pub fn fromIsolate(isolate: js.Isolate) struct { *Context, *const v8.Context } { +/// Returns null if neither context has a valid Context struct (both were destroyed). +pub fn fromIsolate(isolate: js.Isolate) ?struct { *Context, *const v8.Context } { const v8_context = v8.v8__Isolate__GetCurrentContext(isolate.handle).?; if (fromC(v8_context)) |ctx| { return .{ ctx, v8_context }; @@ -146,7 +147,8 @@ pub fn fromIsolate(isolate: js.Isolate) struct { *Context, *const v8.Context } { // The current context's Context struct has been freed (e.g., iframe navigated away). // Fall back to the incumbent context (the calling context). const v8_incumbent = v8.v8__Isolate__GetIncumbentContext(isolate.handle).?; - return .{ fromC(v8_incumbent).?, v8_incumbent }; + const ctx = fromC(v8_incumbent) orelse return null; + return .{ ctx, v8_incumbent }; } pub fn deinit(self: *Context) void { @@ -806,7 +808,9 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul const then_callback = newFunctionWithData(local, struct { pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { var c: Caller = undefined; - c.initFromHandle(callback_handle); + if (!c.initFromHandle(callback_handle)) { + return; + } defer c.deinit(); const info = Caller.FunctionCallbackInfo{ .handle = callback_handle.? }; @@ -830,7 +834,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul const catch_callback = newFunctionWithData(local, struct { pub fn callback(callback_handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { var c: Caller = undefined; - c.initFromHandle(callback_handle); + if (!c.initFromHandle(callback_handle)) return; defer c.deinit(); const info = Caller.FunctionCallbackInfo{ .handle = callback_handle.? }; diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index bae6a8f0..2c1ebf38 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -519,7 +519,7 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?; const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?; const isolate = js.Isolate{ .handle = v8_isolate }; - const ctx, const v8_context = Context.fromIsolate(isolate); + const ctx, const v8_context = Context.fromIsolate(isolate) orelse return; const local = js.Local{ .ctx = ctx, diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 8fbdc315..93c51f78 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -116,7 +116,9 @@ pub const Constructor = struct { fn wrap(handle: ?*const v8.FunctionCallbackInfo) callconv(.c) void { const v8_isolate = v8.v8__FunctionCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; - caller.init(v8_isolate); + if (!caller.init(v8_isolate)) { + return; + } defer caller.deinit(); caller.constructor(T, func, handle.?, .{ @@ -216,7 +218,9 @@ pub const Indexed = struct { fn wrap(idx: u32, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; - caller.init(v8_isolate); + if (!caller.init(v8_isolate)) { + return 0; + } defer caller.deinit(); return caller.getIndex(T, getter, idx, handle.?, .{ @@ -232,7 +236,9 @@ pub const Indexed = struct { fn wrap(handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; - caller.init(v8_isolate); + if (!caller.init(v8_isolate)) { + return 0; + } defer caller.deinit(); return caller.getEnumerator(T, enumerator, handle.?, .{}); } @@ -258,7 +264,9 @@ pub const NamedIndexed = struct { fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; - caller.init(v8_isolate); + if (!caller.init(v8_isolate)) { + return 0; + } defer caller.deinit(); return caller.getNamedIndex(T, getter, c_name.?, handle.?, .{ @@ -272,7 +280,9 @@ pub const NamedIndexed = struct { fn wrap(c_name: ?*const v8.Name, c_value: ?*const v8.Value, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; - caller.init(v8_isolate); + if (!caller.init(v8_isolate)) { + return 0; + } defer caller.deinit(); return caller.setNamedIndex(T, setter, c_name.?, c_value.?, handle.?, .{ @@ -286,7 +296,9 @@ pub const NamedIndexed = struct { fn wrap(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; - caller.init(v8_isolate); + if (!caller.init(v8_isolate)) { + return 0; + } defer caller.deinit(); return caller.deleteNamedIndex(T, deleter, c_name.?, handle.?, .{ @@ -387,7 +399,9 @@ pub const Property = struct { pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; - caller.init(v8_isolate); + if (!caller.init(v8_isolate)) { + return 0; + } defer caller.deinit(); const local = &caller.local; @@ -465,7 +479,9 @@ pub fn unknownObjectPropertyCallback(comptime JsApi: type) *const fn (?*const v8 const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; var caller: Caller = undefined; - caller.init(v8_isolate); + if (!caller.init(v8_isolate)) { + return 0; + } defer caller.deinit(); const local = &caller.local;