CVE-2026-118020
Overview
Background
- `InspectorUtils`
- A privileged DevTools-facing C++ interface (
layout/inspector/InspectorUtils.cpp) exposing style/layout introspection such asGetComputationStepsandGetSubstitutedValue, 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 rawElement*whose lifetime is owned by the DOM tree, not the caller.- `RefPtr<Element>`
- Gecko’s strong smart pointer; assigning the parent into a
RefPtrholds 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 toOwnerDoc().
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.
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
- Reach InspectorUtils
Trigger a code path that calls
InspectorUtils::GetComputationSteps/GetSubstitutedValue, orGetParentFontSizePercentageBasisfor a non-pseudo element (the DevTools computed-style/inspector introspection path; web-reachability of this specific API is inferred). - 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). - Capture raw parent
Pre-fix, the parent is stored as
Element* parent. - Force a flush that frees the parent
GetCleanComputedStyleForElement(parent, ...)flushes style/layout; mutation observable during that flush drops the parent element, freeing it. - Use freed parent
The flush returns and the now-dangling
parentis dereferenced. - Primitive achieved
Use-after-free of a layout/DOM
Elementobject, yielding a dangling-object read/call (potential controlled reuse of the freed allocation is inferred, not shown).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
iflayout/inspector/InspectorUtils.cpp |
modified |
Files Changed
layout/inspector/InspectorUtils.cpp
Audit Directions
- Raw node pointers across flushesAudit
layout/inspectorand other style/layout callers for bareElement*/nsINode*locals held acrossGetCleanComputedStyleForElement,FlushPendingNotifications, or any style/layout resolution that can re-enter and mutate the tree. - GetFlattenedTreeParentElement callersReview other users of
GetFlattenedTreeParentElement()that retain the result across potentially-destructive operations without a strong reference. - Composed-doc vs owner-docLook for code caching
GetComposedDoc()early and using it after work that could detach the element; preferOwnerDoc()or re-fetching at use time.
Patch
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