Moderate firefox UAF 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionUse-after-free in the Graphics component
ComponentGraphics
Bug ClassUAF
Tracker2076415
Fix commit067383468b9a (firefox) +33/-17
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

`TextureClient` / `TextureChild`
TextureClient (gfx/layers/client/TextureClient.cpp) is the client-side GPU texture object; TextureChild is its IPDL actor. A TextureChild holds a raw back-pointer mTextureClient, and TextureClient may be destroyed asynchronously with respect to its actor, so this back-pointer behaves like a weak reference.
`AtomicRefCountedWithFinalize`
The atomic refcount base (gfx/layers/AtomicRefCountedWithFinalize.h) for TextureClient; it owns mRefCount and the finalize-on-zero machinery.
`AsTextureClient`
Recovers a strong RefPtr<TextureClient> from a TextureChild actor, used to safely get back to the client from the actor side.
Weak-reference revival / the "AddRef from zero" hazard
If code reads a raw back-pointer and does a plain RefPtr assignment (which AddRefs) after the refcount has already reached zero (object being finalized/destroyed), it “revives” a dead object — a use-after-free.
ImageBridge vs non-ImageBridge textures / `mUsesImageBridge`
Some textures are driven over the ImageBridge; the actor lock is only meaningful/needed for ImageBridge textures. mUsesImageBridge records this per actor.

Root Cause Analysis

AsTextureClient must recover a strong reference to the TextureClient from its actor under the actor lock, because the client can be destroyed asynchronously. Pre-fix it did:

RefPtr<TextureClient> texture = tc->mTextureClient;

This plain assignment unconditionally AddRefs whatever mTextureClient points at. But mTextureClient is effectively a weak back-pointer: the TextureClient’s refcount may have already dropped to zero and its destruction be in progress (just not yet disconnected from the actor), in which case AddRef’ing it revives a dead/freed object — a use-after-free. The patch introduces AtomicRefCountedWithFinalize::TryAddRef(), which performs a CAS loop that only increments the count while it is strictly positive:

int currCount = mRefCount;
while (currCount > 0) {
  if (mRefCount.compareExchange(currCount, currCount + 1)) { ... return already_AddRefed<T>(...); }
  currCount = mRefCount;
}
return nullptr;

If the count has reached zero, TryAddRef returns null instead of reviving the object. AsTextureClient now calls tc->mTextureClient->TryAddRef() and only yields a texture if the object was still live, closing the revive-a-dead-object UAF.

The diff also fixes the locking that protects this. The actor lock was only taken for ImageBridge textures (Lock()/Unlock() guarded by mUsesImageBridge), yet LockActor/UnlockActor previously locked unconditionally and Destroy() used a condition (mActor && !mIsLocked) that didn’t account for non-ImageBridge textures where mIsLocked doesn’t imply the actor lock is held. The patch makes TextureChild::Lock()/Unlock() take the lock unconditionally, makes LockActor()/UnlockActor() only act for mUsesImageBridge actors, records mUsesImageBridge in both InitIPDLActor overloads, and adjusts Destroy()’s condition to (!mIsLocked || !mActor->mUsesImageBridge) so the actor lock is actually held while AsTextureClient runs for non-ImageBridge textures. Without a consistently-held lock, AsTextureClient could read mTextureClient racing the client’s destruction — the race that made the plain AddRef dangerous. The advisory’s “use-after-free in Graphics” matches: the diff substantiates both the revival UAF (fixed by TryAddRef) and the lock gap that exposed the race.

Key insight
AsTextureClient AddRef’d the actor’s weak mTextureClient back-pointer with a plain RefPtr assignment, which could revive a TextureClient whose refcount had already hit zero (a use-after-free), compounded by the actor lock not actually being held for non-ImageBridge textures; the fix adds a count-greater-than-zero TryAddRef CAS and tightens the locking.

Attack Path

  1. Allocate textures from content Web content that drives GPU-backed rendering (canvas/WebGL/video) causes TextureClient/TextureChild pairs to be created (web-reachable graphics path; precise trigger inferred).
  2. Race destruction against recovery Arrange for a TextureClient to begin asynchronous destruction (refcount reaching zero) on one thread while another path calls AsTextureClient on its actor.
  3. Read the weak back-pointer at zero Pre-fix, AsTextureClient reads tc->mTextureClient and plain-assigns it into a RefPtr, AddRef’ing an object whose count is already zero / being finalized.
  4. Revive the dead object The AddRef resurrects the dying/freed TextureClient, and the returned RefPtr is used afterward.
  5. Use freed/revived texture Subsequent use of the revived TextureClient touches freed or inconsistently-finalized memory.
  6. Primitive achieved Use-after-free of a TextureClient in the graphics/compositor path (controlled reuse and escalation inferred, not shown by the diff).

Impact Assessment

The affected code is in the graphics/compositor client layer, exercised from content-process rendering and across the texture threads (ImageBridge/compositor), so the race straddles threads. The attacker needs to drive GPU texture lifetimes and win a destruction-vs-recovery race; exact web-triggerability and the ability to groom the revived allocation are inferred. The demonstrated primitive is a use-after-free from reviving a zero-refcount object plus the lock gap that permitted the race; the diff proves the lifetime/locking defects but not a full exploit, so escalation to code execution is inferred. Mozilla rates it moderate.

Changed Functions

FunctionChangeNotes
while
gfx/layers/AtomicRefCountedWithFinalize.h
modified
if
gfx/layers/client/TextureClient.cpp
modified

Files Changed

  • gfx/layers/AtomicRefCountedWithFinalize.h
  • gfx/layers/client/TextureClient.cpp

Audit Directions

  • Weak back-pointers AddRef'd directly
    Audit other graphics actors (and IPDL actors generally) that keep a raw back-pointer to a refcounted object and recover it with a plain RefPtr assignment; these can revive zero-count objects and should use a TryAddRef-style CAS.
  • `AtomicRefCountedWithFinalize` consumers
    Review all users of this base for recovery paths that assume the object is still live without checking the count.
  • ImageBridge locking assumptions
    Check other mUsesImageBridge-gated lock/unlock pairs for mismatches between where the lock is taken and where callers assume it is held (the Destroy/LockActor inconsistency fixed here).
diff --git a/gfx/layers/AtomicRefCountedWithFinalize.h b/gfx/layers/AtomicRefCountedWithFinalize.h
index 95764564c9d..8175a3c4957 100644
--- a/gfx/layers/AtomicRefCountedWithFinalize.h
+++ b/gfx/layers/AtomicRefCountedWithFinalize.h
@@ -55,6 +55,21 @@ class AtomicRefCountedWithFinalize {
     }
   }
 
+  already_AddRefed<T> TryAddRef() {
+    // A weak reference must not revive an object after its count reaches zero.
+    int currCount = mRefCount;
+    while (currCount > 0) {
+      if (mRefCount.compareExchange(currCount, currCount + 1)) {
+#ifdef NS_BUILD_REFCNT_LOGGING
+        NS_LOG_ADDREF(this, currCount + 1, mName, sizeof(*this));
+#endif
+        return already_AddRefed<T>(static_cast<T*>(this));
+      }
+      currCount = mRefCount;
+    }
+    return nullptr;
+  }
+
  public:
   // Mark user classes that are considered flawless.
   template <class U>
diff --git a/gfx/layers/client/TextureClient.cpp b/gfx/layers/client/TextureClient.cpp
index 0fc488f9674..05ef4319080 100644
--- a/gfx/layers/client/TextureClient.cpp
+++ b/gfx/layers/client/TextureClient.cpp
@@ -145,17 +145,9 @@ class TextureChild final : public PTextureChild {
 
   void ActorDestroy(ActorDestroyReason why) override;
 
-  void Lock() const {
-    if (mUsesImageBridge) {
-      mLock.Enter();
-    }
-  }
+  void Lock() const { mLock.Enter(); }
 
-  void Unlock() const {
-    if (mUsesImageBridge) {
-      mLock.Leave();
-    }
-  }
+  void Unlock() const { mLock.Leave(); }
 
  private:
   /// The normal way to destroy the actor.
@@ -493,7 +485,11 @@ void TextureClient::Destroy() {
   // Async paints should have been flushed by now.
   MOZ_RELEASE_ASSERT(mPaintThreadRefs == 0);
 
-  if (mActor && !mIsLocked) {
+  // LockActor() only acquires the actor lock for ImageBridge textures.
+  // If an ImageBridge texture is already locked, reuse that lock; this assumes
+  // Destroy() is called on the thread that acquired it. For other textures,
+  // mIsLocked does not imply that the actor lock is held, so acquire it here.
+  if (mActor && (!mIsLocked || !mActor->mUsesImageBridge)) {
     mActor->Lock();
   }
 
@@ -544,13 +540,13 @@ void TextureClient::Destroy() {
 }
 
 void TextureClient::LockActor() const {
-  if (mActor) {
+  if (mActor && mActor->mUsesImageBridge) {
     mActor->Lock();
   }
 }
 
 void TextureClient::UnlockActor() const {
-  if (mActor) {
+  if (mActor && mActor->mUsesImageBridge) {
     mActor->Unlock();
   }
 }
@@ -1024,15 +1020,18 @@ already_AddRefed<TextureClient> TextureClient::AsTextureClient(
   tc->Lock();
 
   // Since TextureClient may be destroyed asynchronously with respect to its
-  // IPDL actor, we must acquire a reference within a lock. The mDestroyed bit
-  // tells us whether or not the main thread has disconnected the TextureClient
-  // from its actor.
+  // IPDL actor, we must acquire a live reference within the actor's lock. The
+  // mDestroyed bit tells us whether the TextureClient has disconnected from
+  // its actor.
   if (tc->mDestroyed) {
     tc->Unlock();
     return nullptr;
   }
 
-  RefPtr<TextureClient> texture = tc->mTextureClient;
+  RefPtr<TextureClient> texture;
+  if (tc->mTextureClient) {
+    texture = tc->mTextureClient->TryAddRef();
+  }
   tc->Unlock();
 
   return texture.forget();
@@ -1197,6 +1196,7 @@ bool TextureClient::InitIPDLActor(CompositableForwarder* aForwarder) {
   mActor = actor.forget().downcast<TextureChild>();
   mActor->mCompositableForwarder = aForwarder;
   mActor->mTextureForwarder = textureFwd;
+  mActor->mUsesImageBridge = textureFwd->UsesImageBridge();
   mActor->mTextureClient = this;
 
   // If the TextureClient is already locked, we have to lock TextureChild's
@@ -1265,6 +1265,7 @@ bool TextureClient::InitIPDLActor(KnowsCompositor* aKnowsCompositor,
 
   mActor = actor.forget().downcast<TextureChild>();
   mActor->mTextureForwarder = textureFwd;
+  mActor->mUsesImageBridge = textureFwd->UsesImageBridge();
   mActor->mTextureClient = this;
 
   // If the TextureClient is already locked, we have to lock TextureChild's
Loading diff…