mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 17:55:59 -04:00
Merge pull request #2742 from lightpanda-io/window_reuse
uaf, webapi: Window reuse
This commit is contained in:
2
.github/actions/install/action.yml
vendored
2
.github/actions/install/action.yml
vendored
@@ -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
|
||||
|
||||
2
.github/actions/v8-snapshot/action.yml
vendored
2
.github/actions/v8-snapshot/action.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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 = .{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}).?;
|
||||
|
||||
|
||||
2
src/browser/tests/frames/support/win_a.html
Normal file
2
src/browser/tests/frames/support/win_a.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<title>win_a</title>
|
||||
2
src/browser/tests/frames/support/win_b.html
Normal file
2
src/browser/tests/frames/support/win_b.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<title>win_b</title>
|
||||
55
src/browser/tests/frames/window_reuse.html
Normal file
55
src/browser/tests/frames/window_reuse.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<head></head>
|
||||
<body>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<!--
|
||||
Navigating a browsing context in place (iframe.src change / popup re-nav) reuses
|
||||
the same Window object, so a cached `iframe.contentWindow` / `window.open()`
|
||||
return value keeps a VALID `.location` across the navigation. Previously the
|
||||
in-place Frame re-init built a brand-new Window and freed the old one's
|
||||
Location, so the cached reference dangled — reading `.location` re-wrapped the
|
||||
freed Location into a double-free (URL.zig release-overflow).
|
||||
|
||||
Note: strict WindowProxy identity (`w === iframe.contentWindow` across the
|
||||
navigation) is NOT yet provided — a Window is a v8 context's global object and
|
||||
the context is recreated per navigation. Achieving that needs v8 global-proxy
|
||||
reuse and is tracked separately. This test guards the UAF, not the identity.
|
||||
-->
|
||||
|
||||
<script id=iframe_contentwindow_location_survives_navigation type=module>
|
||||
const state = await testing.async();
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
const onceLoad = () => new Promise((r) => { iframe.onload = () => r(); });
|
||||
|
||||
let loaded = onceLoad();
|
||||
iframe.src = 'support/win_a.html';
|
||||
document.body.appendChild(iframe);
|
||||
await loaded;
|
||||
|
||||
// Cache the window WITHOUT reading .location first, so its initial Location
|
||||
// never gets a JS wrapper pinning it (the original crash condition).
|
||||
const w = iframe.contentWindow;
|
||||
|
||||
loaded = onceLoad();
|
||||
iframe.src = 'support/win_b.html'; // in-place re-init; Window is reused
|
||||
await loaded;
|
||||
|
||||
state.resolve();
|
||||
await state.done(() => {
|
||||
// Same Window object across the navigation: the v8 global proxy is reused
|
||||
// (WindowProxy identity).
|
||||
testing.expectEqual(true, w === iframe.contentWindow);
|
||||
// And reading the cached window's location returns the new, valid location
|
||||
// (previously a UAF double-free on the freed Location).
|
||||
testing.expectEqual(true, w.location.href.endsWith('/support/win_b.html'));
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
The popup re-navigation path (Session.processPopupNavigation) reuses the Window
|
||||
the same way; it's exercised by the manual repro at wpt/_lprepro/opener.html
|
||||
(real HTTP popup load + postMessage aren't driven by this htmlRunner harness).
|
||||
-->
|
||||
</body>
|
||||
Reference in New Issue
Block a user