CVE-2026-117997
Overview
Background
- APZ (Async Pan/Zoom)
- the Firefox subsystem (
gfx/layers/apz) that handles input events (touch, mouse, pan, pinch, tap, wheel, keyboard) for scrolling/zooming; input processing happens in the UI/parent process. - `IAPZCTreeManager` / `APZBridge`
IAPZCTreeManageris the APZ tree-manager interface;APZBridgeis the synchronous input-delivery interface.APZBridge::ReceiveInputEventprocesses an input event synchronously and returns anAPZEventResult.- `IAPZCTreeManager::Bridge()`
- returns the
APZBridgeto deliver input through. Implementations areAPZCTreeManager(in-process, returnsthis) andAPZCTreeManagerChild(out-of-process, returns itsmBridge, anAPZBridgeChild). - `APZBridgeChild`
- the child-side IPC actor that forwards input events to the GPU/compositor process over
PAPZBridge; it has anmIsOpenflag and synchronousSendReceive*IPC sends. - Raw-pointer vs `RefPtr` ownership
- the pre-patch
Bridge()returned a rawAPZBridge*whose lifetime was documented to “match the lifetime of this IAPZCTreeManager implementation”; aRefPtr<APZBridge>is a strong reference that keeps the object alive.
Root Cause Analysis
IAPZCTreeManager::Bridge() returned a raw APZBridge* and promised the returned object would stay valid for the manager’s lifetime. Callers therefore did manager->Bridge()->ReceiveInputEvent(...) holding only a borrowed pointer. But ReceiveInputEvent runs synchronously and can re-enter code that destroys the bridge’s owner: the new gtest DestroyingAPZBridgeChild::ReceiveInputEvent calls mManager.Destroy() in the middle of handling the event. Destroying the owning APZCTreeManagerChild drops the only strong reference to its mBridge (APZBridgeChild), so the APZBridge on which ReceiveInputEvent is still executing — and whose return value the caller is about to read — is freed out from under the active call. That is the use-after-free: the raw pointer returned by Bridge() dangles the moment the owner is torn down during synchronous event processing.
The patch makes Bridge() return RefPtr<APZBridge> (IAPZCTreeManager.h, overridden in APZCTreeManager.h and APZCTreeManagerChild.cpp), and gives the abstract APZBridge class refcounting via NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING (plus the nsISupportsImpl.h/mozilla/RefPtr.h includes). Now the caller holds a strong reference across the whole synchronous ReceiveInputEvent call, so even if the owner is destroyed mid-call the bridge survives until the caller releases it. APZBridgeChild::ReceiveInputEvent is hardened in tandem: the hard MOZ_ASSERT(mIsOpen) is replaced by an early if (!mIsOpen) return res;, each SendReceive*InputEvent return value is now checked and returns a default APZEventResult() on failure (the actor having been torn down), and the delayed-callback registration is gated on mIsOpen; ProcessUnhandledEvent and UpdateWheelTransaction likewise early-return instead of asserting when the actor is closed. The advisory (“sandbox escape due to use-after-free in the Widget component”) matches the diff: the UAF is the raw-pointer lifetime bug and the fix is the strong-reference lifetime extension.
Bridge() handed out a borrowed raw pointer to an object whose owner could be destroyed during the very synchronous ReceiveInputEvent call the pointer was used for; returning a RefPtr keeps the bridge alive for the duration of the call.Attack Path
- Compromised content/GPU process an attacker with code execution in a sandboxed process able to drive APZ input delivery (inferred; the UAF is in the UI/parent process which processes these events).
- Dispatch input event
the parent calls
manager->Bridge()->ReceiveInputEvent(event)on the borrowedAPZBridge*. - Re-enter and destroy owner
synchronous processing of the event (IPC send/reply, callback, or actor teardown) causes the owning
APZCTreeManagerChild/manager to be destroyed, releasing the last strong ref to theAPZBridgeChildbridge. - Dangling call
control returns into the now-freed
APZBridge, and the caller reads theAPZEventResultfrom freed memory. - Reclaim/groom the attacker arranges the freed bridge allocation to be reused with controlled data (inferred).
- Primitive use-after-free in the privileged UI/parent process, the basis for the sandbox escape the advisory names (full control inferred).
Impact Assessment
APZBridge during synchronous input processing — a parent-process memory-safety bug, hence the advisory’s sandbox-escape framing. Exploitability to full escape depends on grooming the freed allocation and is (inferred); the diff proves the lifetime bug and its fix, plus defensive mIsOpen/send-failure handling that also prevents use of a closed IPC actor. Mozilla rates it moderate.Changed Functions
| Function | Change | Notes |
|---|---|---|
APZBridgegfx/layers/apz/public/APZBridge.h |
modified | |
ifgfx/layers/ipc/APZBridgeChild.cpp |
modified | |
switchgfx/layers/ipc/APZBridgeChild.cpp |
modified | |
DestroyingAPZBridgeChildgfx/layers/apz/test/gtest/TestAPZBridge.cpp |
modified | |
APZBridgeTestergfx/layers/apz/test/gtest/TestAPZBridge.cpp |
modified | |
TEST_Fgfx/layers/apz/test/gtest/TestAPZBridge.cpp |
modified |
Files Changed
gfx/layers/apz/public/APZBridge.hgfx/layers/apz/public/IAPZCTreeManager.hgfx/layers/apz/src/APZCTreeManager.hgfx/layers/apz/test/gtest/TestAPZBridge.cppgfx/layers/apz/test/gtest/moz.buildgfx/layers/ipc/APZBridgeChild.cppgfx/layers/ipc/APZCTreeManagerChild.cppgfx/layers/ipc/APZCTreeManagerChild.h
Audit Directions
- Raw-pointer accessors to refcounted objectsaudit other APZ/layers accessors that return a bare
T*to a refcounted interface used across synchronous or re-entrant calls (search for*Bridge()-style getters returning.get()). - Owner-destroyed-during-sync-callreview synchronous IPC handlers (
SendReceive*, nested event loops) for code that can destroy the actor/owner mid-call while a borrowed pointer to it remains live. - `mIsOpen` / closed-actor guardscheck sibling
APZBridgeChild/*Childmethods that stillMOZ_ASSERT(mIsOpen)or send without checking the result, where a closed actor could be used after teardown.
Patch
diff --git a/gfx/layers/apz/public/APZBridge.h b/gfx/layers/apz/public/APZBridge.h
index 0d668d34c26..5c27db587cc 100644
--- a/gfx/layers/apz/public/APZBridge.h
+++ b/gfx/layers/apz/public/APZBridge.h
@@ -10,6 +10,7 @@
#include "mozilla/layers/APZPublicUtils.h" // for APZWheelAction, DispatchToContent
#include "mozilla/layers/LayersTypes.h" // for ScrollDirections
#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid
+#include "nsISupportsImpl.h"
namespace mozilla {
@@ -231,6 +232,8 @@ struct APZEventResult {
* APZCTreeManager overrides and implements these methods directly.
*/
class APZBridge {
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
public:
using InputBlockCallback = std::function<void(
uint64_t aInputBlockId, const APZHandledResult& aHandledResult)>;
diff --git a/gfx/layers/apz/public/IAPZCTreeManager.h b/gfx/layers/apz/public/IAPZCTreeManager.h
index 7a6061de38f..512c9af974e 100644
--- a/gfx/layers/apz/public/IAPZCTreeManager.h
+++ b/gfx/layers/apz/public/IAPZCTreeManager.h
@@ -8,6 +8,7 @@
#include <stdint.h> // for uint64_t, uint32_t
#include "Units.h" // for CSSRect, etc
+#include "mozilla/RefPtr.h" // for RefPtr
#include "mozilla/layers/LayersTypes.h" // for TouchBehaviorFlags
#include "mozilla/layers/ScrollableLayerGuid.h" // for ScrollableLayerGuid, etc
#include "mozilla/layers/ZoomConstraints.h" // for ZoomConstraints
@@ -118,12 +119,11 @@ class IAPZCTreeManager {
/**
* Returns an APZBridge interface that can be used to send input
- * events to APZ in a synchronous manner. This will always be non-null, and
- * the returned object's lifetime will match the lifetime of this
- * IAPZCTreeManager implementation.
+ * events to APZ in a synchronous manner. The returned strong reference keeps
+ * the bridge alive if its owner is destroyed during a synchronous call.
* It is only valid to call this function in the UI process.
*/
- virtual APZBridge* Bridge() = 0;
+ virtual RefPtr<APZBridge> Bridge() = 0;
protected:
// Discourage destruction outside of decref
diff --git a/gfx/layers/apz/src/APZCTreeManager.h b/gfx/layers/apz/src/APZCTreeManager.h
index 64ff3e118b8..c9d92950f0f 100644
--- a/gfx/layers/apz/src/APZCTreeManager.h
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -455,7 +455,7 @@ class APZCTreeManager : public IAPZCTreeManager, public APZBridge {
*/
bool ChainHasFastPathApzAwareListener(const ScrollableLayerGuid& aHitGuid);
- APZBridge* Bridge() override { return this; }
+ RefPtr<APZBridge> Bridge() override { return this; }
/**
* Add a callback to be invoked when |aInputBlockId| is ready for handling.
diff --git a/gfx/layers/ipc/APZBridgeChild.cpp b/gfx/layers/ipc/APZBridgeChild.cpp
index 8c5c3135524..39ad6fc6f4d 100644
--- a/gfx/layers/ipc/APZBridgeChild.cpp
+++ b/gfx/layers/ipc/APZBridgeChild.cpp
@@ -104,17 +104,21 @@ void APZBridgeChild::ActorDestroy(ActorDestroyReason aWhy) {
APZEventResult APZBridgeChild::ReceiveInputEvent(
InputData& aEvent, InputBlockCallback&& aCallback) {
- MOZ_ASSERT(mIsOpen);
APZThreadUtils::AssertOnControllerThread();
APZEventResult res;
+ if (!mIsOpen) {
+ return res;
+ }
switch (aEvent.mInputType) {
case MULTITOUCH_INPUT: {
MultiTouchInput& event = aEvent.AsMultiTouchInput();
MultiTouchInput processedEvent;
- SendReceiveMultiTouchInputEvent(event, !!aCallback, &res,
- &processedEvent);
+ if (!SendReceiveMultiTouchInputEvent(event, !!aCallback, &res,
+ &processedEvent)) {
+ return APZEventResult();
+ }
event = std::move(processedEvent);
break;
@@ -123,7 +127,10 @@ APZEventResult APZBridgeChild::ReceiveInputEvent(
MouseInput& event = aEvent.AsMouseInput();
MouseInput processedEvent;
- SendReceiveMouseInputEvent(event, !!aCallback, &res, &processedEvent);
+ if (!SendReceiveMouseInputEvent(event, !!aCallback, &res,
+ &processedEvent)) {
+ return APZEventResult();
+ }
event = std::move(processedEvent);
break;
@@ -132,8 +139,10 @@ APZEventResult APZBridgeChild::ReceiveInputEvent(
PanGestureInput& event = aEvent.AsPanGestureInput();
PanGestureInput processedEvent;
- SendReceivePanGestureInputEvent(event, !!aCallback, &res,
- &processedEvent);
+ if (!SendReceivePanGestureInputEvent(event, !!aCallback, &res,
+ &processedEvent)) {
+ return APZEventResult();
+ }
event = std::move(processedEvent);
break;
@@ -142,8 +151,10 @@ APZEventResult APZBridgeChild::ReceiveInputEvent(
PinchGestureInput& event = aEvent.AsPinchGestureInput();
PinchGestureInput processedEvent;
- SendReceivePinchGestureInputEvent(event, !!aCallback, &res,
- &processedEvent);
+ if (!SendReceivePinchGestureInputEvent(event, !!aCallback, &res,
+ &processedEvent)) {
+ return APZEventResult();
+ }
event = std::move(processedEvent);
break;
@@ -152,8 +163,10 @@ APZEventResult APZBridgeChild::ReceiveInputEvent(
TapGestureInput& event = aEvent.AsTapGestureInput();
TapGestureInput processedEvent;
- SendReceiveTapGestureInputEvent(event, !!aCallback, &res,
- &processedEvent);
+ if (!SendReceiveTapGestureInputEvent(event, !!aCallback, &res,
+ &processedEvent)) {
+ return APZEventResult();
+ }
event = std::move(processedEvent);
break;
@@ -162,8 +175,10 @@ APZEventResult APZBridgeChild::ReceiveInputEvent(
ScrollWheelInput& event = aEvent.AsScrollWheelInput();
ScrollWheelInput processedEvent;
- SendReceiveScrollWheelInputEvent(event, !!aCallback, &res,
- &processedEvent);
+ if (!SendReceiveScrollWheelInputEvent(event, !!aCallback, &res,
+ &processedEvent)) {
+ return APZEventResult();
+ }
event = std::move(processedEvent);
break;
@@ -172,7 +187,10 @@ APZEventResult APZBridgeChild::ReceiveInputEvent(
KeyboardInput& event = aEvent.AsKeyboardInput();
KeyboardInput processedEvent;
- SendReceiveKeyboardInputEvent(event, !!aCallback, &res, &processedEvent);
+ if (!SendReceiveKeyboardInputEvent(event, !!aCallback, &res,
+ &processedEvent)) {
+ return APZEventResult();
+ }
event = std::move(processedEvent);
break;
@@ -184,7 +202,7 @@ APZEventResult APZBridgeChild::ReceiveInputEvent(
}
}
- if (aCallback && res.WillHaveDelayedResult()) {
+ if (mIsOpen && aCallback && res.WillHaveDelayedResult()) {
mInputBlockCallbacks.emplace(res.mInputBlockId, std::move(aCallback));
}
@@ -334,9 +352,12 @@ void APZBridgeChild::ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint,
ScrollableLayerGuid* aOutTargetGuid,
uint64_t* aOutFocusSequenceNumber,
LayersId* aOutLayersId) {
- MOZ_ASSERT(mIsOpen);
APZThreadUtils::AssertOnControllerThread();
+ if (!mIsOpen) {
+ return;
+ }
+
SendProcessUnhandledEvent(*aRefPoint, aRefPoint, aOutTargetGuid,
aOutFocusSequenceNumber, aOutLayersId);
}
@@ -344,9 +365,12 @@ void APZBridgeChild::ProcessUnhandledEvent(LayoutDeviceIntPoint* aRefPoint,
void APZBridgeChild::UpdateWheelTransaction(
LayoutDeviceIntPoint aRefPoint, EventMessage aEventMessage,
const Maybe<ScrollableLayerGuid>& aTargetGuid) {
- MOZ_ASSERT(mIsOpen);
APZThreadUtils::AssertOnControllerThread();
+ if (!mIsOpen) {
+ return;
+ }
+
SendUpdateWheelTransaction(aRefPoint, aEventMessage, aTargetGuid);
}
diff --git a/gfx/layers/ipc/APZCTreeManagerChild.cpp b/gfx/layers/ipc/APZCTreeManagerChild.cpp
index 6d819a68c23..176a62e2782 100644
--- a/gfx/layers/ipc/APZCTreeManagerChild.cpp
+++ b/gfx/layers/ipc/APZCTreeManagerChild.cpp
@@ -97,11 +97,11 @@ void APZCTreeManagerChild::NotifyApzAwareListenerAdded(
}
}
-APZBridge* APZCTreeManagerChild::Bridge() {
+RefPtr<APZBridge> APZCTreeManagerChild::Bridge() {
MOZ_ASSERT(XRE_IsParentProcess());
MOZ_ASSERT(mBridge);
- return mBridge.get();
+ return mBridge;
}
} // namespace layers
diff --git a/gfx/layers/ipc/APZCTreeManagerChild.h b/gfx/layers/ipc/APZCTreeManagerChild.h
index e91dadac4bc..bd47ddcbcbc 100644
--- a/gfx/layers/ipc/APZCTreeManagerChild.h
+++ b/gfx/layers/ipc/APZCTreeManagerChild.h
@@ -52,7 +52,7 @@ class APZCTreeManagerChild final : public IAPZCTreeManager,
void NotifyApzAwareListenerAdded(const ScrollableLayerGuid& aGuid) override;
- APZBridge* Bridge() override;
+ RefPtr<APZBridge> Bridge() override;
protected:
virtual ~APZCTreeManagerChild();
diff --git a/gfx/layers/apz/test/gtest/TestAPZBridge.cpp b/gfx/layers/apz/test/gtest/TestAPZBridge.cpp
new file mode 100644
index 00000000000..bca1ce0a448
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestAPZBridge.cpp
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/layers/APZBridgeChild.h"
+#include "mozilla/layers/APZCTreeManagerChild.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::layers;
+
+namespace {
+
+class DestroyingAPZBridgeChild final : public APZBridgeChild {
+ public:
+ DestroyingAPZBridgeChild(APZCTreeManagerChild& aManager, bool& aIsDestroyed)
+ : APZBridgeChild(0), mManager(aManager), mIsDestroyed(aIsDestroyed) {}
+
+ APZEventResult ReceiveInputEvent(InputData& aEvent,
+ InputBlockCallback&& aCallback) override {
+ bool& isDestroyed = mIsDestroyed;
+ mManager.Destroy();
+ EXPECT_FALSE(isDestroyed);
+ return APZEventResult();
+ }
+
+ private:
+ ~DestroyingAPZBridgeChild() override { mIsDestroyed = true; }
+
+ APZCTreeManagerChild& mManager;
+ bool& mIsDestroyed;
+};
+
+class APZBridgeTester : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ APZThreadUtils::SetControllerThread(NS_GetCurrentThread());
+ }
+
+ void TearDown() override { APZThreadUtils::SetControllerThread(nullptr); }
+};
+
+} // namespace
+
+TEST_F(APZBridgeTester, DestroyOwnerDuringMouseInput) {
+ bool isDestroyed = false;
+ auto manager = MakeRefPtr<APZCTreeManagerChild>();
+ manager->SetBridge(
+ MakeRefPtr<DestroyingAPZBridgeChild>(*manager, isDestroyed));
+
+ WidgetMouseEvent event(true, eMouseMove, nullptr, WidgetMouseEvent::eReal);
+ EXPECT_EQ(manager->Bridge()->ReceiveInputEvent(event).GetStatus(),
+ nsEventStatus_eIgnore);
+ EXPECT_TRUE(isDestroyed);
+}
diff --git a/gfx/layers/apz/test/gtest/moz.build b/gfx/layers/apz/test/gtest/moz.build
index d781ce211e3..e328f36ae0e 100644
--- a/gfx/layers/apz/test/gtest/moz.build
+++ b/gfx/layers/apz/test/gtest/moz.build
@@ -6,6 +6,7 @@ UNIFIED_SOURCES += [
"APZTestAccess.cpp",
"APZTestCommon.cpp",
"MockHitTester.cpp",
+ "TestAPZBridge.cpp",
"TestAxisLock.cpp",
"TestBasic.cpp",
"TestDisplayPort.cpp",
Regression Test / PoC
diff --git a/gfx/layers/apz/test/gtest/TestAPZBridge.cpp b/gfx/layers/apz/test/gtest/TestAPZBridge.cpp
new file mode 100644
index 00000000000..bca1ce0a448
--- /dev/null
+++ b/gfx/layers/apz/test/gtest/TestAPZBridge.cpp
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/layers/APZBridgeChild.h"
+#include "mozilla/layers/APZCTreeManagerChild.h"
+#include "mozilla/layers/APZThreadUtils.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::layers;
+
+namespace {
+
+class DestroyingAPZBridgeChild final : public APZBridgeChild {
+ public:
+ DestroyingAPZBridgeChild(APZCTreeManagerChild& aManager, bool& aIsDestroyed)
+ : APZBridgeChild(0), mManager(aManager), mIsDestroyed(aIsDestroyed) {}
+
+ APZEventResult ReceiveInputEvent(InputData& aEvent,
+ InputBlockCallback&& aCallback) override {
+ bool& isDestroyed = mIsDestroyed;
+ mManager.Destroy();
+ EXPECT_FALSE(isDestroyed);
+ return APZEventResult();
+ }
+
+ private:
+ ~DestroyingAPZBridgeChild() override { mIsDestroyed = true; }
+
+ APZCTreeManagerChild& mManager;
+ bool& mIsDestroyed;
+};
+
+class APZBridgeTester : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ APZThreadUtils::SetControllerThread(NS_GetCurrentThread());
+ }
+
+ void TearDown() override { APZThreadUtils::SetControllerThread(nullptr); }
+};
+
+} // namespace
+
+TEST_F(APZBridgeTester, DestroyOwnerDuringMouseInput) {
+ bool isDestroyed = false;
+ auto manager = MakeRefPtr<APZCTreeManagerChild>();
+ manager->SetBridge(
+ MakeRefPtr<DestroyingAPZBridgeChild>(*manager, isDestroyed));
+
+ WidgetMouseEvent event(true, eMouseMove, nullptr, WidgetMouseEvent::eReal);
+ EXPECT_EQ(manager->Bridge()->ReceiveInputEvent(event).GetStatus(),
+ nsEventStatus_eIgnore);
+ EXPECT_TRUE(isDestroyed);
+}
diff --git a/gfx/layers/apz/test/gtest/moz.build b/gfx/layers/apz/test/gtest/moz.build
index d781ce211e3..e328f36ae0e 100644
--- a/gfx/layers/apz/test/gtest/moz.build
+++ b/gfx/layers/apz/test/gtest/moz.build
@@ -6,6 +6,7 @@ UNIFIED_SOURCES += [
"APZTestAccess.cpp",
"APZTestCommon.cpp",
"MockHitTester.cpp",
+ "TestAPZBridge.cpp",
"TestAxisLock.cpp",
"TestBasic.cpp",
"TestDisplayPort.cpp",