Medium firefox UAF 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionUse-after-free in the Graphics: Canvas2D component
ComponentGraphics
Bug ClassUAF
Tracker2060245
Fix commite98b25b5ee45 (firefox) +5/-4
CISA KEVNot listed
Creditedr00tdaddy
Disclosed2026-08-18

Background

Canvas2D remoting
Firefox’s mechanism for recording <canvas> drawing operations in the content process and replaying them in the GPU/compositor process via IPDL.
`CanvasChild`
the content-side IPDL child actor that records canvas commands and coordinates snapshots with the remote canvas translator.
`SnapshotExternalCanvas`
a CanvasChild method that captures a SourceSurface from an external WebGL-backed canvas by synchronizing WebGL and accelerated Canvas2D (AC2D).
Use-after-free (UAF)
a memory-safety bug where a pointer is dereferenced after the object it points to has been freed, potentially yielding attacker-controlled behavior.

Root Cause Analysis

In CanvasChild::SnapshotExternalCanvas, the code recorded a RecordedResolveExternalSnapshot event and only afterward dereferenced aActor to read aActor->Id() and Manager() to read the manager id. That recording path can pause and resume events while synchronizing between WebGL and AC2D on the same thread, and during that reentrant window the external canvas actor (aActor) can be torn down, so the subsequent aActor->Id() read touches freed memory. The function also never validated that aActor was non-null before using it.

The fix hoists the managerId and canvasId reads to the top of the function — before the surface allocation and the reentrant recording step — so the identifiers are captured while the actor is still guaranteed live, and adds an early !aActor guard that returns nullptr instead of dereferencing a null or about-to-be-freed pointer. This preserves the invariant that aActor is only dereferenced at a point where its lifetime is known to be valid.

Key insight
The core mistake was reading identity fields off aActor after a reentrant event-synchronization step that can destroy the actor; the fix captures those identifiers up front (and null-checks aActor) so no dereference happens after the actor may have been freed.

Attack Path

  1. Trigger an external-canvas snapshot Web content drives a WebGL-backed canvas into a path that calls SnapshotExternalCanvas with accelerated Canvas2D and the canvas-translator-event pref enabled.
  2. Reach the reentrant recording The RecordedResolveExternalSnapshot recording synchronizes WebGL and AC2D on the same thread, pausing and resuming events.
  3. Free the actor mid-flight During that paused/resumed window, the external canvas actor (aActor) is torn down, freeing its memory.
  4. Dereference the freed actor Execution resumes and reads aActor->Id(), accessing the freed object (use-after-free).

Impact Assessment

An attacker running script in a web page could induce a use-after-free of the external canvas actor in the content process’s graphics/Canvas2D remoting code. Exploitation could lead to a crash or potentially corrupt memory in a way influencing later execution, though it requires accelerated Canvas2D and the canvas-translator-event startup pref to be enabled and a specific reentrant teardown of the actor. Mozilla rated the impact moderate.

Files Changed

  • gfx/layers/ipc/CanvasChild.cpp

Audit Directions

  • Read-after-reentrancy
    Flag any code that dereferences an IPDL actor or other object after a synchronous event-pump, pause/resume, or cross-process sync step that can reentrantly destroy that object.
  • Missing actor null checks
    Audit CanvasChild and sibling actors for methods that dereference a passed-in actor pointer (e.g. aActor->Id()) without first validating it is non-null.
  • Capture-before-side-effect
    Look for identity or handle values read from long-lived-looking objects after allocations or recordings, and prefer capturing them before any step that can invalidate the source.
diff --git a/gfx/layers/ipc/CanvasChild.cpp b/gfx/layers/ipc/CanvasChild.cpp
index dab2f83b4aa..c752f8454db 100644
--- a/gfx/layers/ipc/CanvasChild.cpp
+++ b/gfx/layers/ipc/CanvasChild.cpp
@@ -828,10 +828,14 @@ already_AddRefed<gfx::SourceSurface> CanvasChild::SnapshotExternalCanvas(
   // running under the same thread, and that events can be paused or resumed
   // while synchronizing between WebGL and AC2D.
   if (!gfx::gfxVars::UseAcceleratedCanvas2D() ||
-      !StaticPrefs::gfx_canvas_remote_use_canvas_translator_event_AtStartup()) {
+      !StaticPrefs::gfx_canvas_remote_use_canvas_translator_event_AtStartup() ||
+      !aActor) {
     return nullptr;
   }
 
+  uint32_t managerId = static_cast<gfx::CanvasManagerChild*>(Manager())->Id();
+  ActorId canvasId = aActor->Id();
+
   gfx::SurfaceFormat format = aCanvas->GetIsOpaque()
                                   ? gfx::SurfaceFormat::B8G8R8X8
                                   : gfx::SurfaceFormat::B8G8R8A8;
@@ -857,9 +861,6 @@ already_AddRefed<gfx::SourceSurface> CanvasChild::SnapshotExternalCanvas(
                          RecordedResolveExternalSnapshot(
                              syncId, gfx::ReferencePtr(surface), size, format));
 
-  uint32_t managerId = static_cast<gfx::CanvasManagerChild*>(Manager())->Id();
-  ActorId canvasId = aActor->Id();
-
   // Actually send the request via IPDL to snapshot the external WebGL canvas.
   SendSnapshotExternalCanvas(syncId, managerId, canvasId);
 
Loading diff…