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
Tracker2079311
Fix commitefb071ae86a6 (firefox) +97/-39
CISA KEVNot listed
CreditedNguyen Tuan Hung
Disclosed2026-10-27

Background

`BufferTextureData` / `ShmemTextureData`
Texture-data backends in gfx/layers. ShmemTextureData keeps its pixel buffer in an IPC shared-memory segment (ipc::Shmem).
`ShmemHolder`
A refcounted object that owns the ipc::Shmem and, on destruction, deallocates it via the IPC allocator. Formerly a nested class of ShmemTextureData; the patch promotes it to a standalone, independently-retainable class.
`LayersIPCChannel` (`mAllocator`)
The IPC allocator used to DeallocShmem. It is only safe to call while IPC is open and on its own thread (GetThread()).
`TextureClient`
Client-side texture handle. BorrowMappedYCbCrData hands out raw pointers into the mapped buffer. Destroy() can detach the underlying mData.
`SharedPlanarYCbCrImage`
An Image that caches the Y/Cb/Cr plane pointers obtained from a TextureClient’s mapped data and uses them later, possibly outside the lock/unlock window.
ImageBridge shutdown
Can explicitly destroy a TextureClient while an Image still holds its borrowed plane pointers.

Root Cause Analysis

SharedPlanarYCbCrImage::CreateEmptyBuffer (gfx/layers/ipc/SharedPlanarYCbCrImage.cpp) borrowed the mapped Y/Cb/Cr plane pointers out of mTextureClient and retained them in the Image, relying — as the in-code comment admits — on the BufferTextureData staying mapped “even outside of the lock/unlock interval.” That assumption breaks for shmem-backed textures: if ImageBridge shutdown explicitly destroys the TextureClient, the ShmemTextureData and its ShmemHolder are torn down and the shmem mapping is released, leaving the Image’s cached plane pointers dangling — a use-after-free of the shared-memory pixel buffer. The lifetime of the mapping was tied to the TextureClient, not to the borrowed pointers that outlived it.

The patch decouples the mapping’s lifetime from the TextureClient. ShmemHolder is promoted to a standalone threadsafe-refcounted class and exposed via BufferTextureData::GetShmemHolder(). A new overload TextureClient::BorrowMappedYCbCrData(MappedYCbCrTextureData&, RefPtr<ShmemHolder>&) acquires the mapping and its owning ShmemHolder together under mMutex, so callers cannot get borrowed pointers without their keep-alive. SharedPlanarYCbCrImage now stores a RefPtr<ShmemHolder> mShmemHolder (declared to outlive mTextureClient), so the shmem mapping survives even if the TextureClient is destroyed. Separately, ShmemHolder now captures its deallocation mDeallocationTarget (aAllocator->GetThread()) at construction and defers DeallocShmem to a runnable that only runs while IPCOpen(), because the Image (and thus the holder) may now outlive the allocator thread; this replaces the old ~ShmemHolder() that called mAllocator->DeallocShmem directly and the NS_ProxyRelease-based DeleteShmemHolderUserData.

Key insight
SharedPlanarYCbCrImage cached raw plane pointers into a shmem buffer whose lifetime was bound to the TextureClient, not to the borrowed pointers; destroying the TextureClient at ImageBridge shutdown freed the mapping out from under the still-referenced pointers.

Attack Path

  1. Produce a YCbCr image Web content causes creation of a SharedPlanarYCbCrImage backed by a shmem ShmemTextureData (e.g. decoded video/ImageBitmap frames routed through ImageBridge). (reachability inferred)
  2. Cache borrowed pointers CreateEmptyBuffer borrows the plane pointers and keeps them in the Image beyond the lock/unlock window.
  3. Force TextureClient destruction Trigger ImageBridge shutdown (or other explicit TextureClient::Destroy()) while the Image is still alive and holding those pointers.
  4. Mapping freed Destroying the TextureClient tears down ShmemTextureData/ShmemHolder, releasing the shmem mapping.
  5. Dangling access Later use of the Image’s cached plane pointers reads/writes freed shared memory — a use-after-free.
  6. Primitive UAF of the pixel buffer in the content/compositor process; with grooming of the freed mapping this is an exploitable read/write primitive rather than a mere crash. (exploit strength inferred)

Impact Assessment

The affected code is in the layers/ImageBridge client path (content/compositor process); no elevated privilege is needed beyond generating YCbCr image content. The flaw is a use-after-free of a shared-memory-backed pixel buffer when the TextureClient is destroyed while an Image still holds borrowed plane pointers. Exploitation is gated on winning the shutdown/teardown race and grooming the freed shmem mapping. The advisory rates it moderate; the diff proves the UAF but does not itself demonstrate cross-process escalation.

Changed Functions

FunctionChangeNotes
if
gfx/layers/BufferTexture.cpp
modified
MemoryTextureData
gfx/layers/BufferTexture.cpp
modified
ShmemHolder
gfx/layers/BufferTexture.cpp
modified
ShmemHolder
gfx/layers/BufferTexture.h
modified
BufferTextureData
gfx/layers/BufferTexture.h
modified
PTextureChild
gfx/layers/client/TextureClient.h
modified
TextureChild
gfx/layers/client/TextureClient.h
modified
TextureData
gfx/layers/client/TextureClient.h
modified
ShmemHolder
gfx/layers/client/TextureClient.h
modified
GPUVideoTextureData
gfx/layers/client/TextureClient.h
modified
TextureClient
gfx/layers/client/TextureClient.h
modified
ITextureClientRecycleAllocator
gfx/layers/client/TextureClient.h
modified
ImageClient
gfx/layers/ipc/SharedPlanarYCbCrImage.h
modified
ShmemHolder
gfx/layers/ipc/SharedPlanarYCbCrImage.h
modified
TextureClient
gfx/layers/ipc/SharedPlanarYCbCrImage.h
modified
TextureClientRecycleAllocator
gfx/layers/ipc/SharedPlanarYCbCrImage.h
modified

Files Changed

  • gfx/layers/BufferTexture.cpp
  • gfx/layers/BufferTexture.h
  • gfx/layers/client/TextureClient.cpp
  • gfx/layers/client/TextureClient.h
  • gfx/layers/ipc/SharedPlanarYCbCrImage.cpp
  • gfx/layers/ipc/SharedPlanarYCbCrImage.h

Audit Directions

  • Borrowed-pointer caching
    Audit every caller of BorrowMappedData/BorrowMappedYCbCrData (and similar Borrow*) that retains the returned pointers past Unlock()/the lock scope without retaining a keep-alive for the backing storage.
  • TextureClient destruction vs. Image lifetime
    Review ImageBridge/compositable shutdown paths for other Image types that hold state borrowed from a TextureClient that shutdown can destroy explicitly.
  • Shmem deallocation threading
    Check other Shmem-owning objects for destructors that call DeallocShmem directly on an arbitrary thread or after the allocator thread/IPC may have shut down.
diff --git a/gfx/layers/BufferTexture.cpp b/gfx/layers/BufferTexture.cpp
index 0ff1518adc6..b9423a90ddd 100644
--- a/gfx/layers/BufferTexture.cpp
+++ b/gfx/layers/BufferTexture.cpp
@@ -15,7 +15,7 @@
 #include "mozilla/layers/ISurfaceAllocator.h"
 #include "mozilla/layers/ImageDataSerializer.h"
 #include "mozilla/layers/TextureForwarder.h"
-#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
 
 #ifdef MOZ_WIDGET_GTK
 #  include "gfxPlatformGtk.h"
@@ -26,6 +26,41 @@ using mozilla::ipc::IShmemAllocator;
 namespace mozilla {
 namespace layers {
 
+ShmemHolder::ShmemHolder(LayersIPCChannel* aAllocator, ipc::Shmem aShmem)
+    : mAllocator(aAllocator),
+      mDeallocationTarget(aAllocator->GetThread()),
+      mShmem(std::move(aShmem)) {}
+
+ShmemHolder::~ShmemHolder() {
+  if (!mDeallocShmem) {
+    return;
+  }
+
+  // The Image may outlive both its TextureClient and the allocator thread.
+  // Only touch the IPC shmem registry on the allocator thread while IPC is
+  // open. Otherwise dropping mShmem releases our local mapping; IPC teardown
+  // releases the registry's reference independently.
+  // Use the target retained at construction rather than querying the
+  // allocator after its thread may have been shut down.
+  if (!mDeallocationTarget) {
+    return;
+  }
+
+  RefPtr<Runnable> task = NS_NewRunnableFunction(
+      "ShmemHolder::DeallocShmem",
+      [allocator = mAllocator, shmem = mShmem]() mutable {
+        if (allocator->IPCOpen()) {
+          allocator->DeallocShmem(shmem);
+        }
+      });
+  if (mDeallocationTarget->IsOnCurrentThread()) {
+    task->Run();
+  } else {
+    // If dispatch fails, destroying the task still releases its local mapping.
+    (void)mDeallocationTarget->Dispatch(task.forget());
+  }
+}
+
 class MemoryTextureData : public BufferTextureData {
  public:
   static MemoryTextureData* Create(
@@ -92,28 +127,6 @@ class ShmemTextureData : public BufferTextureData {
 
   virtual void Deallocate(LayersIPCChannel* aAllocator) override;
 
-  class ShmemHolder final {
-   public:
-    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ShmemHolder)
-
-    ShmemHolder(LayersIPCChannel* aAllocator, mozilla::ipc::Shmem aShmem)
-        : mAllocator(aAllocator), mShmem(std::move(aShmem)) {}
-    mozilla::ipc::Shmem& GetShmem() { return mShmem; }
-    void SetDeallocShmem() { mDeallocShmem = true; }
-
-    const RefPtr<LayersIPCChannel> mAllocator;
-
-   protected:
-    ~ShmemHolder() {
-      if (mDeallocShmem) {
-        mAllocator->DeallocShmem(mShmem);
-      }
-    }
-
-    mozilla::ipc::Shmem mShmem;
-    mozilla::Atomic<bool> mDeallocShmem{false};
-  };
-
   ShmemTextureData(const BufferDescriptor& aDesc,
                    gfx::BackendType aMoz2DBackend,
                    RefPtr<ShmemHolder>& aShmemHolder, bool aIsClear = false)
@@ -133,6 +146,8 @@ class ShmemTextureData : public BufferTextureData {
 
   virtual void OnBorrowDrawTarget(gfx::DrawTarget* aDrawTarget) override;
 
+  ShmemHolder* GetShmemHolder() override { return mShmemHolder; }
+
  protected:
   const RefPtr<ShmemHolder> mShmemHolder;
 };
@@ -175,8 +190,7 @@ BufferTextureData* BufferTextureData::CreateInternal(
 
     bool isClear = aDesc.type() == BufferDescriptor::TRGBDescriptor &&
                    !IsOpaque(aDesc.get_RGBDescriptor().format());
-    RefPtr shmemHolder =
-        MakeRefPtr<ShmemTextureData::ShmemHolder>(aAllocator, shm);
+    RefPtr shmemHolder = MakeRefPtr<ShmemHolder>(aAllocator, shm);
     return new ShmemTextureData(aDesc, aMoz2DBackend, shmemHolder, isClear);
   }
 }
@@ -283,21 +297,14 @@ gfx::SurfaceFormat BufferTextureData::GetFormat() const {
 }
 
 struct ShmemHolderUserData {
-  explicit ShmemHolderUserData(ShmemTextureData::ShmemHolder* aShmemHolder)
+  explicit ShmemHolderUserData(ShmemHolder* aShmemHolder)
       : mShmemHolder(aShmemHolder) {}
   ~ShmemHolderUserData() = default;
-  RefPtr<ShmemTextureData::ShmemHolder> mShmemHolder;
+  RefPtr<ShmemHolder> mShmemHolder;
 };
 
 void DeleteShmemHolderUserData(void* aClosure) {
   ShmemHolderUserData* data = reinterpret_cast<ShmemHolderUserData*>(aClosure);
-  nsCOMPtr<nsIEventTarget> eventTarget =
-      data->mShmemHolder->mAllocator->GetThread();
-
-  // Do not release the holder on the wrong thread if dispatch fails.
-  NS_ProxyRelease("DeleteShmemHolderUserData", eventTarget,
-                  data->mShmemHolder.forget());
-
   delete data;
 }
 
@@ -613,8 +620,7 @@ ShmemTextureData* ShmemTextureData::Create(
   BufferDescriptor descriptor =
       RGBDescriptor(aSize, aFormat, aColorSpace, aTransferFunction);
   bool isClear = (aAllocFlags & ALLOC_CLEAR_BUFFER) || !IsOpaque(aFormat);
-  RefPtr shmemHolder =
-      MakeRefPtr<ShmemTextureData::ShmemHolder>(aAllocator, shm);
+  RefPtr shmemHolder = MakeRefPtr<ShmemHolder>(aAllocator, shm);
   return new ShmemTextureData(descriptor, aMoz2DBackend, shmemHolder, isClear);
 }
 
diff --git a/gfx/layers/BufferTexture.h b/gfx/layers/BufferTexture.h
index 6ecd93f7679..d272bdf7875 100644
--- a/gfx/layers/BufferTexture.h
+++ b/gfx/layers/BufferTexture.h
@@ -8,10 +8,33 @@
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/Types.h"
 #include "mozilla/layers/TextureClient.h"
+#include "nsCOMPtr.h"
+#include "nsISerialEventTarget.h"
 
 namespace mozilla {
 namespace layers {
 
+// Keeps a shmem mapping alive independently of TextureClient and IPC teardown.
+class ShmemHolder final {
+ public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ShmemHolder)
+
+  ShmemHolder(LayersIPCChannel* aAllocator, ipc::Shmem aShmem);
+  ipc::Shmem& GetShmem() { return mShmem; }
+  void SetDeallocShmem() { mDeallocShmem = true; }
+
+  const RefPtr<LayersIPCChannel> mAllocator;
+
+ private:
+  ~ShmemHolder();
+
+  // Capture while the allocator is usable. GetThread() need not be safe to
+  // call from an arbitrary thread during allocator shutdown.
+  const nsCOMPtr<nsISerialEventTarget> mDeallocationTarget;
+  ipc::Shmem mShmem;
+  Atomic<bool> mDeallocShmem{false};
+};
+
 class BufferTextureData : public TextureData {
  public:
   // ShmemAllocator needs to implement IShmemAllocator and IsSameProcess,
@@ -83,6 +106,8 @@ class BufferTextureData : public TextureData {
 
   virtual size_t GetBufferSize() = 0;
 
+  virtual ShmemHolder* GetShmemHolder() { return nullptr; }
+
   virtual void OnBorrowDrawTarget(gfx::DrawTarget* aDrawTarget) {}
 
  protected:
diff --git a/gfx/layers/client/TextureClient.cpp b/gfx/layers/client/TextureClient.cpp
index 05ef4319080..0119add93e6 100644
--- a/gfx/layers/client/TextureClient.cpp
+++ b/gfx/layers/client/TextureClient.cpp
@@ -998,6 +998,22 @@ bool TextureClient::BorrowMappedYCbCrData(MappedYCbCrTextureData& aMap) {
   return mData ? mData->BorrowMappedYCbCrData(aMap) : false;
 }
 
+bool TextureClient::BorrowMappedYCbCrData(MappedYCbCrTextureData& aMap,
+                                          RefPtr<ShmemHolder>& aShmemHolder) {
+  MOZ_ASSERT(IsLocked());
+  // Acquire the mapping and its owner together, before Destroy() can detach
+  // mData. Do not return borrowed shmem pointers without their keep-alive.
+  MutexAutoLock lock(mMutex);
+  aShmemHolder = nullptr;
+  if (!mData || !mData->BorrowMappedYCbCrData(aMap)) {
+    return false;
+  }
+  if (auto* bufferData = mData->AsBufferTextureData()) {
+    aShmemHolder = bufferData->GetShmemHolder();
+  }
+  return true;
+}
+
 bool TextureClient::ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) {
   MutexAutoLock lock(mMutex);
   return mData ? mData->Serialize(aOutDescriptor) : false;
diff --git a/gfx/layers/client/TextureClient.h b/gfx/layers/client/TextureClient.h
index 747296d3ba0..3e857fa1ea6 100644
--- a/gfx/layers/client/TextureClient.h
+++ b/gfx/layers/client/TextureClient.h
@@ -56,6 +56,7 @@ class Image;
 class PTextureChild;
 class TextureChild;
 class TextureData;
+class ShmemHolder;
 class GPUVideoTextureData;
 class TextureClient;
 class ITextureClientRecycleAllocator;
@@ -478,6 +479,11 @@ class TextureClient : public AtomicRefCountedWithFinalize<TextureClient> {
   bool BorrowMappedData(MappedTextureData&);
   bool BorrowMappedYCbCrData(MappedYCbCrTextureData&);
 
+  // Also retain the shmem mapping when keeping plane pointers beyond Unlock().
+  // aShmemHolder is null for textures which do not use shmem.
+  bool BorrowMappedYCbCrData(MappedYCbCrTextureData& aMap,
+                             RefPtr<ShmemHolder>& aShmemHolder);
+
   /**
    * This function can be used to update the contents of the TextureClient
    * off the main thread.
diff --git a/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp b/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp
index f081163bd79..de37d4b416e 100644
--- a/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp
+++ b/gfx/layers/ipc/SharedPlanarYCbCrImage.cpp
@@ -150,10 +150,11 @@ nsresult SharedPlanarYCbCrImage::CreateEmptyBuffer(
   // The locking here is sort of a lie. The SharedPlanarYCbCrImage just pulls
   // pointers out of the TextureClient and keeps them around, which works only
   // because the underlyin BufferTextureData is always mapped in memory even
-  // outside of the lock/unlock interval. That's sad and new code should follow
-  // this example.
+  // outside of the lock/unlock interval. For shmem, mShmemHolder also keeps the
+  // mapping alive if ImageBridge shutdown destroys the TextureClient. New code
+  // should not retain borrowed pointers without retaining their storage.
   if (!mTextureClient->Lock(OpenMode::OPEN_READ) ||
-      !mTextureClient->BorrowMappedYCbCrData(mapped)) {
+      !mTextureClient->BorrowMappedYCbCrData(mapped, mShmemHolder)) {
     MOZ_CRASH("GFX: Cannot lock or borrow mapped YCbCr");
   }
 
diff --git a/gfx/layers/ipc/SharedPlanarYCbCrImage.h b/gfx/layers/ipc/SharedPlanarYCbCrImage.h
index 4c56cf6ae7a..2beb84ed04d 100644
--- a/gfx/layers/ipc/SharedPlanarYCbCrImage.h
+++ b/gfx/layers/ipc/SharedPlanarYCbCrImage.h
@@ -21,6 +21,7 @@ namespace mozilla {
 namespace layers {
 
 class ImageClient;
+class ShmemHolder;
 class TextureClient;
 class TextureClientRecycleAllocator;
 
@@ -54,6 +55,9 @@ class SharedPlanarYCbCrImage : public PlanarYCbCrImage {
   TextureClientRecycleAllocator* RecycleAllocator();
 
  private:
+  // Outlives mTextureClient so cached plane pointers retain their mapping even
+  // when ImageBridge shutdown destroys the TextureClient explicitly.
+  RefPtr<ShmemHolder> mShmemHolder;
   RefPtr<TextureClient> mTextureClient;
   RefPtr<ImageClient> mCompositable;
   RefPtr<TextureClientRecycleAllocator> mRecycleAllocator;
Loading diff…