mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-21 20:24:42 +00:00
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:
@@ -5,10 +5,10 @@
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.v8 = .{
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy",
|
||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.2.tar.gz",
|
||||
.hash = "v8-0.0.0-xddH6wx-BABNgL7YIDgbnFgKZuXZ68yZNngNSrV6OjrY",
|
||||
},
|
||||
//.v8 = .{ .path = "../zig-v8-fork" },
|
||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||
.brotli = .{
|
||||
// v1.2.0
|
||||
.url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz",
|
||||
|
||||
@@ -190,6 +190,8 @@ _queued_navigation: ?*QueuedNavigation = null,
|
||||
// The URL of the current page
|
||||
url: [:0]const u8 = "about:blank",
|
||||
|
||||
origin: ?[]const u8 = null,
|
||||
|
||||
// The base url specifies the base URL used to resolve the relative urls.
|
||||
// It is set by a <base> tag.
|
||||
// If null the url must be used.
|
||||
@@ -388,10 +390,6 @@ pub fn getTitle(self: *Page) !?[]const u8 {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
|
||||
return try URL.getOrigin(allocator, self.url);
|
||||
}
|
||||
|
||||
// Add comon headers for a request:
|
||||
// * cookies
|
||||
// * referer
|
||||
@@ -449,7 +447,7 @@ pub fn releaseArena(self: *Page, allocator: Allocator) void {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -472,6 +470,14 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
||||
// page and dispatch the events.
|
||||
if (std.mem.eql(u8, "about:blank", request_url)) {
|
||||
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.
|
||||
// It's important to force a reset during the following navigation.
|
||||
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;
|
||||
|
||||
self.url = try self.arena.dupeZ(u8, request_url);
|
||||
self.origin = try URL.getOrigin(self.arena, self.url);
|
||||
|
||||
self._req_id = req_id;
|
||||
self._navigated_options = .{
|
||||
@@ -825,9 +832,15 @@ fn notifyParentLoadComplete(self: *Page) void {
|
||||
fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
|
||||
var self: *Page = @ptrCast(@alignCast(transfer.ctx));
|
||||
|
||||
// would be different than self.url in the case of a redirect
|
||||
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.document._location = self.window._location;
|
||||
|
||||
@@ -23,6 +23,7 @@ const log = @import("../../log.zig");
|
||||
const js = @import("js.zig");
|
||||
const Env = @import("Env.zig");
|
||||
const bridge = @import("bridge.zig");
|
||||
const Origin = @import("Origin.zig");
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
@@ -74,12 +75,7 @@ call_depth: usize = 0,
|
||||
// context.localScope
|
||||
local: ?*const js.Local = null,
|
||||
|
||||
// Serves two purposes. Like `global_objects`, this is used to free
|
||||
// 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,
|
||||
origin: *Origin,
|
||||
|
||||
// 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
|
||||
@@ -87,26 +83,9 @@ identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
|
||||
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty,
|
||||
finalizer_callback_pool: std.heap.MemoryPool(FinalizerCallback),
|
||||
|
||||
// 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.
|
||||
global_values: std.ArrayList(v8.Global) = .empty,
|
||||
global_objects: std.ArrayList(v8.Global) = .empty,
|
||||
// Unlike other v8 types, like functions or objects, modules are not shared
|
||||
// across origins.
|
||||
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.
|
||||
module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty,
|
||||
@@ -174,12 +153,6 @@ pub fn deinit(self: *Context) void {
|
||||
// this can release objects
|
||||
self.scheduler.deinit();
|
||||
|
||||
{
|
||||
var it = self.identity_map.valueIterator();
|
||||
while (it.next()) |global| {
|
||||
v8.v8__Global__Reset(global);
|
||||
}
|
||||
}
|
||||
{
|
||||
var it = self.finalizer_callbacks.valueIterator();
|
||||
while (it.next()) |finalizer| {
|
||||
@@ -188,50 +161,11 @@ pub fn deinit(self: *Context) void {
|
||||
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| {
|
||||
v8.v8__Global__Reset(global);
|
||||
}
|
||||
|
||||
for (self.global_functions.items) |*global| {
|
||||
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);
|
||||
}
|
||||
}
|
||||
env.releaseOrigin(self.origin);
|
||||
|
||||
v8.v8__Global__Reset(&self.handle);
|
||||
env.isolate.notifyContextDisposed();
|
||||
@@ -241,6 +175,38 @@ pub fn deinit(self: *Context) void {
|
||||
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 {
|
||||
const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -279,7 +245,7 @@ pub fn release(self: *Context, item: anytype) void {
|
||||
if (@TypeOf(item) == *anyopaque) {
|
||||
// Existing *anyopaque path for identity_map. Called internally from
|
||||
// finalizers
|
||||
var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse {
|
||||
var global = self.origin.identity_map.fetchRemove(@intFromPtr(item)) orelse {
|
||||
if (comptime IS_DEBUG) {
|
||||
// should not be possible
|
||||
std.debug.assert(false);
|
||||
@@ -301,14 +267,14 @@ pub fn release(self: *Context, item: anytype) void {
|
||||
return;
|
||||
}
|
||||
|
||||
var map = switch (@TypeOf(item)) {
|
||||
js.Value.Temp => &self.global_values_temp,
|
||||
js.Promise.Temp => &self.global_promises_temp,
|
||||
js.Function.Temp => &self.global_functions_temp,
|
||||
if (comptime IS_DEBUG) {
|
||||
switch (@TypeOf(item)) {
|
||||
js.Value.Temp, js.Promise.Temp, js.Function.Temp => {},
|
||||
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;
|
||||
v8.v8__Global__Reset(&global);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const App = @import("../../App.zig");
|
||||
const log = @import("../../log.zig");
|
||||
|
||||
const bridge = @import("bridge.zig");
|
||||
const Origin = @import("Origin.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const Isolate = @import("Isolate.zig");
|
||||
const Platform = @import("Platform.zig");
|
||||
@@ -57,6 +58,8 @@ const Env = @This();
|
||||
|
||||
app: *App,
|
||||
|
||||
allocator: Allocator,
|
||||
|
||||
platform: *const Platform,
|
||||
|
||||
// the global isolate
|
||||
@@ -70,6 +73,9 @@ isolate_params: *v8.CreateParams,
|
||||
|
||||
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
|
||||
eternal_function_templates: []v8.Eternal,
|
||||
|
||||
@@ -206,6 +212,7 @@ pub fn init(app: *App, opts: InitOpts) !Env {
|
||||
return .{
|
||||
.app = app,
|
||||
.context_id = 0,
|
||||
.allocator = allocator,
|
||||
.contexts = undefined,
|
||||
.context_count = 0,
|
||||
.isolate = isolate,
|
||||
@@ -228,7 +235,17 @@ pub fn deinit(self: *Env) void {
|
||||
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| {
|
||||
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
|
||||
const global_obj = v8.v8__Context__Global(v8_context).?;
|
||||
|
||||
{
|
||||
// Store our TAO inside the internal field of the global object. This
|
||||
// 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);
|
||||
}
|
||||
|
||||
// our window wrapped in a v8::Global
|
||||
var global_global: v8.Global = undefined;
|
||||
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;
|
||||
self.context_id = context_id + 1;
|
||||
|
||||
const origin = try self.getOrCreateOrigin(null);
|
||||
errdefer self.releaseOrigin(origin);
|
||||
|
||||
const context = try context_arena.create(Context);
|
||||
context.* = .{
|
||||
.env = self,
|
||||
.page = page,
|
||||
.origin = origin,
|
||||
.id = context_id,
|
||||
.isolate = isolate,
|
||||
.arena = context_arena,
|
||||
@@ -309,7 +332,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context {
|
||||
.scheduler = .init(context_arena),
|
||||
.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
|
||||
// a v8 context, we can get our context out
|
||||
@@ -350,6 +373,41 @@ pub fn destroyContext(self: *Env, context: *Context) void {
|
||||
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 {
|
||||
if (self.microtask_queues_are_running == false) {
|
||||
const v8_isolate = self.isolate.handle;
|
||||
|
||||
@@ -209,9 +209,9 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
if (comptime is_global) {
|
||||
try ctx.global_functions.append(ctx.arena, global);
|
||||
try ctx.trackGlobal(global);
|
||||
} else {
|
||||
try ctx.global_functions_temp.put(ctx.arena, global.data_ptr, global);
|
||||
try ctx.trackTemp(global);
|
||||
}
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
|
||||
.pointer => |ptr| {
|
||||
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) {
|
||||
// we've seen this instance before, return the same object
|
||||
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn persist(self: Object) !Global {
|
||||
var global: v8.Global = undefined;
|
||||
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 };
|
||||
}
|
||||
|
||||
148
src/browser/js/Origin.zig
Normal file
148
src/browser/js/Origin.zig
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -62,9 +62,9 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
if (comptime is_global) {
|
||||
try ctx.global_promises.append(ctx.arena, global);
|
||||
try ctx.trackGlobal(global);
|
||||
} else {
|
||||
try ctx.global_promises_temp.put(ctx.arena, global.data_ptr, global);
|
||||
try ctx.trackTemp(global);
|
||||
}
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn persist(self: PromiseResolver) !Global {
|
||||
var ctx = self.local.ctx;
|
||||
var global: v8.Global = undefined;
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -259,9 +259,9 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
|
||||
var global: v8.Global = undefined;
|
||||
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
|
||||
if (comptime is_global) {
|
||||
try ctx.global_values.append(ctx.arena, global);
|
||||
try ctx.trackGlobal(global);
|
||||
} else {
|
||||
try ctx.global_values_temp.put(ctx.arena, global.data_ptr, global);
|
||||
try ctx.trackTemp(global);
|
||||
}
|
||||
return .{ .handle = global };
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ pub fn ArrayBufferRef(comptime kind: ArrayType) type {
|
||||
var ctx = self.local.ctx;
|
||||
var global: v8.Global = undefined;
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -64,11 +64,12 @@
|
||||
// child frame's top.parent is itself (root has no parent)
|
||||
testing.expectEqual(window, window[0].top.parent);
|
||||
|
||||
// Todo: Context security tokens
|
||||
// testing.expectEqual(true, window.sub1_loaded);
|
||||
// testing.expectEqual(true, window.sub2_loaded);
|
||||
// testing.expectEqual(1, window.sub1_count);
|
||||
// testing.expectEqual(2, window.sub2_count);
|
||||
// Cross-frame property access
|
||||
testing.expectEqual(true, window.sub1_loaded);
|
||||
testing.expectEqual(true, window.sub2_loaded);
|
||||
testing.expectEqual(1, window.sub1_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>
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
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
|
||||
// a test is correct. There are a few tweaks we need to do to make this a
|
||||
// seemless, namely around adapting paths/urls.
|
||||
|
||||
@@ -243,11 +243,10 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
|
||||
var uuid_buf: [36]u8 = undefined;
|
||||
@import("../../id.zig").uuidv4(&uuid_buf);
|
||||
|
||||
const origin = (try page.getOrigin(page.call_arena)) orelse "null";
|
||||
const blob_url = try std.fmt.allocPrint(
|
||||
page.arena,
|
||||
"blob:{s}/{s}",
|
||||
.{ origin, uuid_buf },
|
||||
.{ page.origin orelse "null", uuid_buf },
|
||||
);
|
||||
try page._blob_urls.put(page.arena, blob_url, blob);
|
||||
return blob_url;
|
||||
|
||||
@@ -414,7 +414,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
|
||||
bc.inspector_session.inspector.contextCreated(
|
||||
&ls.local,
|
||||
"",
|
||||
try page.getOrigin(arena) orelse "",
|
||||
page.origin orelse "",
|
||||
aux_data,
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -414,15 +414,6 @@ fn runWebApiTest(test_file: [:0]const u8) !void {
|
||||
try_catch.init(&ls.local);
|
||||
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, .{});
|
||||
_ = test_session.wait(2000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user