CVE-2026-16360
Overview
Background
- `PlanarYCbCrImage` / `NVImage`
- Gecko
layers::Imagesubclasses that hold decoded YCbCr/NV video frame data and can lazily rasterize it to an RGB surface. - `mSourceSurface`
- A per-image cache field holding the already-rasterized
gfx::DataSourceSurfaceso repeatedGetAsSourceSurfacecalls avoid re-doing YUV-to-RGB conversion. - `gfx::DataSourceSurface`
- A reference-counted CPU-accessible surface whose refcount and lifetime are not thread-safe and which must be released on the main thread.
- `CachedSurface`
- A new helper class introduced by the fix that wraps the cached surface in a
mozilla::DataMutexfor thread-safe get/set and main-thread release.
Root Cause Analysis
PlanarYCbCrImage and NVImage cached their rasterized RGB output in a plain RefPtr<gfx::DataSourceSurface> mSourceSurface that could be read and written from multiple threads simultaneously — GetAsSourceSurface and BuildSurfaceDescriptorBuffer both test mSourceSurface, then dereference it (GetSize, GetFormat, ScopedMap) and lazily assign it. Because RefPtr assignment and the non-atomic DataSourceSurface refcount are not synchronized, one thread could assign mSourceSurface while another read a torn/stale pointer or raced the refcount, and a check like if (mSourceSurface) could pass before the pointer was concurrently replaced or released, yielding a use-after-free or double-free on the surface. The invariant violated is that the cached surface’s refcount transitions and pointer swaps must be atomic with respect to concurrent readers.
The fix replaces the raw RefPtr field with a CachedSurface that stores the pointer inside a DataMutex, so Get() returns a private owning RefPtr reference and Set() only installs a surface if none is cached yet, both under the lock. Each vulnerable method now snapshots the surface once into a local RefPtr via mSourceSurface.Get() and operates on that stable local instead of repeatedly touching the shared field.
RefPtr<DataSourceSurface> cache across threads without synchronization, letting concurrent check-then-use/assign sequences race the surface’s lifetime; the fix serializes all access behind a DataMutex inside CachedSurface and forces callers to work off a locally-owned snapshot.Attack Path
- Trigger concurrent rasterization
Web content drives the same decoded video frame (
PlanarYCbCrImageorNVImage) to be rasterized and consumed from more than one thread at once (e.g. rendering plus surface-descriptor building). - Race the cache field
One thread executes
GetAsSourceSurfaceand assignsmSourceSurface = surfacewhile another thread readsmSourceSurfacein itsifcheck. - Dereference a stale pointer
The reading thread passes the null/validity check, then dereferences the field for
GetSize,GetFormat, orScopedMapafter it has been concurrently replaced or its refcount corrupted. - Corrupt surface lifetime
The unsynchronized refcount produces a premature free or double-free of the
DataSourceSurface, leaving a dangling surface pointer in use. - Exploit the freed surface Continued mapping/swizzling through the dangling surface reads or writes freed memory, the memory-corruption primitive the advisory describes as potentially exploitable for code execution.
Impact Assessment
gfx::DataSourceSurface in the process that performs graphics/layers work handling that image. Per the advisory this memory corruption is presumed potentially exploitable for arbitrary code execution. The precondition is content that induces multi-threaded rasterization and consumption of the same PlanarYCbCrImage or NVImage so the unsynchronized cache field is raced.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgfx/layers/ImageContainer.cpp |
modified | |
CachedSurfacegfx/layers/ImageContainer.h |
modified | |
ifgfx/layers/ImageContainer.h |
modified |
Files Changed
gfx/layers/ImageContainer.cppgfx/layers/ImageContainer.h
Audit Directions
- Cross-thread `RefPtr` cachesAudit other lazily-populated
RefPtrcache fields on shared objects for unsynchronized check-then-use or check-then-assign patterns across threads. - Non-thread-safe refcounted graphics typesFlag any
gfx::DataSourceSurfaceor similar non-atomically-refcounted object stored where multiple threads may read and assign it without aDataMutexor equivalent lock. - Main-thread release invariantsVerify that surfaces which may be created off-main-thread are still released via
NS_ReleaseOnMainThread, and that lock-wrapping refactors do not silently drop that guarantee.
Patch
diff --git a/gfx/layers/ImageContainer.cpp b/gfx/layers/ImageContainer.cpp
index 901ced77a29..a243e651089 100644
--- a/gfx/layers/ImageContainer.cpp
+++ b/gfx/layers/ImageContainer.cpp
@@ -794,10 +794,11 @@ nsresult PlanarYCbCrImage::BuildSurfaceDescriptorBuffer(
// If we can copy directly from the surface, let's do that to avoid the YUV
// to RGB conversion.
- if (mSourceSurface && mSourceSurface->GetSize() == size) {
- DataSourceSurface::ScopedMap map(mSourceSurface, DataSourceSurface::READ);
+ RefPtr<gfx::DataSourceSurface> sourceSurface = mSourceSurface.Get();
+ if (sourceSurface && sourceSurface->GetSize() == size) {
+ DataSourceSurface::ScopedMap map(sourceSurface, DataSourceSurface::READ);
if (map.IsMapped() && SwizzleData(map.GetData(), map.GetStride(),
- mSourceSurface->GetFormat(), buffer,
+ sourceSurface->GetFormat(), buffer,
stride, format, size)) {
return NS_OK;
}
@@ -987,9 +988,8 @@ nsresult PlanarYCbCrImage::AdoptData(const Data& aData) {
}
already_AddRefed<gfx::SourceSurface> PlanarYCbCrImage::GetAsSourceSurface() {
- if (mSourceSurface) {
- RefPtr<gfx::SourceSurface> surface(mSourceSurface);
- return surface.forget();
+ if (RefPtr<gfx::DataSourceSurface> cached = mSourceSurface.Get()) {
+ return cached.forget();
}
gfx::IntSize size(mSize);
@@ -1019,30 +1019,24 @@ already_AddRefed<gfx::SourceSurface> PlanarYCbCrImage::GetAsSourceSurface() {
return nullptr;
}
- mSourceSurface = surface;
+ mSourceSurface.Set(surface);
return surface.forget();
}
-PlanarYCbCrImage::~PlanarYCbCrImage() {
- NS_ReleaseOnMainThread("PlanarYCbCrImage::mSourceSurface",
- mSourceSurface.forget());
-}
+PlanarYCbCrImage::~PlanarYCbCrImage() = default;
NVImage::NVImage() : Image(nullptr, ImageFormat::NV_IMAGE), mBufferSize(0) {}
-NVImage::~NVImage() {
- NS_ReleaseOnMainThread("NVImage::mSourceSurface", mSourceSurface.forget());
-}
+NVImage::~NVImage() = default;
IntSize NVImage::GetSize() const { return mSize; }
IntRect NVImage::GetPictureRect() const { return mData.mPictureRect; }
already_AddRefed<SourceSurface> NVImage::GetAsSourceSurface() {
- if (mSourceSurface) {
- RefPtr<gfx::SourceSurface> surface(mSourceSurface);
- return surface.forget();
+ if (RefPtr<gfx::DataSourceSurface> cached = mSourceSurface.Get()) {
+ return cached.forget();
}
// Convert the current NV12 or NV21 data to YUV420P so that we can follow the
@@ -1100,7 +1094,7 @@ already_AddRefed<SourceSurface> NVImage::GetAsSourceSurface() {
return nullptr;
}
- mSourceSurface = surface;
+ mSourceSurface.Set(surface);
return surface.forget();
}
@@ -1122,7 +1116,8 @@ nsresult NVImage::BuildSurfaceDescriptorBuffer(
UniquePtr<uint8_t[]> buffer;
- if (!mSourceSurface) {
+ RefPtr<gfx::DataSourceSurface> sourceSurface = mSourceSurface.Get();
+ if (!sourceSurface) {
const int bufferLength =
ySize.height * mData.mYStride + cbcrSize.height * cbcrSize.width * 2;
buffer = MakeUnique<uint8_t[]>(bufferLength);
@@ -1152,7 +1147,7 @@ nsresult NVImage::BuildSurfaceDescriptorBuffer(
return NS_ERROR_FAILURE;
}
- if (mSourceSurface && mSourceSurface->GetSize() != size) {
+ if (sourceSurface && sourceSurface->GetSize() != size) {
return NS_ERROR_NOT_IMPLEMENTED;
}
@@ -1164,7 +1159,7 @@ nsresult NVImage::BuildSurfaceDescriptorBuffer(
return rv;
}
- if (!mSourceSurface) {
+ if (!sourceSurface) {
rv = gfx::ConvertYCbCrToRGB(aData, format, size, output, stride);
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_ASSERT_UNREACHABLE("Failed to convert YUV into RGB data");
@@ -1173,12 +1168,12 @@ nsresult NVImage::BuildSurfaceDescriptorBuffer(
return NS_OK;
}
- DataSourceSurface::ScopedMap map(mSourceSurface, DataSourceSurface::WRITE);
+ DataSourceSurface::ScopedMap map(sourceSurface, DataSourceSurface::WRITE);
if (NS_WARN_IF(!map.IsMapped())) {
return NS_ERROR_FAILURE;
}
- if (!SwizzleData(map.GetData(), map.GetStride(), mSourceSurface->GetFormat(),
+ if (!SwizzleData(map.GetData(), map.GetStride(), sourceSurface->GetFormat(),
output, stride, format, size)) {
return NS_ERROR_FAILURE;
}
diff --git a/gfx/layers/ImageContainer.h b/gfx/layers/ImageContainer.h
index 0fe66df2a34..952fe1a3c60 100644
--- a/gfx/layers/ImageContainer.h
+++ b/gfx/layers/ImageContainer.h
@@ -9,6 +9,7 @@
#include "ImageTypes.h" // for ImageFormat, etc
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/Assertions.h" // for MOZ_ASSERT_HELPER2
+#include "mozilla/DataMutex.h" // for DataMutex
#include "mozilla/Mutex.h" // for Mutex
#include "mozilla/RecursiveMutex.h" // for RecursiveMutex, etc
#include "mozilla/ThreadSafeWeakPtr.h"
@@ -24,6 +25,7 @@
#include "nsISupportsImpl.h" // for Image::Release, etc
#include "nsTArray.h" // for nsTArray
#include "nsThreadUtils.h" // for NS_IsMainThread
+#include "nsProxyRelease.h" // for NS_ReleaseOnMainThread
#include "mozilla/Atomics.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/EnumeratedArray.h"
@@ -219,6 +221,46 @@ class Image {
static mozilla::Atomic<int32_t> sSerialCounter;
};
+/**
+ * A thread-safe, lazily-populated cache of an Image's rasterized surface.
+ */
+class CachedSurface final {
+ public:
+ CachedSurface() : mSurface("layers::CachedSurface") {}
+
+ ~CachedSurface() {
+ // The surface may have been created off the main thread, but like the rest
+ // of the gfx surface machinery it must be released on the main thread.
+ RefPtr<gfx::DataSourceSurface> surface;
+ {
+ auto guard = mSurface.Lock();
+ surface = guard->forget();
+ }
+ NS_ReleaseOnMainThread("layers::CachedSurface", surface.forget());
+ }
+
+ // Returns the cached surface, or nullptr if nothing has been cached yet.
+ already_AddRefed<gfx::DataSourceSurface> Get() {
+ auto guard = mSurface.Lock();
+ RefPtr<gfx::DataSourceSurface> surface = *guard;
+ return surface.forget();
+ }
+
+ // Stores aSurface unless another thread has already cached one. The caller
+ // may keep using its own surface regardless: both are equivalent
+ // rasterizations of the same image, and keeping the first one avoids dropping
+ // a surface that a concurrent caller may already have handed out.
+ void Set(gfx::DataSourceSurface* aSurface) {
+ auto guard = mSurface.Lock();
+ if (!*guard) {
+ *guard = aSurface;
+ }
+ }
+
+ private:
+ mozilla::DataMutex<RefPtr<gfx::DataSourceSurface>> mSurface;
+};
+
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(Image::BuildSdbFlags)
/**
@@ -915,7 +957,7 @@ class PlanarYCbCrImage : public Image {
gfx::IntSize mSize;
gfx::ColorDepth mColorDepth = gfx::ColorDepth::COLOR_8;
gfxImageFormat mOffscreenFormat;
- RefPtr<gfx::DataSourceSurface> mSourceSurface;
+ CachedSurface mSourceSurface;
uint32_t mBufferSize;
};
@@ -978,7 +1020,7 @@ class NVImage final : public Image {
uint32_t mBufferSize;
gfx::IntSize mSize;
Data mData;
- RefPtr<gfx::DataSourceSurface> mSourceSurface;
+ CachedSurface mSourceSurface;
};
/**