CVE-2026-74949
Overview
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
CanvasChildmethod that captures aSourceSurfacefrom 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.
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
- Trigger an external-canvas snapshot
Web content drives a WebGL-backed canvas into a path that calls
SnapshotExternalCanvaswith accelerated Canvas2D and the canvas-translator-event pref enabled. - Reach the reentrant recording
The
RecordedResolveExternalSnapshotrecording synchronizes WebGL and AC2D on the same thread, pausing and resuming events. - Free the actor mid-flight
During that paused/resumed window, the external canvas actor (
aActor) is torn down, freeing its memory. - Dereference the freed actor
Execution resumes and reads
aActor->Id(), accessing the freed object (use-after-free).
Impact Assessment
Files Changed
gfx/layers/ipc/CanvasChild.cpp
Audit Directions
- Read-after-reentrancyFlag 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 checksAudit
CanvasChildand 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-effectLook 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.
Patch
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);