Visitar URL original
Adoption agency: fix element creation and reparenting with a sanitizer by noamr · Pull Request #13214 · whatwg/html · GitHub
Skip to content

Adoption agency: fix element creation and reparenting with a sanitizer - #13214

Open
noamr wants to merge 10 commits into
mainfrom
noamr/aaa-fixes
Open

noamr wants to merge 10 commits into
mainfrom
noamr/aaa-fixes

Conversation

@noamr

@noamr noamr commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Adoption agency: fix element creation and reparenting with a sanitizer

The adoption agency algorithm reparents and recreates elements, and the parser sanitizer configuration and scoped custom element registries made the context in which it does so observable. With a formatting element that the sanitizer removes, furthestBlock and its descendants are created in an inert document with a null custom element registry, and three things go wrong when the algorithm moves them back into the live tree:

This PR:

  • Creates inner-loop clones with the element they replace as the intended parent.

  • Creates the new formatting element and determines its sanitizer action before inserting lastNode. If the action is Remove, moves furthestBlock's children into the new element first so they are never connected when lastNode is inserted; otherwise preserves the existing order (insert lastNode, move furthestBlock's children into the new element unless Replace with children, then insert the new element into furthestBlock). Note that a customized built-in new element's constructor now runs before lastNode is inserted rather than after.

  • Before inserting lastNode, sets the null registries in its subtree to the registry looked up from target if it is an Element or a ShadowRoot, or from insertionLocation's target parent otherwise, matching "create an element for a token".

Neither Chrome Canary nor Firefox Nightly propagates the scoped registry (tests 2, 3, and 4). In streamHTMLUnsafe(), Chrome Canary also connects and disconnects furthestBlock's children before moving them into the removed new formatting element (test 6), and drops the inner-loop clone of a formatting element whose token has an is attribute, with or without a sanitizer (test 8).

Tests: web-platform-tests/wpt#64909.

Fixes #13093.


(See WHATWG Working Mode: Changes for more details.)


/parsing.html ( diff )

noamr added 3 commits November 7, 2026 22:16
…nt they replace

The inner loop of the adoption agency algorithm created each clone with commonAncestor as the intended parent. That gave the clone commonAncestor's node document and custom element registry, even when the element it replaces lives in the inert template contents document because commonAncestor is a template element, or because the parser sanitizer configuration removed an element between commonAncestor and it in the stack of open elements.

Use the replaced element as the intended parent instead, so that the clone inherits the document and registry of the element it replaces.

Part of #13093.
…renting furthestBlock

The adoption agency algorithm inserted lastNode, the subtree rooted at furthestBlock, at commonAncestor's insertion location before determining the sanitizer action for the new formatting element. When that action is Remove, furthestBlock's children were briefly connected before being moved into the never-inserted new element, running their insertion steps and custom element reactions along the way.

Create the new element and compute its sanitizer action first, and move furthestBlock's children into it before lastNode is inserted. The children, which were created in an inert document because the removed formatting element was, now never become connected. As a result, the new element's constructor (for customized built-in elements) now runs before lastNode is inserted rather than after.

Part of #13093.
… of their new parent

When the adoption agency algorithm moves furthestBlock out of a formatting element that the sanitizer removed, furthestBlock and its descendants were created in an inert document with a null custom element registry. Adopting them into the target's node document only assigns that document's registry, so a scoped registry that applies to the target was lost, and elements parsed later into the subtree could not look one up.

Before inserting lastNode, set the null registries in its subtree to the registry looked up from the target when it is an element or a shadow root, as creating an element for a token does. When the target is a document fragment, such as template contents, the registries stay null.

Part of #13093.
noamr added 2 commits November 8, 2026 08:31
Inclusive descendants of a node are not an Infra list, and the other places
in the spec that iterate descendants in tree order use a plain For each.
furthestBlock's intended parent is the current node at the time, which can
be an element between formattingElement and furthestBlock that is not a
clone (e.g., a span), and furthestBlock's descendants have furthestBlock or
its descendants as intended parents. What matters for the note is only that
the subtree was created below formattingElement.
@noamr

noamr commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

Review-and-fix pass over the current head (242a597), together with web-platform-tests/wpt#64909.

Questions / follow-ups

  • Creating newElement now happens between computing (target, refNode) from commonAncestor and inserting lastNode there. For a customized built-in formatting element with a defined is (outside the fragment case), that runs the constructor at that point, with furthestBlock still at its original position. The existing conditions on refNode, the abort flag, and target's element child cover the tree-shape hazards, and the PR body calls out the ordering change, but if script in that constructor moves target itself, lastNode is inserted into a stale target. The rest of the tree builder has the same property (insertion into whatever the current node is), so I left it; flagging in case you would rather compute the insertion location after newElement is created (the pendingRedirects update would move with it).
  • The note on the inner-loop clone says commonAncestor's node document and registry can differ from node's when commonAncestor is a template. In the template case only the registry differs (both are in the template contents owner document), while the document differs in the removed-formatting-element case. The collective phrasing reads fine to me; mentioning in case you want to split it.
  • Chrome Canary's inner-loop clone of a formatting element whose token has an is attribute disappearing (test 6) reproduces without a sanitizer (<b><i is="x"><p></b>); Firefox matches the spec. I can file that against Blink>HTML>Parser and link it from the implementation-bugs checklist if you want.

Findings

All three commits trace correctly through the six tests in the companion WPT (each expected result follows from the algorithm as written), create an element for a token and insert a foreign element pass the registries and documents the fixes rely on, and DOM's adopt indeed only ever assigns the global registry, so the propagation step before insertion is needed. The build is clean and the algorithm checker reports nothing new in the changed range. Two non-normative fixes, pushed to this branch:

  • 861bb60: <span data-x="list iterate">For each</span> over inclusive descendants, which are not an Infra list; the other tree-order iterations in the spec (get HTML-aware text content, CustomElementRegistry's initialize(), and the autofocus delegate) use a plain "For each".
  • e3c2ecc: the note next to the registry propagation said lastNode and its descendants were created with formattingElement or one of its clones as their intended parent. furthestBlock's intended parent is the current node at the time, which can be a non-clone such as a span between the two, and furthestBlock's descendants have furthestBlock or deeper nodes as theirs. Reworded to say they were created below formattingElement, which is what the inert-document conclusion depends on.

noamr added 3 commits November 8, 2026 10:26
The rationale for the intended-parent change, for moving furthestBlock's
children before inserting lastNode, and for propagating the registry is in the
pull request and commit messages; the algorithm steps stand on their own.
Move furthestBlock's children into newElement before inserting lastNode
whenever sanitizerAction is not "Replace with children", instead of
doing it before lastNode insertion for "Remove" and inside the final
insertion block for "Keep". When sanitizerAction is "Keep", newElement
is not yet connected while lastNode is being inserted, so moving the
children there only removes them from furthestBlock earlier and shrinks
the "Keep" insertion block to the pre-insert validity assert and the
insert.
@noamr

noamr commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

Findings and fixes

Spec (whatwg/html#13214)

  1. Unified moving furthestBlock's children into newElement (7ed91420c)

    • Instead of splitting the transfer of furthestBlock's children into two steps (moving into newElement when kept, or leaving on furthestBlock when removed), unified the transfer into a single step whenever sanitizerAction is not "Replace with children", and inserted newElement into furthestBlock only when sanitizerAction is "Keep".
  2. newElement intendedParent and insertion order when an intermediate non-formatting element is removed (9d6f088e3)

    • In <b><span><p><my-el></b></p> with { sanitizer: { removeElements: ["span"] } }, formattingElement (<b>) is in the live tree with a non-null customElementRegistry, whereas furthestBlock (<p>) and its children were created under the removed <span> in an inert document with a null registry.
    • Using furthestBlock as intendedParent when creating newElement gave newElement a null registry even though formattingElement was kept, and inserting newElement into furthestBlock after lastNode was inserted into target left furthestBlock's original children in the inert document with a null registry.
    • Fixed by passing formattingElement as intendedParent when creating newElement, and inserting newElement into furthestBlock before removing lastNode, fixing up null custom element registries across lastNode's inclusive descendants, and inserting lastNode into target.

WPT (web-platform-tests/wpt#64909)

  1. Added coverage for removed intermediate element between formattingElement and furthestBlock (77fed10a0a)
    • Added "furthestBlock and its children reparented out of a removed intermediate element get the target's scoped registry" in sanitizer-api/sanitizer-adoption-agency-removed-elements.html.

@annevk

annevk commented Nov 9, 2026

Copy link
Copy Markdown
Member
AI feedback

Reviewed at 9d6f088, only for how it interacts with #13215; not a full review.

Suggestions

  • Consider keeping the order on main (lastNode first, then furthestBlock's children into newElement, then newElement into furthestBlock) unless the sanitizer action is Remove. As of 9d6f088, the children move and newElement is inserted before lastNode is moved, also when nothing is sanitized, which changes observable behavior:

    • In <b><i><p><x-aaa></x-aaa></b>, the inner loop has already moved p into the clone of i, which isn't inserted yet, so <x-aaa> gets one disconnected/connected pair instead of two. This is why two of Adoption agency: invoke custom element reactions once the algorithm finishes #13215's tests don't match the spec at its head.
    • In <b><p><span></span></b>, a mutation observer sees p lose span and gain the new b before b loses p.

    WebKit uses the order on main, and the Chrome and Firefox logs in Custom elements: expect adoption agency reactions once the algorithm finishes web-platform-tests/wpt#64911 imply they do too. That order also covers the registry problem 9d6f088 fixes: furthestBlock's children are still inside lastNode when it is inserted, so the fix-up over its inclusive descendants reaches them. Only when the action is Remove do the children need to move out first, so that they never become connected.

  • If the new order stays, the walkthrough in "Misnested tags: <b><p></b></p>" still describes the old one ("The last node gets appended (moved) to the common ancestor", followed by "A new b element is created, and the children of the p element are moved to it").

@noamr

noamr commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Restored main's operation order (lastNode removed and inserted first, then furthestBlock's children moved into newElement and newElement inserted into newElementParent) unless sanitizerAction is "Remove" in 2f98c0621 (with the corresponding reaction log update in web-platform-tests/wpt#64909 at 575dc99c1d).

@noamr
noamr added this pull request to stack #13222 November 9, 2026 19:42
@noamr
noamr marked this pull request as ready for review November 9, 2026 22:24
@annevk

annevk commented Nov 10, 2026

Copy link
Copy Markdown
Member
AI feedback

Reviewed at 2f98c06.

Suggestions

  • Consider updating the description, which still says furthestBlock's children are appended to the new element "(unless the action is Replace with children) before inserting lastNode". Since 2f98c06 that only happens when the action is Remove; otherwise they are appended after lastNode is inserted, as on main.

@noamr

noamr commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

AI feedback

Applied PR description suggestion and WPT. Also finished the checkboxes apart from "multiple implementers interested". @keithamus @annevk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Adoption agency algorithm issues (sanitizer and intended parent)

2 participants