CVE-2026-117972
Overview
Background
- `TextureClient` / `TextureChild`
TextureClient(gfx/layers/client/TextureClient.cpp) is the client-side GPU texture object;TextureChildis its IPDL actor. ATextureChildholds a raw back-pointermTextureClient, andTextureClientmay 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) forTextureClient; it ownsmRefCountand the finalize-on-zero machinery. - `AsTextureClient`
- Recovers a strong
RefPtr<TextureClient>from aTextureChildactor, 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
RefPtrassignment (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.
mUsesImageBridgerecords 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.
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
- Allocate textures from content
Web content that drives GPU-backed rendering (canvas/WebGL/video) causes
TextureClient/TextureChildpairs to be created (web-reachable graphics path; precise trigger inferred). - Race destruction against recovery
Arrange for a
TextureClientto begin asynchronous destruction (refcount reaching zero) on one thread while another path callsAsTextureClienton its actor. - Read the weak back-pointer at zero
Pre-fix,
AsTextureClientreadstc->mTextureClientand plain-assigns it into aRefPtr, AddRef’ing an object whose count is already zero / being finalized. - Revive the dead object
The AddRef resurrects the dying/freed
TextureClient, and the returnedRefPtris used afterward. - Use freed/revived texture
Subsequent use of the revived
TextureClienttouches freed or inconsistently-finalized memory. - Primitive achieved
Use-after-free of a
TextureClientin the graphics/compositor path (controlled reuse and escalation inferred, not shown by the diff).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
whilegfx/layers/AtomicRefCountedWithFinalize.h |
modified | |
ifgfx/layers/client/TextureClient.cpp |
modified |
Files Changed
gfx/layers/AtomicRefCountedWithFinalize.hgfx/layers/client/TextureClient.cpp
Audit Directions
- Weak back-pointers AddRef'd directlyAudit other graphics actors (and IPDL actors generally) that keep a raw back-pointer to a refcounted object and recover it with a plain
RefPtrassignment; these can revive zero-count objects and should use aTryAddRef-style CAS. - `AtomicRefCountedWithFinalize` consumersReview all users of this base for recovery paths that assume the object is still live without checking the count.
- ImageBridge locking assumptionsCheck other
mUsesImageBridge-gated lock/unlock pairs for mismatches between where the lock is taken and where callers assume it is held (theDestroy/LockActorinconsistency fixed here).
Patch
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