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
Tracker2078989
Fix commitcc0cadee4768 (firefox) +12/-3
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

`SourceSurfaceRecording`
A SourceSurface whose lifetime is tracked by the recorder as a “stored object”; its destructor must tell the recorder to remove it and record a RecordedSourceSurfaceDestruction event.
`DrawEventRecorder` main-thread affinity
The recorder’s RemoveStoredObject/RecordEvent mutate recorder state that is main-thread-only; calling them off the main thread races the recorder.
`AddPendingDeletion`
A recorder facility to defer a deletion action so it runs on the correct (main) thread.

Root Cause Analysis

~SourceSurfaceRecording() unconditionally called mRecorder->RemoveStoredObject(this) and mRecorder->RecordEvent(RecordedSourceSurfaceDestruction(...)) directly, from whatever thread released the surface’s last reference. The recorder’s stored-object bookkeeping and event stream are main-thread-only state; when the surface was destroyed off the main thread (e.g. a RefPtr drop on a worker/IPC thread), these calls mutated recorder state concurrently with the main thread, racing the recorder’s internal structures and operating on objects the main thread may be tearing down — a use-after-free / data race on recorder state.

The patch makes the destructor thread-aware. On the main thread it behaves as before (RemoveStoredObject(self) + RecordEvent(...) + return). Off the main thread it instead enqueues the same work via mRecorder->AddPendingDeletion([recorder = mRecorder.get(), self]() { recorder->RemoveStoredObject(self); recorder->RecordEvent(...); }), so the recorder is only ever touched on the correct thread. The comment notes the recorder owns the deletion, so the lambda captures a raw recorder pointer without an extra strong ref. The advisory’s “use-after-free in Graphics” is substantiated: the fix eliminates off-main-thread mutation of recorder state during surface destruction.

Key insight
SourceSurfaceRecording’s destructor mutated main-thread-only recorder state directly, so destroying the surface on another thread raced (and used-after-freed) the recorder’s stored-object/event bookkeeping. The fix defers that work to the recorder’s thread via AddPendingDeletion when not on the main thread.

Attack Path

  1. Record Attacker content drives recorded drawing (remote/GPU canvas or PDF recording) that creates SourceSurfaceRecording objects tracked by the recorder.
  2. Off-thread release The last reference to such a surface is dropped on a non-main thread (worker/IPC/compositor), running ~SourceSurfaceRecording there. (Thread interleaving inferred; the diff shows the unguarded destructor.)
  3. Race RemoveStoredObject/RecordEvent mutate recorder state concurrently with the main thread’s use of the same state.
  4. Use-after-free Concurrent access touches recorder structures/objects mid-teardown.
  5. Primitive UAF/data race on recorder state; controlled exploitation is inferred, not shown by the diff.

Impact Assessment

The code runs in the graphics recording path (content/GPU process, inferred). The diff substantiates a use-after-free/data race: the surface destructor mutated main-thread-only recorder state from arbitrary threads, now deferred via AddPendingDeletion. Privilege/preconditions: an attacker must cause a recorded surface to be destroyed off the main thread while the recorder is active (a timing/threading condition). Strength beyond a UAF/race is inferred. The advisory rates it moderate.

Files Changed

  • gfx/2d/DrawTargetRecording.cpp

Audit Directions

  • Other recorder-touching destructors
    Audit other Moz2D recording objects (SourceSurface/GradientStops/Path recordings) whose destructors call mRecorder->RemoveStoredObject/RecordEvent for the same missing thread check.
  • Off-main-thread refcount drops
    Review which threads can hold the last ref to recording objects; any that aren’t the recorder’s thread need the deferred-deletion path.
  • `AddPendingDeletion` consumers
    Verify pending deletions are actually drained on the main thread and that captured raw recorder pointers can’t outlive the recorder.
diff --git a/gfx/2d/DrawTargetRecording.cpp b/gfx/2d/DrawTargetRecording.cpp
index cef94df95e2..9c73635f515 100644
--- a/gfx/2d/DrawTargetRecording.cpp
+++ b/gfx/2d/DrawTargetRecording.cpp
@@ -86,9 +86,18 @@ class SourceSurfaceRecording final : public SourceSurface {
   }
 
   ~SourceSurfaceRecording() {
-    mRecorder->RemoveStoredObject(this);
-    mRecorder->RecordEvent(
-        RecordedSourceSurfaceDestruction(ReferencePtr(this)));
+    ReferencePtr self(this);
+    if (NS_IsMainThread()) {
+      mRecorder->RemoveStoredObject(self);
+      mRecorder->RecordEvent(RecordedSourceSurfaceDestruction(self));
+      return;
+    }
+
+    // The recorder owns the deletion so no need to hold a strong ref to it.
+    mRecorder->AddPendingDeletion([recorder = mRecorder.get(), self]() {
+      recorder->RemoveStoredObject(self);
+      recorder->RecordEvent(RecordedSourceSurfaceDestruction(self));
+    });
   }
 
   SurfaceType GetType() const override { return SurfaceType::RECORDING; }
Loading diff…