Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Bug ClassUAF
Tracker497151750
Fix commit70f35b06c633 (chromium/src) +83/-56
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
MFAsyncCallbackProxy
media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
modified
VideoRateControlWrapper
media/gpu/windows/media_foundation_video_encode_accelerator_win.h
modified
TemporalScalabilityIdExtractor
media/gpu/windows/media_foundation_video_encode_accelerator_win.h
modified
VEAEncodingLatencyMetricsHelper
media/gpu/windows/media_foundation_video_encode_accelerator_win.h
modified

Files Changed

  • media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
  • media/gpu/windows/media_foundation_video_encode_accelerator_win.h
From 70f35b06c6330d6b46c0689a93545ef33041da79 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
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 <dalecurtis@chromium.org>
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
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;
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • media/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.

  1. Insecure Callback Registration: During initialization and whenever an event is received, MFVEA arms a callback by passing its this pointer 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.

  2. Fake Reference Counting: Although MFVEA implements AddRef and Release, it uses “fake” COM refcounting. The Release() 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 AddRef performed by the Media Foundation framework does not extend the lifetime of the C++ object.

  3. 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() calls activate_->ShutdownObject() and then immediately deletes this:

    void MediaFoundationVideoEncodeAccelerator::Destroy() {
      if (activate_) {
        activate_->ShutdownObject();
      }
      delete this;
    }
    

    However, ShutdownObject() does not synchronously drain the Media Foundation work queue or guarantee that no Invoke() callbacks will run after it returns.

  4. Race Condition and Use-After-Free: A race condition occurs when Destroy() is called while an MF thread is entering or executing Invoke(). The Invoke() 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)

  1. An attacker compromises a renderer process or uses the WebCodecs API (VideoEncoder).
  2. The attacker configures the VideoEncoder for a Windows Media Foundation codec (e.g., H.264).
  3. The GPU process creates and initializes a MediaFoundationVideoEncodeAccelerator (MFVEA), registering this with the MF thread pool.
  4. The attacker abruptly closes the encoder (videoEncoder.close()), destroying the Mojo pipe.
  5. The GPU process executes MFVEA::Destroy(), calling delete this;.
  6. 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.
  7. A pending MF callback executes on an OS worker thread, calling pCallback->Invoke(pAsyncResult).
  8. 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).

Synchronization must be re-introduced to ensure destruction does not proceed while a callback is active. This can be achieved in two ways:

  1. Implement Real COM Reference Counting: Use Microsoft::WRL::RuntimeClass or 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.
  2. 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_). In Destroy(), set in_shutdown_ = true under the lock. At the beginning of Invoke(), acquire the lock and return early if in_shutdown_ is true. Note: this alone does not prevent the UAF if Destroy() still immediately calls delete this;. The object must be kept alive, or Invoke() must not be allowed to execute after delete 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.

View on issue tracker