Improve document.write within an iframe

This fixes two things. First, it better tracks the calling context and the
target context, so that if a parent frame does a document.write within a child
frame, any JavaScript is executed in the child frame's context (previously, it
would be executed in the parent's context).

Second, it ensures that, on document.write, we force-execute any pending
scripts. This is related to https://github.com/lightpanda-io/browser/pull/2542
but applies specifically to document.write.
This commit is contained in:
Karl Seguin
2026-05-25 20:46:53 +08:00
parent 43102317aa
commit d2b495113c
3 changed files with 84 additions and 5 deletions

View File

@@ -398,6 +398,16 @@ pub fn staticScriptsDone(self: *ScriptManagerBase) void {
self.evaluate();
}
// A script-created parser (document.open/write/close) finished. Run any
// deferred scripts it produced. Unlike staticScriptsDone, this can run after
// the initial parse already completed (so it must not re-assert the flag): a
// frame that was loaded (or document.write'd into multiple times) keeps
// static_scripts_done set, and evaluate() only drains defer_scripts when it is.
pub fn scriptCreatedParseDone(self: *ScriptManagerBase) void {
self.static_scripts_done = true;
self.evaluate();
}
pub fn evaluate(self: *ScriptManagerBase) void {
if (self.is_evaluating) {
// It's possible for a script.eval to cause evaluate to be called again.

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<!--
document.write into an iframe must parse and run the written markup in the
IFRAME's own frame, not the calling (main) frame. Previously the scripts ran
in the caller's frame, so their globals / window / parent were the main
page's — which broke cross-frame messaging (parent resolved to self).
-->
<script id=iframe_write_runs_in_iframe>
{
const iframe = document.createElement('iframe');
document.documentElement.appendChild(iframe);
const doc = iframe.contentDocument;
doc.write('<!DOCTYPE html><html><body><script>window.ranHere = true; window.parentIsSelf = (window.parent === window);<\/script></body></html>');
doc.close();
// Read the flags from the IFRAME's window. If the script had (wrongly) run in
// the main frame, they'd be set there and these would read undefined.
testing.expectEqual(true, iframe.contentWindow.ranHere);
// The script's `parent` is the main window, so parent !== self.
testing.expectEqual(false, iframe.contentWindow.parentIsSelf);
}
</script>
<!--
A deferred (module) script written into an iframe must still run. A freshly
loaded iframe never fires its static-parse "done" signal, so document.close()
is what flushes the deferred scripts the write produced.
-->
<script id=iframe_write_flushes_deferred_module type=module>
{
const state = await testing.async();
const iframe = document.createElement('iframe');
document.documentElement.appendChild(iframe);
const doc = iframe.contentDocument;
doc.write('<!DOCTYPE html><html><body><script type="module">window.moduleRan = true;<\/script></body></html>');
doc.close();
// Give the deferred module a tick to run, then confirm it executed in the
// iframe (not the main frame, where contentWindow.moduleRan would be unset).
setTimeout(() => state.resolve(), 5);
await state.done(() => {
testing.expectEqual(true, iframe.contentWindow.moduleRan);
});
}
</script>

View File

@@ -718,7 +718,13 @@ pub fn writeln(self: *Document, text: []const []const u8, frame: *Frame) !void {
return self.writeInternal(text, true, frame);
}
fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, frame: *Frame) !void {
fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool, call_frame: *Frame) !void {
// document.write acts on this document's own frame, which isn't necessarily
// the calling frame — e.g. a parent frame writing into an iframe's document.
// The markup (and any scripts it contains) must be parsed and run in that
// document's context, not the caller's.
const frame = self._frame orelse call_frame;
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -730,10 +736,13 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
const html = blk: {
var joined: std.ArrayList(u8) = .empty;
for (text) |str| {
try joined.appendSlice(frame.call_arena, str);
// Scratch buffer, consumed synchronously below. Keep it on the
// active (calling) frame's call_arena: a script run by the parse
// could reset the document frame's call_arena underfoot.
try joined.appendSlice(call_frame.call_arena, str);
}
if (append_newline) {
try joined.append(frame.call_arena, '\n');
try joined.append(call_frame.call_arena, '\n');
}
break :blk joined.items;
};
@@ -827,7 +836,9 @@ fn writeInternal(self: *Document, text: []const []const u8, append_newline: bool
self._write_insertion_point = children_to_insert.getLast();
}
pub fn open(self: *Document, frame: *Frame) !*Document {
pub fn open(self: *Document, call_frame: *Frame) !*Document {
const frame = self._frame orelse call_frame;
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -869,7 +880,9 @@ pub fn open(self: *Document, frame: *Frame) !*Document {
return self;
}
pub fn close(self: *Document, frame: *Frame) !void {
pub fn close(self: *Document, call_frame: *Frame) !void {
const frame = self._frame orelse call_frame;
if (self._type == .xml) {
return error.InvalidStateError;
}
@@ -889,6 +902,12 @@ pub fn close(self: *Document, frame: *Frame) !void {
defer self._script_created_parser = null;
try self._script_created_parser.?.done();
// The write'd markup is fully parsed; run any deferred scripts it produced
// (e.g. inline modules) before firing the load event. This frame's initial
// parse may never have set static_scripts_done (e.g. a freshly-loaded
// iframe written into via document.write), so we can't rely on it.
frame._script_manager.base.scriptCreatedParseDone();
frame.documentIsComplete();
}