[5] WebCore Range concurrent-GC UAF on lock-free treeOrder
Severity: High | Component: WebCore DOM Range | 621e3bf
Rated High because the diff confirms that Range::setStart/setEnd released m_boundaryPointLock between writing one boundary and reading both for treeOrder, exposing the concurrent GC marker to a torn RangeBoundaryPoint; the commit title attributes this directly to a UAF of container Nodes under Range::visitNodesConcurrently, which is a classic DOM-node UAF primitive in WebContent.
In Range::setStart and Range::setEnd, the patch collapses two separate m_boundaryPointLock critical sections into one. The pre-fix code locked once to write m_start (or m_end), released the lock, re-read both boundary points without the lock to compute treeOrder(...), then re-acquired the lock to fix the ordering by copying one boundary into the other. The new code computes the ordering question first using the parameters and a snapshot of the other boundary, then takes the lock once, writes the updated boundary point, and conditionally writes the second boundary point — all inside the same Locker { m_boundaryPointLock } scope. The seven boundary* static helpers are also rewritten to take a leading Locker<Lock>& parameter, documenting at the type level that the caller must already hold m_boundaryPointLock.
Source/WebCore/dom/Range.cpp
ExceptionOr<void> Range::setStart(Ref<Node>&& container, unsigned offset)
{
auto childNode = checkNodeOffsetPair(container, offset);
if (childNode.hasException())
return childNode.releaseException();
+ bool shouldAlsoSetEnd = !is_lteq(treeOrder(BoundaryPoint(container.copyRef(), offset), makeBoundaryPoint(m_end)));
{
Locker locker { m_boundaryPointLock };
m_start.set(WTF::move(container), offset, childNode.releaseReturnValue());
+ if (shouldAlsoSetEnd)
+ m_end = m_start;
}
- if (!is_lteq(treeOrder(makeBoundaryPoint(m_start), makeBoundaryPoint(m_end)))) {
- Locker locker { m_boundaryPointLock };
- m_end = m_start;
- }
-static inline void NODELETE boundaryTextInserted(RangeBoundaryPoint& boundary, Node& text, unsigned offset, unsigned length)
+static inline void NODELETE boundaryTextInserted(Locker<Lock>&, RangeBoundaryPoint& boundary, Node& text, unsigned offset, unsigned length)
...
void Range::textInserted(Node& text, unsigned offset, unsigned length)
{
Locker locker { m_boundaryPointLock };
- boundaryTextInserted(m_start, text, offset, length);
- boundaryTextInserted(m_end, text, offset, length);
+ boundaryTextInserted(locker, m_start, text, offset, length);
+ boundaryTextInserted(locker, m_end, text, offset, length);
Patch Details
In Range::setStart and Range::setEnd, the pre-fix shape { lock; write_one; } read_unlocked treeOrder; { lock; maybe_write_other; } is replaced with compute_decision_from_args_and_snapshot; { lock; write_one; maybe_write_other; }. The conditional second boundary write now lives inside the same critical section as the first boundary write, so the marker cannot observe a partial update. Seven boundary* static helpers (boundaryNodeChildrenChanged, boundaryNodeChildrenWillBeRemoved, boundaryNodeWillBeRemoved, boundaryTextInserted, boundaryTextRemoved, boundaryTextNodesMerged, boundaryTextNodesSplit) take a leading Locker<Lock>& parameter; their call sites in Range::nodeChildrenChanged, Range::nodeChildrenWillBeRemoved, Range::nodeWillBeRemoved, Range::textInserted, Range::textRemoved, Range::textNodesMerged, and Range::textNodeSplit are updated to forward the existing locker. Only Range.cpp is touched.
Lock-free read of shared multi-word DOM boundary state races a concurrent GC visitor, allowing the marker to retain a stale container Node* past its lifetime.
Background
Range represents a live DOM range — a [start, end] interval over the DOM tree — used by Selection, find-in-page, Highlight, and similar features. Both endpoints are RangeBoundaryPoint values holding a RefPtr<Node> container, an unsigned offset, and a childBefore pointer used to keep the offset coherent across child mutations. The boundary points are multi-word state that cannot be read coherently without synchronization.
JSC's garbage collector runs a concurrent marking phase on a separate thread while JavaScript continues to execute on the main thread. Concurrent visitors call visit*Concurrently overrides on objects that hold native references to GC-managed roots, and these visitors must synchronize with mutator writes to any field they read. Range::visitNodesConcurrently is the per-Range concurrent visitor that traces m_start and m_end's container Nodes. m_boundaryPointLock is the per-Range lock that serializes mutator writes to m_start/m_end against this concurrent visitor.
treeOrder is a comparator that walks the DOM tree to order two boundary points; reading its inputs requires that those inputs be stable for the duration of the call. Locker<Lock>& as a parameter is a WebKit idiom that documents at the function signature that the caller must already hold the named lock — it does not enforce identity, but it forces every call site to materialize a locker, which is mechanically auditable at compile time.
Analysis
The bug is a data race producing a concurrent use-after-free during GC marking. Pre-fix Range::setStart/setEnd exposed two windows in which the GC's concurrent visitor could observe m_start/m_end in an inconsistent or torn state. After updating one boundary inside a lock and releasing it, the code re-read both m_start and m_end without the lock to compute treeOrder(makeBoundaryPoint(m_start), makeBoundaryPoint(m_end)). Because RangeBoundaryPoint is multi-word, that lock-free read races every concurrent writer and the GC marker. Between the first lock release and the conditional second lock re-acquisition, the marker could observe a state where m_start was already advanced past m_end, then the writer would overwrite m_end in a separate critical section.
Aaa Aaaaaaaaaa Aaaaaaaaaaaa Aaa Aaa Aaaaaa Aaaaaa Aa Aaaa Aaa Aaaaaa Aaaaa Aaaaaa a Aaaaa Aaaaaaa Aa a Aaaaaaaaa Aaaaaa Aaaaa Aaaa Aaaa Aaaaaaaaa Aaa Aaaa Aaaa Aaaaaaa Aa Aaa Aaaaaaaa Aaaaaaaaa a Aaaa Aaaaaaaaaaaaaaaaaaaa Aaaa Aaa Aaaaaa Aaaaaa Aaa Aaaaaaa Aaaaaaaaaa Aaaaa Aa Aaaaaaa a Aa Aa a Aaaaaaaaaa Aaaaaaaa Aa Aaa Aaaaaaaa Aaaaaaaa Aaaaa a Aaa Aaaaaa Aaaaaaaaaaaa Aaaa Aaaaaaaa Aaaaaaa Aa Aaaaaaa Aaaaa Aaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaaaa Aaaaaaaaaa Aaaaaaa a Aaaa Aaaaaaaaa Aaaaaaaaa Aaaaa Aaaaaa Aaaaaaaaa Aa Aaa Aa Aaa Aa Aaaa Aa Aaa Aaaaa Aaaaaaaaaa Aaaaa Aaa Aaaaaaa Aaaa Aaaaaaaa Aaaa Aaaaaaaaa Aaaaaaaa a Aaaaaaa Aaaaaaa Aaaaaaaaaa Aaaaaaaaaaaa Aaaa Aaa Aaa Aaaaaaaaaa Aaaaaaa
Aaa Aaaaaaaaa Aaaaaa Aa Aaaa Aaaaaa Aaaaaaaaaaaaa Aaaaaaaaaa Aaa Aaaaaa Aaaaaaaa Aa Aaa Aaaaaaaa Aaaaa Aaa Aaa Aaaaa Aaaaaaa Aaaaa Aaaaaaaaaaa Aa Aaaa Aaaaaaaaaaaaaaa Aaaaaaaaaa Aaaaaaaa Aaa Aaaaaaaa Aaaa Aaa Aaaaaaaa Aa Aaaaaaaa Aaaaaa Aaa Aaaa Aaaaaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaaaaaaaa Aa Aaa Aaaa Aaaaaaaaaaa Aaaaa Aaa Aaaaaaa Aaaa Aaa Aaaaaaaaaaaaaaa Aaaaaaaaa Aaaaa Aa a Aaaaa Aaaaaaaaaa Aaaaaaaaaa a Aaaaa Aaaa Aaaa Aaaa Aaaaaaaaaaa a Aaaaaaaaa Aa a Aaaaaaaaa Aaaa Aaaaaaaa Aa Aaaaaaa Aaaa Aaaaaa Aaaa Aa a Aaaaaaaa Aa Aaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaaaa Aaa Aaaaaaa Aaaaaa Aaaaa a Aaaaaaa Aaaaaaa Aaaaaaaaaaaaaa Aaaaaa Aaaaaaaaaaaaaa a Aaaaa Aaaa Aa a Aaaaaaaaaaaaaaa Aaaaaa Aaa Aa Aaaaaaa Aaa Aaaaa Aaaa Aaaaaa Aaaaa a Aaaaaaaaaa Aaaaaa Aaaa Aaaaa Aaa Aaaa Aaaaa Aaaaaaaaaa Aaaaaaaaaaaa Aaaaa Aaaaa a Aaaaaaaaaaaaaa Aa a Aaa Aaaaaa Aa Aaa Aaaaaaaaaa Aaaaaaaa a Aaaaa Aa Aaaaaaaaa Aaaaaaaaaaaa Aaaaaaaaa Aaa Aaaaaaaa Aaaa
🔒How a narrow window between two critical sections lets the concurrent GC marker observe DOM range boundary state mid-flight, and what that means for renderer memory safety.
Subscribe to read more
Audit directions
a Aaaaaaaaaa Aaaaaaaa Aaaa Aaaaaaa Aaa Aaaa Aaaaaaa a Aaaaa Aaa a Aaaaaaaaa Aaaa Aa Aaa Aaaa Aaaaaa Aaaaaa Aa Aaaaaaa Aaaa Aaaaaaa Aa Aaa Aaa Aaaaaaaaaa Aaaaaaaaa Aaaaa Aaaaa Aaaaaaa Aaaaa Aaaaa Aaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaa Aaa a Aaaaaaaaaa Aaaa Aaaaaa Aaaaa Aaa Aaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aaaa Aaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaa Aaaa Aaaaa Aaaaa Aaaaaaa Aaaaa Aaaa Aa a Aaaaaaaaaaaaaa Aaaaa Aaaaaaa Aaaaaa Aaa Aaaa Aaaaaaaa Aaaa Aaaaa Aaaaaaa
a Aaaaaaaa Aaaaaaaaa Aaaa Aaaaaa Aaaaaaaaaaaaaa Aaaaaa Aaa Aaaa Aaa Aaaaaaa Aaaaa Aa Aaaaaaaaa Aaaaaaa Aaa Aaaaaa Aaaa a Aaaa Aa Aaaaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaa Aaaaaaa Aaaaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaa Aa Aaaaaaa Aaaaaaaa Aaaaa Aaaa Aaa Aaa Aa Aaa Aaaa Aaaaaaa Aaaa Aaaaaaaaaaaaaaaaaa Aaaaa Aaa Aaaaaaa Aaaa Aaaaaa Aaaaa Aaa Aaaaaaaa Aaaaa Aaa Aaaaaaaaaaaaaaa Aaaaaaaaa Aaaaa Aaaaa Aaaa Aa Aaaaa Aaaaaaaaaaaa Aaaaaaaaa Aa Aaaa Aaaaaaaaaaaaa Aaa a Aaaaaaa Aaaaaaaaa
a Aaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaaaa Aaaaaaa Aa Aaaaaa Aaaaa Aaaaaaa a Aaaa Aa Aaaaaa Aaaaaaa a Aaaaaaaaaa Aaaaa Aa Aaaaaaaaa Aaaaa Aaaaaaaaaaaa Aaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaa Aa Aaaa Aaaaaaaaa Aaaa Aaaaaaaaaaaaaa Aaaaaaa Aaaaaaa Aaa Aaaaaaaa Aaaaa Aaa Aaa Aaaa Aaaaaaaaaaaa Aaa Aaaa Aaaaa a Aaa Aaa Aaa Aaaaa Aaaa a Aaaaaaaa Aa Aaa Aaaaa Aaaaa Aaaa Aaaaaa Aaa Aaaaaaaa Aaaaaa Aaa Aaaaaaaa Aaaaaaaa
a Aaaaaaaaaaaa Aaaaa Aaaa Aa Aaaaaaaaaa Aaaaaaa Aaaaaaaaaaaaaaa a Aaaaaa a Aaaaaaaaaaaa Aaaa Aaaaaa Aa Aaaa Aaaaaaaaaaaaa Aaaaaaaaaaa Aaaaa Aaaaaaa Aaaaaaa Aaaa Aaaaaa Aaaaaaaaa Aa Aaaaa Aa a Aaaaaaaaaa Aaaaaa a Aaaaaa Aaaa Aaaaa Aaaaaa Aaaa Aaaaaa Aaaaa Aaa Aaaa Aa Aaaaa Aaaaaaa Aa Aaaaaa Aaaaaaaaa
🔒Four reusable audit patterns for concurrent-GC data races in WebCore, with concrete grep targets across DOM and editing subsystems.
Subscribe to read more