Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Bug ClassUAF
Tracker523718303
Fix commit5e974e82ba31 (chromium/src) +414/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
D3D12CommandQueueMock
media/base/win/d3d12_mocks.h
modified
D3D12CommandAllocatorMock
media/base/win/d3d12_mocks.h
modified
D3D12FenceMock
media/base/win/d3d12_mocks.h
modified
D3D12VideoProcessorMock
media/base/win/d3d12_video_mocks.h
modified

Files Changed

  • media/base/win/d3d12_mocks.cc
  • media/base/win/d3d12_mocks.h
  • media/base/win/d3d12_video_mocks.cc
  • media/base/win/d3d12_video_mocks.h
From 5e974e82ba31124efdd0b6d086572b8f8b157110 Mon Sep 17 00:00:00 2001
From: Qiu Jianlin <[email protected]>
Date: Fri, 26 Jun 2026 16:17:11 -0700
Subject: [PATCH] Fix GPU UAF in D3D12VideoProcessorWrapper teardown.

D3D12VideoEncodeDelegate::Encode() submits asynchronous video
processing work to D3D12VideoProcessorWrapper::ProcessFrames() and
relies on the CPU sync inside D3D12VideoEncoderWrapper::Encode() to
wait on it later. If a codec-specific EncodeImpl() bails between those
two steps (e.g. with kBadReferenceBuffer for a manual reference buffer
that was never populated), that sync is skipped and the subsequent
teardown releases the wrapper's command allocator, command list and
processed input frame while the video processor queue is still using
them.
Wait for in-flight video-processor work whenever EncodeImpl() errors
after ProcessFrames() may have submitted work, so the delegate's
resources are safe by the time NotifyError() begins teardown.

Bug: 523718303
Change-Id: Ifded931b5381a76ed3b3d4c3baae2f8cb703a983
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7976180
Reviewed-by: Eugene Zemtsov <[email protected]>
Commit-Queue: Qiu, Jianlin <[email protected]>
Auto-Submit: Qiu, Jianlin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1653518}
---

diff --git a/media/base/win/d3d12_mocks.cc b/media/base/win/d3d12_mocks.cc
index f3c459f..ac35d36 100644
--- a/media/base/win/d3d12_mocks.cc
+++ b/media/base/win/d3d12_mocks.cc
@@ -15,6 +15,12 @@
 D3D12GraphicsCommandListMock::D3D12GraphicsCommandListMock() = default;
 D3D12GraphicsCommandListMock::~D3D12GraphicsCommandListMock() = default;
 
+D3D12CommandQueueMock::D3D12CommandQueueMock() = default;
+D3D12CommandQueueMock::~D3D12CommandQueueMock() = default;
+
+D3D12CommandAllocatorMock::D3D12CommandAllocatorMock() = default;
+D3D12CommandAllocatorMock::~D3D12CommandAllocatorMock() = default;
+
 D3D12FenceMock::D3D12FenceMock() = default;
 D3D12FenceMock::~D3D12FenceMock() = default;
 
diff --git a/media/base/win/d3d12_mocks.h b/media/base/win/d3d12_mocks.h
index c0e2c63..dd64576 100644
--- a/media/base/win/d3d12_mocks.h
+++ b/media/base/win/d3d12_mocks.h
@@ -417,6 +417,116 @@
                             UINT64 CountBufferOffset));
 };
 
+class D3D12CommandQueueMock
+    : public Microsoft::WRL::RuntimeClass<
+          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+          ID3D12CommandQueue> {
+ public:
+  D3D12CommandQueueMock();
+  ~D3D12CommandQueueMock() override;
+
+  MOCK_METHOD(HRESULT,
+              GetPrivateData,
+              (REFGUID, UINT*, void*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              SetPrivateData,
+              (REFGUID, UINT, const void*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              SetPrivateDataInterface,
+              (REFGUID, const IUnknown*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT, SetName, (LPCWSTR), (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              GetDevice,
+              (REFIID, void**),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(void,
+              UpdateTileMappings,
+              (ID3D12Resource*,
+               UINT,
+               const D3D12_TILED_RESOURCE_COORDINATE*,
+               const D3D12_TILE_REGION_SIZE*,
+               ID3D12Heap*,
+               UINT,
+               const D3D12_TILE_RANGE_FLAGS*,
+               const UINT*,
+               const UINT*,
+               D3D12_TILE_MAPPING_FLAGS),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(void,
+              CopyTileMappings,
+              (ID3D12Resource*,
+               const D3D12_TILED_RESOURCE_COORDINATE*,
+               ID3D12Resource*,
+               const D3D12_TILED_RESOURCE_COORDINATE*,
+               const D3D12_TILE_REGION_SIZE*,
+               D3D12_TILE_MAPPING_FLAGS),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(void,
+              ExecuteCommandLists,
+              (UINT, ID3D12CommandList* const*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(void,
+              SetMarker,
+              (UINT, const void*, UINT),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(void,
+              BeginEvent,
+              (UINT, const void*, UINT),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(void, EndEvent, (), (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              Signal,
+              (ID3D12Fence*, UINT64),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              Wait,
+              (ID3D12Fence*, UINT64),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              GetTimestampFrequency,
+              (UINT64*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              GetClockCalibration,
+              (UINT64*, UINT64*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(D3D12_COMMAND_QUEUE_DESC,
+              GetDesc,
+              (),
+              (Calltype(STDMETHODCALLTYPE)));
+};
+
+class D3D12CommandAllocatorMock
+    : public Microsoft::WRL::RuntimeClass<
+          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+          ID3D12CommandAllocator> {
+ public:
+  D3D12CommandAllocatorMock();
+  ~D3D12CommandAllocatorMock() override;
+
+  MOCK_METHOD(HRESULT,
+              GetPrivateData,
+              (REFGUID, UINT*, void*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              SetPrivateData,
+              (REFGUID, UINT, const void*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              SetPrivateDataInterface,
+              (REFGUID, const IUnknown*),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT, SetName, (LPCWSTR), (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT,
+              GetDevice,
+              (REFIID, void**),
+              (Calltype(STDMETHODCALLTYPE)));
+  MOCK_METHOD(HRESULT, Reset, (), (Calltype(STDMETHODCALLTYPE)));
+};
+
 class D3D12FenceMock
     : public Microsoft::WRL::RuntimeClass<
           Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
diff --git a/media/base/win/d3d12_video_mocks.cc b/media/base/win/d3d12_video_mocks.cc
index ad76d226f..8768db1a 100644
--- a/media/base/win/d3d12_video_mocks.cc
+++ b/media/base/win/d3d12_video_mocks.cc
@@ -9,4 +9,10 @@
 D3D12VideoDevice3Mock::D3D12VideoDevice3Mock() = default;
 D3D12VideoDevice3Mock::~D3D12VideoDevice3Mock() = default;
 
+D3D12VideoProcessorMock::D3D12VideoProcessorMock() = default;
+D3D12VideoProcessorMock::~D3D12VideoProcessorMock() = default;
+
+D3D12VideoProcessCommandListMock::D3D12VideoProcessCommandListMock() = default;
+D3D12VideoProcessCommandListMock::~D3D12VideoProcessCommandListMock() = default;
+
 }  // namespace media
diff --git a/media/base/win/d3d12_video_mocks.h b/media/base/win/d3d12_video_mocks.h
index 9902adb..29a3695 100644
--- a/media/base/win/d3d12_video_mocks.h
+++ b/media/base/win/d3d12_video_mocks.h
@@ -118,6 +118,127 @@
                                void** ppVideoEncoderHeap));
 };
 
+class D3D12VideoProcessorMock
+    : public Microsoft::WRL::RuntimeClass<
+          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+          ID3D12VideoProcessor> {
+ public:
+  D3D12VideoProcessorMock();
+  ~D3D12VideoProcessorMock() override;
+
+  MOCK_METHOD(HRESULT,
+              GetPrivateData,
+              (REFGUID, UINT*, void*),
+              (Calltype(STDMETHODCALLTYPE)));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/gpu/windows/d3d12_video_processor_wrapper_unittest.cc b/media/gpu/windows/d3d12_video_processor_wrapper_unittest.cc
new file mode 100644
index 0000000..dbc0cb5
--- /dev/null
+++ b/media/gpu/windows/d3d12_video_processor_wrapper_unittest.cc
@@ -0,0 +1,121 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/gpu/windows/d3d12_video_processor_wrapper.h"
+
+#include <memory>
+
+#include "media/base/win/d3d12_mocks.h"
+#include "media/base/win/d3d12_video_mocks.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::_;
+using ::testing::NiceMock;
+using ::testing::Return;
+
+namespace media {
+
+class D3D12VideoProcessorWrapperTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    device_ = MakeComPtr<NiceMock<D3D12DeviceMock>>();
+    video_device_ = MakeComPtr<NiceMock<D3D12VideoDevice3Mock>>();
+    command_queue_ = MakeComPtr<NiceMock<D3D12CommandQueueMock>>();
+    command_allocator_ = MakeComPtr<NiceMock<D3D12CommandAllocatorMock>>();
+    command_list_ = MakeComPtr<NiceMock<D3D12VideoProcessCommandListMock>>();
+    video_processor_ = MakeComPtr<NiceMock<D3D12VideoProcessorMock>>();
+    fence_ = MakeComPtr<NiceMock<D3D12FenceMock>>();
+
+    ON_CALL(*video_device_.Get(), QueryInterface(IID_ID3D12Device, _))
+        .WillByDefault(SetComPointeeAndReturnOk<1>(device_.Get()));
+    ON_CALL(*device_.Get(), CreateCommandQueue(_, _, _))
+        .WillByDefault(SetComPointeeAndReturnOk<2>(command_queue_.Get()));
+    ON_CALL(*device_.Get(), CreateCommandAllocator(_, _, _))
+        .WillByDefault(SetComPointeeAndReturnOk<2>(command_allocator_.Get()));
+    ON_CALL(*device_.Get(), CreateCommandList(_, _, _, _, _, _))
+        .WillByDefault(SetComPointeeAndReturnOk<5>(command_list_.Get()));
+    ON_CALL(*device_.Get(), CreateFence(_, _, _, _))
+        .WillByDefault(SetComPointeeAndReturnOk<3>(fence_.Get()));
+    ON_CALL(*command_list_.Get(), Close()).WillByDefault(Return(S_OK));
+    ON_CALL(*command_list_.Get(), Reset(_)).WillByDefault(Return(S_OK));
+    ON_CALL(*command_allocator_.Get(), Reset()).WillByDefault(Return(S_OK));
+    ON_CALL(*command_queue_.Get(), Signal(_, _)).WillByDefault(Return(S_OK));
+    ON_CALL(*fence_.Get(), GetCompletedValue()).WillByDefault(Return(0));
+    ON_CALL(*fence_.Get(), SetEventOnCompletion(_, _))
+        .WillByDefault([](UINT64, HANDLE event) {
+          ::SetEvent(event);
+          return S_OK;
+        });
+    ON_CALL(*video_device_.Get(),
+            CheckFeatureSupport(D3D12_FEATURE_VIDEO_PROCESS_SUPPORT, _, _))
+        .WillByDefault([](D3D12_FEATURE_VIDEO, void* data, UINT) {
+          static_cast<D3D12_FEATURE_DATA_VIDEO_PROCESS_SUPPORT*>(data)
+              ->SupportFlags = D3D12_VIDEO_PROCESS_SUPPORT_FLAG_SUPPORTED;
+          return S_OK;
+        });
+    ON_CALL(*video_device_.Get(), CreateVideoProcessor(_, _, _, _, _, _))
+        .WillByDefault(SetComPointeeAndReturnOk<5>(video_processor_.Get()));
+
+    wrapper_ = std::make_unique<D3D12VideoProcessorWrapper>(video_device_);
+  }
+
+  Microsoft::WRL::ComPtr<ID3D12Resource> CreateInputResource() {
+    auto resource = MakeComPtr<NiceMock<D3D12ResourceMock>>();
+    ON_CALL(*resource.Get(), GetDesc())
+        .WillByDefault(Return(D3D12_RESOURCE_DESC{
+            .Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D,
+            .Width = 1280,
+            .Height = 720,
+            .DepthOrArraySize = 1,
+            .MipLevels = 1,
+            .Format = DXGI_FORMAT_NV12,
+        }));
+    return resource;
+  }
+
+  Microsoft::WRL::ComPtr<D3D12DeviceMock> device_;
+  Microsoft::WRL::ComPtr<D3D12VideoDevice3Mock> video_device_;
+  Microsoft::WRL::ComPtr<D3D12CommandQueueMock> command_queue_;
+  Microsoft::WRL::ComPtr<D3D12CommandAllocatorMock> command_allocator_;
+  Microsoft::WRL::ComPtr<D3D12VideoProcessCommandListMock> command_list_;
+  Microsoft::WRL::ComPtr<D3D12VideoProcessorMock> video_processor_;
+  Microsoft::WRL::ComPtr<D3D12FenceMock> fence_;
+
+  std::unique_ptr<D3D12VideoProcessorWrapper> wrapper_;
+};
+
+TEST_F(D3D12VideoProcessorWrapperTest, DestructorWaitsForPendingWork) {
+  ASSERT_TRUE(wrapper_->Init());
+
+  auto input = CreateInputResource();
+  auto output = CreateInputResource();
+  gfx::Rect rect(0, 0, 1280, 720);
+  EXPECT_CALL(*command_queue_.Get(), ExecuteCommandLists(1, _));
+  auto fence_and_value = wrapper_->ProcessFrames(
+      input.Get(), 0, gfx::ColorSpace::CreateSRGB(), rect, output.Get(), 0,
+      gfx::ColorSpace::CreateREC709(), rect);
+  ASSERT_TRUE(fence_and_value.first);
+  EXPECT_EQ(fence_and_value.second, 1u);
+
+  // The work submitted above has not yet completed on the GPU. Destroying the
+  // wrapper must block until it does so that resources referenced by the
+  // command list are not released early.
+  EXPECT_CALL(*fence_.Get(), SetEventOnCompletion(1, _))
+      .WillOnce([](UINT64, HANDLE event) {
+        ::SetEvent(event);
+        return S_OK;
+      });
+  wrapper_.reset();
+}
+
+TEST_F(D3D12VideoProcessorWrapperTest,
+       DestructorDoesNotWaitWithoutPendingWork) {
+  ASSERT_TRUE(wrapper_->Init());
+
+  EXPECT_CALL(*fence_.Get(), SetEventOnCompletion(_, _)).Times(0);
+  wrapper_.reset();
+}
+
+}  // namespace media
Loading diff…

Original Bug Report

reported by [email protected]

Potential GPU Use-After-Free in D3D12VideoProcessorWrapper via bypassed synchronization

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 compromised renderer can trigger an early error in D3D12 video encoding after asynchronous video processing work has been submitted to the GPU. This early error bypasses the necessary CPU-side synchronization, leading to a Use-After-Free of D3D12 resources when the encoder is destroyed while the GPU is still executing commands.

Affected files:

  • media/gpu/windows/d3d12_video_processor_wrapper.cc
  • media/gpu/windows/d3d12_video_encode_delegate.cc
  • media/gpu/windows/d3d12_video_encode_accelerator.cc
  • media/gpu/windows/d3d12_video_encode_h265_delegate.cc
  • media/gpu/windows/d3d12_video_encode_h264_delegate.cc

Estimated timestamp from git blame: 2026-02-05

Background

In the D3D12 Video Encode Accelerator, when an input frame requires format or color space conversion, the work is asynchronously submitted to the GPU via D3D12VideoProcessorWrapper::ProcessFrames(). The CPU synchronization to wait for this GPU work to finish is normally performed later in the pipeline, specifically inside D3D12VideoEncoderWrapper::Encode(), which calls fence_->SignalAndWaitCPU().

Root Cause

A compromised renderer process can supply a maliciously crafted VideoEncodeOptions struct over the media.mojom.VideoEncodeAccelerator Mojo interface to force an error to occur after ProcessFrames() has submitted work, but before the synchronization in D3D12VideoEncoderWrapper::Encode() is reached.

Specifically, the attacker configures the session for manual reference buffer control. They then send an Encode() request where the reference_buffers array contains an index that is mathematically within the maximum allowed range, but has never been previously populated with a valid reference frame.

Potential Exploit Steps

  1. Attacker Setup: A compromised renderer calls Initialize on the VideoEncodeAccelerator Mojo interface with config.manual_reference_buffer_control = true.
  2. Triggering the UAF:
    • The attacker calls Encode with a frame that requires color conversion (e.g., NV12 input for a P010 profile) and a VideoEncodeOptions struct containing an unpopulated reference buffer index (e.g., index 1).
    • In the GPU process, D3D12VideoEncodeDelegate::Encode performs an “early validation” (lines 286-292). It only checks if the index is less than GetMaxNumOfManualRefBuffers(). Because the index is within bounds, validation passes.
    • Because color conversion is needed, video_processor_wrapper_->ProcessFrames() is called. This submits asynchronous commands to the D3D12 video process queue. The CPU does not wait here.
    • The delegate then calls the virtual EncodeImpl() (e.g., D3D12VideoEncodeH265Delegate::EncodeImpl).
    • Inside EncodeImpl, a secondary validation occurs. The code calls reference_frame_manager_.GetReferenceFrameId(reference_buffers[i]). Because the buffer index was never populated, this returns std::nullopt.
    • EncodeImpl immediately returns an EncoderStatus::Codes::kBadReferenceBuffer error.
    • This early error return bypasses the call to video_encoder_wrapper_->Encode(), meaning the critical SignalAndWaitCPU() synchronization step is entirely skipped.
  3. Teardown and UAF:
    • The error propagates back to D3D12VideoEncodeAccelerator::Encode, which calls NotifyError(), initiating the teardown of the encoding session.
    • During teardown, ~D3D12VideoEncodeAccelerator only explicitly waits on its copy_command_queue_; it does not wait on the video processing queue.
    • The D3D12VideoEncodeDelegate and its D3D12VideoProcessorWrapper member are destroyed. The wrapper’s destructor is defaulted, meaning its D3D12 COM objects (command_allocator_, command_list_, and processed_input_frame_) are released.
    • At this exact moment, the GPU is still actively executing the commands submitted during ProcessFrames(). This results in a highly deterministic D3D12 Use-After-Free in the unsandboxed GPU process.

Impact

This vulnerability allows a compromised renderer process to trigger a Use-After-Free in the GPU process. Because the GPU process is unsandboxed (or highly privileged depending on the specific Windows architecture), an attacker could potentially groom memory and exploit this UAF to achieve arbitrary code execution, resulting in a full Sandbox Escape.

Suggested Fix

Ensure that the CPU always synchronizes with the video processing queue during teardown if there is in-flight work. This could be achieved by adding an explicit wait in the destructor of D3D12VideoEncodeDelegate or by giving D3D12VideoProcessorWrapper a non-default destructor that waits on its internal fence before releasing its D3D12 resources. Alternatively, all validation regarding reference buffers must be completed before ProcessFrames() is called.

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


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.

View on issue tracker