Heap-use-after-free in [@ ImageDecoder::Close] via missing kungFuDeathGrip in [@ WorkerShutdownWatcher::OnShutdown]
Categories
(Core :: Audio/Video: Web Codecs, defect, P1)
Tracking
()
People
(Reporter: bugmon, Assigned: alwu)
References
(Regression)
Details
(6 keywords, Whiteboard: [prefs-checked][adv-esr140.9.1+r][adv-main149.0.2+r])
Attachments
(9 files)
|
2.46 KB,
text/plain
|
Details | |
|
13.25 KB,
text/plain
|
Details | |
|
51 bytes,
text/plain
|
Details | |
|
1.03 KB,
text/html
|
Details | |
|
1.77 KB,
patch
|
dveditz
:
sec-approval-
|
Details | Diff | Splinter Review |
|
12.33 KB,
text/plain
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
dveditz
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
Summary
Heap-use-after-free in dom/media/webcodecs/ImageDecoder.cpp line 1078 (read of mComplete on freed this). Root cause: WorkerShutdownWatcher::OnShutdown() in dom/media/systemservices/MediaUtils.cpp line 268 invokes mConsumer->OnShutdown() through a raw ShutdownConsumer* pointer without taking a strong reference to the consumer. The consumer (ImageDecoder) can be freed by GC→CC→FreeSnowWhite during the nested call because the only remaining strong reference (ImageTrackList::mDecoder) is cleared inside Close() before JS-allocating code runs.
Affected Code
File: dom/media/systemservices/MediaUtils.cpp, lines 256-276
File: dom/media/systemservices/MediaUtils.h, line 254
// MediaUtils.h:254 — the raw pointer
ShutdownConsumer* mConsumer;
// MediaUtils.cpp:256-257 — lambda keeps the WATCHER alive, NOT the consumer
mWorkerRef = dom::WeakWorkerRef::Create(
aWorkerPrivate, [self = RefPtr{this}] { self->OnShutdown(); });
// MediaUtils.cpp:266-271 — no kungFuDeathGrip
void OnShutdown() {
if (mConsumer) {
mConsumer->OnShutdown(); // ← calls through raw pointer; consumer can be
// freed mid-call by GC→CC→FreeSnowWhite
}
Destroy();
}
ShutdownConsumer (MediaUtils.h:221-228) is a pure abstract interface with no refcounting — it cannot be held directly in a RefPtr. The [self = RefPtr{this}] capture keeps the WorkerShutdownWatcher alive for the duration of the callback, but the watcher only holds a raw pointer to the ImageDecoder. Nothing on the stack pins the ImageDecoder.
Why ImageDecoder becomes freeable mid-call — ImageDecoder::Close:
if (mTracks) {
mTracks->MaybeRejectReady(aResult);
mTracks->Destroy(); // ← clears ImageTrackList::mDecoder (RefPtr<ImageDecoder>)
// → last non-wrapper strong ref dropped
}
if (!mComplete) {
aResult.RejectTo(mCompletePromise); // ← JS alloc (DOMException wrapper) — can trigger GC
// GC → wrapper finalized → refcount=0 → SnowWhite
// Worker GC_END → nsCycleCollector_collect → FreeSnowWhite
// → `this` FREED HERE
mComplete = true; // ← write-after-free
}
if (mShutdownWatcher) { // ← read-after-free
mShutdownWatcher->Destroy();
mShutdownWatcher = nullptr;
}
On workers, WorkerJSRuntime::CustomGCCallback runs nsCycleCollector_collect synchronously at JSGC_END, and nsCycleCollector::BeginCollection runs FreeSnowWhite synchronously. So a GC triggered from inside RejectTo frees the object before the GC call returns.
Correct Pattern
void ImageDecoder::OnShutdown() {
RefPtr<ImageDecoder> kungFuDeathGrip(this); // pin `this` across Close()
Close(MediaResult(NS_ERROR_DOM_ABORT_ERR, "Shutdown"_ns));
}
Exploit Chain
- Web content creates a dedicated
Worker. - Inside the worker, creates
new ImageDecoder({ type: "image/png", data: ... })—ImageDecoder::Constructor→media::ShutdownWatcher::Create(this)creates aWorkerShutdownWatcherholding a rawShutdownConsumer*to the decoder and registers aWeakWorkerRefcallback capturing[self = RefPtr{WorkerShutdownWatcher}]. - Worker waits for any pending MozPromise resolutions so their
[self = RefPtr{this}]lambdas drop their refs to the decoder. - Worker drops
decoder = null. TheImageDecoder's JS wrapper is now unreachable but not yet collected. Remaining strong refs: (a) unreachable JS wrapper, (b)ImageTrackList::mDecoder(RefPtr<ImageDecoder>at ImageTrackList.h:91). - Main thread calls
worker.terminate()→WorkerPrivate::NotifyInternal(Canceling)→NotifyWorkerRefs→WeakWorkerRef::Notifyinvokes the stored lambda. - Lambda calls
WorkerShutdownWatcher::OnShutdown()→mConsumer->OnShutdown()(raw pointer, no stack ref) →ImageDecoder::OnShutdown()→ImageDecoder::Close(). - Inside
Close():mTracks->Destroy()setsmDecoder = nullptr. Refcount now = 1 (unreachable wrapper only). - Next JS allocation —
aResult.RejectTo(mCompletePromise)at line 1079 (when!mComplete), which callsPromise::MaybeRejectWith*→ allocates a JSDOMExceptionwrapper — triggers GC under heap pressure. - GC finalizes the unreachable
ImageDecoderwrapper → deferred Release → refcount = 0 → object enters SnowWhite set. - GC end →
WorkerJSRuntime::CustomGCCallback(JSGC_END)→nsCycleCollector_collect→BeginCollection→FreeSnowWhite→SnowWhiteKiller::~SnowWhiteKiller→delete ImageDecoder. RejectToreturns.mComplete = trueat line 1080 writes to freed memory. Subsequentif (mShutdownWatcher)at line 1083 reads freed memory.
Steps to Reproduce
- Pref:
dom.media.webcodecs.image-decoder.enabled = true - Timing patch: Apply the provided patch to
/firefox/dom/media/webcodecs/ImageDecoder.cppwhich insertsJS::NonIncrementalGCimmediately aftermTracks->Destroy(). This makes the natural GC window (normally atRejectTo(mCompletePromise)under heap pressure) deterministic. Rebuild. - Load
test.html— crashes on the first worker iteration.
Security Impact
- Severity: High (sec-high). Heap-use-after-free on a 392-byte cycle-collected DOM object in the content process.
- Attacker capability: Write-after-free (
mComplete = true) on a freed region plus virtual call (mShutdownWatcher->Destroy()) through a member of freed memory. If the freed 392-byte region is reallocated between the free and the continued execution (possible becauseFreeSnowWhitefrees multiple objects andRejectToperforms further allocations), the attacker can corrupt a replacement object, and themShutdownWatcher->Destroy()virtual call reads a vtable pointer from attacker-controlled memory. - Preconditions: (1)
dom.media.webcodecs.image-decoder.enabledenabled. (2) Worker thread — ImageDecoder must be used in a Worker. (3) JS wrapper unreachable at terminate() time — attacker-controllable. (4)!mCompleteat shutdown — achievable by terminating before decode finishes. (5) GC fires duringRejectTo— timing-dependent but reachable under heap pressure, which attacker can induce.
ASAN Report
==509518==ERROR: AddressSanitizer: heap-use-after-free on address 0x7d028bb10dbc at pc 0x7bc26a009205 bp 0x7bc235aec3f0 sp 0x7bc235aec3e8
READ of size 1 at 0x7d028bb10dbc thread T28
#0 0x7bc26a009204 in mozilla::dom::ImageDecoder::Close(mozilla::MediaResult const&) /firefox/dom/media/webcodecs/ImageDecoder.cpp:1100:8
#1 0x7bc26a013ea5 in mozilla::dom::ImageDecoder::OnShutdown() /firefox/dom/media/webcodecs/ImageDecoder.cpp:1120:3
#2 0x7bc26a013ea5 in non-virtual thunk to mozilla::dom::ImageDecoder::OnShutdown() /firefox/dom/media/webcodecs/ImageDecoder.cpp:0:0
#3 0x7bc269e39e31 in mozilla::media::WorkerShutdownWatcher::OnShutdown() /firefox/dom/media/systemservices/MediaUtils.cpp:268:18
#4 0x7bc269e39e31 in mozilla::media::WorkerShutdownWatcher::Initialize(mozilla::dom::WorkerPrivate*)::'lambda'()::operator()() const /firefox/dom/media/systemservices/MediaUtils.cpp:257:55
#10 0x7bc26c25c2cc in mozilla::dom::WorkerRef::Notify() /firefox/dom/workers/WorkerRef.cpp:102:3
#11 0x7bc26c25c8f5 in mozilla::dom::WeakWorkerRef::Notify() /firefox/dom/workers/WorkerRef.cpp:137:14
#12 0x7bc26c240369 in mozilla::dom::WorkerPrivate::NotifyWorkerRefs(mozilla::dom::WorkerStatus) /firefox/dom/workers/WorkerPrivate.cpp:5035:16
#13 0x7bc26c2449db in mozilla::dom::WorkerPrivate::NotifyInternal(mozilla::dom::WorkerStatus) /firefox/dom/workers/WorkerPrivate.cpp:5843:5
#14 0x7bc26c25fd8a in mozilla::dom::WorkerThreadRunnable::Run() /firefox/dom/workers/WorkerRunnable.cpp:438:12
#15 0x7bc26c2440cb in mozilla::dom::WorkerPrivate::ProcessAllControlRunnablesLocked() /firefox/dom/workers/WorkerPrivate.cpp:4684:11
#16 0x7bc26c241367 in mozilla::dom::WorkerPrivate::DoRunLoop(JSContext*) /firefox/dom/workers/WorkerPrivate.cpp:3857:21
#17 0x7bc26c206733 in mozilla::dom::workerinternals::(anonymous namespace)::WorkerThreadPrimaryRunnable::Run() /firefox/dom/workers/RuntimeService.cpp:2307:42
0x7d028bb10dbc is located 380 bytes inside of 392-byte region [0x7d028bb10c40,0x7d028bb10dc8)
freed by thread T28 here:
#0 0x556c680ca6f6 in __interceptor_free _asan_rtl_:3
#1 0x7bc2601dd933 in SnowWhiteKiller::~SnowWhiteKiller() /firefox/xpcom/base/nsCycleCollector.cpp:2657:7
#2 0x7bc2601c1d51 in nsCycleCollector::FreeSnowWhite(bool) /firefox/xpcom/base/nsCycleCollector.cpp:2848:3
#3 0x7bc2601c8f9c in nsCycleCollector::BeginCollection(mozilla::CCReason, ccIsManual, nsICycleCollectorListener*) /firefox/xpcom/base/nsCycleCollector.cpp:3907:3
#4 0x7bc2601c7f4d in nsCycleCollector::Collect(mozilla::CCReason, ccIsManual, JS::SliceBudget&, nsICycleCollectorListener*, bool) /firefox/xpcom/base/nsCycleCollector.cpp:3703:9
#5 0x7bc2601ccaef in nsCycleCollector_collect(mozilla::CCReason, nsICycleCollectorListener*) /firefox/xpcom/base/nsCycleCollector.cpp:4268:28
#6 0x7bc26c20749d in mozilla::dom::workerinternals::(anonymous namespace)::WorkerJSRuntime::CustomGCCallback(JSGCStatus) /firefox/dom/workers/RuntimeService.cpp:915:11
#7 0x7bc26ffc4822 in js::gc::GCRuntime::callGCCallback(JSGCStatus, JS::GCReason) const /firefox/js/src/gc/GC.cpp:1746:3
#13 0x7bc26ffcd8b6 in JS::NonIncrementalGC(JSContext*, JS::GCOptions, JS::GCReason) /firefox/js/src/gc/GCAPI.cpp:300:21
#14 0x7bc26a009010 in mozilla::dom::ImageDecoder::Close(mozilla::MediaResult const&) /firefox/dom/media/webcodecs/ImageDecoder.cpp:1095:7
#15 0x7bc26a013ea5 in mozilla::dom::ImageDecoder::OnShutdown() /firefox/dom/media/webcodecs/ImageDecoder.cpp:1120:3
#17 0x7bc269e39e31 in mozilla::media::WorkerShutdownWatcher::OnShutdown() /firefox/dom/media/systemservices/MediaUtils.cpp:268:18
previously allocated by thread T28 here:
#3 0x7bc26a00bd4b in RefPtr<mozilla::dom::ImageDecoder> mozilla::MakeRefPtr<mozilla::dom::ImageDecoder, ...>
#4 0x7bc26a00bd4b in mozilla::dom::ImageDecoder::Constructor(mozilla::dom::GlobalObject const&, mozilla::dom::ImageDecoderInit const&, mozilla::ErrorResult&) /firefox/dom/media/webcodecs/ImageDecoder.cpp:531:23
#5 0x7bc265c885ad in mozilla::dom::ImageDecoder_Binding::_constructor(JSContext*, unsigned int, JS::Value*) /firefox/obj-x86_64-pc-linux-gnu/dom/bindings/./ImageDecoderBinding.cpp:1582:58
SUMMARY: AddressSanitizer: heap-use-after-free (/firefox/obj-x86_64-pc-linux-gnu/dist/bin/libxul.so+0x1caa8204)
Suggested Fix
Add a kungFuDeathGrip in ImageDecoder::OnShutdown():
void ImageDecoder::OnShutdown() {
// WorkerShutdownWatcher::OnShutdown() calls us through a raw
// ShutdownConsumer* pointer and does not hold a strong ref. Close()
// calls mTracks->Destroy() which drops the ImageTrackList→ImageDecoder
// back-ref, and then calls RejectTo(mCompletePromise) which can GC.
// On workers, GC synchronously runs CC→FreeSnowWhite, which can free
// `this` while Close() is still on the stack.
RefPtr<ImageDecoder> kungFuDeathGrip(this);
Close(MediaResult(NS_ERROR_DOM_ABORT_ERR, "Shutdown"_ns));
}
The same audit should be performed for MainShutdownWatcher::Observe() at MediaUtils.cpp:228-230, which has the identical pattern.
| Reporter | ||
Comment 1•6 months ago
|
||
| Reporter | ||
Comment 2•6 months ago
|
||
| Reporter | ||
Comment 3•6 months ago
|
||
| Reporter | ||
Comment 4•6 months ago
|
||
| Reporter | ||
Comment 5•6 months ago
|
||
| Reporter | ||
Comment 6•6 months ago
|
||
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Comment 8•6 months ago
|
||
Pref check: dom.media.webcodecs.image-decoder.enabled defaults to true on all channels (StaticPrefList.yaml, no ifdef) — supported configuration. The timing patch is a repro aid for the GC race, not a gate.
Updated•6 months ago
|
| Assignee | ||
Updated•6 months ago
|
| Assignee | ||
Comment 9•6 months ago
|
||
Add a temporary RefPtr at the start of Close() to keep the ImageDecoder
alive through its own execution. Close() calls mTracks->Destroy() which
clears ImageTrackList::mDecoder — potentially the last C++ strong reference
— before calling RejectTo() which may allocate a JS object. On workers, this
allocation can trigger a GC cycle that runs FreeSnowWhite synchronously,
freeing the object before Close() returns.
| Assignee | ||
Updated•6 months ago
|
| Assignee | ||
Comment 10•6 months ago
|
||
Comment on attachment 9557763 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Moderate. The patch reveals a UAF triggered when a Worker containing an ImageDecoder is terminated — all reproducible with standard web APIs (Worker, ImageDecoder, worker.terminate()). Reliable triggering requires inducing a GC at the right moment via heap pressure, which is achievable but not trivial.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: firefox after 133
- If not all supported branches, which bug introduced the flaw?: Bug 1927020
- Do you have backports for the affected branches?: Yes
- If not, how different, hard to create, and risky will they be?:
- How likely is this patch to cause regressions; how much testing does it need?: Low. The fix adds a temporary strong reference with no observable behavior change. Standard try CI is sufficient.
- Is the patch ready to land after security approval is given?: No
- Is Android affected?: Yes
| Assignee | ||
Updated•6 months ago
|
Comment 11•6 months ago
|
||
- Is the patch ready to land after security approval is given?: No
That's not the usual answer... mis-click?
Updated•6 months ago
|
Comment 12•6 months ago
|
||
you don't intend to land the POC's "inject a compromise" patch, right? I'm confused why you're requesting sec-approval on those
Updated•6 months ago
|
Comment 13•6 months ago
|
||
Comment on attachment 9557763 [details]
(secure)
sec-approval+ to land now and request uplifts
| Assignee | ||
Comment 14•6 months ago
|
||
(In reply to Daniel Veditz [:dveditz] from comment #12)
you don't intend to land the POC's "inject a compromise" patch, right? I'm confused why you're requesting sec-approval on those
No, I only did it on my own patch, not sure why bugzilla add sec-approval on those PoCs.
| Assignee | ||
Comment 15•6 months ago
•
|
||
(In reply to Daniel Veditz [:dveditz] from comment #11)
- Is the patch ready to land after security approval is given?: No
That's not the usual answer... mis-click?
Ah, sorry for typo. Yes, the patch can land it now.
Comment 16•6 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Security fix.
- Code covered by automated testing?: no
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Low. The fix adds a temporary strong reference with no observable behavior change.
- String changes made/needed?: No
- Is Android affected?: yes
| Assignee | ||
Comment 17•6 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D289679
Comment 18•6 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: Security fix.
- Code covered by automated testing?: no
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Low. The fix adds a temporary strong reference with no observable behavior change.
- String changes made/needed?: No
- Is Android affected?: yes
| Assignee | ||
Comment 19•6 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D289679
Updated•6 months ago
|
Comment 20•6 months ago
|
||
Comment 21•6 months ago
|
||
Updated•6 months ago
|
Updated•6 months ago
|
Comment 22•6 months ago
|
||
| uplift | ||
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Comment 23•6 months ago
|
||
| uplift | ||
Updated•6 months ago
|
Comment 24•6 months ago
|
||
| uplift | ||
Updated•6 months ago
|
Comment 25•6 months ago
|
||
| 140.9.1 uplift | ||
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Comment 26•5 months ago
|
||
a month ago, dveditz placed a reminder on the bug using the whiteboard tag [reminder-test 2026-05-05] .
alwu, please refer to the original comment to better understand the reason for the reminder.
| Assignee | ||
Updated•5 months ago
|
Updated•4 months ago
|
Description
•