diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index b35a6363d..97c3349c1 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.4.7' + default: 'v0.4.8' v8: description: 'v8 version to install' required: false diff --git a/.github/actions/v8-snapshot/action.yml b/.github/actions/v8-snapshot/action.yml index ff797abcb..84a61acaa 100644 --- a/.github/actions/v8-snapshot/action.yml +++ b/.github/actions/v8-snapshot/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig-v8 release tag the prebuilt lib came from' required: false - default: 'v0.4.7' + default: 'v0.4.8' runs: using: "composite" diff --git a/Dockerfile b/Dockerfile index 7cd3e5411..8cecd404b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.4.7 +ARG ZIG_V8=v0.4.8 ARG TARGETPLATFORM RUN apt-get update -yq && \ diff --git a/build.zig.zon b/build.zig.zon index d058875ad..a1c1bc37f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.7.tar.gz", - .hash = "v8-0.0.0-xddH63edBADvfTFAnAlB6WutVta1yhsLuyPdXXt_BnKK", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.4.8.tar.gz", + .hash = "v8-0.0.0-xddH65qdBAA1hmB-gqEmhEzO0-aXKyEsmo1s34mssemb", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 3a8ab9a93..fb9ff1f64 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -288,11 +288,23 @@ pub const HttpHeader = struct { value: []const u8, }; -pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void { +pub const InitOpts = struct { + parent: ?*Frame = null, + + // When a frame/popup re-navigates, we should preserve the same window. + // There are a couple reasons for this. First, iframe.contentWindow should + // maintain the same identity. Secondly, a reference to the window can be + // acquired prior to navigation, and then used after. So it should remain valid. + reuse_window: ?*Window = null, +}; + +pub fn init(self: *Frame, frame_id: u32, page: *Page, opts: InitOpts) !void { if (comptime IS_DEBUG) { log.debug(.frame, "frame.init", .{}); } + const parent = opts.parent; + const session = page.session; const call_arena = try session.getArena(.medium, "call_arena"); errdefer session.releaseArena(call_arena); @@ -341,7 +353,7 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void { }); } - self.window = try factory.eventTarget(Window{ + const window_template = Window{ ._frame = self, ._proto = undefined, ._document = self.document, @@ -350,7 +362,16 @@ pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void { ._screen = screen, ._visual_viewport = visual_viewport, ._cross_origin_wrapper = undefined, - }); + }; + + if (opts.reuse_window) |w| { + const proto = w._proto; + w.* = window_template; + w._proto = proto; + self.window = w; + } else { + self.window = try factory.eventTarget(window_template); + } self.window._cross_origin_wrapper = .{ .window = self.window }; self._style_manager = try StyleManager.init(self); @@ -1463,7 +1484,7 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void { const new_frame = try self.arena.create(Frame); const frame_id = session.nextFrameId(); - try Frame.init(new_frame, frame_id, self._page, self); + try Frame.init(new_frame, frame_id, self._page, .{ .parent = self }); errdefer new_frame.deinit(); self._pending_loads += 1; @@ -1587,7 +1608,7 @@ pub fn openPopup(self: *Frame, opts: OpenPopupOpts) !*Frame { errdefer page.frame_arena.destroy(popup); const frame_id = session.nextFrameId(); - try Frame.init(popup, frame_id, page, null); + try Frame.init(popup, frame_id, page, .{}); errdefer popup.deinit(); popup.window._opener = opts.opener; diff --git a/src/browser/Page.zig b/src/browser/Page.zig index a6fa6749c..bfbf8937e 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -126,7 +126,7 @@ pub fn init(self: *Page, session: *Session, frame_id: u32) !void { }; self.queued_navigation = &self.queued_navigation_1; - try Frame.init(&self.frame, frame_id, self, null); + try Frame.init(&self.frame, frame_id, self, .{}); } // Tear down the Page and its root Frame. Equivalent to the old diff --git a/src/browser/Session.zig b/src/browser/Session.zig index c355bee53..1070b044d 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -530,6 +530,7 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) } const frame_id = frame._frame_id; + const reuse_window = frame.window; const page = self.currentPage().?; frame.deinit(); frame.* = undefined; @@ -546,7 +547,7 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) } } - try Frame.init(frame, frame_id, page, parent); + try Frame.init(frame, frame_id, page, .{ .parent = parent, .reuse_window = reuse_window }); errdefer { if (parent_notified) { parent._pending_loads -= 1; @@ -563,14 +564,16 @@ fn _processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) }; } -// Re-navigates a popup Frame in place. The Frame pointer stays stable -// (scripts in the opener may hold a cached Window ref — though the Window -// object inside is replaced, matching how iframes behave on navigation). +// Re-navigates a popup Frame in place. Both the Frame pointer and its Window +// stay stable across the re-init, so a cached `window.open()` return value keeps +// a valid `.location` instead of dangling against the freed one. fn processPopupNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) !void { // Preserve popup identity fields. _name lives in the Page arena and // survives Frame.deinit; _opener is just a pointer. - const saved_name = frame.window._name; - const saved_opener = frame.window._opener; + + const reuse_window = frame.window; + const saved_name = reuse_window._name; + const saved_opener = reuse_window._opener; const frame_id = frame._frame_id; const page = self.currentPage().?; @@ -587,7 +590,7 @@ fn processPopupNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) } } - try Frame.init(frame, frame_id, page, null); + try Frame.init(frame, frame_id, page, .{ .reuse_window = reuse_window }); errdefer frame.deinit(); frame.window._name = saved_name; diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 63026f8e1..ae5550f28 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -254,11 +254,20 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context const microtask_queue = v8.v8__MicrotaskQueue__New(isolate.handle, v8.kExplicit).?; errdefer v8.v8__MicrotaskQueue__DELETE(microtask_queue); + // Reuse the window's existing global proxy across an in-place navigation. + // The same *Window keeps its identity_map entry so reattaching it to the + // new context preserves WindowProxy identity + const reuse_global_object: ?*const v8.Value = blk: { + if (comptime !is_frame) break :blk null; + const existing = params.identity.identity_map.getPtr(@intFromPtr(global.window)) orelse break :blk null; + break :blk @ptrCast(v8.v8__Global__Get(existing, isolate.handle)); + }; + // Restore the context from the snapshot (0 = Page, 1 = Worker) const snapshot_index: u32 = if (comptime is_frame) 0 else 1; const v8_context = v8.v8__Context__FromSnapshot__Config(isolate.handle, snapshot_index, &.{ .global_template = null, - .global_object = null, + .global_object = reuse_global_object, .microtask_queue = microtask_queue, }).?; diff --git a/src/browser/tests/frames/support/win_a.html b/src/browser/tests/frames/support/win_a.html new file mode 100644 index 000000000..d12e6fcbc --- /dev/null +++ b/src/browser/tests/frames/support/win_a.html @@ -0,0 +1,2 @@ + +