Frames on the same origin share v8 data

Depends on: https://github.com/lightpanda-io/zig-v8-fork/pull/153

In some ways this is an extension of
https://github.com/lightpanda-io/browser/pull/1635 but it has more implications
with respect to correctness.

A js.Context wraps a v8::Context. One of the important thing it adds is the
identity_map so that, given a Zig instance we always return the same v8::Object.

But imagine code running in a frame. This frame has its own Context, and thus
its own identity_map. What happens when that frame does:

```js
window.top.frame_loaded = true;
```

From Zig's point of view, `Window.getTop` will return the correct Zig instance.
It will return the *Window references by the "root" page. When that instance is
passed to the bridge, we'll look for the v8::Object in the Context's
`identity_map` but wont' find it. The mapping exists in the root context
`identity_map`, but not within this frame. So we create a new v8::Object and now
our 1 zig instance has N v8::Objects for every page/frame that tries to access
it.

This breaks cross-frame scripting which should work, at least to some degree,
even when frames are on the same origin.

This commit adds a `js.Origin` which contains the `identity_map`, along with our
other `v8::Global` storage. The `Env` now contains a `*js.Origin` lookup,
mapping an origin string (e.g. lightpanda.io:443) to an *Origin. When a Page's
URL is changed, we call `self.js.setOrigin(new_url)` which will then either get
or create an origin from the Env's origin lookup map.

js.Origin is reference counted so that it remains valid so long as at least 1
frame references them.

There's some special handling for null-origins (i.e. about:blank). At the root,
null origins get a distinct/isolated Origin. For a frame, the parent's origin
is used.

Above, we talked about `identity_map`, but a `js.Context` has 8 other fields
to track v8 values, e.g. `global_objects`, `global_functions`,
`global_values_temp`, etc. These all must be shared by frames on the same
origin. So all of these have also been moved to js.Origin. They've also been
merged so that we now have 3 fields: `identity_map`, `globals` and `temps`.

Finally, when the origin of a context is changed, we set the v8::Context's
SecurityToken (to that origin). This is a key part of how v8 allows cross-
context access.
This commit is contained in:
Karl Seguin
2026-03-09 07:47:33 +08:00
parent 3626f70d3e
commit 94ce5edd20
17 changed files with 295 additions and 119 deletions

View File

@@ -5,8 +5,8 @@
.minimum_zig_version = "0.15.2", .minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
.v8 = .{ .v8 = .{
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz", .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.2.tar.gz",
.hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy", .hash = "v8-0.0.0-xddH6wx-BABNgL7YIDgbnFgKZuXZ68yZNngNSrV6OjrY",
}, },
// .v8 = .{ .path = "../zig-v8-fork" }, // .v8 = .{ .path = "../zig-v8-fork" },
.brotli = .{ .brotli = .{

View File

@@ -190,6 +190,8 @@ _queued_navigation: ?*QueuedNavigation = null,
// The URL of the current page // The URL of the current page
url: [:0]const u8 = "about:blank", url: [:0]const u8 = "about:blank",
origin: ?[]const u8 = null,
// The base url specifies the base URL used to resolve the relative urls. // The base url specifies the base URL used to resolve the relative urls.
// It is set by a <base> tag. // It is set by a <base> tag.
// If null the url must be used. // If null the url must be used.
@@ -388,10 +390,6 @@ pub fn getTitle(self: *Page) !?[]const u8 {
return null; return null;
} }
pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
return try URL.getOrigin(allocator, self.url);
}
// Add comon headers for a request: // Add comon headers for a request:
// * cookies // * cookies
// * referer // * referer
@@ -449,7 +447,7 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
} }
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool { pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
const current_origin = (try URL.getOrigin(self.call_arena, self.url)) orelse return false; const current_origin = self.origin orelse return false;
return std.mem.startsWith(u8, url, current_origin); return std.mem.startsWith(u8, url, current_origin);
} }
@@ -472,6 +470,14 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// page and dispatch the events. // page and dispatch the events.
if (std.mem.eql(u8, "about:blank", request_url)) { if (std.mem.eql(u8, "about:blank", request_url)) {
self.url = "about:blank"; self.url = "about:blank";
if (self.parent) |parent| {
self.origin = parent.origin;
} else {
self.origin = null;
}
try self.js.setOrigin(self.origin);
// Assume we parsed the document. // Assume we parsed the document.
// It's important to force a reset during the following navigation. // It's important to force a reset during the following navigation.
self._parse_state = .complete; self._parse_state = .complete;
@@ -518,6 +524,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
var http_client = session.browser.http_client; var http_client = session.browser.http_client;
self.url = try self.arena.dupeZ(u8, request_url); self.url = try self.arena.dupeZ(u8, request_url);
self.origin = try URL.getOrigin(self.arena, self.url);
self._req_id = req_id; self._req_id = req_id;
self._navigated_options = .{ self._navigated_options = .{
@@ -825,9 +832,15 @@ fn notifyParentLoadComplete(self: *Page) void {
fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool { fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
var self: *Page = @ptrCast(@alignCast(transfer.ctx)); var self: *Page = @ptrCast(@alignCast(transfer.ctx));
// would be different than self.url in the case of a redirect
const header = &transfer.response_header.?; const header = &transfer.response_header.?;
self.url = try self.arena.dupeZ(u8, std.mem.span(header.url));
const response_url = std.mem.span(header.url);
if (std.mem.eql(u8, response_url, self.url) == false) {
// would be different than self.url in the case of a redirect
self.url = try self.arena.dupeZ(u8, response_url);
self.origin = try URL.getOrigin(self.arena, self.url);
}
try self.js.setOrigin(self.origin);
self.window._location = try Location.init(self.url, self); self.window._location = try Location.init(self.url, self);
self.document._location = self.window._location; self.document._location = self.window._location;

View File

@@ -23,6 +23,7 @@ const log = @import("../../log.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const Env = @import("Env.zig"); const Env = @import("Env.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig"); const Scheduler = @import("Scheduler.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -74,12 +75,7 @@ call_depth: usize = 0,
// context.localScope // context.localScope
local: ?*const js.Local = null, local: ?*const js.Local = null,
// Serves two purposes. Like `global_objects`, this is used to free origin: *Origin,
// every Global(Object) we've created during the lifetime of the context.
// 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,
// Any type that is stored in the identity_map which has a finalizer declared // 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 // will have its finalizer stored here. This is only used when shutting down
@@ -87,26 +83,9 @@ identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty, finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
finalizer_callback_pool: std.heap.MemoryPool(FinalizerCallback), finalizer_callback_pool: std.heap.MemoryPool(FinalizerCallback),
// Some web APIs have to manage opaque values. Ideally, they use an // Unlike other v8 types, like functions or objects, modules are not shared
// js.Object, but the js.Object has no lifetime guarantee beyond the // across origins.
// 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.
global_values: std.ArrayList(v8.Global) = .empty,
global_objects: std.ArrayList(v8.Global) = .empty,
global_modules: std.ArrayList(v8.Global) = .empty, global_modules: std.ArrayList(v8.Global) = .empty,
global_promises: std.ArrayList(v8.Global) = .empty,
global_functions: std.ArrayList(v8.Global) = .empty,
global_promise_resolvers: std.ArrayList(v8.Global) = .empty,
// Temp variants stored in HashMaps for O(1) early cleanup.
// Key is global.data_ptr.
global_values_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
global_promises_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
global_functions_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Our module cache: normalized module specifier => module. // Our module cache: normalized module specifier => module.
module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty, module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty,
@@ -174,12 +153,6 @@ pub fn deinit(self: *Context) void {
// this can release objects // this can release objects
self.scheduler.deinit(); self.scheduler.deinit();
{
var it = self.identity_map.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
{ {
var it = self.finalizer_callbacks.valueIterator(); var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| { while (it.next()) |finalizer| {
@@ -188,50 +161,11 @@ pub fn deinit(self: *Context) void {
self.finalizer_callback_pool.deinit(); self.finalizer_callback_pool.deinit();
} }
for (self.global_values.items) |*global| {
v8.v8__Global__Reset(global);
}
for (self.global_objects.items) |*global| {
v8.v8__Global__Reset(global);
}
for (self.global_modules.items) |*global| { for (self.global_modules.items) |*global| {
v8.v8__Global__Reset(global); v8.v8__Global__Reset(global);
} }
for (self.global_functions.items) |*global| { env.releaseOrigin(self.origin);
v8.v8__Global__Reset(global);
}
for (self.global_promises.items) |*global| {
v8.v8__Global__Reset(global);
}
for (self.global_promise_resolvers.items) |*global| {
v8.v8__Global__Reset(global);
}
{
var it = self.global_values_temp.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
{
var it = self.global_promises_temp.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
{
var it = self.global_functions_temp.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
v8.v8__Global__Reset(&self.handle); v8.v8__Global__Reset(&self.handle);
env.isolate.notifyContextDisposed(); env.isolate.notifyContextDisposed();
@@ -241,6 +175,38 @@ pub fn deinit(self: *Context) void {
v8.v8__MicrotaskQueue__DELETE(self.microtask_queue); v8.v8__MicrotaskQueue__DELETE(self.microtask_queue);
} }
pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
const env = self.env;
const isolate = env.isolate;
const origin = try env.getOrCreateOrigin(key);
errdefer env.releaseOrigin(origin);
try self.origin.transferTo(origin);
self.origin.deinit(env.app);
self.origin = origin;
{
var ls: js.Local.Scope = undefined;
self.localScope(&ls);
defer ls.deinit();
// Set the V8::Context SecurityToken, which is a big part of what allows
// one context to access another.
const token_local = v8.v8__Global__Get(&origin.security_token, isolate.handle);
v8.v8__Context__SetSecurityToken(ls.local.handle, token_local);
}
}
pub fn trackGlobal(self: *Context, global: v8.Global) !void {
return self.origin.trackGlobal(global);
}
pub fn trackTemp(self: *Context, global: v8.Global) !void {
return self.origin.trackTemp(global);
}
pub fn weakRef(self: *Context, obj: anytype) void { pub fn weakRef(self: *Context, obj: anytype) void {
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse { const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
@@ -279,7 +245,7 @@ pub fn release(self: *Context, item: anytype) void {
if (@TypeOf(item) == *anyopaque) { if (@TypeOf(item) == *anyopaque) {
// Existing *anyopaque path for identity_map. Called internally from // Existing *anyopaque path for identity_map. Called internally from
// finalizers // finalizers
var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse { var global = self.origin.identity_map.fetchRemove(@intFromPtr(item)) orelse {
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
// should not be possible // should not be possible
std.debug.assert(false); std.debug.assert(false);
@@ -301,14 +267,14 @@ pub fn release(self: *Context, item: anytype) void {
return; return;
} }
var map = switch (@TypeOf(item)) { if (comptime IS_DEBUG) {
js.Value.Temp => &self.global_values_temp, switch (@TypeOf(item)) {
js.Promise.Temp => &self.global_promises_temp, js.Value.Temp, js.Promise.Temp, js.Function.Temp => {},
js.Function.Temp => &self.global_functions_temp,
else => |T| @compileError("Context.release cannot be called with a " ++ @typeName(T)), else => |T| @compileError("Context.release cannot be called with a " ++ @typeName(T)),
}; }
}
if (map.fetchRemove(item.handle.data_ptr)) |kv| { if (self.origin.temps.fetchRemove(item.handle.data_ptr)) |kv| {
var global = kv.value; var global = kv.value;
v8.v8__Global__Reset(&global); v8.v8__Global__Reset(&global);
} }

View File

@@ -26,6 +26,7 @@ const App = @import("../../App.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const Origin = @import("Origin.zig");
const Context = @import("Context.zig"); const Context = @import("Context.zig");
const Isolate = @import("Isolate.zig"); const Isolate = @import("Isolate.zig");
const Platform = @import("Platform.zig"); const Platform = @import("Platform.zig");
@@ -57,6 +58,8 @@ const Env = @This();
app: *App, app: *App,
allocator: Allocator,
platform: *const Platform, platform: *const Platform,
// the global isolate // the global isolate
@@ -70,6 +73,9 @@ isolate_params: *v8.CreateParams,
context_id: usize, context_id: usize,
// Maps origin -> shared Origin contains, for v8 values shared across same-origin Contexts
origins: std.StringHashMapUnmanaged(*Origin) = .empty,
// Global handles that need to be freed on deinit // Global handles that need to be freed on deinit
eternal_function_templates: []v8.Eternal, eternal_function_templates: []v8.Eternal,
@@ -206,6 +212,7 @@ pub fn init(app: *App, opts: InitOpts) !Env {
return .{ return .{
.app = app, .app = app,
.context_id = 0, .context_id = 0,
.allocator = allocator,
.contexts = undefined, .contexts = undefined,
.context_count = 0, .context_count = 0,
.isolate = isolate, .isolate = isolate,
@@ -228,7 +235,17 @@ pub fn deinit(self: *Env) void {
ctx.deinit(); ctx.deinit();
} }
const allocator = self.app.allocator; const app = self.app;
const allocator = app.allocator;
{
var it = self.origins.valueIterator();
while (it.next()) |value| {
value.*.deinit(app);
}
self.origins.deinit(allocator);
}
if (self.inspector) |i| { if (self.inspector) |i| {
i.deinit(allocator); i.deinit(allocator);
} }
@@ -272,6 +289,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
// get the global object for the context, this maps to our Window // get the global object for the context, this maps to our Window
const global_obj = v8.v8__Context__Global(v8_context).?; const global_obj = v8.v8__Context__Global(v8_context).?;
{ {
// Store our TAO inside the internal field of the global object. This // Store our TAO inside the internal field of the global object. This
// maps the v8::Object -> Zig instance. Almost all objects have this, and // maps the v8::Object -> Zig instance. Almost all objects have this, and
@@ -287,6 +305,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
}; };
v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao);
} }
// our window wrapped in a v8::Global // our window wrapped in a v8::Global
var global_global: v8.Global = undefined; var global_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, global_obj, &global_global); v8.v8__Global__New(isolate.handle, global_obj, &global_global);
@@ -294,10 +313,14 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
const context_id = self.context_id; const context_id = self.context_id;
self.context_id = context_id + 1; self.context_id = context_id + 1;
const origin = try self.getOrCreateOrigin(null);
errdefer self.releaseOrigin(origin);
const context = try context_arena.create(Context); const context = try context_arena.create(Context);
context.* = .{ context.* = .{
.env = self, .env = self,
.page = page, .page = page,
.origin = origin,
.id = context_id, .id = context_id,
.isolate = isolate, .isolate = isolate,
.arena = context_arena, .arena = context_arena,
@@ -309,7 +332,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
.scheduler = .init(context_arena), .scheduler = .init(context_arena),
.finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator), .finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator),
}; };
try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global); try context.origin.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global);
// Store a pointer to our context inside the v8 context so that, given // Store a pointer to our context inside the v8 context so that, given
// a v8 context, we can get our context out // a v8 context, we can get our context out
@@ -350,6 +373,41 @@ pub fn destroyContext(self: *Env, context: *Context) void {
context.deinit(); context.deinit();
} }
pub fn getOrCreateOrigin(self: *Env, key_: ?[]const u8) !*Origin {
const key = key_ orelse {
var opaque_origin: [36]u8 = undefined;
@import("../../id.zig").uuidv4(&opaque_origin);
// Origin.init will dupe opaque_origin. It's fine that this doesn't
// get added to self.origins. In fact, it further isolates it. When the
// context is freed, it'll call env.releaseOrigin which will free it.
return Origin.init(self.app, self.isolate, &opaque_origin);
};
const gop = try self.origins.getOrPut(self.allocator, key);
if (gop.found_existing) {
const origin = gop.value_ptr.*;
origin.rc += 1;
return origin;
}
errdefer _ = self.origins.remove(key);
const origin = try Origin.init(self.app, self.isolate, key);
gop.key_ptr.* = origin.key;
gop.value_ptr.* = origin;
return origin;
}
pub fn releaseOrigin(self: *Env, origin: *Origin) void {
const rc = origin.rc;
if (rc == 1) {
_ = self.origins.remove(origin.key);
origin.deinit(self.app);
} else {
origin.rc = rc - 1;
}
}
pub fn runMicrotasks(self: *Env) void { pub fn runMicrotasks(self: *Env) void {
if (self.microtask_queues_are_running == false) { if (self.microtask_queues_are_running == false) {
const v8_isolate = self.isolate.handle; const v8_isolate = self.isolate.handle;

View File

@@ -209,9 +209,9 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.global_functions.append(ctx.arena, global); try ctx.trackGlobal(global);
} else { } else {
try ctx.global_functions_temp.put(ctx.arena, global.data_ptr, global); try ctx.trackTemp(global);
} }
return .{ .handle = global }; return .{ .handle = global };
} }

View File

@@ -171,7 +171,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
.pointer => |ptr| { .pointer => |ptr| {
const resolved = resolveValue(value); const resolved = resolveValue(value);
const gop = try ctx.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr)); const gop = try ctx.origin.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr));
if (gop.found_existing) { if (gop.found_existing) {
// we've seen this instance before, return the same object // we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);

View File

@@ -97,7 +97,7 @@ pub fn persist(self: Object) !Global {
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_objects.append(ctx.arena, global); try ctx.trackGlobal(global);
return .{ .handle = global }; return .{ .handle = global };
} }

148
src/browser/js/Origin.zig Normal file
View File

@@ -0,0 +1,148 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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 <https://www.gnu.org/licenses/>.
// 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.
const std = @import("std");
const js = @import("js.zig");
const App = @import("../../App.zig");
const v8 = js.v8;
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("build").mode == .Debug;
const Origin = @This();
rc: usize = 1,
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
// 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,
pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin {
const arena = try app.arena_pool.acquire();
errdefer app.arena_pool.release(arena);
var hs: js.HandleScope = undefined;
hs.init(isolate);
defer hs.deinit();
const owned_key = try arena.dupe(u8, key);
const token_local = isolate.initStringHandle(owned_key);
var token_global: v8.Global = undefined;
v8.v8__Global__New(isolate.handle, token_local, &token_global);
const self = try arena.create(Origin);
self.* = .{
.rc = 1,
.arena = arena,
.key = owned_key,
.globals = .empty,
.temps = .empty,
.security_token = token_global,
};
return self;
}
pub fn deinit(self: *Origin, app: *App) void {
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 fn trackTemp(self: *Origin, global: v8.Global) !void {
return self.temps.put(self.arena, global.data_ptr, global);
}
pub fn transferTo(self: *Origin, dest: *Origin) !void {
const arena = dest.arena;
try dest.globals.ensureUnusedCapacity(arena, self.globals.items.len);
for (self.globals.items) |obj| {
dest.globals.appendAssumeCapacity(obj);
}
self.globals.clearRetainingCapacity();
{
try dest.temps.ensureUnusedCapacity(arena, self.temps.count());
var it = self.temps.iterator();
while (it.next()) |kv| {
try dest.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*);
}
self.temps.clearRetainingCapacity();
}
{
try dest.identity_map.ensureUnusedCapacity(arena, self.identity_map.count());
var it = self.identity_map.iterator();
while (it.next()) |kv| {
try dest.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*);
}
self.identity_map.clearRetainingCapacity();
}
}

View File

@@ -62,9 +62,9 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.global_promises.append(ctx.arena, global); try ctx.trackGlobal(global);
} else { } else {
try ctx.global_promises_temp.put(ctx.arena, global.data_ptr, global); try ctx.trackTemp(global);
} }
return .{ .handle = global }; return .{ .handle = global };
} }

View File

@@ -79,7 +79,7 @@ pub fn persist(self: PromiseResolver) !Global {
var ctx = self.local.ctx; var ctx = self.local.ctx;
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_promise_resolvers.append(ctx.arena, global); try ctx.trackGlobal(global);
return .{ .handle = global }; return .{ .handle = global };
} }

View File

@@ -259,9 +259,9 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
if (comptime is_global) { if (comptime is_global) {
try ctx.global_values.append(ctx.arena, global); try ctx.trackGlobal(global);
} else { } else {
try ctx.global_values_temp.put(ctx.arena, global.data_ptr, global); try ctx.trackTemp(global);
} }
return .{ .handle = global }; return .{ .handle = global };
} }

View File

@@ -161,7 +161,7 @@ pub fn ArrayBufferRef(comptime kind: ArrayType) type {
var ctx = self.local.ctx; var ctx = self.local.ctx;
var global: v8.Global = undefined; var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_values.append(ctx.arena, global); try ctx.trackGlobal(global);
return .{ .handle = global }; return .{ .handle = global };
} }

View File

@@ -64,11 +64,12 @@
// child frame's top.parent is itself (root has no parent) // child frame's top.parent is itself (root has no parent)
testing.expectEqual(window, window[0].top.parent); testing.expectEqual(window, window[0].top.parent);
// Todo: Context security tokens // Cross-frame property access
// testing.expectEqual(true, window.sub1_loaded); testing.expectEqual(true, window.sub1_loaded);
// testing.expectEqual(true, window.sub2_loaded); testing.expectEqual(true, window.sub2_loaded);
// testing.expectEqual(1, window.sub1_count); testing.expectEqual(1, window.sub1_count);
// testing.expectEqual(2, window.sub2_count); // depends on how far the initial load got before it was cancelled.
testing.expectEqual(true, window.sub2_count == 1 || window.sub2_count == 2);
}); });
</script> </script>

View File

@@ -118,7 +118,7 @@
BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/', BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/',
}; };
if (!IS_TEST_RUNNER) { if (window.navigator.userAgent.startsWith("Lightpanda/") == false) {
// The page is running in a different browser. Probably a developer making sure // The page is running in a different browser. Probably a developer making sure
// a test is correct. There are a few tweaks we need to do to make this a // a test is correct. There are a few tweaks we need to do to make this a
// seemless, namely around adapting paths/urls. // seemless, namely around adapting paths/urls.

View File

@@ -243,11 +243,10 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
var uuid_buf: [36]u8 = undefined; var uuid_buf: [36]u8 = undefined;
@import("../../id.zig").uuidv4(&uuid_buf); @import("../../id.zig").uuidv4(&uuid_buf);
const origin = (try page.getOrigin(page.call_arena)) orelse "null";
const blob_url = try std.fmt.allocPrint( const blob_url = try std.fmt.allocPrint(
page.arena, page.arena,
"blob:{s}/{s}", "blob:{s}/{s}",
.{ origin, uuid_buf }, .{ page.origin orelse "null", uuid_buf },
); );
try page._blob_urls.put(page.arena, blob_url, blob); try page._blob_urls.put(page.arena, blob_url, blob);
return blob_url; return blob_url;

View File

@@ -414,7 +414,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
bc.inspector_session.inspector.contextCreated( bc.inspector_session.inspector.contextCreated(
&ls.local, &ls.local,
"", "",
try page.getOrigin(arena) orelse "", page.origin orelse "",
aux_data, aux_data,
true, true,
); );

View File

@@ -414,15 +414,6 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
try_catch.init(&ls.local); try_catch.init(&ls.local);
defer try_catch.deinit(); defer try_catch.deinit();
// by default, on load, testing.js will call testing.assertOk(). This makes our
// tests work well in a browser. But, for our test runner, we disable that
// and call it explicitly. This gives us better error messages.
ls.local.eval("window._lightpanda_skip_auto_assert = true;", "auto_assert") catch |err| {
const caught = try_catch.caughtOrError(arena_allocator, err);
std.debug.print("disable auto assert failure\nError: {f}\n", .{caught});
return err;
};
try page.navigate(url, .{}); try page.navigate(url, .{});
_ = test_session.wait(2000); _ = test_session.wait(2000);