CVE-2024-9936
Overview
Background
- `SelectionNodeCache`
- A per-selection cache that records which
nsINodeobjects are fully selected so repeated selection queries avoid re-walking every range. - `AbstractRange`
- The base type for a selection range, subclassed by dynamic
nsRangeandStaticRange, exposing boundary accessors likeMayCrossShadowBoundaryStartRef. - `StaticRange`
- An
AbstractRangevariant that stores fixed boundary points and, unlikensRange, is not automatically kept valid as the DOM mutates, so it can reference stale or mismatched nodes. - Range boundary
- A
RangeBoundarypairing a container node with an offset; a range is well-formed only when its start and end boundaries are consistent within the same tree.
Root Cause Analysis
The vulnerable path is SelectionNodeCache::MaybeCollect, which iterates over every range of a Selection via GetAbstractRangeAt and reads each range’s start and end boundaries through MayCrossShadowBoundaryStartRef and MayCrossShadowBoundaryEndRef to compute the set of fully selected nodes. The code assumed every range was non-collapsed and internally valid, but a StaticRange can become invalid after DOM mutation while still being present in the selection, and a collapsed range yields degenerate boundaries. When such a range was processed, the boundary comparison operated on inconsistent or stale endpoints, violating the invariant that boundaries used for node collection belong to a coherent, live range.
The fix restores that invariant by asserting the range is non-null and then skipping any range that Collapsed() reports as empty or that is a StaticRange failing IsValid(), so only well-formed ranges contribute to the cache. This prevents the cache from being populated from garbage boundaries that could drive later logic into an inconsistent, exploitable state.
StaticRange objects; the fix addresses it by filtering those degenerate ranges out before their boundaries are ever dereferenced.Attack Path
- Create a selection with a static range
Script adds a range to a
Selectionthat is (or can become) aStaticRange, whose boundaries are not auto-corrected on mutation. - Mutate the DOM to invalidate it
The attacker moves or removes nodes so the
StaticRangebecomes invalid or collapses, while it remains attached to the selection. - Trigger cache collection
An operation that queries fully selected nodes invokes
SelectionNodeCache::MaybeCollect, which iterates the now-inconsistent range. - Dereference stale boundaries
MayCrossShadowBoundaryStartRefandMayCrossShadowBoundaryEndRefreturn mismatched or stale endpoints, and the cache is built from unexpected node relationships. - Reach an exploitable crash The resulting inconsistent state leads to unexpected behavior and a potentially exploitable memory-safety crash.
Impact Assessment
StaticRange; no special privileges beyond ordinary web content are needed, matching the high-severity rating.Files Changed
dom/base/Selection.cpp
Audit Directions
- Unvalidated StaticRange traversalSearch for other consumers of
GetAbstractRangeAtor range iteration that read boundaries without checkingIsStaticRange/IsValidorCollapsed. - Boundary access without invariantsReview call sites of
MayCrossShadowBoundaryStartRefandMayCrossShadowBoundaryEndRefthat assume live, consistent endpoints across DOM mutations. - Cache population from selection stateAudit any cache or precomputed set built from
Selectionranges to confirm degenerate or stale ranges are filtered before contributing entries.
Patch
diff --git a/dom/base/Selection.cpp b/dom/base/Selection.cpp
index b080f98ebbc..0ad66b0882b 100644
--- a/dom/base/Selection.cpp
+++ b/dom/base/Selection.cpp
@@ -331,6 +331,13 @@ const nsTHashSet<const nsINode*>& SelectionNodeCache::MaybeCollect(
nsTHashSet<const nsINode*> fullySelectedNodes;
for (size_t rangeIndex = 0; rangeIndex < sel->RangeCount(); ++rangeIndex) {
AbstractRange* range = sel->GetAbstractRangeAt(rangeIndex);
+ MOZ_ASSERT(range);
+ if (range->Collapsed()) {
+ continue;
+ }
+ if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) {
+ continue;
+ }
const RangeBoundary& startRef = range->MayCrossShadowBoundaryStartRef();
const RangeBoundary& endRef = range->MayCrossShadowBoundaryEndRef();