
CVE-2026-74943, Use after free in Firefox RasterImage (sec-high)
This directory holds an honest reproduction scaffold for a cross-thread
use-after-free in mozilla::image::RasterImage::OnSurfaceDiscarded. It is meant
to be run against a locally downloaded vulnerable Firefox build using only
synthetic image data. It never touches any live or third party system.
Matching PoC repository: https://github.com/defineid/Revenant
Writeup: writeup-rasterimage-uaf.html in this portfolio.
These were confirmed by disassembling the vulnerable build (libxul.so, Firefox
140.9.0esr) during the original research; the committed screenshot
assets/writeup-uaf-disasm.png is that analysis. Function names come from the
official Mozilla Breakpad symbol file for that exact build; the shipped
libxul.so is stripped. They were not re-disassembled in the environment
that assembled this PoC package (see Verification status).
SurfaceCacheImpl::Remove notifies the owning image that a decoded surface was
discarded by calling OnSurfaceDiscarded through a raw, non-owning
ImageKey pointer, on a decoder / ImageIO thread, while holding the surface
cache mutex. In the vulnerable build that handler:
this+0xC0 early (cmp BYTE PTR [rdi+0xc0],0x0).
The writeup identifies this as mIsBeingDestroyed, an earlier mitigation.this by incrementing the RasterImage refcount at
this+0x40 (lock inc QWORD PTR [rbx+0x40]). This is a plain
add-ref, not an atomic add-ref-if-nonzero.moz_xmalloc(0x20)-allocates a RunnableFunction that captures this
(mov QWORD PTR [rax+0x10],rbx), dispatches it to a global event target
(call QWORD PTR [rcx+0x28]), then releases the local reference
(lock dec QWORD PTR [rbx+0x40]).Because reading the flag and incrementing the count are two separate
operations against a lock-free refcount, they can straddle the moment the main
thread drops the last reference. The decoder thread then AddRefs from zero,
resurrecting an object whose destructor has already committed. When the
destructor finishes and frees the RasterImage, the queued runnable later runs
on the main thread and reads freed storage.
The fix stops referencing the image directly: OnSurfaceDiscarded captures a
strong reference to the separately refcounted ProgressTracker and recovers the
image on the main thread through the tracker's weak back-reference, promoting it
with an atomic add-ref-if-live; the mIsBeingDestroyed flag is removed.
An ordinary web page in the content process drives surface-cache pressure (many discardable decoded surfaces) while releasing the frame that owns the image requests. No click, permission, extension, or pref change is required. The race is between the decoder thread's discard notification and the main thread's final release of the same image.
https://ftp.mozilla.org/pub/firefox/releases/140.9.0esr/.
An ASAN build (Nightly asan or a local --enable-address-sanitizer
build) makes the use-after-free observable as a clean report instead of an
occasional silent crash.<canvas> at runtime; it loads nothing from the network.Open trigger.html in the vulnerable build, ideally from a file:// path or a
local static server you control:
/path/to/firefox --new-instance --profile ./poc-profile ./trigger.html
By default the driver runs a tight microtask loop for maximum decode/release
overlap. Appending ?tick=N to the URL switches it to a yielding setTimeout(N)
loop that churns just as hard but lets the event loop breathe, which is what
allows an automated driver to step and screenshot the page while it runs. The
committed trigger-running.png was captured exactly that way, driving the local
140.11.0esr build through Marionette with ?tick=4: it shows the harness live
(cycles and teardowns climbing). That build is patched, so no crash occurs; the
image only proves the driver runs. On headless the off-screen images are not
painted, so the "images decoded" counter stays at 0 there; a headed vulnerable
build decodes them and is where the race is exercised.
Let the page run. It repeatedly decodes many distinct large images (to fill the surface cache and force eviction) while tearing down the DOM subtree that owns them (to drive the owning image's final release). This maximizes the overlap of "surface discarded" on the decoder thread with "last release" on the main thread. Races are probabilistic; leave it running.
heap-use-after-free whose READ frame is inside
RasterImage::OnSurfaceDiscardedInternal / the dispatched runnable, with the
freed frame in RasterImage::Release teardown. Without ASAN it may surface
as an intermittent content-process crash.ProgressTracker weak back-reference and drops the
notification when destruction has already committed.libxul.so (140.9.0esr) was disassembled and every binary-level
fact in the first table above was confirmed byte-for-byte against the official
Mozilla symbol file; the committed assets/writeup-uaf-disasm.png is that
work. The three structural claims from the writeup (early this+0xC0 flag
read; lock inc AddRef at this+0x40; moz_xmalloc(0x20) runnable that
captures this, dispatches, then Releases) all hold.This targets a locally downloaded build with synthetic, self-generated data, for a writeup of an already-patched CVE. It does not attack any live or third party service and contains no exploitation primitive beyond triggering the defect.
| Fact | Evidence | Source |
|---|
Vulnerable function is RasterImage::OnSurfaceDiscarded(SurfaceKey const&) | FUNC 3eba200 16c 0 mozilla::image::RasterImage::OnSurfaceDiscarded(...) | Mozilla symbol server .sym |
Early byte read at this+0xC0 (writeup: mIsBeingDestroyed) | 3eba20a: cmp BYTE PTR [rdi+0xc0],0x0 with rdi = this | objdump -d libxul.so |
AddRef on the RasterImage refcount at this+0x40 | 3eba25a: lock inc QWORD PTR [rbx+0x40] (issued twice), rbx = this | objdump -d libxul.so |
A 0x20-byte runnable is allocated and captures this | 3eba264: mov edi,0x20 → call moz_xmalloc@plt; 3eba280: mov QWORD PTR [rax+0x10],rbx | objdump -d libxul.so |
The runnable is dispatched, then this is Released | 3eba298: call QWORD PTR [rcx+0x28] (virtual dispatch); 3eba29b: lock dec QWORD PTR [rbx+0x40] | objdump -d libxul.so |
Refcount reaching 0 runs ~RasterImage then free | je 3eba345 → call 3ebbe90 <~RasterImage> → call free@plt | objdump + .sym |
Dispatched runnable captures the OnSurfaceDiscarded::$_0 lambda | FUNC 5279d90 ... RunnableFunction<...OnSurfaceDiscarded(...)::$_0>::Run() | Mozilla symbol server .sym |
| Field | Value |
|---|
| CVE | CVE-2026-74943 |
| Component | Core / Graphics: ImageLib |
| Class | Use-After-Free (CWE-416) |
| Severity | sec-high (web-reachable memory safety) |
| Fixed in | Firefox 154 · ESR 140.14 |
| Bug | bugzilla.mozilla.org/show_bug.cgi?id=2057308 (security restricted) |