CVE-2026-117989
Overview
Background
- `BufferTextureData` / `ShmemTextureData`
- Texture-data backends in gfx/layers.
ShmemTextureDatakeeps its pixel buffer in an IPC shared-memory segment (ipc::Shmem). - `ShmemHolder`
- A refcounted object that owns the
ipc::Shmemand, on destruction, deallocates it via the IPC allocator. Formerly a nested class ofShmemTextureData; 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.
BorrowMappedYCbCrDatahands out raw pointers into the mapped buffer.Destroy()can detach the underlyingmData. - `SharedPlanarYCbCrImage`
- An
Imagethat caches the Y/Cb/Cr plane pointers obtained from aTextureClient’s mapped data and uses them later, possibly outside the lock/unlock window. - ImageBridge shutdown
- Can explicitly destroy a
TextureClientwhile anImagestill 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.
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
- Produce a YCbCr image
Web content causes creation of a
SharedPlanarYCbCrImagebacked by a shmemShmemTextureData(e.g. decoded video/ImageBitmapframes routed through ImageBridge). (reachability inferred) - Cache borrowed pointers
CreateEmptyBufferborrows the plane pointers and keeps them in theImagebeyond the lock/unlock window. - Force TextureClient destruction
Trigger ImageBridge shutdown (or other explicit
TextureClient::Destroy()) while theImageis still alive and holding those pointers. - Mapping freed
Destroying the
TextureClienttears downShmemTextureData/ShmemHolder, releasing the shmem mapping. - Dangling access
Later use of the
Image’s cached plane pointers reads/writes freed shared memory — a use-after-free. - 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
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
| Function | Change | Notes |
|---|---|---|
ifgfx/layers/BufferTexture.cpp |
modified | |
MemoryTextureDatagfx/layers/BufferTexture.cpp |
modified | |
ShmemHoldergfx/layers/BufferTexture.cpp |
modified | |
ShmemHoldergfx/layers/BufferTexture.h |
modified | |
BufferTextureDatagfx/layers/BufferTexture.h |
modified | |
PTextureChildgfx/layers/client/TextureClient.h |
modified | |
TextureChildgfx/layers/client/TextureClient.h |
modified | |
TextureDatagfx/layers/client/TextureClient.h |
modified | |
ShmemHoldergfx/layers/client/TextureClient.h |
modified | |
GPUVideoTextureDatagfx/layers/client/TextureClient.h |
modified | |
TextureClientgfx/layers/client/TextureClient.h |
modified | |
ITextureClientRecycleAllocatorgfx/layers/client/TextureClient.h |
modified | |
ImageClientgfx/layers/ipc/SharedPlanarYCbCrImage.h |
modified | |
ShmemHoldergfx/layers/ipc/SharedPlanarYCbCrImage.h |
modified | |
TextureClientgfx/layers/ipc/SharedPlanarYCbCrImage.h |
modified | |
TextureClientRecycleAllocatorgfx/layers/ipc/SharedPlanarYCbCrImage.h |
modified |
Files Changed
gfx/layers/BufferTexture.cppgfx/layers/BufferTexture.hgfx/layers/client/TextureClient.cppgfx/layers/client/TextureClient.hgfx/layers/ipc/SharedPlanarYCbCrImage.cppgfx/layers/ipc/SharedPlanarYCbCrImage.h
Audit Directions
- Borrowed-pointer cachingAudit every caller of
BorrowMappedData/BorrowMappedYCbCrData(and similarBorrow*) that retains the returned pointers pastUnlock()/the lock scope without retaining a keep-alive for the backing storage. - TextureClient destruction vs. Image lifetimeReview ImageBridge/compositable shutdown paths for other
Imagetypes that hold state borrowed from aTextureClientthat shutdown can destroy explicitly. - Shmem deallocation threadingCheck other
Shmem-owning objects for destructors that callDeallocShmemdirectly on an arbitrary thread or after the allocator thread/IPC may have shut down.
Patch
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;