Consolidate DocumentFragment insertions into a single notification
0bc30db
Source/WebCore/dom/ContainerNode.cpp
+template<typename DOMInsertionWork>
+static ALWAYS_INLINE void executeNodeInsertionWithScriptAssertion(ContainerNode& containerNode, NodeVector& children, Node* beforeChild,
+ ContainerNode::ChildChange::Source source, ReplacedAllChildren replacedAllChildren, NOESCAPE const DOMInsertionWork& doNodeInsertion)
+{
+ if (children.isEmpty())
+ return;
+
+ auto childChange = makeChildChangeForInsertion(containerNode, children, beforeChild, source, replacedAllChildren);
+
+ NodeVector postInsertionNotificationTargets;
+ {
+ WidgetHierarchyUpdatesSuspensionScope suspendWidgetHierarchyUpdates;
+ ScriptDisallowedScope::InMainThread scriptDisallowedScope;
+ Style::ChildChangeInvalidation styleInvalidation(containerNode, childChange);
+
+ if (containerNode.isShadowRoot() || containerNode.isInShadowTree()) [[unlikely]]
+ containerNode.containingShadowRoot()->resolveSlotsBeforeNodeInsertionOrRemoval();
+
+ for (auto& child : children) {
+ doNodeInsertion(child);
+ ChildListMutationScope(containerNode).childAdded(child);
+ notifyChildNodeInserted(containerNode, child, postInsertionNotificationTargets);
+ }
+ }
+
+ containerNode.childrenChanged(childChange);
+
+ for (auto& target : postInsertionNotificationTargets)
+ target->didFinishInsertingNode();
+
+ if (source == ContainerNode::ChildChange::Source::API) {
+ for (auto& child : children)
+ dispatchChildInsertionEvents(child);
+ }
+}
DOM 명세는 DocumentFragment 삽입이 원자적으로 이루어질 것을 요구합니다. fragment의 자식 노드들은 하나의 단위로 대상에 이동되어야 하며, 모든 노드가 제자리에 놓이기 전까지는 script나 mutation event가 발생해서는 안 됩니다.
기존 WebKit 구현은 executeNodeInsertionWithScriptAssertion을 자식 노드 하나마다 개별적으로 호출하는 방식이었습니다. 이로 인해 childrenChanged와 mutation event dispatch가 노드마다 순차적으로 발생했습니다. 즉, node N이 삽입된 뒤 node N+1이 삽입되기 전 MutationObserver callback이 실행되면 트리를 수정할 수 있었습니다. 이 경우 이후 삽입 단계가 의존하는 sibling 관계나 노드 수가 달라지는 상황이 발생할 수 있었습니다.
새로 도입된 Vector 기반의 executeNodeInsertionWithScriptAssertion 변형은 모든 자식 삽입을 단일 ScriptDisallowedScope 블록 안에서 처리합니다. 이 블록 안에서는 script 실행이 차단된 상태로 각 자식에 대해 doNodeInsertion, ChildListMutationScope::childAdded, notifyChildNodeInserted가 순서대로 호출됩니다. scope를 벗어난 이후에야 childrenChanged(배치 전체에 대해 1회), didFinishInsertingNode(대상별), dispatchChildInsertionEvents(자식별)가 발생합니다. 삽입 후 처리 로직을 갖는 HTMLSelectElement, HTMLDetailsElement, SVGAnimateMotionElement 등의 요소는 다중 노드 callback을 처리할 수 있도록 수정되었습니다.
Before (per-node loop):
container.appendChild(fragment):
insert node[0] → childrenChanged() → MutationEvent ← script runs
insert node[1] → childrenChanged() → MutationEvent ← script runs
After (atomic batch):
container.appendChild(fragment):
ScriptDisallowedScope {
insert node[0]; insert node[1]; ...
}
childrenChanged(all nodes)
didFinishInsertingNode(each)
dispatchChildInsertionEvents(node[0]) ← script only after all inserted
dispatchChildInsertionEvents(node[1])
Significance
fragment 삽입 도중 DOM이 부분적으로 조립된 상태일 때, mutation observer나 inline event handler가 이를 관찰하고 수정할 수 있었던 TOCTOU window 유형이 제거되었습니다. 아울러 MutationObserver record 구조도 변경됩니다. 동시에 삽입된 노드들은 이제 여러 record에 분산되지 않고 단일 record의 addedNodes 목록에 함께 나타납니다. 노드 하나당 record 하나를 가정하는 코드는 이 동작 변경의 영향을 받습니다.
Audit directions
Aa Aaaaaaaaa Aa Aaa Aa Aaaaaaaaaa Aa Aaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaa Aa Aa Aaa Aa a Aa Aaaaaa Aa Aaaaaaa Aaa Aaaa Aaa Aaaaaaaaa Aaa Aaaa Aaaaa Aaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa Aaa a Aa Aaaa Aaaa Aaaaaaa Aa Aaa Aaaa Aaaaaaa Aaaaaaaaaaa Aaaaaaaaaaaa Aaa Aaaa Aaaaaa Aaaa Aaaa Aa Aaaaaaaaaaaaaaaaaaaaaaa Aaa Aa Aa Aaa Aa Aaa Aaaaa Aaaa Aa Aa a Aaaaa Aaaaaa Aaa Aaaaaa Aaa Aaaa Aaaaaaa Aa Aa Aaaa a Aaaa Aa Aa Aaaaaaaaa Aaaa Aa Aaaaaa Aaaaaaa Aa Aaaaa Aaa Aaaaaa
🔒New multi-node callback paths in security-sensitive element types and style invalidation — several edge cases are worth security investigation.
더 확인하려면 구독해 주세요