High firefox Memory Corruption 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionMemory safety bugs present in Firefox ESR 115.37, Firefox ESR 140.12 and Firefox 152. Some of these bugs showed evidence of memory corruption and we presume that with enough effort some of these could have been exploited to run arbitrary code.
ComponentGraphics
Bug ClassMemory Corruption
Tracker2022635
Fix commit6ef63c5869cf (firefox) +62/-25
CISA KEVNot listed
CreditedAndrew McCreight, Jan de Mooij, Tom Ritter, Vincent Hilla and the Mozilla Fuzzing Team
Disclosed2026-07-21

Background

`PlanarYCbCrImage` / `NVImage`
Gecko layers::Image subclasses 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::DataSourceSurface so repeated GetAsSourceSurface calls 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::DataMutex for 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.

Key insight
The single core mistake was sharing a non-thread-safe 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

  1. Trigger concurrent rasterization Web content drives the same decoded video frame (PlanarYCbCrImage or NVImage) to be rasterized and consumed from more than one thread at once (e.g. rendering plus surface-descriptor building).
  2. Race the cache field One thread executes GetAsSourceSurface and assigns mSourceSurface = surface while another thread reads mSourceSurface in its if check.
  3. Dereference a stale pointer The reading thread passes the null/validity check, then dereferences the field for GetSize, GetFormat, or ScopedMap after it has been concurrently replaced or its refcount corrupted.
  4. Corrupt surface lifetime The unsynchronized refcount produces a premature free or double-free of the DataSourceSurface, leaving a dangling surface pointer in use.
  5. 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

An attacker who can get concurrent access to a shared decoded image gains a memory-corruption primitive (use-after-free / double-free) on a 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

FunctionChangeNotes
if
gfx/layers/ImageContainer.cpp
modified
CachedSurface
gfx/layers/ImageContainer.h
modified
if
gfx/layers/ImageContainer.h
modified

Files Changed

  • gfx/layers/ImageContainer.cpp
  • gfx/layers/ImageContainer.h

Audit Directions

  • Cross-thread `RefPtr` caches
    Audit other lazily-populated RefPtr cache fields on shared objects for unsynchronized check-then-use or check-then-assign patterns across threads.
  • Non-thread-safe refcounted graphics types
    Flag any gfx::DataSourceSurface or similar non-atomically-refcounted object stored where multiple threads may read and assign it without a DataMutex or equivalent lock.
  • Main-thread release invariants
    Verify 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.
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;
 };
 
 /**
Loading diff…