CVE-2026-117964
Overview
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 callsScrollToWithOrigin, 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.
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
- 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).
- APZ scroll applied
An APZ repaint request drives
APZCCallbackHelperintoScrollFrameTo/ScrollFrame, which callScrollToCSSPixelsForApz. - Frame freed mid-call
ScrollToWithOriginflushes layout / runs script, destroying theScrollContainerFrame. - Dangling dereference (pre-patch)
Control returns and the caller reads
aFrame->GetScrollPosition()/ reusessf, dereferencing freed memory. - Primitive
Use-after-free of a
ScrollContainerFramein 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
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
| Function | Change | Notes |
|---|---|---|
ifgfx/layers/apz/util/APZCCallbackHelper.cpp |
modified |
Files Changed
gfx/layers/apz/util/APZCCallbackHelper.cpplayout/generic/ScrollContainerFrame.cpplayout/generic/ScrollContainerFrame.h
Audit Directions
- Post-scroll frame dereferencesAudit 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+AutoWeakFramepattern applies. - Stale frame pointers across reflow in APZReview other
APZCCallbackHelperpaths that cacheScrollContainerFrame*/nsIContent*across calls that flush layout, and prefer re-fetching viaFindScrollContainerFrameFor.
Patch
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