CVE-2026-8581
Overview
Background
- `MojoVideoDecoder`
- the renderer-side media client that proxies decode requests over Mojo to a remote (GPU-process) video decoder.
- `MojoVideoDecoder::Stop`
- the handler invoked on Mojo disconnection that drains pending callbacks before dropping outstanding work.
- On-stack `WeakPtr`
- a
base::WeakPtr<MojoVideoDecoder>captured locally so the method can detect whether a callback reentrantly destroyedthis. - `weak_factory_.InvalidateWeakPtrs`
- the call that invalidates all outstanding weak pointers to the object.
Root Cause Analysis
In the pre-fix MojoVideoDecoder::Stop (media/mojo/clients/mojo_video_decoder.cc:462-493), the method captures weak_this = weak_this_ at line 472, then runs init_cb_ followed by if (!weak_this) return; (lines 474-478), and runs each entry of pending_decodes_ with its own if (!weak_this) return; (lines 480-485). The reset callback, however, was executed at lines 488-489 (if (reset_cb_) std::move(reset_cb_).Run();) with no following liveness check before reaching weak_factory_.InvalidateWeakPtrs(); at line 492. The invariant is “after running any callback that may reentrantly destroy this, the method must not touch member state unless the on-stack WeakPtr confirms the object is still alive.” Because reset_cb_ can synchronously destroy the decoder, control could return to line 492 and invoke weak_factory_ on freed memory — a use-after-free.
The fix wraps the reset callback so it is immediately followed by if (!weak_this) { return; }, matching the pattern already used for init_cb_ and pending_decodes_; it also brackets the init_cb_ check inside the if (init_cb_) block. The change TIGHTENS the method by adding the previously missing post-callback liveness check.
Stop not guarded by the on-stack WeakPtr check, so if it destroyed the object the subsequent weak_factory_.InvalidateWeakPtrs() ran on freed memory; the fix adds the same if (!weak_this) return; guard after reset_cb_.Attack Path
- Arm a pending reset
A reset is in flight on the
MojoVideoDecoderso thatreset_cb_is populated whenStopruns. (inferred) - Drop the connection
The remote end disconnects the Mojo pipe, invoking
Stopas the disconnect handler (mojo_video_decoder.cc:462). - Reentrant destruction
Running
std::move(reset_cb_).Run()at line 489 synchronously destroysthisthrough the reset-completion chain. (inferred) - Use after free
With no guard after the reset callback pre-fix, control reaches
weak_factory_.InvalidateWeakPtrs()at line 492 operating on the freed object.
Impact Assessment
MojoVideoDecoder in the process that hosts the media client (the renderer per the reporter). The advisory rates this medium severity and not exploited in the wild. The reporter’s escalation to a double-free and remote code execution goes beyond what the shown code demonstrates and is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/mojo/clients/mojo_video_decoder.cc |
modified | |
formedia/mojo/clients/mojo_video_decoder.cc |
modified |
Files Changed
media/mojo/clients/mojo_video_decoder.cc
Audit Directions
- Post-callback livenessIn any handler that captures an on-stack
WeakPtrand runs multiple callbacks, confirm a!weak_thischeck follows every callback that could reentrantly destroy the object, not just some. - Disconnect handlersTreat Mojo disconnect/error handlers as reentrancy-prone and audit them for member access after running client-supplied callbacks.
- Trailing member useFlag a final member-method call (e.g.
weak_factory_.InvalidateWeakPtrs()) that executes after a reentrant callback with no intervening validity guard.
Patch
From d193b6540a3aa70d8154d0590a11c5782dca2edf Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
Date: Tue, 31 Mar 2026 10:37:26 -0700
Subject: [PATCH] Check WeakPtr more in MojoVideoDecoder::Stop()
No known issues, but this unifies the WeakPtr checks across all
the possible callbacks which could trigger destruction.
Fixed: 497292072
Change-Id: I3b9cf94a0f6b3982ee7bf08282b43f680a3dd9c2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7712494
Commit-Queue: Dale Curtis <dalecurtis@chromium.org>
Auto-Submit: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1607962}
---
diff --git a/media/mojo/clients/mojo_video_decoder.cc b/media/mojo/clients/mojo_video_decoder.cc
index 611fec4..87d3d52c 100644
--- a/media/mojo/clients/mojo_video_decoder.cc
+++ b/media/mojo/clients/mojo_video_decoder.cc
@@ -467,26 +467,30 @@
// |init_cb_| is likely to reentrantly destruct |this|, so we check for that
// using an on-stack WeakPtr.
- // TODO(sandersd): Update the VideoDecoder API to be explicit about what
- // reentrancy is allowed, and therefore which callbacks must be posted.
- base::WeakPtr<MojoVideoDecoder> weak_this = weak_this_;
+ auto weak_this = weak_this_;
- if (init_cb_)
+ if (init_cb_) {
std::move(init_cb_).Run(DecoderStatus::Codes::kDisconnected);
-
- if (!weak_this)
- return;
+ if (!weak_this) {
+ return;
+ }
+ }
for (auto& pending_decode : pending_decodes_) {
// It would be ideal if we could get a reason for the interruption.
std::move(pending_decode.second).Run(DecoderStatus::Codes::kDisconnected);
- if (!weak_this)
+ if (!weak_this) {
return;
+ }
}
pending_decodes_.clear();
- if (reset_cb_)
+ if (reset_cb_) {
std::move(reset_cb_).Run();
+ if (!weak_this) {
+ return;
+ }
+ }
// Drop any outstanding callbacks.
weak_factory_.InvalidateWeakPtrs();
Original Bug Report
Potential Use-After-Free and Double Free in MojoVideoDecoder::Stop()
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A synchronous destruction chain triggered by a compromised GPU process disconnecting a Mojo pipe during a track change causes a Use-After-Free in MojoVideoDecoder::Stop(). Accessing weak_factory_ on the freed object subsequently causes a deterministic Double Free of a PartitionAlloc object, potentially allowing Remote Code Execution in the Renderer process.
Affected files:
media/mojo/clients/mojo_video_decoder.cc
Estimated timestamp from git blame: 2024-07-15
Description
A Use-After-Free (UAF) vulnerability exists in media::MojoVideoDecoder::Stop(), which acts as the Mojo disconnect handler for the mojom::VideoDecoder pipe. If the GPU process drops the connection while a reset is pending, a synchronous callback chain can destroy the MojoVideoDecoder instance while it is still executing Stop(). This UAF is cleanly transformed into a deterministic PartitionAlloc Double Free, offering a strong primitive for Remote Code Execution (RCE).
In MojoVideoDecoder::Stop(), the handler executes several callbacks. While the first two (init_cb_ and pending_decodes_) are followed by a local WeakPtr liveness check (if (!weak_this) return;), the execution of reset_cb_ lacks this check. If reset_cb_ destroys this, the subsequent call to weak_factory_.InvalidateWeakPtrs() executes on freed memory.
The Double Free Mechanism
- At the start of
Stop(), an on-stackWeakPtris created:base::WeakPtr<MojoVideoDecoder> weak_this = weak_this_;. This increments the reference count of theWeakReference::Flagto 2. Stop()callsstd::move(reset_cb_).Run(). During a track change, this propagates throughDecoderStream::OnDecoderReset,VideoRendererImpl::OnVideoDecoderStreamResetDone, andRendererImpl::CleanUpTrackChange.- This chain culminates in
RendererImpl::ReinitializeVideoRenderer, which synchronously assigns a newstd::unique_ptr<VideoDecoderStream>. The old stream is destroyed, which synchronously destroys theMojoVideoDecoder(this). - The
MojoVideoDecoderdestructor destroys itsweak_factory_. Thescoped_refptrholding theWeakReference::Flagdrops its refcount to 1. TheMojoVideoDecodermemory is then freed to PartitionAlloc. - Execution returns to
Stop(). The method callsweak_factory_.InvalidateWeakPtrs();on the freed object. - This function instantiates a new
Flagand uses thescoped_refptrassignment operator on the freed memory. It overwrites the oldFlagpointer and drops the oldFlag’s refcount from 1 to 0, causing the oldFlagto be deleted (First Free). Stop()returns. The on-stackweak_thisgoes out of scope, dropping the oldFlag’s refcount from 0 to -1, resulting in a Double Free of theFlagobject.
Note: MiraclePtr (BackupRefPtr) does not protect against this because the UAF occurs via the this pointer already loaded into the execution frame, and the Double Free occurs on a secondary Flag object.
Potential Attacker Steps
(Note: This analysis is based on code paths; a fully weaponized proof-of-concept has not been run by our tooling.)
- An attacker compromises the GPU process.
- The attacker hosts a malicious webpage that forces the victim’s Renderer to initiate a video track change (e.g., via MSE), flushing the video stream and generating a
ResetIPC to the GPU. - The compromised GPU process intentionally disconnects the
mojom::VideoDecoderpipe instead of responding to theResetIPC. - The Renderer executes the disconnect handler (
Stop()), triggering the synchronous destruction chain and subsequent Double Free of theWeakReference::Flag. - The attacker grooms the Renderer’s PartitionAlloc heap from the GPU process (e.g., using other Mojo interfaces) to overlap the double-freed memory, gaining an arbitrary read/write primitive and achieving RCE to bypass Site Isolation.
Suggested Fix
Add a liveness check immediately after executing reset_cb_ in MojoVideoDecoder::Stop(), matching the pattern used for the other callbacks in the same method:
if (reset_cb_)
std::move(reset_cb_).Run();
if (!weak_this)
return;
// Drop any outstanding callbacks.
weak_factory_.InvalidateWeakPtrs();
Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.