CVE-2026-8544
Overview
Background
- `MediaFoundationVideoEncodeAccelerator` (MF VEA)
- Chrome’s Windows hardware video encoder wrapper (runs in the GPU process) that drives the Media Foundation async encode model.
- `IMFAsyncCallback`
- a COM interface Media Foundation calls (via
Invoke) on an arbitrary OS thread when an encode event is ready; the object is registered throughevent_generator_->BeginGetEvent(callback, state). - Fake COM refcounting
- the MF VEA implemented
AddRef/Releasebacked byasync_callback_ref_that did not actually keep the object alive, so registeringthisas the callback did not pin the encoder’s lifetime. - `base::WeakPtr`
- a pointer that becomes null when its referent is destroyed, used to safely abandon queued tasks targeting a freed object.
Root Cause Analysis
The encoder registered its own this pointer as the IMFAsyncCallback with Media Foundation, but its AddRef/Release were fake (tracking async_callback_ref_ without owning a reference), so the OS held a raw, non-owning pointer to the encoder. If the encoder was destroyed while an OS callback was still pending, Media Foundation would later call Invoke on the freed this, and Invoke dereferenced event_generator_ and posted to task_runner_ using the dead object — a use-after-free on a random OS thread.
The fix decouples the two lifetimes: a new true COM object, MFAsyncCallbackProxy, is registered instead of this, holds a base::WeakPtr<MediaFoundationVideoEncodeAccelerator>, and in Invoke posts MediaEventHandler through that weak pointer; if the encoder is gone the weak pointer is invalid and the posted task is dropped.
The patch also removes the encoder’s own IMFAsyncCallback implementation (GetParameters, Invoke, AddRef, Release, QueryInterface), the async_callback_ref_ member, and the DCHECK(async_callback_ref_.IsOne()) in the destructor, and updates the two BeginGetEvent call sites to pass proxy_callback_.Get() with error handling.
this as an async callback, so a pending OS callback could fire on a freed encoder; the fix routes callbacks through a real COM proxy that holds only a WeakPtr, dropping the task when the encoder is gone.Attack Path
- Begin async encoding
A page/renderer drives hardware H.264/HEVC encoding (e.g. via WebCodecs/WebRTC), causing the MF VEA to call
BeginGetEvent(this, ...)and leave an OS callback outstanding. - Destroy the encoder mid-flight
The encoder is torn down (encode stream closed/reconfigured) while Media Foundation still holds a pending event for the now-freed
this. - OS invokes freed object
Media Foundation later calls
Invokeon an arbitrary OS thread using the dangling encoder pointer, dereferencing freed members (event_generator_,task_runner_) — the use-after-free. - Memory-corruption leverage
Controlling heap reuse of the freed encoder could turn the stale
Invoke/posted-task access into a controlled corruption primitive (inferred; the patch demonstrates the UAF, not a specific exploit primitive).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
MFAsyncCallbackProxymedia/gpu/windows/media_foundation_video_encode_accelerator_win.cc |
modified | |
VideoRateControlWrappermedia/gpu/windows/media_foundation_video_encode_accelerator_win.h |
modified | |
TemporalScalabilityIdExtractormedia/gpu/windows/media_foundation_video_encode_accelerator_win.h |
modified | |
VEAEncodingLatencyMetricsHelpermedia/gpu/windows/media_foundation_video_encode_accelerator_win.h |
modified |
Files Changed
media/gpu/windows/media_foundation_video_encode_accelerator_win.ccmedia/gpu/windows/media_foundation_video_encode_accelerator_win.h
Audit Directions
- Non-owning `this` as OS/COM callbackAudit any place Chrome registers
thiswith an OS async API (Media Foundation, WASAPI, DirectX, XPC) without a real owning reference; decouple via a proxy +WeakPtr. - Fake/stubbed refcountingSearch for hand-rolled
AddRef/Releasethat do not actually extend lifetime (counters likeasync_callback_ref_); these create objects the OS believes it owns but Chrome frees. - Pending-callback vs. destruction racesReview destructors of objects registered as async sinks to confirm outstanding callbacks are cancelled/neutralized before (or safely survive) teardown, especially for callbacks delivered on arbitrary threads.
Patch
From 70f35b06c6330d6b46c0689a93545ef33041da79 Mon Sep 17 00:00:00 2001 From: Eugene Zemtsov <[email protected]> Date: Wed, 01 Apr 2026 01:41:59 -0700 Subject: [PATCH] media: Fix UAF bug in MediaFoundationVideoEncodeAccelerator The MF VEA used fake COM refcounting while registering its `this` pointer with the Media Foundation API. This led to a use after free bug if the encoder was deleted while an OS callback was pending. This change decouples the encoder's lifetime from the OS callback's lifetime by removing `IMFAsyncCallback` from the main class and introducing `MFAsyncCallbackProxy`, a true COM object. The proxy receives the events and safely posts them to the parent class via `base::WeakPtr`, ensuring that the main object is not accessed after it is freed. Bug: 497151750 Change-Id: I29d6969cc5db8d30e9e2ee2793d34ab326d560f3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7712934 Reviewed-by: Dale Curtis <[email protected]> Commit-Queue: Eugene Zemtsov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1608366} --- diff --git a/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc b/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc index 36c3960c..58983a5 100644 --- a/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc +++ b/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc @@ -11,6 +11,7 @@ #include <mfapi.h> #include <mferror.h> #include <mftransform.h> +#include <wrl/implements.h> #include <algorithm> #include <iterator> @@ -212,6 +213,67 @@ } // namespace +// A proxy class that implements IMFAsyncCallback and routes the events back to +// the MediaFoundationVideoEncodeAccelerator safely via a WeakPtr. This +// decouples the encoder's lifetime from the OS callback's lifetime. If the +// encoder is destroyed while a callback is pending, the WeakPtr will be +// invalidated, and the posted task will be safely dropped, preventing a +// use-after-free. +class MFAsyncCallbackProxy + : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, + IMFAsyncCallback> { + public: + MFAsyncCallbackProxy( + scoped_refptr<base::SequencedTaskRunner> task_runner, + base::WeakPtr<MediaFoundationVideoEncodeAccelerator> parent) + : task_runner_(std::move(task_runner)), + parent_weak_ptr_(std::move(parent)) {} + + ~MFAsyncCallbackProxy() override = default; + + IFACEMETHODIMP GetParameters(DWORD* pdwFlags, DWORD* pdwQueue) override { + *pdwFlags = MFASYNC_FAST_IO_PROCESSING_CALLBACK; + *pdwQueue = MFASYNC_CALLBACK_QUEUE_TIMER; + return S_OK; + } + + IFACEMETHODIMP Invoke(IMFAsyncResult* pAsyncResult) override { + MediaEventType event_type = MEUnknown; + HRESULT status = GetEvent(pAsyncResult, &event_type); + + // Invoke() is called on some random OS thread, so we must post to our event + // handler since MediaFoundationVideoEncodeAccelerator is single threaded. + task_runner_->PostTask( + FROM_HERE, + base::BindOnce( + &MediaFoundationVideoEncodeAccelerator::MediaEventHandler, + parent_weak_ptr_, event_type, status)); + return S_OK; + } + + private: + HRESULT GetEvent(IMFAsyncResult* pAsyncResult, MediaEventType* event_type) { + Microsoft::WRL::ComPtr<IUnknown> state; + RETURN_IF_FAILED(pAsyncResult->GetState(&state)); + + Microsoft::WRL::ComPtr<IMFMediaEventGenerator> event_generator; + RETURN_IF_FAILED(state.As(&event_generator)); + + Microsoft::WRL::ComPtr<IMFMediaEvent> media_event; + RETURN_IF_FAILED(event_generator->EndGetEvent(pAsyncResult, &media_event)); + + RETURN_IF_FAILED(media_event->GetType(event_type)); + + HRESULT status = S_OK; + RETURN_IF_FAILED(media_event->GetStatus(&status)); + return status; + } + + scoped_refptr<base::SequencedTaskRunner> task_runner_; + base::WeakPtr<MediaFoundationVideoEncodeAccelerator> parent_weak_ptr_; +}; + struct MediaFoundationVideoEncodeAccelerator::PendingInput { PendingInput() = default; ~PendingInput() = default; @@ -280,7 +342,6 @@ ~MediaFoundationVideoEncodeAccelerator() { DVLOG(3) << __func__; DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); - DCHECK(async_callback_ref_.IsOne()); } VideoEncodeAccelerator::SupportedProfiles @@ -617,7 +678,16 @@ return false; } - event_generator_->BeginGetEvent(this, nullptr); + proxy_callback_ = Microsoft::WRL::Make<MFAsyncCallbackProxy>( + task_runner_, weak_factory_.GetWeakPtr()); + + hr = event_generator_->BeginGetEvent(proxy_callback_.Get(), + event_generator_.Get()); + if (FAILED(hr)) { + NotifyErrorStatus({EncoderStatus::Codes::kEncoderInitializationError, + "Couldn't begin get event: " + PrintHr(hr)}); + return false; + } // Start the asynchronous processing model hr = encoder_->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0); @@ -2615,7 +2685,12 @@ default: break; } - event_generator_->BeginGetEvent(this, nullptr); + HRESULT hr = event_generator_->BeginGetEvent(proxy_callback_.Get(), + event_generator_.Get()); + if (FAILED(hr)) { + NotifyErrorStatus({EncoderStatus::Codes::kSystemAPICallError, + "Failed to begin get event: " + PrintHr(hr)}); + } } void MediaFoundationVideoEncodeAccelerator::SetState(State state) { @@ -3004,47 +3079,4 @@ return initialized; } -HRESULT MediaFoundationVideoEncodeAccelerator::GetParameters(DWORD* pdwFlags, - DWORD* pdwQueue) { - *pdwFlags = MFASYNC_FAST_IO_PROCESSING_CALLBACK; - *pdwQueue = MFASYNC_CALLBACK_QUEUE_TIMER; - return S_OK; -} - -HRESULT MediaFoundationVideoEncodeAccelerator::Invoke( - IMFAsyncResult* pAsyncResult) { - ComMFMediaEvent media_event; - RETURN_IF_FAILED(event_generator_->EndGetEvent(pAsyncResult, &media_event)); - - MediaEventType event_type = MEUnknown; - RETURN_IF_FAILED(media_event->GetType(&event_type)); - - HRESULT status = S_OK; - media_event->GetStatus(&status); - - // Invoke() is called on some random OS thread, so we must post to our event - // handler since MediaFoundationVideoEncodeAccelerator is single threaded. - task_runner_->PostTask( - FROM_HERE, - base::BindOnce(&MediaFoundationVideoEncodeAccelerator::MediaEventHandler, - weak_ptr_, event_type, status)); - return status; -} - -ULONG MediaFoundationVideoEncodeAccelerator::AddRef() { - return async_callback_ref_.Increment(); -} - -ULONG MediaFoundationVideoEncodeAccelerator::Release() { - DCHECK(!async_callback_ref_.IsOne()); - return async_callback_ref_.Decrement() ? 1 : 0; -} - -HRESULT MediaFoundationVideoEncodeAccelerator::QueryInterface(REFIID riid, - void** ppv) { - static const QITAB kQI[] = { - QITABENT(MediaFoundationVideoEncodeAccelerator, IMFAsyncCallback), {0}}; - return QISearch(this, kQI, riid, ppv); -} - } // namespace media diff --git a/media/gpu/windows/media_foundation_video_encode_accelerator_win.h b/media/gpu/windows/media_foundation_video_encode_accelerator_win.h index e59a26c2..746b4cd 100644 --- a/media/gpu/windows/media_foundation_video_encode_accelerator_win.h +++ b/media/gpu/windows/media_foundation_video_encode_accelerator_win.h @@ -45,6 +45,7 @@ class VideoRateControlWrapper; class TemporalScalabilityIdExtractor; class VEAEncodingLatencyMetricsHelper;
Original Bug Report
Potential Use-after-free in MediaFoundationVideoEncodeAccelerator via Windows MF callback
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential use-after-free (UAF) vulnerability exists in the MediaFoundationVideoEncodeAccelerator due to a race condition between object destruction and callbacks from the Windows Media Foundation thread pool. The class registers a raw pointer with the OS but uses fake COM reference counting, meaning explicit destruction does not wait for pending callbacks to drain. If an attacker triggers destruction while an OS callback is pending, the OS thread will invoke a virtual method on the freed object, potentially leading to Remote Code Execution (RCE) in the GPU process.
Affected files:
media/gpu/windows/media_foundation_video_encode_accelerator_win.ccmedia/gpu/windows/media_foundation_video_encode_accelerator_win.h
Estimated timestamp from git blame: 2023-07-10
Background
The MediaFoundationVideoEncodeAccelerator (MFVEA) class in the Windows GPU process implements the IMFAsyncCallback interface to receive events from the Windows Media Foundation (MF) framework. It uses an asynchronous processing model where events are handled via callbacks on a system-managed thread pool (MFASYNC_CALLBACK_QUEUE_TIMER).
The Vulnerability
The vulnerability is a potential use-after-free (UAF) that occurs because MFVEA objects can be deleted while a callback is still pending or actively executing on a Media Foundation thread.
-
Insecure Callback Registration: During initialization and whenever an event is received, MFVEA arms a callback by passing its
thispointer to the MF framework:event_generator_->BeginGetEvent(this, nullptr);The MF framework stores this raw pointer and invokes the
Invoke()method on an OS thread Chromium does not control. -
Fake Reference Counting: Although MFVEA implements
AddRefandRelease, it uses “fake” COM refcounting. TheRelease()method decrements an atomic counter but never deletes the object:ULONG MediaFoundationVideoEncodeAccelerator::Release() { DCHECK(!async_callback_ref_.IsOne()); return async_callback_ref_.Decrement() ? 1 : 0; }Consequently, the internal
AddRefperformed by the Media Foundation framework does not extend the lifetime of the C++ object. -
Unsafe Destruction: The object’s lifetime is managed by the
Destroy()method, which is reachable from a renderer process via Mojo (e.g.,videoEncoder.close()).Destroy()callsactivate_->ShutdownObject()and then immediately deletesthis:void MediaFoundationVideoEncodeAccelerator::Destroy() { if (activate_) { activate_->ShutdownObject(); } delete this; }However,
ShutdownObject()does not synchronously drain the Media Foundation work queue or guarantee that noInvoke()callbacks will run after it returns. -
Race Condition and Use-After-Free: A race condition occurs when
Destroy()is called while an MF thread is entering or executingInvoke(). TheInvoke()method is a virtual function call. If the object is deleted before the MF thread makes this call, the OS thread will execute a virtual method call on a freed object pointer.
MiraclePtr Bypass
Because the dangling pointer is held internally by the Windows Media Foundation framework (as a raw C/C++ pointer) rather than by a Chromium data structure using base::raw_ptr<T>, PartitionAlloc’s MiraclePtr (BackupRefPtr) does not track this reference. The memory is fully freed and not quarantined, making it immediately available for reallocation and exploitation.
Potential Attacker Steps (Unverified)
- An attacker compromises a renderer process or uses the WebCodecs API (
VideoEncoder). - The attacker configures the
VideoEncoderfor a Windows Media Foundation codec (e.g., H.264). - The GPU process creates and initializes a
MediaFoundationVideoEncodeAccelerator(MFVEA), registeringthiswith the MF thread pool. - The attacker abruptly closes the encoder (
videoEncoder.close()), destroying the Mojo pipe. - The GPU process executes
MFVEA::Destroy(), callingdelete this;. - The attacker rapidly allocates memory in the GPU process (e.g., via WebGL or creating other media objects) to reclaim the freed MFVEA memory block, placing a fake vtable at the start.
- A pending MF callback executes on an OS worker thread, calling
pCallback->Invoke(pAsyncResult). - The OS thread dereferences the attacker’s fake vtable, hijacking control flow and achieving RCE in the sandboxed GPU process (a Sandbox Escape from the renderer).
Recommended Fix
Synchronization must be re-introduced to ensure destruction does not proceed while a callback is active. This can be achieved in two ways:
- Implement Real COM Reference Counting: Use
Microsoft::WRL::RuntimeClassor similar mechanisms so that the object is only deleted when all OS-level references are released. This ensures the object outlives the MF thread pool callbacks. - Restore the Shutdown Lock Pattern: Similar to other Chromium Media Foundation implementations (e.g.,
MediaEngineNotifyImpl), introduce a lock (destroy_lock_) and a shutdown flag (in_shutdown_). InDestroy(), setin_shutdown_ = trueunder the lock. At the beginning ofInvoke(), acquire the lock and return early ifin_shutdown_is true. Note: this alone does not prevent the UAF ifDestroy()still immediately callsdelete this;. The object must be kept alive, orInvoke()must not be allowed to execute afterdelete this;. Therefore, proper COM refcounting is the most robust solution.
Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939
Results from 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.