CVE-2026-17723
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
D3D12CommandQueueMockmedia/base/win/d3d12_mocks.h |
modified | |
D3D12CommandAllocatorMockmedia/base/win/d3d12_mocks.h |
modified | |
D3D12FenceMockmedia/base/win/d3d12_mocks.h |
modified | |
D3D12VideoProcessorMockmedia/base/win/d3d12_video_mocks.h |
modified |
Files Changed
media/base/win/d3d12_mocks.ccmedia/base/win/d3d12_mocks.hmedia/base/win/d3d12_video_mocks.ccmedia/base/win/d3d12_video_mocks.h
Patch
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)));
Regression Test / PoC
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
Original Bug Report
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.ccmedia/gpu/windows/d3d12_video_encode_delegate.ccmedia/gpu/windows/d3d12_video_encode_accelerator.ccmedia/gpu/windows/d3d12_video_encode_h265_delegate.ccmedia/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
- Attacker Setup: A compromised renderer calls
Initializeon theVideoEncodeAcceleratorMojo interface withconfig.manual_reference_buffer_control = true. - Triggering the UAF:
- The attacker calls
Encodewith a frame that requires color conversion (e.g., NV12 input for a P010 profile) and aVideoEncodeOptionsstruct containing an unpopulated reference buffer index (e.g., index1). - In the GPU process,
D3D12VideoEncodeDelegate::Encodeperforms an “early validation” (lines 286-292). It only checks if the index is less thanGetMaxNumOfManualRefBuffers(). 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 callsreference_frame_manager_.GetReferenceFrameId(reference_buffers[i]). Because the buffer index was never populated, this returnsstd::nullopt. EncodeImplimmediately returns anEncoderStatus::Codes::kBadReferenceBuffererror.- This early error return bypasses the call to
video_encoder_wrapper_->Encode(), meaning the criticalSignalAndWaitCPU()synchronization step is entirely skipped.
- The attacker calls
- Teardown and UAF:
- The error propagates back to
D3D12VideoEncodeAccelerator::Encode, which callsNotifyError(), initiating the teardown of the encoding session. - During teardown,
~D3D12VideoEncodeAcceleratoronly explicitly waits on itscopy_command_queue_; it does not wait on the video processing queue. - The
D3D12VideoEncodeDelegateand itsD3D12VideoProcessorWrappermember are destroyed. The wrapper’s destructor is defaulted, meaning its D3D12 COM objects (command_allocator_,command_list_, andprocessed_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.
- The error propagates back to
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.