CVE-2026-74943
Overview
Background
- `RasterImage`
- Gecko’s
ImageLibclass representing a decoded raster (bitmap) image and owner of its decoders and cached surfaces. - `ProgressTracker`
- A separately reference-counted helper object that tracks an image’s load/decode progress and holds a back-reference to its
Image. - `OnSurfaceDiscarded`
- A
RasterImagecallback invoked when the surface cache evicts one of the image’s decoded surfaces, which then dispatches work to the main thread. - Object resurrection
- Taking a new
RefPtrto an object whose refcount has already reached zero (i.e. one being destroyed), briefly reviving it and triggering a second destruction.
Root Cause Analysis
RasterImage::OnSurfaceDiscarded captured this into a strong RefPtr<RasterImage> image = this and enqueued a main-thread runnable that later called image->OnSurfaceDiscardedInternal(...). Because surface discard can occur while the RasterImage is being torn down (its destructor completes the mSourceBuffer and terminates outstanding decoders), taking a RefPtr to this at refcount zero resurrects a dying object, and dropping that RefPtr when the runnable finishes drives a second destruction, yielding a use-after-free/double-free. The prior guard, an Atomic<bool> mIsBeingDestroyed set at the top of ~RasterImage() with an early return in OnSurfaceDiscarded, was a fragile check-then-act mitigation with a race window between reading the flag and constructing the strong self-reference.
The fix removes both the flag and the strong self-capture; instead the runnable captures a RefPtr<ProgressTracker> and re-derives the image via progressTracker->GetImage(), returning early when that yields null. Since GetImage() returns null once the image is gone, liveness is checked without ever resurrecting the object. This preserves the invariant that no strong reference to a RasterImage is created after its refcount has dropped to zero.
RefPtr to this in a deferred runnable from a path reachable during destruction, so the fix routes liveness through the independently owned ProgressTracker and only reconstructs the image pointer if ProgressTracker::GetImage() still returns it.Attack Path
- Trigger surface caching
Cause a
RasterImageto decode and populate the surface cache, ideally with an animated image so both static and animated surfaces exist. - Force teardown and eviction to race
Drop the last reference to the
RasterImage(e.g. navigate away or remove the element) while the surface cache concurrently discards one of its surfaces. - Invoke the vulnerable callback during destruction
OnSurfaceDiscardedruns as the object is being destroyed and capturesthisinto aRefPtr, resurrecting the zero-refcount object. - Double destruction
When the dispatched runnable completes and its
RefPtris released, theRasterImageis destroyed a second time, freeing memory still referenced elsewhere. - Exploit the freed object
Reclaim the freed allocation to influence subsequent dereferences of the stale
RasterImage.
Impact Assessment
RasterImage in the content/graphics context where ImageLib runs. Successful reclamation of the freed memory can corrupt heap state and potentially lead to controlled memory access; the precondition is triggering surface discard concurrently with the image’s final release, which web content can influence via navigation, DOM manipulation, and animated/large images that stress the surface cache.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifimage/RasterImage.cpp |
modified |
Files Changed
image/RasterImage.cppimage/RasterImage.h
Audit Directions
- Strong self-capture in deferred workAudit lambdas/runnables that capture
thisorRefPtr<Self>on paths reachable from destructors or decoder-teardown; prefer routing liveness through a separately owned tracker. - Flag-based destruction guardsTreat
Atomic<bool>/mIsBeingDestroyed-style check-then-act guards as suspect, since the gap between the read and taking a reference is a classic resurrection race. - Surface-cache callbacksReview other
image/callbacks such as surface-discard and decode-completion notifications for the same pattern of extending object lifetime during shutdown.
Patch
diff --git a/image/RasterImage.cpp b/image/RasterImage.cpp
index 25cd5d034db..318d72e061c 100644
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -76,8 +76,6 @@ RasterImage::RasterImage(nsIURI* aURI /* = nullptr */)
//******************************************************************************
RasterImage::~RasterImage() {
- mIsBeingDestroyed = true;
-
// Make sure our SourceBuffer is marked as complete. This will ensure that any
// outstanding decoders terminate.
if (!mSourceBuffer->IsComplete()) {
@@ -486,19 +484,21 @@ RasterImage::WillDrawOpaqueNow() {
void RasterImage::OnSurfaceDiscarded(const SurfaceKey& aSurfaceKey) {
MOZ_ASSERT(mProgressTracker);
- if (mIsBeingDestroyed) {
- return;
- }
-
bool animatedFramesDiscarded =
aSurfaceKey.Playback() == PlaybackType::eAnimated;
nsCOMPtr<nsIEventTarget> eventTarget = do_GetMainThread();
- RefPtr<RasterImage> image = this;
- nsCOMPtr<nsIRunnable> ev =
- NS_NewRunnableFunction("RasterImage::OnSurfaceDiscarded", [=]() -> void {
- image->OnSurfaceDiscardedInternal(animatedFramesDiscarded);
+ RefPtr<ProgressTracker> progressTracker = mProgressTracker;
+ nsCOMPtr<nsIRunnable> ev = NS_NewRunnableFunction(
+ "RasterImage::OnSurfaceDiscarded",
+ [progressTracker, animatedFramesDiscarded]() -> void {
+ RefPtr<Image> image = progressTracker->GetImage();
+ if (!image) {
+ return;
+ }
+ static_cast<RasterImage*>(image.get())
+ ->OnSurfaceDiscardedInternal(animatedFramesDiscarded);
});
eventTarget->Dispatch(ev.forget(), NS_DISPATCH_NORMAL);
}
diff --git a/image/RasterImage.h b/image/RasterImage.h
index 7b7bba84196..30e90640dfb 100644
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -380,8 +380,6 @@ class RasterImage final : public ImageResource,
// This is currently only used for statistics
int32_t mDecodeCount;
- Atomic<bool> mIsBeingDestroyed{false};
-
#ifdef DEBUG
uint32_t mFramesNotified;
#endif