Medium firefox UAF 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionRace condition, use-after-free in the Graphics component
ComponentGraphics
Bug ClassUAF
Tracker2060357
Fix commitae4cc36924af (firefox) +17/-9
CISA KEVNot listed
Creditedr00tdaddy
Disclosed2026-08-18

Background

`SharedRGBImage`
a Gecko layers Image subclass that wraps a shared-memory RGB buffer backed by a TextureClient for IPC compositing.
`mSourceSurface`
a cached RefPtr<gfx::SourceSurface> member of SharedRGBImage lazily populated by GetAsSourceSurface.
`DataMutex`
a Mozilla template (mozilla/DataMutex.h) that bundles a value with a mutex so the value can only be accessed while its lock is held.
`NS_ReleaseOnMainThread`
a helper that hands a refcounted pointer to the main thread for release, used when an object must be destroyed there.

Root Cause Analysis

The cached mSourceSurface field of SharedRGBImage was a plain RefPtr<gfx::SourceSurface> accessed without synchronization, while GetAsSourceSurface only asserted (NS_ASSERTION) rather than enforced main-thread execution, and the destructor read and forget()-ed the same field. Because a SharedRGBImage can be touched from more than one thread, two threads could concurrently read, write (mSourceSurface = surface), and release the pointer, violating the invariant that a non-atomic RefPtr must be mutated by only one thread at a time. This race allowed the surface’s refcount to be corrupted or the object to be freed while another thread still held or was about to store a reference, producing a use-after-free.

The fix converts mSourceSurface into a DataMutex<RefPtr<gfx::SourceSurface>>, so every read in GetAsSourceSurface (*guard), every write (*guard = surface), and the destructor’s forget() now occur under mSourceSurface.Lock(). Serializing all access to the cached surface eliminates the concurrent-mutation window and makes the caching path thread-safe.

Key insight
The single mistake was caching a shared surface in a non-atomic RefPtr guarded only by a main-thread assertion instead of a real lock, leaving concurrent access unsynchronized; wrapping the field in a DataMutex forces every access under a mutex and closes the race.

Attack Path

  1. Trigger concurrent surface access Cause content that maps to a SharedRGBImage to have its GetAsSourceSurface invoked from a thread other than the main thread while another thread also operates on the same image.
  2. Race the cache write Two threads simultaneously reach the caching logic, one reading mSourceSurface while the other assigns to it, corrupting the non-atomic refcount.
  3. Force premature free The refcount corruption or interleaving with the destructor’s forget() drops the surface’s last reference while another thread still references it.
  4. Reuse the freed surface A thread dereferences the now-freed gfx::SourceSurface, yielding a use-after-free.
  5. Leverage the dangling object Reclaiming the freed allocation with attacker-influenced data can turn the dangling access into memory disclosure or corruption.

Impact Assessment

An attacker who can drive concurrent graphics operations on a SharedRGBImage gains a use-after-free of a gfx::SourceSurface in the process running the compositing/graphics code, which may lead to memory corruption. Exploitation requires winning a timing race between threads accessing the same image, so it is non-deterministic. Rated moderate/medium with no assigned CVSS or CWE in the metadata.

Changed Functions

FunctionChangeNotes
mSourceSurface
gfx/layers/ipc/SharedRGBImage.cpp
modified
mRecycleAllocator
gfx/layers/ipc/SharedRGBImage.cpp
modified
if
gfx/layers/ipc/SharedRGBImage.cpp
modified

Files Changed

  • gfx/layers/ipc/SharedRGBImage.cpp
  • gfx/layers/ipc/SharedRGBImage.h

Audit Directions

  • Non-atomic `RefPtr` cache fields
    Flag class members that lazily cache refcounted pointers and are read or written from more than one thread without a lock or atomic wrapper.
  • Assertion-only thread guards
    Treat NS_ASSERTION(NS_IsMainThread()) (or similar) as documentation, not enforcement; verify actual synchronization exists on any field the “asserted” thread invariant is meant to protect.
  • Destructor releases of shared state
    Audit destructors that forget() or release members which other threads may still touch, ensuring teardown takes the same lock as the accessors.
diff --git a/gfx/layers/ipc/SharedRGBImage.cpp b/gfx/layers/ipc/SharedRGBImage.cpp
index 135065743bb..d078d5a0877 100644
--- a/gfx/layers/ipc/SharedRGBImage.cpp
+++ b/gfx/layers/ipc/SharedRGBImage.cpp
@@ -51,20 +51,27 @@ class TextureClientForRawBufferAccessAllocationHelper
 };
 
 SharedRGBImage::SharedRGBImage(ImageClient* aCompositable)
-    : Image(nullptr, ImageFormat::SHARED_RGB), mCompositable(aCompositable) {
+    : Image(nullptr, ImageFormat::SHARED_RGB),
+      mCompositable(aCompositable),
+      mSourceSurface("SharedRGBImage::mSourceSurface") {
   MOZ_COUNT_CTOR(SharedRGBImage);
 }
 
 SharedRGBImage::SharedRGBImage(TextureClientRecycleAllocator* aRecycleAllocator)
     : Image(nullptr, ImageFormat::SHARED_RGB),
-      mRecycleAllocator(aRecycleAllocator) {
+      mRecycleAllocator(aRecycleAllocator),
+      mSourceSurface("SharedRGBImage::mSourceSurface") {
   MOZ_COUNT_CTOR(SharedRGBImage);
 }
 
 SharedRGBImage::~SharedRGBImage() {
   MOZ_COUNT_DTOR(SharedRGBImage);
-  NS_ReleaseOnMainThread("SharedRGBImage::mSourceSurface",
-                         mSourceSurface.forget());
+  RefPtr<gfx::SourceSurface> surface;
+  {
+    auto guard = mSourceSurface.Lock();
+    surface = guard->forget();
+  }
+  NS_ReleaseOnMainThread("SharedRGBImage::mSourceSurface", surface.forget());
 }
 
 TextureClientRecycleAllocator* SharedRGBImage::RecycleAllocator() {
@@ -110,10 +117,10 @@ static void ReleaseTextureClient(void* aData) {
 static gfx::UserDataKey sTextureClientKey;
 
 already_AddRefed<gfx::SourceSurface> SharedRGBImage::GetAsSourceSurface() {
-  NS_ASSERTION(NS_IsMainThread(), "Must be main thread");
+  auto guard = mSourceSurface.Lock();
 
-  if (mSourceSurface) {
-    RefPtr<gfx::SourceSurface> surface(mSourceSurface);
+  if (*guard) {
+    RefPtr<gfx::SourceSurface> surface(*guard);
     return surface.forget();
   }
 
@@ -148,7 +155,7 @@ already_AddRefed<gfx::SourceSurface> SharedRGBImage::GetAsSourceSurface() {
     }
   }
 
-  mSourceSurface = surface;
+  *guard = surface;
   return surface.forget();
 }
 
diff --git a/gfx/layers/ipc/SharedRGBImage.h b/gfx/layers/ipc/SharedRGBImage.h
index 77a5cb471df..1151d4880fc 100644
--- a/gfx/layers/ipc/SharedRGBImage.h
+++ b/gfx/layers/ipc/SharedRGBImage.h
@@ -10,6 +10,7 @@
 
 #include "ImageContainer.h"  // for ISharedImage, Image, etc
 #include "gfxTypes.h"
+#include "mozilla/DataMutex.h"  // for DataMutex
 #include "mozilla/RefPtr.h"     // for RefPtr
 #include "mozilla/gfx/Point.h"  // for IntSize
 #include "mozilla/gfx/Types.h"  // for SurfaceFormat
@@ -49,7 +50,7 @@ class SharedRGBImage : public Image {
   RefPtr<TextureClient> mTextureClient;
   RefPtr<ImageClient> mCompositable;
   RefPtr<TextureClientRecycleAllocator> mRecycleAllocator;
-  RefPtr<gfx::SourceSurface> mSourceSurface;
+  DataMutex<RefPtr<gfx::SourceSurface>> mSourceSurface;
 };
 
 }  // namespace layers
Loading diff…