Throw on dynamic markup in custom element callbacks during parsing

Custom element callbacks aren't allowed to call document.open/close/write while
parsing.

Fixes WPT crash:
/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions.html
This commit is contained in:
Karl Seguin
2026-03-11 18:41:06 +08:00
parent 7d835ef99d
commit e1dd26b307
4 changed files with 97 additions and 0 deletions

View File

@@ -1459,6 +1459,8 @@ pub fn adoptNodeTree(self: *Page, node: *Node, new_owner: *Document) !void {
} }
pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node { pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const u8, attribute_iterator: anytype) !*Node {
const from_parser = @TypeOf(attribute_iterator) == Parser.AttributeIterator;
switch (namespace) { switch (namespace) {
.html => { .html => {
switch (name.len) { switch (name.len) {
@@ -2129,6 +2131,15 @@ pub fn createElementNS(self: *Page, namespace: Element.Namespace, name: []const
self.js.localScope(&ls); self.js.localScope(&ls);
defer ls.deinit(); defer ls.deinit();
if (from_parser) {
// There are some things custom elements aren't allowed to do
// when we're parsing.
self.document._throw_on_dynamic_markup_insertion_counter += 1;
}
defer if (from_parser) {
self.document._throw_on_dynamic_markup_insertion_counter -= 1;
};
var caught: JS.TryCatch.Caught = undefined; var caught: JS.TryCatch.Caught = undefined;
_ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| { _ = ls.toLocal(def.constructor).newInstance(&caught) catch |err| {
log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught, .type = self._type, .url = self.url }); log.warn(.js, "custom element constructor", .{ .name = name, .err = err, .caught = caught, .type = self._type, .url = self.url });

View File

@@ -23,6 +23,9 @@ const h5e = @import("html5ever.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Node = @import("../webapi/Node.zig"); const Node = @import("../webapi/Node.zig");
const Element = @import("../webapi/Element.zig"); const Element = @import("../webapi/Element.zig");
pub const AttributeIterator = h5e.AttributeIterator;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<head>
<script src="../testing.js"></script>
<script>
// Test that document.open/write/close throw InvalidStateError during custom element
// reactions when the element is parsed from HTML
window.constructorOpenException = null;
window.constructorWriteException = null;
window.constructorCloseException = null;
window.constructorCalled = false;
class ThrowTestElement extends HTMLElement {
constructor() {
super();
window.constructorCalled = true;
// Try document.open on the same document during constructor - should throw
try {
document.open();
} catch (e) {
window.constructorOpenException = e;
}
// Try document.write on the same document during constructor - should throw
try {
document.write('<b>test</b>');
} catch (e) {
window.constructorWriteException = e;
}
// Try document.close on the same document during constructor - should throw
try {
document.close();
} catch (e) {
window.constructorCloseException = e;
}
}
}
customElements.define('throw-test-element', ThrowTestElement);
</script>
</head>
<body>
<!-- This element will be parsed from HTML, triggering the constructor -->
<throw-test-element id="test-element"></throw-test-element>
<script id="verify_throws">
{
// Verify the constructor was called
testing.expectEqual(true, window.constructorCalled);
// Verify document.open threw InvalidStateError
testing.expectEqual(true, window.constructorOpenException !== null);
testing.expectEqual('InvalidStateError', window.constructorOpenException.name);
// Verify document.write threw InvalidStateError
testing.expectEqual(true, window.constructorWriteException !== null);
testing.expectEqual('InvalidStateError', window.constructorWriteException.name);
// Verify document.close threw InvalidStateError
testing.expectEqual(true, window.constructorCloseException !== null);
testing.expectEqual('InvalidStateError', window.constructorCloseException.name);
}
</script>
</body>

View File

@@ -63,6 +63,11 @@ _script_created_parser: ?Parser.Streaming = null,
_adopted_style_sheets: ?js.Object.Global = null, _adopted_style_sheets: ?js.Object.Global = null,
_selection: Selection = .init, _selection: Selection = .init,
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter
// Incremented during custom element reactions when parsing. When > 0,
// document.open/close/write/writeln must throw InvalidStateError.
_throw_on_dynamic_markup_insertion_counter: u32 = 0,
_on_selectionchange: ?js.Function.Global = null, _on_selectionchange: ?js.Function.Global = null,
pub fn getOnSelectionChange(self: *Document) ?js.Function.Global { pub fn getOnSelectionChange(self: *Document) ?js.Function.Global {
@@ -641,6 +646,10 @@ pub fn write(self: *Document, text: []const []const u8, page: *Page) !void {
return error.InvalidStateError; return error.InvalidStateError;
} }
if (self._throw_on_dynamic_markup_insertion_counter > 0) {
return error.InvalidStateError;
}
const html = blk: { const html = blk: {
var joined: std.ArrayList(u8) = .empty; var joined: std.ArrayList(u8) = .empty;
for (text) |str| { for (text) |str| {
@@ -723,6 +732,10 @@ pub fn open(self: *Document, page: *Page) !*Document {
return error.InvalidStateError; return error.InvalidStateError;
} }
if (self._throw_on_dynamic_markup_insertion_counter > 0) {
return error.InvalidStateError;
}
if (page._load_state == .parsing) { if (page._load_state == .parsing) {
return self; return self;
} }
@@ -761,6 +774,10 @@ pub fn close(self: *Document, page: *Page) !void {
return error.InvalidStateError; return error.InvalidStateError;
} }
if (self._throw_on_dynamic_markup_insertion_counter > 0) {
return error.InvalidStateError;
}
if (self._script_created_parser == null) { if (self._script_created_parser == null) {
return; return;
} }