mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +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,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 = .{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
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;
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user