Skip to content

Handle NodeIterator node removal during filtering#1479

Merged
annevk merged 3 commits into
mainfrom
nodeiterator-traverse-live-state
Jul 3, 2026
Merged

Handle NodeIterator node removal during filtering#1479
annevk merged 3 commits into
mainfrom
nodeiterator-traverse-live-state

Conversation

@annevk

@annevk annevk commented Jul 2, 2026

Copy link
Copy Markdown
Member

The NodeIterator traverse algorithm read the reference into local variables, ran the filter (which can execute author code), and then wrote those locals back. Filtering can remove nodes and run the NodeIterator pre-remove steps, but those adjusted only the stored reference, not the in-progress traversal position held in traverse's locals. As a result a node removed during its own filtering could leave the reference pointing at a detached node, and traversal could continue from there.

This models a reference as a node pointer (a tuple of a node and a pointer before) and gives each NodeIterator a candidate reference node pointer that holds the in-progress traversal position. traverse advances the candidate reference, sets the reference to it only once a node is accepted, and returns the node passed to the filter. The pre-remove steps adjust both the reference and, while traversing, the candidate reference.

referenceNode continues to report the last accepted node during filtering.

Matches Gecko, Blink, and WebKit (originally found via Acid3).

Tests: web-platform-tests/wpt#61035


Preview | Diff

annevk added 2 commits July 2, 2026 13:33
The traverse algorithm read the reference into local variables, ran the
filter (which can execute author code), and then wrote those locals
back. Filtering can remove nodes and run the NodeIterator pre-remove
steps, but those adjusted only the stored reference, not the in-progress
traversal position held in traverse's locals. As a result a node removed
during its own filtering could leave the reference pointing at a
detached node, and traversal could continue from there.

Model a reference as a node pointer (a tuple of a node and a pointer
before), and give each NodeIterator a candidate node pointer that holds
the in-progress traversal position. traverse advances the candidate,
sets the reference to it only once a node is accepted, and returns the
node passed to the filter. The pre-remove steps adjust both the
reference and, while traversing, the candidate.

referenceNode continues to report the last accepted node during
filtering.

Matches Gecko, Blink, and WebKit (originally found via Acid3).

Tests: web-platform-tests/wpt#61035
Comment thread dom.bs
<ol>
<li><p>If <var>toBeRemovedNode</var> is not an <a for=tree>inclusive ancestor</a> of
<var>nodeIterator</var>'s <a for=NodeIterator>reference</a>, or <var>toBeRemovedNode</var> is an
<var>nodePointer</var>'s <a for="node pointer">node</a>, or <var>toBeRemovedNode</var> is an

@shannonbooth shannonbooth Jul 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same approach ladybird does to fix this scenario (factored slightly differently, but I think this is nicer). I don't have any ideas for a less invasive approach.

Though the thing I noticed adjusting the implementation to match this exactly is if I understand correctly, I think nodePointer needs to be defined as a variable here from nodeIterator since it's not passed through in "given"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I fully understand what you meant, but I made it more explicitly a variable of adjust. Does that help?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, sorry for being unclear thats what I meant

Per review: the node pointer being adjusted was dangling off the
operation name rather than declared as an input, unlike e.g. clone a
single node. Move it into the given clause.
@annevk
annevk merged commit 7137274 into main Jul 3, 2026
2 checks passed
@annevk
annevk deleted the nodeiterator-traverse-live-state branch July 3, 2026 08:36
shannonbooth added a commit to shannonbooth/ladybird that referenced this pull request Jul 3, 2026
Align NodeIterator with the current DOM spec traversal and pre-removal
algorithms. The spec now covers the cases we were previously working
around to match the behaviour of other engines. The algorithm we had
implemented to protect against removal during filtering matches
the spec algorithm quite closely, with slightly different factoring.

See:
 * whatwg/dom#1479
 * whatwg/dom#1477

This fixes the case where removing an ancestor of the reference or
candidate reference, with the pointer before it and no following
node inside the root, should retarget to the previous in-tree node
instead of leaving the iterator on a detached node.
shannonbooth added a commit to LadybirdBrowser/ladybird that referenced this pull request Jul 3, 2026
Align NodeIterator with the current DOM spec traversal and pre-removal
algorithms. The spec now covers the cases we were previously working
around to match the behaviour of other engines. The algorithm we had
implemented to protect against removal during filtering matches
the spec algorithm quite closely, with slightly different factoring.

See:
 * whatwg/dom#1479
 * whatwg/dom#1477

This fixes the case where removing an ancestor of the reference or
candidate reference, with the pointer before it and no following
node inside the root, should retarget to the previous in-tree node
instead of leaving the iterator on a detached node.
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.

2 participants