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

@@ -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);