Stale paint_children / hoisted children cause hit_inner panic after remove_and_drop_node
Problem
DocumentMutator::remove_and_drop_node updates parent.children (line 516: retain), but does not clean up the derived paint_children list or stacking_context hoisted children lists. These are only rebuilt during the next resolve() pass.
If a hit test runs between remove_and_drop_node and the next resolve(), Node::hit_inner iterates these stale lists and calls self.with(stale_id), which does tree().get(id).unwrap() and panics.
Affected call sites
Three places in hit_inner (node.rs):
| Line |
List |
Path |
| 1271 |
paint_children |
child traversal |
| 1260 |
pos_z_hoisted_children |
positive z-index hoisted children |
| 1283 |
neg_z_hoisted_children |
negative z-index hoisted children |
Reproduction
Tests in tests/blitz-tests/tests/hit_test_after_remove_node.rs:
- Build a DOM with child elements (with/without z-index)
pump() to build paint_children and hoisted children lists
remove_and_drop_node on a child (without pumping)
hit() in the area where the removed child used to be
- Panic at
self.tree().get(id).unwrap()
Root cause
paint_children and hoisted children are derived data built during resolve(). remove_and_drop_node updates the DOM tree (children / parent) but not these derived lists. Between removal and the next resolve(), the lists contain stale NodeIds pointing to dropped nodes.
Stale
paint_children/ hoisted children causehit_innerpanic afterremove_and_drop_nodeProblem
DocumentMutator::remove_and_drop_nodeupdatesparent.children(line 516:retain), but does not clean up the derivedpaint_childrenlist orstacking_contexthoisted children lists. These are only rebuilt during the nextresolve()pass.If a hit test runs between
remove_and_drop_nodeand the nextresolve(),Node::hit_inneriterates these stale lists and callsself.with(stale_id), which doestree().get(id).unwrap()and panics.Affected call sites
Three places in
hit_inner(node.rs):paint_childrenpos_z_hoisted_childrenneg_z_hoisted_childrenReproduction
Tests in
tests/blitz-tests/tests/hit_test_after_remove_node.rs:pump()to buildpaint_childrenand hoisted children listsremove_and_drop_nodeon a child (without pumping)hit()in the area where the removed child used to beself.tree().get(id).unwrap()Root cause
paint_childrenand hoisted children are derived data built duringresolve().remove_and_drop_nodeupdates the DOM tree (children/parent) but not these derived lists. Between removal and the nextresolve(), the lists contain stale NodeIds pointing to dropped nodes.