Moderate firefox UAF 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSandbox escape due to use-after-free in the Widget component
ComponentGraphics
Bug ClassUAF
Tracker2081560
Fix commit81587730f71f (firefox) +110/-24
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

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`
IAPZCTreeManager is the APZ tree-manager interface; APZBridge is the synchronous input-delivery interface. APZBridge::ReceiveInputEvent processes an input event synchronously and returns an APZEventResult.
`IAPZCTreeManager::Bridge()`
returns the APZBridge to deliver input through. Implementations are APZCTreeManager (in-process, returns this) and APZCTreeManagerChild (out-of-process, returns its mBridge, an APZBridgeChild).
`APZBridgeChild`
the child-side IPC actor that forwards input events to the GPU/compositor process over PAPZBridge; it has an mIsOpen flag and synchronous SendReceive* IPC sends.
Raw-pointer vs `RefPtr` ownership
the pre-patch Bridge() returned a raw APZBridge* whose lifetime was documented to “match the lifetime of this IAPZCTreeManager implementation”; a RefPtr<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.

Key insight
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

  1. 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).
  2. Dispatch input event the parent calls manager->Bridge()->ReceiveInputEvent(event) on the borrowed APZBridge*.
  3. 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 the APZBridgeChild bridge.
  4. Dangling call control returns into the now-freed APZBridge, and the caller reads the APZEventResult from freed memory.
  5. Reclaim/groom the attacker arranges the freed bridge allocation to be reused with controlled data (inferred).
  6. Primitive use-after-free in the privileged UI/parent process, the basis for the sandbox escape the advisory names (full control inferred).

Impact Assessment

The vulnerable object lives in the UI/parent (chrome-privileged) process, which is where APZ input is handled; a sandboxed content or GPU process that can trigger input delivery and provoke owner destruction mid-call can reach it. The primitive is a use-after-free of the 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

FunctionChangeNotes
APZBridge
gfx/layers/apz/public/APZBridge.h
modified
if
gfx/layers/ipc/APZBridgeChild.cpp
modified
switch
gfx/layers/ipc/APZBridgeChild.cpp
modified
DestroyingAPZBridgeChild
gfx/layers/apz/test/gtest/TestAPZBridge.cpp
modified
APZBridgeTester
gfx/layers/apz/test/gtest/TestAPZBridge.cpp
modified
TEST_F
gfx/layers/apz/test/gtest/TestAPZBridge.cpp
modified

Files Changed

  • gfx/layers/apz/public/APZBridge.h
  • gfx/layers/apz/public/IAPZCTreeManager.h
  • gfx/layers/apz/src/APZCTreeManager.h
  • gfx/layers/apz/test/gtest/TestAPZBridge.cpp
  • gfx/layers/apz/test/gtest/moz.build
  • gfx/layers/ipc/APZBridgeChild.cpp
  • gfx/layers/ipc/APZCTreeManagerChild.cpp
  • gfx/layers/ipc/APZCTreeManagerChild.h

Audit Directions

  • Raw-pointer accessors to refcounted objects
    audit 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-call
    review 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 guards
    check sibling APZBridgeChild/*Child methods that still MOZ_ASSERT(mIsOpen) or send without checking the result, where a closed actor could be used after teardown.
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",
Loading diff…

Regression Test / PoC

shipped with the fix
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",
Loading diff…