High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Sub-componentMedia
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497151750
Fix commit70f35b06c633 (chromium/src) +83/-56
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-28 (per vendor advisory)
Fix landed2026-04-01 (public git)
Disclosed2026-05-12
Patch gap41 days — fix public in source before the advisory

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 through event_generator_->BeginGetEvent(callback, state).
Fake COM refcounting
the MF VEA implemented AddRef/Release backed by async_callback_ref_ that did not actually keep the object alive, so registering this as 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.

Key insight
The encoder handed Media Foundation a non-owning 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

  1. 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.
  2. 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.
  3. OS invokes freed object Media Foundation later calls Invoke on an arbitrary OS thread using the dangling encoder pointer, dereferencing freed members (event_generator_, task_runner_) — the use-after-free.
  4. 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

The advisory rates this high severity (Use after free in Media). An attacker who can drive and then tear down hardware video encoding can trigger a use-after-free in the GPU process when a pending Media Foundation callback fires on a freed encoder; the precondition is Windows with the MF hardware encoder path active and a race between encoder destruction and a pending OS event. Any escalation to code execution or sandbox impact is inferred and not demonstrated by the patch.

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

Audit Directions

  • Non-owning `this` as OS/COM callback
    Audit any place Chrome registers this with an OS async API (Media Foundation, WASAPI, DirectX, XPC) without a real owning reference; decouple via a proxy + WeakPtr.
  • Fake/stubbed refcounting
    Search for hand-rolled AddRef/Release that do not actually extend lifetime (counters like async_callback_ref_); these create objects the OS believes it owns but Chrome frees.
  • Pending-callback vs. destruction races
    Review 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.
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;
Loading diff…

Original Bug Report

reported by [email protected]

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