Moderate firefox UAF 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionUse-after-free in the Layout component
ComponentLayout
Bug ClassUAF
Tracker2075977
Fix commiteb0a1c052905 (firefox) +7/-18
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

`InspectorUtils`
A privileged DevTools-facing C++ interface (layout/inspector/InspectorUtils.cpp) exposing style/layout introspection such as GetComputationSteps and GetSubstitutedValue, callable from the Inspector/computed-style panels.
Flattened tree parent
Element::GetFlattenedTreeParentElement() returns the element’s parent in the flattened (post-shadow-DOM, post-::before/::after) tree. It returns a raw Element* whose lifetime is owned by the DOM tree, not the caller.
`RefPtr<Element>`
Gecko’s strong smart pointer; assigning the parent into a RefPtr holds a reference across subsequent calls so the element cannot be freed while in use.
`GetCleanComputedStyleForElement`
Resolves a clean computed style for an element, which can flush pending layout/style and, critically, run script-observable work (e.g. flushing can trigger frame reconstruction or mutation that drops DOM nodes).
`EnsureStyleSet()` / style set data
The Servo style set backing the document, obtained per-document; the patch also switches from a cached GetComposedDoc() document pointer to OwnerDoc().

Root Cause Analysis

In GetParentFontSizePercentageBasis, the parent element was captured into a bare pointer: else if (Element* parent = aElement.GetFlattenedTreeParentElement()). That raw pointer was then used to call GetCleanComputedStyleForElement(parent, ...). Resolving a clean computed style flushes style/layout, which is re-entrant and can run work that mutates the DOM and frees the parent element — leaving parent dangling and used afterward, a classic Gecko use-after-free from a raw node pointer held across a style/layout flush. The patch changes the capture to RefPtr<Element> parent = aElement.GetFlattenedTreeParentElement(), keeping the parent alive for the duration of the style resolution.

The patch additionally hardens GetComputationSteps and GetSubstitutedValue. Both previously fetched Document* doc = aElement.GetComposedDoc() up front, early-returned if null, and later called doc->EnsureStyleSet().RawData(). The fix removes those early GetComposedDoc() captures/guards and instead calls aElement.OwnerDoc()->EnsureStyleSet().RawData() directly at the point of use. The owner document is a stable, always-present document for the element (unlike the composed document, which can become stale/null across intervening work), so this both avoids relying on a possibly-stale cached doc pointer and removes the now-unnecessary composed-doc null dance. Taken together the change is a lifetime fix: stop holding raw, flush-spanning references (to the flattened-tree parent, and to a cached document) and instead hold a strong RefPtr for the element and re-derive the style set from the element’s owner document at use time. The advisory’s “use-after-free in Layout” is directly substantiated by the Element* → RefPtr<Element> change.

Key insight
A flattened-tree parent element was held as a raw pointer across GetCleanComputedStyleForElement, which flushes style/layout and can free that element — a use-after-free fixed by holding the parent in a RefPtr (and by re-deriving the style set from OwnerDoc() rather than a cached composed-document pointer).

Attack Path

  1. Reach InspectorUtils Trigger a code path that calls InspectorUtils::GetComputationSteps/GetSubstitutedValue, or GetParentFontSizePercentageBasis for a non-pseudo element (the DevTools computed-style/inspector introspection path; web-reachability of this specific API is inferred).
  2. Target an element with a flattened-tree parent Choose an element whose GetFlattenedTreeParentElement() returns a parent that can be destroyed during a style/layout flush (e.g. via shadow-DOM/slot reconfiguration or pending frame reconstruction).
  3. Capture raw parent Pre-fix, the parent is stored as Element* parent.
  4. Force a flush that frees the parent GetCleanComputedStyleForElement(parent, ...) flushes style/layout; mutation observable during that flush drops the parent element, freeing it.
  5. Use freed parent The flush returns and the now-dangling parent is dereferenced.
  6. Primitive achieved Use-after-free of a layout/DOM Element object, yielding a dangling-object read/call (potential controlled reuse of the freed allocation is inferred, not shown).

Impact Assessment

The affected code runs in the content/renderer process within layout and style introspection. The precondition is reaching InspectorUtils with an element configuration where a style/layout flush can free the flattened-tree parent; the diff does not establish how web content directly reaches this path (InspectorUtils is primarily DevTools-facing), so web-reachability is inferred. The gained primitive is a use-after-free of an Element during style resolution; exploitation to code execution would require grooming the freed allocation and is not shown by the diff. Mozilla rates it moderate.

Changed Functions

FunctionChangeNotes
if
layout/inspector/InspectorUtils.cpp
modified

Files Changed

  • layout/inspector/InspectorUtils.cpp

Audit Directions

  • Raw node pointers across flushes
    Audit layout/inspector and other style/layout callers for bare Element*/nsINode* locals held across GetCleanComputedStyleForElement, FlushPendingNotifications, or any style/layout resolution that can re-enter and mutate the tree.
  • GetFlattenedTreeParentElement callers
    Review other users of GetFlattenedTreeParentElement() that retain the result across potentially-destructive operations without a strong reference.
  • Composed-doc vs owner-doc
    Look for code caching GetComposedDoc() early and using it after work that could detach the element; prefer OwnerDoc() or re-fetching at use time.
diff --git a/layout/inspector/InspectorUtils.cpp b/layout/inspector/InspectorUtils.cpp
index 75595ae1e8d..70ad8cec8c9 100644
--- a/layout/inspector/InspectorUtils.cpp
+++ b/layout/inspector/InspectorUtils.cpp
@@ -1527,7 +1527,8 @@ static float GetParentFontSizePercentageBasis(
   if (!aPseudo.IsNotPseudo()) {
     parentStyle = GetCleanComputedStyleForElement(
         &aElement, PseudoStyleRequest::NotPseudo());
-  } else if (Element* parent = aElement.GetFlattenedTreeParentElement()) {
+  } else if (RefPtr<Element> parent =
+                 aElement.GetFlattenedTreeParentElement()) {
     // For non pseudo-element, we resolve against the parent element's
     // font-size.
     parentStyle = GetCleanComputedStyleForElement(
@@ -1639,11 +1640,6 @@ void InspectorUtils::GetComputationSteps(GlobalObject& aGlobalObject,
                                          Element& aElement,
                                          const nsAString& aPseudo,
                                          nsTArray<nsCString>& aResult) {
-  Document* doc = aElement.GetComposedDoc();
-  if (!doc) {
-    return;
-  }
-
   auto pseudo = PseudoStyleRequest::Parse(
       aPseudo, aElement.OwnerDoc()->DefaultStyleAttrURLData());
   if (!pseudo) {
@@ -1662,7 +1658,8 @@ void InspectorUtils::GetComputationSteps(GlobalObject& aGlobalObject,
       GetPercentageBasisFor(aProperty, aElement, *pseudo, *computedStyle);
 
   Servo_GetComputationSteps(&aExpression, &aElement, pseudo->mType,
-                            computedStyle, doc->EnsureStyleSet().RawData(),
+                            computedStyle,
+                            aElement.OwnerDoc()->EnsureStyleSet().RawData(),
                             percentageBasis, &aResult);
 }
 
@@ -1672,14 +1669,6 @@ void InspectorUtils::GetSubstitutedValue(GlobalObject& aGlobalObject,
                                          Element& aElement,
                                          const nsAString& aPseudo,
                                          nsACString& aResult) {
-  Document* doc = aElement.GetComposedDoc();
-  if (!doc) {
-    // Return null when the computation couldn't be done so we can differentiate
-    // from valid empty strings.
-    aResult.SetIsVoid(true);
-    return;
-  }
-
   auto pseudo = PseudoStyleRequest::Parse(
       aPseudo, aElement.OwnerDoc()->DefaultStyleAttrURLData());
   if (!pseudo) {
@@ -1700,9 +1689,9 @@ void InspectorUtils::GetSubstitutedValue(GlobalObject& aGlobalObject,
     return;
   }
 
-  Servo_GetSubstitutedValue(&aExpression, &aElement, pseudo->mType,
-                            computedStyle, doc->EnsureStyleSet().RawData(),
-                            &aResult);
+  Servo_GetSubstitutedValue(
+      &aExpression, &aElement, pseudo->mType, computedStyle,
+      aElement.OwnerDoc()->EnsureStyleSet().RawData(), &aResult);
 }
 
 }  // namespace mozilla::dom
Loading diff…