CVE-2026-19171
Overview
Background
- `SharedImageReadLock`
- a COM object (created with
Microsoft::WRL::Make) inmedia/base/win/mf_helpersthat keeps agpu::VideoImageRepresentationand itsScopedReadAccessalive while a GPU video frame’s D3D11 texture is being consumed. It can be attached to anIMFSamplehanded to Media Foundation, so its lifetime is controlled by COM reference counting. - `gpu::MemoryTypeTracker`
- a per-client accounting helper that a shared-image representation is given at
ProduceVideotime and uses for memory tracking during its lifetime. It wraps a ref-countedgpu::MemoryTracker, but is not itself ref-counted. - `CommandBufferHelper::GetMemoryTypeTracker`
- returns a raw pointer to a
MemoryTypeTrackerowned by theCommandBufferHelper. Before the patch, this was the tracker passed toProduceVideo.
Root Cause Analysis
In the Windows hardware-encode input path, GenerateResourceOnSyncTokenReleased (in mf_helpers.cc) and GenerateResourceOnSynTokenReleased (in d3d12_video_encode_accelerator.cc) produced a VideoImageRepresentation with command_buffer_helper->GetMemoryTypeTracker(). The hunks do not name these functions, but they are the enclosing functions in the source. The representation keeps that raw pointer and was then moved into a SharedImageReadLock. The lock can be passed on to an IMFSample, so it, and the representation inside it, can outlive the CommandBufferHelper that owns the tracker. When the lock is finally released, ~SharedImageReadLock posts DestroySharedImageResourcesOnGpuThread, which destroys the representation. If the helper and its tracker are already gone, that teardown touches a freed MemoryTypeTracker; the commit describes this as a UAF “based on unexpected destruction orders”.
The fix makes each read lock own its own std::unique_ptr<gpu::MemoryTypeTracker>, constructed around a base::WrapRefCounted reference to the stub’s MemoryTracker. That tracker is passed to ProduceVideo and moved into the lock, and DestroySharedImageResourcesOnGpuThread resets it only after scoped_read_access and representation have been destroyed.
IMFSample, borrowed a raw MemoryTypeTracker* from a shorter-lived owner. The fix gives the lock its own tracker, which keeps the underlying ref-counted MemoryTracker alive, and destroys it last.Attack Path
- Start hardware video encoding on Windows
Content drives an encode through the Media Foundation path (
mf_helpers.cc) or the D3D12 encoder (d3d12_video_encode_accelerator.cc) using GPU-backedVideoFrames with shared images. That an ordinary web API such as WebCodecs, WebRTC or MediaRecorder reaches this is (inferred). - Create the read lock
When the frame’s sync token is released, the GPU-side helper produces a video representation tied to the
CommandBufferHelper’s tracker and wraps it in aSharedImageReadLock, which may be attached to anIMFSample. - Tear down out of order
The encoder, command buffer or stub is destroyed (for example by closing the encoder or the page) while Media Foundation or the encoder still holds the sample or lock, which frees the
CommandBufferHelper’sMemoryTypeTracker(inferred timing). - Use after free on release
The final release runs
DestroySharedImageResourcesOnGpuThread, and destroying the representation dereferences the stale tracker pointer in the GPU process.
Impact Assessment
IMFSample or lock. Whether unprivileged web content can reliably steer that ordering, and whether the freed tracker gives a corruption primitive leading to GPU-process code execution, are (inferred) and not shown by the patch. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/base/win/mf_helpers.cc |
modified | |
IMFMediaTypemedia/base/win/mf_helpers.h |
modified | |
MemoryTypeTrackermedia/base/win/mf_helpers.h |
modified | |
SharedContextStatemedia/base/win/mf_helpers.h |
modified | |
ifmedia/gpu/windows/d3d12_video_encode_accelerator.cc |
modified |
Files Changed
media/gpu/windows/d3d12_video_encode_accelerator.ccmedia/base/win/mf_helpers.ccmedia/base/win/mf_helpers.h
Audit Directions
- Raw tracker pointers in representationsFind every
ProduceVideo/Produce*call that passesGetMemoryTypeTracker()from a helper or stub, especially those whose representation is handed to COM, platform APIs, or posted tasks (media/gpu/windows,media/gpu/mac,media/mojo/services). Confirm that the tracker owner strictly outlives the representation. - COM/WRL objects wrapping Chromium-owned stateLook at
Microsoft::WRL::Makeobjects andIMFSample/IMFMediaBufferattachments that holdunique_ptrs or raw pointers to GPU-side objects. Their release is driven by platform reference counts and can occur after the Chromium owner has been torn down. - Deferred destruction tasksReview
PostTaskdestructors such asDestroySharedImageResourcesOnGpuThreadfor objects whose dependencies (trackers, context state, helpers) are not moved into the task. Check the reset order inside it.
Patch
From 38acc8b39421655426c218609986023ad483c241 Mon Sep 17 00:00:00 2001 From: Saifuddin Hitawala <[email protected]> Date: Thu, 23 Jul 2026 12:29:16 -0700 Subject: [PATCH] [media] Keep ref on MemoryTypeTracker in SharedImageReadLock Add MemoryTypeTracker ref on SharedImageReadLock to avoid UAF based on unexpected destruction orders, as the lock can be passed to IMFSample. We will eventually get rid of MemoryTypeTracker in SharedImage to instead use MemoryTracker directly which is RefCountedThreadSafe and should avoid future issues. Note that this does not pass CommandBufferHelper as we want to get rid of it as well eventually. Bug: 537832446 Change-Id: I048a2e20527748c0e169d6b285d0cab5d6c22fa9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8139742 Reviewed-by: Dale Curtis <[email protected]> Reviewed-by: Vasiliy Telezhnikov <[email protected]> Commit-Queue: Saifuddin Hitawala <[email protected]> Cr-Commit-Position: refs/heads/main@{#1667317} --- diff --git a/media/base/win/mf_helpers.cc b/media/base/win/mf_helpers.cc index 4e3cfa2..00e115a 100644 --- a/media/base/win/mf_helpers.cc +++ b/media/base/win/mf_helpers.cc @@ -51,17 +51,16 @@ std::unique_ptr<gpu::VideoImageRepresentation> representation, std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess> scoped_read_access, - scoped_refptr<gpu::SharedContextState> context_state) { - if (context_state && context_state->MakeCurrent(nullptr, /*needs_gl=*/true)) { - scoped_read_access.reset(); - representation.reset(); - } else { - if (representation) { - representation->OnContextLost(); - } - scoped_read_access.reset(); - representation.reset(); + scoped_refptr<gpu::SharedContextState> context_state, + std::unique_ptr<gpu::MemoryTypeTracker> tracker) { + const bool context_current = + context_state && context_state->MakeCurrent(nullptr, /*needs_gl=*/true); + if (representation && !context_current) { + representation->OnContextLost(); } + scoped_read_access.reset(); + representation.reset(); + tracker.reset(); } } // namespace @@ -71,11 +70,13 @@ std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess> scoped_read_access, scoped_refptr<VideoFrame> frame, - scoped_refptr<gpu::SharedContextState> context_state) + scoped_refptr<gpu::SharedContextState> context_state, + std::unique_ptr<gpu::MemoryTypeTracker> tracker) : representation_(std::move(representation)), scoped_read_access_(std::move(scoped_read_access)), frame_(std::move(frame)), context_state_(std::move(context_state)), + tracker_(std::move(tracker)), task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {} SharedImageReadLock::~SharedImageReadLock() { @@ -83,7 +84,7 @@ FROM_HERE, base::BindOnce(&DestroySharedImageResourcesOnGpuThread, std::move(representation_), std::move(scoped_read_access_), - std::move(context_state_))); + std::move(context_state_), std::move(tracker_))); } using Microsoft::WRL::ComPtr; @@ -1134,10 +1135,11 @@ bool use_same_device = (encoder_device.Get() == shared_d3d11_device.Get()); gpu::SharedImageManager* shared_image_manager = command_buffer_helper->GetSharedImageManager(); + auto tracker = std::make_unique<gpu::MemoryTypeTracker>( + base::WrapRefCounted(shared_image_stub->memory_tracker())); std::unique_ptr<gpu::VideoImageRepresentation> image_representation = shared_image_manager->ProduceVideo( - shared_d3d11_device, frame->shared_image()->mailbox(), - command_buffer_helper->GetMemoryTypeTracker()); + shared_d3d11_device, frame->shared_image()->mailbox(), tracker.get()); RETURN_ON_FAILURE_WITH_CALLBACK(image_representation ? S_OK : E_FAIL, "Failed to produce video"); @@ -1152,7 +1154,7 @@ ComPtr<SharedImageReadLock> si_lock = Microsoft::WRL::Make<SharedImageReadLock>( std::move(image_representation), std::move(scoped_read_access), frame, - std::move(shared_context_state)); + std::move(shared_context_state), std::move(tracker)); if (!si_lock) { RETURN_ON_FAILURE_WITH_CALLBACK(E_OUTOFMEMORY, "Failed to create SharedImageReadLock"); diff --git a/media/base/win/mf_helpers.h b/media/base/win/mf_helpers.h index a5a2a42..73db391f 100644 --- a/media/base/win/mf_helpers.h +++ b/media/base/win/mf_helpers.h @@ -33,6 +33,7 @@ class IMFMediaType; namespace gpu { +class MemoryTypeTracker; class SharedContextState; } // namespace gpu @@ -261,7 +262,8 @@ std::unique_ptr<gpu::VideoImageRepresentation::ScopedReadAccess> scoped_read_access, scoped_refptr<VideoFrame> frame, - scoped_refptr<gpu::SharedContextState> context_state); + scoped_refptr<gpu::SharedContextState> context_state, + std::unique_ptr<gpu::MemoryTypeTracker> tracker); gpu::VideoImageRepresentation::ScopedReadAccess* access() const { return scoped_read_access_.get(); @@ -275,6 +277,7 @@ scoped_read_access_; scoped_refptr<VideoFrame> frame_; scoped_refptr<gpu::SharedContextState> context_state_; + std::unique_ptr<gpu::MemoryTypeTracker> tracker_; scoped_refptr<base::SequencedTaskRunner> task_runner_; }; diff --git a/media/gpu/windows/d3d12_video_encode_accelerator.cc b/media/gpu/windows/d3d12_video_encode_accelerator.cc index e7b6bae..1762d35 100644 --- a/media/gpu/windows/d3d12_video_encode_accelerator.cc +++ b/media/gpu/windows/d3d12_video_encode_accelerator.cc @@ -149,10 +149,11 @@ gpu::SharedImageManager* shared_image_manager = command_buffer_helper->GetSharedImageManager(); + auto tracker = std::make_unique<gpu::MemoryTypeTracker>( + base::WrapRefCounted(shared_image_stub->memory_tracker())); std::unique_ptr<gpu::VideoImageRepresentation> representation = shared_image_manager->ProduceVideo( - d3d11_device, frame->shared_image()->mailbox(), - command_buffer_helper->GetMemoryTypeTracker()); + d3d11_device, frame->shared_image()->mailbox(), tracker.get()); RETURN_ON_FAILURE_WITH_CALLBACK(representation ? S_OK : E_FAIL, "Failed to produce video"); @@ -167,7 +168,7 @@ Microsoft::WRL::ComPtr<SharedImageReadLock> si_lock = Microsoft::WRL::Make<SharedImageReadLock>( std::move(representation), std::move(scoped_read_access), frame, - std::move(shared_context_state)); + std::move(shared_context_state), std::move(tracker)); if (!si_lock) { RETURN_ON_FAILURE_WITH_CALLBACK(E_OUTOFMEMORY, "Failed to create SharedImageReadLock");
Original Bug Report
Potential UAF in GPU process via asynchronous release of SharedImageReadLock by Media Foundation
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability exists in the Windows GPU process when using MediaFoundationVideoEncodeAccelerator (MFVEA). SharedImageReadLock wraps a VideoImageRepresentation holding a raw pointer to a CommandBufferHelperImpl member but does not maintain a reference to keep the helper alive. If an asynchronous hardware encoder releases an IMFSample holding the lock after the encoder and its helper are destroyed, the lock’s destruction accesses freed memory on the GPU main thread.
Affected files:
media/base/win/mf_helpers.hmedia/base/win/mf_helpers.ccmedia/gpu/windows/media_foundation_video_encode_accelerator_win.cc
Estimated timestamp from git blame: 2026-04-24
Root Cause Analysis
In media/base/win/mf_helpers.cc, a SharedImageReadLock is constructed to manage the lifetime of a VideoImageRepresentation and its ScopedReadAccess. The VideoImageRepresentation holds a raw pointer (tracker_) to an inline MemoryTypeTracker member of a CommandBufferHelperImpl instance:
// media/base/win/mf_helpers.cc
std::unique_ptr<gpu::VideoImageRepresentation> image_representation =
shared_image_manager->ProduceVideo(
shared_d3d11_device, frame->shared_image()->mailbox(),
command_buffer_helper->GetMemoryTypeTracker());
However, SharedImageReadLock does not hold a scoped_refptr<CommandBufferHelper> to guarantee the lifetime of the tracker.
When MFVEA populates input samples for the asynchronous Media Foundation Transform (HMFT), it attaches the SharedImageReadLock directly to the IMFSample COM attributes using SetUnknown with SharedImageReadLock::kSampleExtensionGUID and drops its local queue references. This hands ownership of the lock’s lifetime over to the Media Foundation driver and the OS-managed COM runtime graph.
Upon encoder destruction (MediaFoundationVideoEncodeAccelerator::Destroy()), MFVEA is deleted. No flush command is processed beforehand, which leaves active sample references inside the driver’s queued pipeline. The destruction of MFVEA’s command_buffer_helper_ member drops the last Chrome-side reference to CommandBufferHelperImpl, posting its deletion (~CommandBufferHelperImpl) to the GPU main thread.
Subsequently, when the HMFT completes and releases the IMFSample on an OS background thread, the COM reference count of the SharedImageReadLock drops to 0. Its destructor (~SharedImageReadLock()) runs on the background thread and posts DestroySharedImageResourcesOnGpuThread to the GPU main thread.
Because both deletion tasks are executed sequentially on the GPU main thread, the CommandBufferHelperImpl is deallocated first. The subsequent cleanup task then executes TrackMemFree on the dangling MemoryTypeTracker pointer, causing a Use-After-Free with a virtual call to TrackMemoryAllocatedChange on the freed memory space.
Potential Trigger Path
Note: These are potential steps that an attacker might follow; our analysis is based on static code tracing as we do not have a running PoC.
- From a sandboxed renderer on Windows, initiate hardware encoding via the
mojom::VideoEncodeAcceleratorProviderinterface. - Submit a GPU-backed
VideoFramewhoseSharedImageis non-mappable, triggering the creation of aSharedImageReadLockwrapping theVideoImageRepresentationand its read-access token. - The lock is attached to the input
IMFSampleviaSetUnknownand handed off to the async HMFT viaProcessInput(). - Drop the local queue references in MFVEA via
pending_input_queue_.pop_front()so that the lock is kept alive only by the OS-owned driver sample. - Release the source
VideoFramemailbox reference in the renderer so that the representation wrapped by the lock becomes the owning reference (refs_[0]) in theSharedImageBacking. - Close/destroy the encoder from the renderer. MFVEA’s
Destroy()runs, deleting the object and posting~CommandBufferHelperImplto the GPU main thread. - The async HMFT releases the
IMFSampleon a background thread after the helper deletion task has been queued, postingDestroySharedImageResourcesOnGpuThreadto the GPU main thread. - The GPU main thread executes the tasks: it deallocates
CommandBufferHelperImpl(freeing its inlineMemoryTypeTrackermember) and then executesTrackMemFreeon the dangling tracker pointer during representation cleanup, resulting in a virtual call on freed memory.
Suggested Fix
Modify SharedImageReadLock (media/base/win/mf_helpers.h and media/base/win/mf_helpers.cc) to capture and hold a scoped_refptr<CommandBufferHelper> member. Passing the helper into the lock’s constructor guarantees that the CommandBufferHelperImpl (and its inline MemoryTypeTracker) remains alive until all outstanding SharedImageReadLock instances are fully destroyed, preventing the raw tracker pointer from dangling.
Evaluated with Chrome root at commit: 5a99d0c5d2ec6c066f5131e7868ff637440cc3dc
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.