mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-31 01:36:15 -04:00
webapi: structured-clone window/MessagePort postMessage messages
window.postMessage and MessagePort.postMessage stashed the message js.Value.Temp verbatim and handed it straight into the MessageEvent, with no structuredClone on the path. The receiver got the literal object the sender posted, so a mutation on either side was visible to the other; worse, posting to a different same-origin frame exposed the source realm's object directly into the target realm instead of minting a fresh one there. The spec requires StructuredSerialize in postMessage and deserialize in the destination realm.
This commit is contained in:
@@ -66,6 +66,43 @@
|
||||
testing.expectEqual(42, received4.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id="structured-clone" type=module>
|
||||
const state = await testing.async();
|
||||
|
||||
const channel = new MessageChannel();
|
||||
const payload = { n: 1 };
|
||||
let received = null;
|
||||
|
||||
await new Promise((resolve) => {
|
||||
channel.port2.onmessage = (e) => { received = e.data; resolve(); };
|
||||
channel.port1.postMessage(payload);
|
||||
});
|
||||
|
||||
state.resolve();
|
||||
await state.done(() => {
|
||||
testing.expectEqual(false, received === payload);
|
||||
testing.expectEqual(1, received.n);
|
||||
// Mutating the sender's object after posting doesn't affect the clone.
|
||||
payload.n = 999;
|
||||
testing.expectEqual(1, received.n);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id="structured-clone-sync" type=module>
|
||||
{
|
||||
const channel = new MessageChannel();
|
||||
let threw = false;
|
||||
try {
|
||||
channel.port1.postMessage(function () {});
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
testing.expectEqual('DataCloneError', e.name);
|
||||
}
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</content>
|
||||
</invoke>
|
||||
|
||||
41
src/browser/tests/window/post_message.html
Normal file
41
src/browser/tests/window/post_message.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<script id=postMessageStructuredClone type=module>
|
||||
// window.postMessage delivers a structured clone: the receiver gets a fresh,
|
||||
// independent copy, not the literal object the sender posted.
|
||||
{
|
||||
const state = await testing.async();
|
||||
const payload = { n: 1 };
|
||||
let received = null;
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
received = e.data;
|
||||
state.resolve();
|
||||
});
|
||||
|
||||
window.postMessage(payload, '*');
|
||||
|
||||
await state.done(() => {
|
||||
testing.expectEqual(false, received === payload);
|
||||
testing.expectEqual(1, received.n);
|
||||
// Mutating the sender's object after posting doesn't affect the clone.
|
||||
payload.n = 999;
|
||||
testing.expectEqual(1, received.n);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=postMessageDataCloneError>
|
||||
// A value that cannot be structured-cloned throws synchronously (DataCloneError).
|
||||
{
|
||||
let threw = false;
|
||||
try {
|
||||
window.postMessage(function () {}, '*');
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
testing.expectEqual('DataCloneError', e.name);
|
||||
}
|
||||
testing.expectEqual(true, threw);
|
||||
}
|
||||
</script>
|
||||
@@ -50,7 +50,7 @@ pub fn entangle(port1: *MessagePort, port2: *MessagePort) void {
|
||||
port2._entangled_port = port1;
|
||||
}
|
||||
|
||||
pub fn postMessage(self: *MessagePort, message: js.Value.Temp, frame: *Frame) !void {
|
||||
pub fn postMessage(self: *MessagePort, message: js.Value, frame: *Frame) !void {
|
||||
if (self._closed) {
|
||||
return;
|
||||
}
|
||||
@@ -60,16 +60,39 @@ pub fn postMessage(self: *MessagePort, message: js.Value.Temp, frame: *Frame) !v
|
||||
return;
|
||||
}
|
||||
|
||||
// StructuredSerialize runs synchronously (per spec): clone the message into a
|
||||
// fresh, self-owned temp now so the receiver gets an independent copy (a
|
||||
// mutation on one side isn't visible to the other) and an unserializable
|
||||
// value throws a DataCloneError to the caller. Mirrors Worker.postMessage.
|
||||
const cloned = blk: {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
// Contain any V8 exception from a failed serialization so it surfaces as
|
||||
// a clean DataCloneError; deinit() (no rethrow) clears it.
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(&ls.local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const c = message.structuredCloneTo(&ls.local) catch {
|
||||
return error.DataClone;
|
||||
};
|
||||
break :blk try c.temp();
|
||||
};
|
||||
errdefer cloned.release();
|
||||
|
||||
// Create callback to deliver message
|
||||
const callback = try frame._factory.create(PostMessageCallback{
|
||||
.frame = frame,
|
||||
.port = other,
|
||||
.message = message,
|
||||
.message = cloned,
|
||||
});
|
||||
|
||||
try frame.js.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||
.name = "MessagePort.postMessage",
|
||||
.low_priority = false,
|
||||
.finalizer = PostMessageCallback.cancelled,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,6 +134,14 @@ const PostMessageCallback = struct {
|
||||
message: js.Value.Temp,
|
||||
frame: *Frame,
|
||||
|
||||
// Called by the scheduler if the task is dropped before it runs. `run` and
|
||||
// `cancelled` are mutually exclusive, so the temp is released exactly once.
|
||||
fn cancelled(ctx: *anyopaque) void {
|
||||
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
||||
self.message.release();
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn deinit(self: *PostMessageCallback) void {
|
||||
self.frame._factory.destroy(self);
|
||||
}
|
||||
@@ -120,26 +151,34 @@ const PostMessageCallback = struct {
|
||||
defer self.deinit();
|
||||
const frame = self.frame;
|
||||
|
||||
// The MessageEvent takes ownership of the cloned temp and releases it on
|
||||
// teardown; on any path where we don't hand it over, release it here so
|
||||
// it doesn't leak.
|
||||
if (self.port._closed) {
|
||||
self.message.release();
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = self.port.asEventTarget();
|
||||
if (frame._event_manager.hasDirectListeners(target, "message", self.port._on_message)) {
|
||||
const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||
.data = .{ .value = self.message },
|
||||
.origin = "",
|
||||
.source = null,
|
||||
}, frame._page) catch |err| {
|
||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||
return null;
|
||||
}).asEvent();
|
||||
|
||||
frame._event_manager.dispatchDirect(target, event, self.port._on_message, .{ .context = "MessagePort message" }) catch |err| {
|
||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||
};
|
||||
if (!frame._event_manager.hasDirectListeners(target, "message", self.port._on_message)) {
|
||||
self.message.release();
|
||||
return null;
|
||||
}
|
||||
|
||||
const event = (MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||
.data = .{ .value = self.message },
|
||||
.origin = "",
|
||||
.source = null,
|
||||
}, frame._page) catch |err| {
|
||||
self.message.release();
|
||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||
return null;
|
||||
}).asEvent();
|
||||
|
||||
frame._event_manager.dispatchDirect(target, event, self.port._on_message, .{ .context = "MessagePort message" }) catch |err| {
|
||||
log.err(.dom, "MessagePort.postMessage", .{ .err = err });
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -153,7 +192,7 @@ pub const JsApi = struct {
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
};
|
||||
|
||||
pub const postMessage = bridge.function(MessagePort.postMessage, .{});
|
||||
pub const postMessage = bridge.function(MessagePort.postMessage, .{ .dom_exception = true });
|
||||
pub const start = bridge.function(MessagePort.start, .{});
|
||||
pub const close = bridge.function(MessagePort.close, .{});
|
||||
|
||||
|
||||
@@ -623,7 +623,7 @@ pub fn close(self: *Window) void {
|
||||
page.session.queueFrameDestruction(frame);
|
||||
}
|
||||
|
||||
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, transfer: ?[]const *MessagePort, frame: *Frame) !void {
|
||||
pub fn postMessage(self: *Window, message: js.Value, target_origin: ?[]const u8, transfer: ?[]const *MessagePort, frame: *Frame) !void {
|
||||
// For now, we ignore targetOrigin checking and just dispatch the message
|
||||
// In a full implementation, we would validate the origin
|
||||
_ = target_origin;
|
||||
@@ -634,12 +634,35 @@ pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]cons
|
||||
const arena = try target_frame.getArena(.medium, "Window.postMessage");
|
||||
errdefer target_frame.releaseArena(arena);
|
||||
|
||||
// StructuredSerialize runs synchronously (per spec): clone the message into
|
||||
// the target window's realm now. The receiver gets a fresh, independent copy
|
||||
// minted in its own realm (not the source realm's object), an unserializable
|
||||
// value throws a DataCloneError to the caller, and the source-realm temp
|
||||
// doesn't leak into the destination context. Mirrors Worker.postMessage.
|
||||
const cloned = blk: {
|
||||
var ls: js.Local.Scope = undefined;
|
||||
target_frame.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
|
||||
// Contain any V8 exception from a failed serialization so it surfaces as
|
||||
// a clean DataCloneError; deinit() (no rethrow) clears it.
|
||||
var try_catch: js.TryCatch = undefined;
|
||||
try_catch.init(&ls.local);
|
||||
defer try_catch.deinit();
|
||||
|
||||
const c = message.structuredCloneTo(&ls.local) catch {
|
||||
return error.DataClone;
|
||||
};
|
||||
break :blk try c.temp();
|
||||
};
|
||||
errdefer cloned.release();
|
||||
|
||||
// Origin should be the source window's origin (where the message came from)
|
||||
const origin = try source_window._location.getOrigin(&frame.js.execution);
|
||||
const callback = try arena.create(PostMessageCallback);
|
||||
callback.* = .{
|
||||
.arena = arena,
|
||||
.message = message,
|
||||
.message = cloned,
|
||||
.frame = target_frame,
|
||||
.source = source_window,
|
||||
.origin = try arena.dupe(u8, origin),
|
||||
@@ -872,8 +895,11 @@ const PostMessageCallback = struct {
|
||||
self.frame.releaseArena(self.arena);
|
||||
}
|
||||
|
||||
// Called by the scheduler if the task is dropped before it runs. `run` and
|
||||
// `cancelled` are mutually exclusive, so the temp is released exactly once.
|
||||
fn cancelled(ctx: *anyopaque) void {
|
||||
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
|
||||
self.message.release();
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
@@ -885,18 +911,24 @@ const PostMessageCallback = struct {
|
||||
const window = frame.window;
|
||||
|
||||
const event_target = window.asEventTarget();
|
||||
if (frame._event_manager.hasDirectListeners(event_target, "message", window._on_message)) {
|
||||
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||
.data = .{ .value = self.message },
|
||||
.origin = self.origin,
|
||||
.source = self.source,
|
||||
.ports = self.ports,
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
}, frame._page)).asEvent();
|
||||
try frame._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
|
||||
|
||||
// The MessageEvent takes ownership of the cloned temp and releases it on
|
||||
// teardown; if there are no listeners, release it here so it doesn't leak.
|
||||
if (!frame._event_manager.hasDirectListeners(event_target, "message", window._on_message)) {
|
||||
self.message.release();
|
||||
return null;
|
||||
}
|
||||
|
||||
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
|
||||
.data = .{ .value = self.message },
|
||||
.origin = self.origin,
|
||||
.source = self.source,
|
||||
.ports = self.ports,
|
||||
.bubbles = false,
|
||||
.cancelable = false,
|
||||
}, frame._page)).asEvent();
|
||||
try frame._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" });
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -982,7 +1014,7 @@ pub const JsApi = struct {
|
||||
pub const requestIdleCallback = bridge.function(Window.requestIdleCallback, .{});
|
||||
pub const cancelIdleCallback = bridge.function(Window.cancelIdleCallback, .{});
|
||||
pub const matchMedia = bridge.function(Window.matchMedia, .{});
|
||||
pub const postMessage = bridge.function(Window.postMessage, .{});
|
||||
pub const postMessage = bridge.function(Window.postMessage, .{ .dom_exception = true });
|
||||
pub const btoa = bridge.function(Window.btoa, .{ .dom_exception = true });
|
||||
pub const atob = bridge.function(Window.atob, .{ .dom_exception = true });
|
||||
pub const reportError = bridge.function(Window.reportError, .{});
|
||||
@@ -1069,7 +1101,7 @@ pub const JsApi = struct {
|
||||
const CrossOriginWindow = struct {
|
||||
window: *Window,
|
||||
|
||||
pub fn postMessage(self: *CrossOriginWindow, message: js.Value.Temp, target_origin: ?[]const u8, transfer: ?[]const *MessagePort, frame: *Frame) !void {
|
||||
pub fn postMessage(self: *CrossOriginWindow, message: js.Value, target_origin: ?[]const u8, transfer: ?[]const *MessagePort, frame: *Frame) !void {
|
||||
return self.window.postMessage(message, target_origin, transfer, frame);
|
||||
}
|
||||
|
||||
@@ -1094,7 +1126,7 @@ const CrossOriginWindow = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const postMessage = bridge.function(CrossOriginWindow.postMessage, .{});
|
||||
pub const postMessage = bridge.function(CrossOriginWindow.postMessage, .{ .dom_exception = true });
|
||||
pub const top = bridge.accessor(CrossOriginWindow.getTop, null, .{});
|
||||
pub const parent = bridge.accessor(CrossOriginWindow.getParent, null, .{});
|
||||
pub const length = bridge.accessor(CrossOriginWindow.getFramesLength, null, .{});
|
||||
|
||||
Reference in New Issue
Block a user