CVE-2026-117986
Overview
Background
- `SourceSurfaceRecording`
- A
SourceSurfacewhose lifetime is tracked by the recorder as a “stored object”; its destructor must tell the recorder to remove it and record aRecordedSourceSurfaceDestructionevent. - `DrawEventRecorder` main-thread affinity
- The recorder’s
RemoveStoredObject/RecordEventmutate 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.
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
- Record
Attacker content drives recorded drawing (remote/GPU canvas or PDF recording) that creates
SourceSurfaceRecordingobjects tracked by the recorder. - Off-thread release
The last reference to such a surface is dropped on a non-main thread (worker/IPC/compositor), running
~SourceSurfaceRecordingthere. (Thread interleaving inferred; the diff shows the unguarded destructor.) - Race
RemoveStoredObject/RecordEventmutate recorder state concurrently with the main thread’s use of the same state. - Use-after-free Concurrent access touches recorder structures/objects mid-teardown.
- Primitive UAF/data race on recorder state; controlled exploitation is inferred, not shown by the diff.
Impact Assessment
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 destructorsAudit other Moz2D recording objects (
SourceSurface/GradientStops/Pathrecordings) whose destructors callmRecorder->RemoveStoredObject/RecordEventfor the same missing thread check. - Off-main-thread refcount dropsReview which threads can hold the last ref to recording objects; any that aren’t the recorder’s thread need the deferred-deletion path.
- `AddPendingDeletion` consumersVerify pending deletions are actually drained on the main thread and that captured raw
recorderpointers can’t outlive the recorder.
Patch
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; }