From 041d9d41fb0a3e9cca082336cbd20372727f6fd3 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 17 Mar 2026 18:04:44 +0800 Subject: [PATCH] Fallback to the Incumbent Context when the Current Context is dangling This specifically fixes a WPT crash running: /html/browsers/browsing-the-web/history-traversal/001.html (And probably a few others). Isolate::GetCurrentContext can return a 'detached' context. And, for us, that's a problem, because 'detached' v8::Context references a js.Context that we've deinit'd. This seems to only happen when frames pass values around to other frames and then those frames are removed. It might also require some async'ing, I'm not sure. To solve this, when we destroy a js.Context, we store null in the v8::Context's embedder data, removing the link to our (dead) js.Context. When we load a js.Context from a v8.Context, we check for null. If it is null, we return the Incumbent context instead. This should never be null, as it's always the context currently executing code. I'm not sure if falling back to the Incumbent context is always correct, but it does solve the crash. --- src/browser/js/Caller.zig | 8 +++----- src/browser/js/Context.zig | 29 ++++++++++++++++++++++------- src/browser/js/Env.zig | 8 ++++---- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 297d4ac1..9ecd65b2 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -40,8 +40,8 @@ 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 v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?; - initWithContext(self, Context.fromC(v8_context), v8_context); + const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate }); + initWithContext(self, ctx, v8_context); } fn initWithContext(self: *Caller, ctx: *Context, v8_context: *const v8.Context) void { @@ -537,9 +537,7 @@ 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 v8_context = v8.v8__Isolate__GetCurrentContext(v8_isolate).?; - - const ctx = Context.fromC(v8_context); + const ctx, const v8_context = Context.fromIsolate(.{ .handle = v8_isolate }); 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 ea1b6f7a..da7362aa 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -119,12 +119,22 @@ const ModuleEntry = struct { resolver_promise: ?js.Promise.Global = null, }; -pub fn fromC(c_context: *const v8.Context) *Context { +pub fn fromC(c_context: *const v8.Context) ?*Context { return @ptrCast(@alignCast(v8.v8__Context__GetAlignedPointerFromEmbedderData(c_context, 1))); } -pub fn fromIsolate(isolate: js.Isolate) *Context { - return fromC(v8.v8__Isolate__GetCurrentContext(isolate.handle).?); +/// 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 } { + const v8_context = v8.v8__Isolate__GetCurrentContext(isolate.handle).?; + if (fromC(v8_context)) |ctx| { + return .{ ctx, 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 }; } pub fn deinit(self: *Context) void { @@ -155,6 +165,11 @@ pub fn deinit(self: *Context) void { self.session.releaseOrigin(self.origin); + // Clear the embedder data so that if V8 keeps this context alive + // (because objects created in it are still referenced), we don't + // have a dangling pointer to our freed Context struct. + v8.v8__Context__SetAlignedPointerInEmbedderData(entered.handle, 1, null); + v8.v8__Global__Reset(&self.handle); env.isolate.notifyContextDisposed(); // There can be other tasks associated with this context that we need to @@ -255,7 +270,7 @@ pub fn toLocal(self: *Context, global: anytype) js.Local.ToLocalReturnType(@Type } pub fn getIncumbent(self: *Context) *Page { - return fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).page; + return fromC(v8.v8__Isolate__GetIncumbentContext(self.env.isolate.handle).?).?.page; } pub fn stringToPersistedFunction( @@ -479,7 +494,7 @@ fn resolveModuleCallback( ) callconv(.c) ?*const v8.Module { _ = import_attributes; - const self = fromC(c_context.?); + const self = fromC(c_context.?).?; const local = js.Local{ .ctx = self, .handle = c_context.?, @@ -512,7 +527,7 @@ pub fn dynamicModuleCallback( _ = host_defined_options; _ = import_attrs; - const self = fromC(c_context.?); + const self = fromC(c_context.?).?; const local = js.Local{ .ctx = self, .handle = c_context.?, @@ -559,7 +574,7 @@ pub fn dynamicModuleCallback( pub fn metaObjectCallback(c_context: ?*v8.Context, c_module: ?*v8.Module, c_meta: ?*v8.Value) callconv(.c) void { // @HandleScope implement this without a fat context/local.. - const self = fromC(c_context.?); + const self = fromC(c_context.?).?; var local = js.Local{ .ctx = self, .handle = c_context.?, diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 1ac9e6b3..09117eb0 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -497,13 +497,13 @@ pub fn terminate(self: *const Env) void { fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) void { const promise_handle = v8.v8__PromiseRejectMessage__GetPromise(&message_handle).?; const v8_isolate = v8.v8__Object__GetIsolate(@ptrCast(promise_handle)).?; - const js_isolate = js.Isolate{ .handle = v8_isolate }; - const ctx = Context.fromIsolate(js_isolate); + const isolate = js.Isolate{ .handle = v8_isolate }; + const ctx, const v8_context = Context.fromIsolate(isolate); const local = js.Local{ .ctx = ctx, - .isolate = js_isolate, - .handle = v8.v8__Isolate__GetCurrentContext(v8_isolate).?, + .isolate = isolate, + .handle = v8_context, .call_arena = ctx.call_arena, };