Moderate firefox UAF 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionUse-after-free in the Panning and Zooming component
ComponentLayout
Bug ClassUAF
Tracker2074199
Fix commit7f97c05f47fc (firefox) +18/-9
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

APZ (Async Panning and Zooming)
The compositor-side subsystem that scrolls/zooms asynchronously. After an APZ scroll, APZCCallbackHelper (gfx/layers/apz/util/APZCCallbackHelper.cpp) applies the result back to the main-thread layout frames.
`ScrollContainerFrame`
The layout frame (layout/generic/ScrollContainerFrame.cpp) for a scrollable element. ScrollToCSSPixelsForApz() scrolls it to a target; internally it calls ScrollToWithOrigin, which can flush layout and run script, so the frame may be reflowed away — the code carries the comment “’this’ might be destroyed here.”
`AutoWeakFrame`
A helper that observes a frame and reports via IsAlive() whether it has been destroyed, the standard Gecko idiom for detecting frame destruction across a call that can run script/reflow.
`FindScrollContainerFrameFor(scrollId)`
Looks up the current scroll container frame for an APZ scroll id, used to re-acquire a frame pointer that may have been invalidated.

Root Cause Analysis

ScrollToCSSPixelsForApz could destroy its own frame: ScrollToWithOrigin can flush layout and run content script, after which this may be freed (the method even documented “’this’ might be destroyed here”). Despite that, the method returned void and its callers kept using the frame pointer afterward. In ScrollFrameTo, after calling aFrame->ScrollToCSSPixelsForApz(...) the code immediately did geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition()) — dereferencing aFrame which may have just been destroyed. In ScrollFrame, the sf pointer was reused after ScrollFrameTo (which calls into the same scroll path) returned, again with no re-validation. These are use-after-free reads of a ScrollContainerFrame whenever the APZ-driven scroll triggers a reflow that tears the frame down.

The patch makes frame destruction observable and handled. ScrollToCSSPixelsForApz now returns bool: it wraps the ScrollToWithOrigin call with AutoWeakFrame weakFrame(this) and returns weakFrame.IsAlive(), and the header marks it [[nodiscard]]. ScrollFrameTo checks the result: if (!aFrame->ScrollToCSSPixelsForApz(...)) { return targetScrollPosition; }, bailing out before touching the (now-destroyed) aFrame. ScrollFrame re-acquires the frame after the call with sf = nsLayoutUtils::FindScrollContainerFrameFor(aRequest.GetScrollId()) instead of reusing the possibly-dangling sf. In UpdateRootFrame, where sf genuinely is not used after the call, the result is explicitly discarded (void) with a comment noting that is the only reason ignoring it is safe. Together these close every post-call dereference of a frame the scroll may have freed.

Key insight
ScrollToCSSPixelsForApz could free its own ScrollContainerFrame by flushing layout mid-call, yet it returned void and callers kept dereferencing the frame afterward, producing a use-after-free.

Attack Path

  1. Scrollable content Attacker page presents a scroll container and arranges layout that will be reflowed/destroyed during a scroll (e.g. style/DOM changes that remove the scroll frame) (inferred: reachable from ordinary scripted scrolling plus DOM mutation).
  2. APZ scroll applied An APZ repaint request drives APZCCallbackHelper into ScrollFrameTo/ScrollFrame, which call ScrollToCSSPixelsForApz.
  3. Frame freed mid-call ScrollToWithOrigin flushes layout / runs script, destroying the ScrollContainerFrame.
  4. Dangling dereference (pre-patch) Control returns and the caller reads aFrame->GetScrollPosition() / reuses sf, dereferencing freed memory.
  5. Primitive Use-after-free of a ScrollContainerFrame in the main/content process; (inferred) a reclaimed-and-controlled allocation could turn this into a stronger read/write primitive, though the diff shows only the dangling access.

Impact Assessment

Content-process use-after-free in layout/APZ, reachable from web content with no special privilege via scripted scrolling that reflows away a scroll container. The diff substantiates a UAF read of a freed ScrollContainerFrame; whether it is weaponizable into controlled memory corruption/RCE depends on heap grooming and is inferred rather than shown. Severity is “moderate.” The fix eliminates the dangling access by signaling frame destruction (bool/AutoWeakFrame) and either bailing out or re-fetching the frame.

Changed Functions

FunctionChangeNotes
if
gfx/layers/apz/util/APZCCallbackHelper.cpp
modified

Files Changed

  • gfx/layers/apz/util/APZCCallbackHelper.cpp
  • layout/generic/ScrollContainerFrame.cpp
  • layout/generic/ScrollContainerFrame.h

Audit Directions

  • Post-scroll frame dereferences
    Audit every caller of ScrollToCSSPixelsForApz/ScrollToCSSPixels/ScrollToWithOrigin (which all carry “’this’ might be destroyed”) for use of the frame pointer after the call.
  • Void methods that can destroy `this`
    Look for other layout methods documented as possibly destroying this/the frame but returning void, where callers cannot detect the destruction; the [[nodiscard]] bool + AutoWeakFrame pattern applies.
  • Stale frame pointers across reflow in APZ
    Review other APZCCallbackHelper paths that cache ScrollContainerFrame*/nsIContent* across calls that flush layout, and prefer re-fetching via FindScrollContainerFrameFor.
diff --git a/gfx/layers/apz/util/APZCCallbackHelper.cpp b/gfx/layers/apz/util/APZCCallbackHelper.cpp
index 83d3f9ebe75..ee33ace4fd9 100644
--- a/gfx/layers/apz/util/APZCCallbackHelper.cpp
+++ b/gfx/layers/apz/util/APZCCallbackHelper.cpp
@@ -128,9 +128,11 @@ static CSSPoint ScrollFrameTo(ScrollContainerFrame* aFrame,
   bool scrollInProgress = APZCCallbackHelper::IsScrollInProgress(aFrame);
   if (!scrollInProgress) {
     ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds();
-    aFrame->ScrollToCSSPixelsForApz(targetScrollPosition,
-                                    std::move(snapTargetIds),
-                                    aRequest.GetScrollGenerationOnApz());
+    if (!aFrame->ScrollToCSSPixelsForApz(targetScrollPosition,
+                                         std::move(snapTargetIds),
+                                         aRequest.GetScrollGenerationOnApz())) {
+      return targetScrollPosition;
+    }
     geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
     aSuccessOut = true;
   }
@@ -181,6 +183,8 @@ static DisplayPortMargins ScrollFrame(nsIContent* aContent,
       sf, aRequest.GetDisplayPortMargins());
   CSSPoint apzScrollOffset = aRequest.GetVisualScrollOffset();
   CSSPoint actualScrollOffset = ScrollFrameTo(sf, aRequest, scrollUpdated);
+  // sf might have been destroyed by the call to ScrollFrameTo, so re-get it.
+  sf = nsLayoutUtils::FindScrollContainerFrameFor(aRequest.GetScrollId());
   CSSPoint scrollDelta = apzScrollOffset - actualScrollOffset;
 
   if (scrollUpdated) {
@@ -387,8 +391,10 @@ void APZCCallbackHelper::UpdateRootFrame(const RepaintRequest& aRequest) {
     CSSPoint currentScrollPosition =
         CSSPoint::FromAppUnits(sf->GetScrollPosition());
     ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds();
-    sf->ScrollToCSSPixelsForApz(currentScrollPosition, std::move(snapTargetIds),
-                                sf->ScrollGenerationOnApz());
+    // Ignoring the result is safe only because sf isn't used after this call.
+    (void)sf->ScrollToCSSPixelsForApz(currentScrollPosition,
+                                      std::move(snapTargetIds),
+                                      sf->ScrollGenerationOnApz());
   }
 
   // Do this as late as possible since scrolling can flush layout. It also
diff --git a/layout/generic/ScrollContainerFrame.cpp b/layout/generic/ScrollContainerFrame.cpp
index 676c882127d..ece43c2ae84 100644
--- a/layout/generic/ScrollContainerFrame.cpp
+++ b/layout/generic/ScrollContainerFrame.cpp
@@ -2487,7 +2487,7 @@ void ScrollContainerFrame::ScrollToCSSPixels(const CSSPoint& aScrollPosition,
   // 'this' might be destroyed here
 }
 
-void ScrollContainerFrame::ScrollToCSSPixelsForApz(
+bool ScrollContainerFrame::ScrollToCSSPixelsForApz(
     const CSSPoint& aScrollPosition, ScrollSnapTargetIds&& aLastSnapTargetIds,
     const APZScrollGeneration& aGenerationOnApz) {
   mScrollGenerationOnApz = aGenerationOnApz;
@@ -2495,11 +2495,13 @@ void ScrollContainerFrame::ScrollToCSSPixelsForApz(
   nscoord halfRange = nsPresContext::CSSPixelsToAppUnits(1000);
   nsRect range(pt.x - halfRange, pt.y - halfRange, 2 * halfRange - 1,
                2 * halfRange - 1);
+  AutoWeakFrame weakFrame(this);
   ScrollToWithOrigin(
       pt, &range,
       ScrollOperationParams{ScrollMode::Instant, ScrollOrigin::Apz,
                             std::move(aLastSnapTargetIds)});
   // 'this' might be destroyed here
+  return weakFrame.IsAlive();
 }
 
 CSSIntPoint ScrollContainerFrame::GetRoundedScrollPositionCSSPixels() {
diff --git a/layout/generic/ScrollContainerFrame.h b/layout/generic/ScrollContainerFrame.h
index 058ea5045c6..1261cff98ea 100644
--- a/layout/generic/ScrollContainerFrame.h
+++ b/layout/generic/ScrollContainerFrame.h
@@ -509,10 +509,11 @@ class ScrollContainerFrame : public nsContainerFrame,
    * aScrollPosition afterward. It tries to scroll as close to
    * aScrollPosition as possible while scrolling by an integer
    * number of layer pixels (so the operation is fast and looks clean).
+   * Returns false if the frame was destroyed.
    */
-  void ScrollToCSSPixelsForApz(const CSSPoint& aScrollPosition,
-                               ScrollSnapTargetIds&& aLastSnapTargetIds,
-                               const APZScrollGeneration& aGenerationOnApz);
+  [[nodiscard]] bool ScrollToCSSPixelsForApz(
+      const CSSPoint& aScrollPosition, ScrollSnapTargetIds&& aLastSnapTargetIds,
+      const APZScrollGeneration& aGenerationOnApz);
 
   /**
    * Returns the scroll position in integer CSS pixels, rounded to the nearest
Loading diff…