diff --git a/src/ArenaPool.zig b/src/ArenaPool.zig index 2f8de17ff..2e3f25a4b 100644 --- a/src/ArenaPool.zig +++ b/src/ArenaPool.zig @@ -17,12 +17,15 @@ // along with this program. If not, see . const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const ArenaPool = @This(); +const IS_DEBUG = builtin.mode == .Debug; + allocator: Allocator, retain_bytes: usize, free_list_len: u16 = 0, @@ -30,10 +33,17 @@ free_list: ?*Entry = null, free_list_max: u16, entry_pool: std.heap.MemoryPool(Entry), mutex: std.Thread.Mutex = .{}, +// Debug mode: track acquire/release counts per debug name to detect leaks and double-frees +_leak_track: if (IS_DEBUG) std.StringHashMapUnmanaged(isize) else void = if (IS_DEBUG) .empty else {}, const Entry = struct { next: ?*Entry, arena: ArenaAllocator, + debug: if (IS_DEBUG) []const u8 else void = if (IS_DEBUG) "" else {}, +}; + +pub const DebugInfo = struct { + debug: []const u8 = "", }; pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) ArenaPool { @@ -42,10 +52,26 @@ pub fn init(allocator: Allocator, free_list_max: u16, retain_bytes: usize) Arena .free_list_max = free_list_max, .retain_bytes = retain_bytes, .entry_pool = .init(allocator), + ._leak_track = if (IS_DEBUG) .empty else {}, }; } pub fn deinit(self: *ArenaPool) void { + if (IS_DEBUG) { + var has_leaks = false; + var it = self._leak_track.iterator(); + while (it.next()) |kv| { + if (kv.value_ptr.* != 0) { + std.debug.print("ArenaPool leak detected: '{s}' count={d}\n", .{ kv.key_ptr.*, kv.value_ptr.* }); + has_leaks = true; + } + } + if (has_leaks) { + @panic("ArenaPool: leaked arenas detected"); + } + self._leak_track.deinit(self.allocator); + } + var entry = self.free_list; while (entry) |e| { entry = e.next; @@ -54,13 +80,21 @@ pub fn deinit(self: *ArenaPool) void { self.entry_pool.deinit(); } -pub fn acquire(self: *ArenaPool) !Allocator { +pub fn acquire(self: *ArenaPool, dbg: DebugInfo) !Allocator { self.mutex.lock(); defer self.mutex.unlock(); if (self.free_list) |entry| { self.free_list = entry.next; self.free_list_len -= 1; + if (IS_DEBUG) { + entry.debug = dbg.debug; + const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug); + if (!gop.found_existing) { + gop.value_ptr.* = 0; + } + gop.value_ptr.* += 1; + } return entry.arena.allocator(); } @@ -68,8 +102,16 @@ pub fn acquire(self: *ArenaPool) !Allocator { entry.* = .{ .next = null, .arena = ArenaAllocator.init(self.allocator), + .debug = if (IS_DEBUG) dbg.debug else {}, }; + if (IS_DEBUG) { + const gop = try self._leak_track.getOrPut(self.allocator, dbg.debug); + if (!gop.found_existing) { + gop.value_ptr.* = 0; + } + gop.value_ptr.* += 1; + } return entry.arena.allocator(); } @@ -83,6 +125,19 @@ pub fn release(self: *ArenaPool, allocator: Allocator) void { self.mutex.lock(); defer self.mutex.unlock(); + if (IS_DEBUG) { + if (self._leak_track.getPtr(entry.debug)) |count| { + count.* -= 1; + if (count.* < 0) { + std.debug.print("ArenaPool double-free detected: '{s}'\n", .{entry.debug}); + @panic("ArenaPool: double-free detected"); + } + } else { + std.debug.print("ArenaPool release of untracked arena: '{s}'\n", .{entry.debug}); + @panic("ArenaPool: release of untracked arena"); + } + } + const free_list_len = self.free_list_len; if (free_list_len == self.free_list_max) { arena.deinit(); @@ -106,7 +161,7 @@ test "arena pool - basic acquire and use" { var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); defer pool.deinit(); - const alloc = try pool.acquire(); + const alloc = try pool.acquire(.{ .debug = "test" }); const buf = try alloc.alloc(u8, 64); @memset(buf, 0xAB); try testing.expectEqual(@as(u8, 0xAB), buf[0]); @@ -118,14 +173,14 @@ test "arena pool - reuse entry after release" { var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); defer pool.deinit(); - const alloc1 = try pool.acquire(); + const alloc1 = try pool.acquire(.{ .debug = "test" }); try testing.expectEqual(@as(u16, 0), pool.free_list_len); pool.release(alloc1); try testing.expectEqual(@as(u16, 1), pool.free_list_len); // The same entry should be returned from the free list. - const alloc2 = try pool.acquire(); + const alloc2 = try pool.acquire(.{ .debug = "test" }); try testing.expectEqual(@as(u16, 0), pool.free_list_len); try testing.expectEqual(alloc1.ptr, alloc2.ptr); @@ -136,9 +191,9 @@ test "arena pool - multiple concurrent arenas" { var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); defer pool.deinit(); - const a1 = try pool.acquire(); - const a2 = try pool.acquire(); - const a3 = try pool.acquire(); + const a1 = try pool.acquire(.{ .debug = "test1" }); + const a2 = try pool.acquire(.{ .debug = "test2" }); + const a3 = try pool.acquire(.{ .debug = "test3" }); // All three must be distinct arenas. try testing.expect(a1.ptr != a2.ptr); @@ -161,8 +216,8 @@ test "arena pool - free list respects max limit" { var pool = ArenaPool.init(testing.allocator, 1, 1024 * 16); defer pool.deinit(); - const a1 = try pool.acquire(); - const a2 = try pool.acquire(); + const a1 = try pool.acquire(.{ .debug = "test1" }); + const a2 = try pool.acquire(.{ .debug = "test2" }); pool.release(a1); try testing.expectEqual(@as(u16, 1), pool.free_list_len); @@ -176,7 +231,7 @@ test "arena pool - reset clears memory without releasing" { var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); defer pool.deinit(); - const alloc = try pool.acquire(); + const alloc = try pool.acquire(.{ .debug = "test" }); const buf = try alloc.alloc(u8, 128); @memset(buf, 0xFF); @@ -200,8 +255,8 @@ test "arena pool - deinit with entries in free list" { // detected by the test allocator). var pool = ArenaPool.init(testing.allocator, 512, 1024 * 16); - const a1 = try pool.acquire(); - const a2 = try pool.acquire(); + const a1 = try pool.acquire(.{ .debug = "test1" }); + const a2 = try pool.acquire(.{ .debug = "test2" }); _ = try a1.alloc(u8, 256); _ = try a2.alloc(u8, 512); pool.release(a1); diff --git a/src/browser/Page.zig b/src/browser/Page.zig index c3a6b5a3d..eef70bbc4 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -302,7 +302,11 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self); errdefer self._script_manager.deinit(); - self.js = try browser.env.createContext(self); + self.js = try browser.env.createContext(self, .{ + .identity = &session.identity, + .identity_arena = session.page_arena, + .call_arena = self.call_arena, + }); errdefer self.js.deinit(); document._page = self; diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 73b6b26e1..e318e486f 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -24,6 +24,7 @@ const log = @import("../log.zig"); const App = @import("../App.zig"); const js = @import("js/js.zig"); +const v8 = js.v8; const storage = @import("webapi/storage/storage.zig"); const Navigation = @import("webapi/navigation/Navigation.zig"); const History = @import("webapi/History.zig"); @@ -65,17 +66,14 @@ page_arena: Allocator, // Origin map for same-origin context sharing. Scoped to the root page lifetime. origins: std.StringHashMapUnmanaged(*js.Origin) = .empty, +// Identity tracking for the main world. All main world contexts share this, +// ensuring object identity works across same-origin frames. +identity: js.Identity = .{}, + // Shared resources for all pages in this session. // These live for the duration of the page tree (root + frames). arena_pool: *ArenaPool, -// In Debug, we use this to see if anything fails to release an arena back to -// the pool. -_arena_pool_leak_track: if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, struct { - owner: []const u8, - count: usize, -}) else void = if (IS_DEBUG) .empty else {}, - page: ?Page, queued_navigation: std.ArrayList(*Page), @@ -84,17 +82,17 @@ queued_navigation: std.ArrayList(*Page), // about:blank navigations (which may add to queued_navigation). queued_queued_navigation: std.ArrayList(*Page), -page_id_gen: u32, -frame_id_gen: u32, +page_id_gen: u32 = 0, +frame_id_gen: u32 = 0, pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { const allocator = browser.app.allocator; const arena_pool = browser.arena_pool; - const arena = try arena_pool.acquire(); + const arena = try arena_pool.acquire(.{ .debug = "Session" }); errdefer arena_pool.release(arena); - const page_arena = try arena_pool.acquire(); + const page_arena = try arena_pool.acquire(.{ .debug = "Session.page_arena" }); errdefer arena_pool.release(page_arena); self.* = .{ @@ -104,8 +102,6 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi .page_arena = page_arena, .factory = Factory.init(page_arena), .history = .{}, - .page_id_gen = 0, - .frame_id_gen = 0, // The prototype (EventTarget) for Navigation is created when a Page is created. .navigation = .{ ._proto = undefined }, .storage_shed = .{}, @@ -171,32 +167,11 @@ pub const GetArenaOpts = struct { }; pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator { - const allocator = try self.arena_pool.acquire(); - if (comptime IS_DEBUG) { - // Use session's arena (not page_arena) since page_arena gets reset between pages - const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr)); - if (gop.found_existing and gop.value_ptr.count != 0) { - log.err(.bug, "ArenaPool Double Use", .{ .owner = gop.value_ptr.*.owner }); - @panic("ArenaPool Double Use"); - } - gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 }; - } - return allocator; + return self.arena_pool.acquire(.{ .debug = opts.debug }); } pub fn releaseArena(self: *Session, allocator: Allocator) void { - if (comptime IS_DEBUG) { - const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?; - if (found.count != 1) { - log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count }); - if (comptime builtin.is_test) { - @panic("ArenaPool Double Free"); - } - return; - } - found.count = 0; - } - return self.arena_pool.release(allocator); + self.arena_pool.release(allocator); } pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin { @@ -237,18 +212,9 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void { /// Reset page_arena and factory for a clean slate. /// Called when root page is removed. fn resetPageResources(self: *Session) void { - // Check for arena leaks before releasing - if (comptime IS_DEBUG) { - var it = self._arena_pool_leak_track.valueIterator(); - while (it.next()) |value_ptr| { - if (value_ptr.count > 0) { - log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); - } - } - self._arena_pool_leak_track.clearRetainingCapacity(); - } + self.identity.deinit(); + self.identity = .{}; - // All origins should have been released when contexts were destroyed if (comptime IS_DEBUG) { std.debug.assert(self.origins.count() == 0); } @@ -259,10 +225,9 @@ fn resetPageResources(self: *Session) void { while (it.next()) |value| { value.*.deinit(app); } - self.origins.clearRetainingCapacity(); + self.origins = .empty; } - // Release old page_arena and acquire fresh one self.frame_id_gen = 0; self.arena_pool.reset(self.page_arena, 64 * 1024); self.factory = Factory.init(self.page_arena); @@ -632,16 +597,6 @@ fn processRootQueuedNavigation(self: *Session) !void { defer self.arena_pool.release(qn.arena); - // HACK - // Mark as released in tracking BEFORE removePage clears the map. - // We can't call releaseArena() because that would also return the arena - // to the pool, making the memory invalid before we use qn.url/qn.opts. - if (comptime IS_DEBUG) { - if (self._arena_pool_leak_track.getPtr(@intFromPtr(qn.arena.ptr))) |found| { - found.count = 0; - } - } - self.removePage(); self.page = @as(Page, undefined); @@ -672,3 +627,36 @@ pub fn nextPageId(self: *Session) u32 { self.page_id_gen = id; return id; } + +// A type that has a finalizer can have its finalizer called one of two ways. +// The first is from V8 via the WeakCallback we give to weakRef. But that isn't +// guaranteed to fire, so we track this in finalizer_callbacks and call them on +// page reset. +pub const FinalizerCallback = struct { + arena: Allocator, + session: *Session, + ptr: *anyopaque, + global: v8.Global, + identity: *js.Identity, + zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void, + + pub fn deinit(self: *FinalizerCallback) void { + self.zig_finalizer(self.ptr, self.session); + self.session.releaseArena(self.arena); + } + + /// Release this item from the identity tracking maps (called after finalizer runs from V8) + pub fn releaseIdentity(self: *FinalizerCallback) void { + const session = self.session; + const id = @intFromPtr(self.ptr); + + if (self.identity.identity_map.fetchRemove(id)) |kv| { + var global = kv.value; + v8.v8__Global__Reset(&global); + } + + _ = self.identity.finalizer_callbacks.remove(id); + + session.releaseArena(self.arena); + } +}; diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index da7362aa8..b46ec11de 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -63,7 +63,9 @@ templates: []*const v8.FunctionTemplate, // Arena for the lifetime of the context arena: Allocator, -// The page.call_arena +// The call_arena for this context. For main world contexts this is +// page.call_arena. For isolated world contexts this is a separate arena +// owned by the IsolatedWorld. call_arena: Allocator, // Because calls can be nested (i.e.a function calling a callback), @@ -79,6 +81,16 @@ local: ?*const js.Local = null, origin: *Origin, +// Identity tracking for this context. For main world contexts, this points to +// Session's Identity. For isolated world contexts (CDP inspector), this points +// to IsolatedWorld's Identity. This ensures same-origin frames share object +// identity while isolated worlds have separate identity tracking. +identity: *js.Identity, + +// Allocator to use for identity map operations. For main world contexts this is +// session.page_arena, for isolated worlds it's the isolated world's arena. +identity_arena: Allocator, + // Unlike other v8 types, like functions or objects, modules are not shared // across origins. global_modules: std.ArrayList(v8.Global) = .empty, @@ -185,9 +197,8 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void { lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc }); const origin = try self.session.getOrCreateOrigin(key); - errdefer self.session.releaseOrigin(origin); - try origin.takeover(self.origin); + self.session.releaseOrigin(self.origin); self.origin = origin; { @@ -203,16 +214,16 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void { } pub fn trackGlobal(self: *Context, global: v8.Global) !void { - return self.origin.trackGlobal(global); + return self.identity.globals.append(self.identity_arena, global); } pub fn trackTemp(self: *Context, global: v8.Global) !void { - return self.origin.trackTemp(global); + return self.identity.temps.put(self.identity_arena, global.data_ptr, global); } pub fn weakRef(self: *Context, obj: anytype) void { const resolved = js.Local.resolveValue(obj); - const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { + const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); @@ -224,7 +235,7 @@ pub fn weakRef(self: *Context, obj: anytype) void { pub fn safeWeakRef(self: *Context, obj: anytype) void { const resolved = js.Local.resolveValue(obj); - const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { + const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); @@ -237,7 +248,7 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void { pub fn strongRef(self: *Context, obj: anytype) void { const resolved = js.Local.resolveValue(obj); - const fc = self.origin.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { + const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); @@ -247,6 +258,48 @@ pub fn strongRef(self: *Context, obj: anytype) void { v8.v8__Global__ClearWeak(&fc.global); } +pub const IdentityResult = struct { + value_ptr: *v8.Global, + found_existing: bool, +}; + +pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult { + const gop = try self.identity.identity_map.getOrPut(self.identity_arena, ptr); + return .{ + .value_ptr = gop.value_ptr, + .found_existing = gop.found_existing, + }; +} + +pub fn releaseTemp(self: *Context, global: v8.Global) void { + if (self.identity.temps.fetchRemove(global.data_ptr)) |kv| { + var g = kv.value; + v8.v8__Global__Reset(&g); + } +} + +pub fn createFinalizerCallback( + self: *Context, + global: v8.Global, + ptr: *anyopaque, + zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void, +) !*Session.FinalizerCallback { + const session = self.session; + const arena = try session.getArena(.{ .debug = "FinalizerCallback" }); + errdefer session.releaseArena(arena); + const fc = try arena.create(Session.FinalizerCallback); + fc.* = .{ + .arena = arena, + .session = session, + .ptr = ptr, + .global = global, + .zig_finalizer = zig_finalizer, + // Store identity pointer for cleanup when V8 GCs the object + .identity = self.identity, + }; + return fc; +} + // Any operation on the context have to be made from a local. pub fn localScope(self: *Context, ls: *js.Local.Scope) void { const isolate = self.isolate; diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 09117eb05..9b3a1b4ce 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -34,6 +34,7 @@ const Snapshot = @import("Snapshot.zig"); const Inspector = @import("Inspector.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const Window = @import("../webapi/Window.zig"); const JsApis = bridge.JsApis; @@ -254,8 +255,15 @@ pub fn deinit(self: *Env) void { allocator.destroy(self.isolate_params); } -pub fn createContext(self: *Env, page: *Page) !*Context { - const context_arena = try self.app.arena_pool.acquire(); +pub const ContextParams = struct { + identity: *js.Identity, + identity_arena: Allocator, + call_arena: Allocator, + debug_name: []const u8 = "Context", +}; + +pub fn createContext(self: *Env, page: *Page, params: ContextParams) !*Context { + const context_arena = try self.app.arena_pool.acquire(.{ .debug = params.debug_name }); errdefer self.app.arena_pool.release(context_arena); const isolate = self.isolate; @@ -300,33 +308,43 @@ pub fn createContext(self: *Env, page: *Page) !*Context { v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); } - // our window wrapped in a v8::Global - var global_global: v8.Global = undefined; - v8.v8__Global__New(isolate.handle, global_obj, &global_global); - const context_id = self.context_id; self.context_id = context_id + 1; - const origin = try page._session.getOrCreateOrigin(null); - errdefer page._session.releaseOrigin(origin); + const session = page._session; + const origin = try session.getOrCreateOrigin(null); + errdefer session.releaseOrigin(origin); const context = try context_arena.create(Context); context.* = .{ .env = self, .page = page, - .session = page._session, .origin = origin, .id = context_id, + .session = session, .isolate = isolate, .arena = context_arena, .handle = context_global, .templates = self.templates, - .call_arena = page.call_arena, + .call_arena = params.call_arena, .microtask_queue = microtask_queue, .script_manager = &page._script_manager, .scheduler = .init(context_arena), + .identity = params.identity, + .identity_arena = params.identity_arena, }; - try context.origin.identity_map.putNoClobber(origin.arena, @intFromPtr(page.window), global_global); + + { + // Multiple contexts can be created for the same Window (via CDP). We only + // need to register the first one. + const gop = try params.identity.identity_map.getOrPut(params.identity_arena, @intFromPtr(page.window)); + if (gop.found_existing == false) { + // our window wrapped in a v8::Global + var global_global: v8.Global = undefined; + v8.v8__Global__New(isolate.handle, global_obj, &global_global); + gop.value_ptr.* = global_global; + } + } // Store a pointer to our context inside the v8 context so that, given // a v8 context, we can get our context out diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index bfb5e53d4..4c84a08f6 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -21,6 +21,7 @@ const js = @import("js.zig"); const v8 = js.v8; const log = @import("../../log.zig"); +const Session = @import("../Session.zig"); const Function = @This(); @@ -210,10 +211,10 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { try ctx.trackGlobal(global); - return .{ .handle = global, .origin = {} }; + return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .origin = ctx.origin }; + return .{ .handle = global, .temps = &ctx.identity.temps }; } pub fn tempWithThis(self: *const Function, value: anytype) !Temp { @@ -237,7 +238,7 @@ const GlobalType = enum(u8) { fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - origin: if (global_type == .temp) *js.Origin else void, + temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void, const Self = @This(); @@ -257,7 +258,10 @@ fn G(comptime global_type: GlobalType) type { } pub fn release(self: *const Self) void { - self.origin.releaseTemp(self.handle); + if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| { + var g = kv.value; + v8.v8__Global__Reset(&g); + } } }; } diff --git a/src/browser/js/Identity.zig b/src/browser/js/Identity.zig new file mode 100644 index 000000000..323ae9096 --- /dev/null +++ b/src/browser/js/Identity.zig @@ -0,0 +1,76 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Identity manages the mapping between Zig instances and their v8::Object wrappers. +// This provides object identity semantics - the same Zig instance always maps to +// the same JS object within a given Identity scope. +// +// Main world contexts share a single Identity (on Session), ensuring that +// `window.top.document === top's document` works across same-origin frames. +// +// Isolated worlds (CDP inspector) have their own Identity, ensuring their +// v8::Global wrappers don't leak into the main world. + +const std = @import("std"); +const js = @import("js.zig"); + +const Session = @import("../Session.zig"); + +const v8 = js.v8; +const Allocator = std.mem.Allocator; + +const Identity = @This(); + +// Maps Zig instance pointers to their v8::Global(Object) wrappers. +identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, + +// Tracked global v8 objects that need to be released on cleanup. +globals: std.ArrayList(v8.Global) = .empty, + +// Temporary v8 globals that can be released early. Key is global.data_ptr. +temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, + +// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance. +finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty, + +pub fn deinit(self: *Identity) void { + { + var it = self.finalizer_callbacks.valueIterator(); + while (it.next()) |finalizer| { + finalizer.*.deinit(); + } + } + + { + var it = self.identity_map.valueIterator(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); + } + } + + for (self.globals.items) |*global| { + v8.v8__Global__Reset(global); + } + + { + var it = self.temps.valueIterator(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); + } + } +} diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index a45b35df2..9080e3bc4 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -202,20 +202,20 @@ pub fn compileAndRun(self: *const Local, src: []const u8, name: ?[]const u8) !js // we can just grab it from the identity_map) pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, value: anytype) !js.Object { const ctx = self.ctx; - const origin_arena = ctx.origin.arena; + const context_arena = ctx.arena; const T = @TypeOf(value); switch (@typeInfo(T)) { .@"struct" => { // Struct, has to be placed on the heap - const heap = try origin_arena.create(T); + const heap = try context_arena.create(T); heap.* = value; return self.mapZigInstanceToJs(js_obj_handle, heap); }, .pointer => |ptr| { const resolved = resolveValue(value); - const gop = try ctx.origin.addIdentity(@intFromPtr(resolved.ptr)); + const gop = try ctx.addIdentity(@intFromPtr(resolved.ptr)); if (gop.found_existing) { // we've seen this instance before, return the same object return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); @@ -244,7 +244,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, // The TAO contains the pointer to our Zig instance as // well as any meta data we'll need to use it later. // See the TaggedOpaque struct for more details. - const tao = try origin_arena.create(TaggedOpaque); + const tao = try context_arena.create(TaggedOpaque); tao.* = .{ .value = resolved.ptr, .prototype_chain = resolved.prototype_chain.ptr, @@ -276,10 +276,10 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, // Instead, we check if the base has finalizer. The assumption // here is that if a resolve type has a finalizer, then the base // should have a finalizer too. - const fc = try ctx.origin.createFinalizerCallback(ctx.session, gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?); + const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?); { errdefer fc.deinit(); - try ctx.origin.finalizer_callbacks.put(ctx.origin.arena, @intFromPtr(resolved.ptr), fc); + try ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc); } conditionallyReference(value); diff --git a/src/browser/js/Origin.zig b/src/browser/js/Origin.zig index 9dc5857bb..c6c6bf81d 100644 --- a/src/browser/js/Origin.zig +++ b/src/browser/js/Origin.zig @@ -16,19 +16,21 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -// Origin represents the shared Zig<->JS bridge state for all contexts within -// the same origin. Multiple contexts (frames) from the same origin share a -// single Origin, ensuring that JS objects maintain their identity across frames. +// Origin represents the security token for contexts within the same origin. +// Multiple contexts (frames) from the same origin share a single Origin, +// which provides the V8 SecurityToken that allows cross-context access. +// +// Note: Identity tracking (mapping Zig instances to v8::Objects) is managed +// separately via js.Identity - Session has the main world Identity, and +// IsolatedWorlds have their own Identity instances. const std = @import("std"); const js = @import("js.zig"); const App = @import("../../App.zig"); -const Session = @import("../Session.zig"); const v8 = js.v8; const Allocator = std.mem.Allocator; -const IS_DEBUG = @import("builtin").mode == .Debug; const Origin = @This(); @@ -38,40 +40,12 @@ arena: Allocator, // The key, e.g. lightpanda.io:443 key: []const u8, -// Security token - all contexts in this realm must use the same v8::Value instance +// Security token - all contexts in this origin must use the same v8::Value instance // as their security token for V8 to allow cross-context access security_token: v8.Global, -// Serves two purposes. Like `global_objects`, this is used to free -// every Global(Object) we've created during the lifetime of the realm. -// More importantly, it serves as an identity map - for a given Zig -// instance, we map it to the same Global(Object). -// The key is the @intFromPtr of the Zig value -identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, - -// Some web APIs have to manage opaque values. Ideally, they use an -// js.Object, but the js.Object has no lifetime guarantee beyond the -// current call. They can call .persist() on their js.Object to get -// a `Global(Object)`. We need to track these to free them. -// This used to be a map and acted like identity_map; the key was -// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without -// a reliable way to know if an object has already been persisted, -// we now simply persist every time persist() is called. -globals: std.ArrayList(v8.Global) = .empty, - -// Temp variants stored in HashMaps for O(1) early cleanup. -// Key is global.data_ptr. -temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, - -// Any type that is stored in the identity_map which has a finalizer declared -// will have its finalizer stored here. This is only used when shutting down -// if v8 hasn't called the finalizer directly itself. -finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty, - -taken_over: std.ArrayList(*Origin), - pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin { - const arena = try app.arena_pool.acquire(); + const arena = try app.arena_pool.acquire(.{ .debug = "Origin" }); errdefer app.arena_pool.release(arena); var hs: js.HandleScope = undefined; @@ -88,175 +62,12 @@ pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin { .rc = 1, .arena = arena, .key = owned_key, - .temps = .empty, - .globals = .empty, - .taken_over = .empty, .security_token = token_global, }; return self; } pub fn deinit(self: *Origin, app: *App) void { - for (self.taken_over.items) |o| { - o.deinit(app); - } - - // Call finalizers before releasing anything - { - var it = self.finalizer_callbacks.valueIterator(); - while (it.next()) |finalizer| { - finalizer.*.deinit(); - } - } - v8.v8__Global__Reset(&self.security_token); - - { - var it = self.identity_map.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } - - for (self.globals.items) |*global| { - v8.v8__Global__Reset(global); - } - - { - var it = self.temps.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } - app.arena_pool.release(self.arena); } - -pub fn trackGlobal(self: *Origin, global: v8.Global) !void { - return self.globals.append(self.arena, global); -} - -pub const IdentityResult = struct { - value_ptr: *v8.Global, - found_existing: bool, -}; - -pub fn addIdentity(self: *Origin, ptr: usize) !IdentityResult { - const gop = try self.identity_map.getOrPut(self.arena, ptr); - return .{ - .value_ptr = gop.value_ptr, - .found_existing = gop.found_existing, - }; -} - -pub fn trackTemp(self: *Origin, global: v8.Global) !void { - return self.temps.put(self.arena, global.data_ptr, global); -} - -pub fn releaseTemp(self: *Origin, global: v8.Global) void { - if (self.temps.fetchRemove(global.data_ptr)) |kv| { - var g = kv.value; - v8.v8__Global__Reset(&g); - } -} - -/// Release an item from the identity_map (called after finalizer runs from V8) -pub fn release(self: *Origin, item: *anyopaque) void { - var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse { - if (comptime IS_DEBUG) { - std.debug.assert(false); - } - return; - }; - v8.v8__Global__Reset(&global.value); - - // The item has been finalized, remove it from the finalizer callback so that - // we don't try to call it again on shutdown. - const kv = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse { - if (comptime IS_DEBUG) { - std.debug.assert(false); - } - return; - }; - const fc = kv.value; - fc.session.releaseArena(fc.arena); -} - -pub fn createFinalizerCallback( - self: *Origin, - session: *Session, - global: v8.Global, - ptr: *anyopaque, - zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void, -) !*FinalizerCallback { - const arena = try session.getArena(.{ .debug = "FinalizerCallback" }); - errdefer session.releaseArena(arena); - const fc = try arena.create(FinalizerCallback); - fc.* = .{ - .arena = arena, - .origin = self, - .session = session, - .ptr = ptr, - .global = global, - .zig_finalizer = zig_finalizer, - }; - return fc; -} - -pub fn takeover(self: *Origin, original: *Origin) !void { - const arena = self.arena; - - try self.globals.ensureUnusedCapacity(arena, original.globals.items.len); - for (original.globals.items) |obj| { - self.globals.appendAssumeCapacity(obj); - } - original.globals.clearRetainingCapacity(); - - { - try self.temps.ensureUnusedCapacity(arena, original.temps.count()); - var it = original.temps.iterator(); - while (it.next()) |kv| { - try self.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*); - } - original.temps.clearRetainingCapacity(); - } - - { - try self.finalizer_callbacks.ensureUnusedCapacity(arena, original.finalizer_callbacks.count()); - var it = original.finalizer_callbacks.iterator(); - while (it.next()) |kv| { - kv.value_ptr.*.origin = self; - try self.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*); - } - original.finalizer_callbacks.clearRetainingCapacity(); - } - - { - try self.identity_map.ensureUnusedCapacity(arena, original.identity_map.count()); - var it = original.identity_map.iterator(); - while (it.next()) |kv| { - try self.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*); - } - original.identity_map.clearRetainingCapacity(); - } - - try self.taken_over.append(self.arena, original); -} - -// A type that has a finalizer can have its finalizer called one of two ways. -// The first is from V8 via the WeakCallback we give to weakRef. But that isn't -// guaranteed to fire, so we track this in finalizer_callbacks and call them on -// origin shutdown. -pub const FinalizerCallback = struct { - arena: Allocator, - origin: *Origin, - session: *Session, - ptr: *anyopaque, - global: v8.Global, - zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void, - - pub fn deinit(self: *FinalizerCallback) void { - self.zig_finalizer(self.ptr, self.session); - self.session.releaseArena(self.arena); - } -}; diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig index 372d25782..4e83c0988 100644 --- a/src/browser/js/Promise.zig +++ b/src/browser/js/Promise.zig @@ -16,9 +16,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const std = @import("std"); const js = @import("js.zig"); const v8 = js.v8; +const Session = @import("../Session.zig"); + const Promise = @This(); local: *const js.Local, @@ -63,10 +66,10 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { try ctx.trackGlobal(global); - return .{ .handle = global, .origin = {} }; + return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .origin = ctx.origin }; + return .{ .handle = global, .temps = &ctx.identity.temps }; } pub const Temp = G(.temp); @@ -80,7 +83,7 @@ const GlobalType = enum(u8) { fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - origin: if (global_type == .temp) *js.Origin else void, + temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void, const Self = @This(); @@ -96,7 +99,10 @@ fn G(comptime global_type: GlobalType) type { } pub fn release(self: *const Self) void { - self.origin.releaseTemp(self.handle); + if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| { + var g = kv.value; + v8.v8__Global__Reset(&g); + } } }; } diff --git a/src/browser/js/String.zig b/src/browser/js/String.zig index 47be227af..2cbe6a171 100644 --- a/src/browser/js/String.zig +++ b/src/browser/js/String.zig @@ -56,7 +56,7 @@ fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) ! pub fn toSSO(self: String, comptime global: bool) !(if (global) SSO.Global else SSO) { if (comptime global) { - return .{ .str = try self.toSSOWithAlloc(self.local.ctx.origin.arena) }; + return .{ .str = try self.toSSOWithAlloc(self.local.ctx.session.page_arena) }; } return self.toSSOWithAlloc(self.local.call_arena); } diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 8e05690bf..d71e2f83d 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -25,6 +25,7 @@ const v8 = js.v8; const IS_DEBUG = @import("builtin").mode == .Debug; const Allocator = std.mem.Allocator; +const Session = @import("../Session.zig"); const Value = @This(); @@ -300,10 +301,10 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { try ctx.trackGlobal(global); - return .{ .handle = global, .origin = {} }; + return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .origin = ctx.origin }; + return .{ .handle = global, .temps = &ctx.identity.temps }; } pub fn toZig(self: Value, comptime T: type) !T { @@ -361,7 +362,7 @@ const GlobalType = enum(u8) { fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - origin: if (global_type == .temp) *js.Origin else void, + temps: if (global_type == .temp) *std.AutoHashMapUnmanaged(usize, v8.Global) else void, const Self = @This(); @@ -381,7 +382,10 @@ fn G(comptime global_type: GlobalType) type { } pub fn release(self: *const Self) void { - self.origin.releaseTemp(self.handle); + if (self.temps.fetchRemove(self.handle.data_ptr)) |kv| { + var g = kv.value; + v8.v8__Global__Reset(&g); + } } }; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 4fc96b7e0..8135e7d7b 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -27,7 +27,6 @@ const v8 = js.v8; const Caller = @import("Caller.zig"); const Context = @import("Context.zig"); -const Origin = @import("Origin.zig"); const IS_DEBUG = @import("builtin").mode == .Debug; @@ -117,13 +116,12 @@ pub fn Builder(comptime T: type) type { .from_v8 = struct { fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void { const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?; - const fc: *Origin.FinalizerCallback = @ptrCast(@alignCast(ptr)); + const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr)); - const origin = fc.origin; const value_ptr = fc.ptr; - if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { + if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { func(@ptrCast(@alignCast(value_ptr)), false, fc.session); - origin.release(value_ptr); + fc.releaseIdentity(); } else { // A bit weird, but v8 _requires_ that we release it // If we don't. We'll 100% crash. diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 0c196e5bd..108671673 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -25,6 +25,7 @@ pub const Env = @import("Env.zig"); pub const bridge = @import("bridge.zig"); pub const Caller = @import("Caller.zig"); pub const Origin = @import("Origin.zig"); +pub const Identity = @import("Identity.zig"); pub const Context = @import("Context.zig"); pub const Local = @import("Local.zig"); pub const Inspector = @import("Inspector.zig"); diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 8586a11d1..74a5d79e7 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -93,12 +93,12 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I } pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void { - if (shutdown) { - self._callback.release(); - session.releaseArena(self._arena); - } else if (comptime IS_DEBUG) { - std.debug.assert(false); + self._callback.release(); + if ((comptime IS_DEBUG) and !shutdown) { + std.debug.assert(self._observing.items.len == 0); } + + session.releaseArena(self._arena); } pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { @@ -111,6 +111,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void // Register with page if this is our first observation if (self._observing.items.len == 0) { + page.js.strongRef(self); try page.registerIntersectionObserver(self); } @@ -145,18 +146,22 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi break; } } + + if (self._observing.items.len == 0) { + page.js.safeWeakRef(self); + } } pub fn disconnect(self: *IntersectionObserver, page: *Page) void { + page.unregisterIntersectionObserver(self); + self._observing.clearRetainingCapacity(); self._previous_states.clearRetainingCapacity(); for (self._pending_entries.items) |entry| { entry.deinit(false, page._session); } self._pending_entries.clearRetainingCapacity(); - - self._observing.clearRetainingCapacity(); - page.unregisterIntersectionObserver(self); + page.js.safeWeakRef(self); } pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { @@ -358,6 +363,7 @@ pub const JsApi = struct { pub const name = "IntersectionObserver"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; pub const finalizer = bridge.finalizer(IntersectionObserver.deinit); }; diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index df86d1e19..b86083813 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -86,12 +86,12 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver { } pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void { - if (shutdown) { - self._callback.release(); - session.releaseArena(self._arena); - } else if (comptime IS_DEBUG) { - std.debug.assert(false); + self._callback.release(); + if ((comptime IS_DEBUG) and !shutdown) { + std.debug.assert(self._observing.items.len == 0); } + + session.releaseArena(self._arena); } pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void { @@ -158,6 +158,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, // Register with page if this is our first observation if (self._observing.items.len == 0) { + page.js.strongRef(self); try page.registerMutationObserver(self); } @@ -168,13 +169,13 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, } pub fn disconnect(self: *MutationObserver, page: *Page) void { + page.unregisterMutationObserver(self); + self._observing.clearRetainingCapacity(); for (self._pending_records.items) |record| { record.deinit(false, page._session); } self._pending_records.clearRetainingCapacity(); - - self._observing.clearRetainingCapacity(); - page.unregisterMutationObserver(self); + page.js.safeWeakRef(self); } pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord { @@ -440,6 +441,7 @@ pub const JsApi = struct { pub const name = "MutationObserver"; pub const prototype_chain = bridge.prototypeChain(); pub var class_id: bridge.ClassId = undefined; + pub const weak = true; pub const finalizer = bridge.finalizer(MutationObserver.deinit); }; diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 58ed11b98..d3a2f64e8 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -489,12 +489,16 @@ pub fn BrowserContext(comptime CDP_T: type) type { pub fn createIsolatedWorld(self: *Self, world_name: []const u8, grant_universal_access: bool) !*IsolatedWorld { const browser = &self.cdp.browser; - const arena = try browser.arena_pool.acquire(); + const arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld" }); errdefer browser.arena_pool.release(arena); + const call_arena = try browser.arena_pool.acquire(.{ .debug = "IsolatedWorld.call_arena" }); + errdefer browser.arena_pool.release(call_arena); + const world = try arena.create(IsolatedWorld); world.* = .{ .arena = arena, + .call_arena = call_arena, .context = null, .browser = browser, .name = try arena.dupe(u8, world_name), @@ -745,13 +749,20 @@ pub fn BrowserContext(comptime CDP_T: type) type { /// An object id is unique across all contexts, different object ids can refer to the same Node in different contexts. const IsolatedWorld = struct { arena: Allocator, + call_arena: Allocator, browser: *Browser, name: []const u8, context: ?*js.Context = null, grant_universal_access: bool, + // Identity tracking for this isolated world (separate from main world). + // This ensures CDP inspector contexts don't share v8::Globals with main world. + identity: js.Identity = .{}, + pub fn deinit(self: *IsolatedWorld) void { self.removeContext() catch {}; + self.identity.deinit(); + self.browser.arena_pool.release(self.call_arena); self.browser.arena_pool.release(self.arena); } @@ -759,6 +770,8 @@ const IsolatedWorld = struct { const ctx = self.context orelse return error.NoIsolatedContextToRemove; self.browser.env.destroyContext(ctx); self.context = null; + self.identity.deinit(); + self.identity = .{}; } // The isolate world must share at least some of the state with the related page, specifically the DocumentHTML @@ -768,7 +781,13 @@ const IsolatedWorld = struct { // Currently we have only 1 page/frame and thus also only 1 state in the isolate world. pub fn createContext(self: *IsolatedWorld, page: *Page) !*js.Context { if (self.context == null) { - self.context = try self.browser.env.createContext(page); + const ctx = try self.browser.env.createContext(page, .{ + .identity = &self.identity, + .identity_arena = self.arena, + .call_arena = self.call_arena, + .debug_name = "IsolatedContext", + }); + self.context = ctx; } else { log.warn(.cdp, "not implemented", .{ .feature = "createContext: Not implemented second isolated context creation",