Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Codecs
DescriptionUse after free in Codecs
ComponentCodecs
Bug ClassUAF
Tracker500174874
Fix commitc73fcf79f77a (chromium/src) +228/-47
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
media/gpu/windows/d3d12_video_encode_accelerator.cc
modified
TEST_F
media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc
modified

Files Changed

  • media/gpu/windows/d3d12_video_encode_accelerator.cc
  • media/gpu/windows/d3d12_video_encode_accelerator.h
  • media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc
From c73fcf79f77a96a4ede5f4a4d3e50a1b5d833219 Mon Sep 17 00:00:00 2001
From: Qiu Jianlin <[email protected]>
Date: Thu, 16 Apr 2026 15:50:25 -0700
Subject: [PATCH] Fix GPU UAF via early return in D3D12 VEA.

Early return for encoding failures, and avoid reset GPU command
allocator when the command list is still being executed.

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

diff --git a/media/gpu/windows/d3d12_video_encode_accelerator.cc b/media/gpu/windows/d3d12_video_encode_accelerator.cc
index 76e1a0a..c629c2a 100644
--- a/media/gpu/windows/d3d12_video_encode_accelerator.cc
+++ b/media/gpu/windows/d3d12_video_encode_accelerator.cc
@@ -845,6 +845,10 @@
 void D3D12VideoEncodeAccelerator::TryEncodeFrames() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(encoder_sequence_checker_);
 
+  if (error_occurred_) {
+    return;
+  }
+
   while (!input_frames_queue_.empty() && !bitstream_buffers_.empty()) {
     auto& next_input = input_frames_queue_.front();
     if (next_input.resolving_shared_image ||
@@ -856,9 +860,12 @@
       break;
     }
 
-    DoEncodeTask(next_input, bitstream_buffers_.front());
+    const bool success = DoEncodeTask(next_input, bitstream_buffers_.front());
     input_frames_queue_.pop_front();
     bitstream_buffers_.pop();
+    if (!success) {
+      break;
+    }
   }
 
   if (flush_requested_ && input_frames_queue_.empty()) {
@@ -869,7 +876,7 @@
   }
 }
 
-void D3D12VideoEncodeAccelerator::DoEncodeTask(
+bool D3D12VideoEncodeAccelerator::DoEncodeTask(
     const InputFrameRef& input_frame,
     const BitstreamBuffer& bitstream_buffer) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(encoder_sequence_checker_);
@@ -882,9 +889,10 @@
     } else {
       frame = ConvertToMemoryMappedFrame(std::move(frame));
       if (!frame) {
-        return NotifyError(
+        NotifyError(
             {EncoderStatus::Codes::kInvalidInputFrame,
              "Failed to convert shared memory mappable SI for encoding"});
+        return false;
       }
       picture_buffer = CreateResourceForSharedMemoryVideoFrame(*frame);
     }
@@ -893,19 +901,22 @@
   } else if (frame->HasSharedImage()) {
     picture_buffer = input_frame.resolved_picture;
   } else {
-    return NotifyError({EncoderStatus::Codes::kInvalidInputFrame,
-                        "Unsupported frame storage type for encoding"});
+    NotifyError({EncoderStatus::Codes::kInvalidInputFrame,
+                 "Unsupported frame storage type for encoding"});
+    return false;
   }
   if (!picture_buffer.resource) {
-    return NotifyError({EncoderStatus::Codes::kInvalidInputFrame,
-                        "Failed to create input_texture"});
+    NotifyError({EncoderStatus::Codes::kInvalidInputFrame,
+                 "Failed to create input_texture"});
+    return false;
   }
 
   auto result_or_error =
       encoder_->Encode(picture_buffer, frame->ColorSpace(), bitstream_buffer,
                        input_frame.options);
   if (!result_or_error.has_value()) {
-    return NotifyError(std::move(result_or_error).error());
+    NotifyError(std::move(result_or_error).error());
+    return false;
   }
 
   D3D12VideoEncodeDelegate::EncodeResult result =
@@ -924,6 +935,7 @@
   child_task_runner_->PostTask(
       FROM_HERE, BindOnce(&Client::BitstreamBufferReady, client_,
                           result.bitstream_buffer_id, result.metadata));
+  return true;
 }
 
 void D3D12VideoEncodeAccelerator::DestroyTask() {
@@ -933,26 +945,36 @@
 }
 
 void D3D12VideoEncodeAccelerator::NotifyError(EncoderStatus status) {
+  // We return here when `error_occurred_` was already true, as this is not the
+  // first error that is reported.
+  if (error_occurred_.exchange(true)) {
+    return;
+  }
+
+  CHECK(!status.is_ok());
   base::UmaHistogramEnumeration(
       GetEncoderStatusHistogramName(config_.output_profile), status.code());
 
   if (!child_task_runner_->RunsTasksInCurrentSequence()) {
     child_task_runner_->PostTask(
-        FROM_HERE, BindOnce(&D3D12VideoEncodeAccelerator::NotifyError,
-                            child_weak_this_, std::move(status)));
+        FROM_HERE,
+        BindOnce(&D3D12VideoEncodeAccelerator::NotifyErrorOnChildSequence,
+                 child_weak_this_, std::move(status)));
     return;
   }
 
-  CHECK(!status.is_ok());
+  NotifyErrorOnChildSequence(std::move(status));
+}
+
+void D3D12VideoEncodeAccelerator::NotifyErrorOnChildSequence(
+    EncoderStatus status) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(child_sequence_checker_);
   MEDIA_LOG(ERROR, media_log_)
       << "D3D12VEA error " << static_cast<int32_t>(status.code()) << ": "
       << status.message();
-  if (!error_occurred_) {
-    if (client_) {
-      client_->NotifyErrorStatus(status);
-      client_ptr_factory_->InvalidateWeakPtrs();
-    }
-    error_occurred_ = true;
+  if (client_) {
+    client_->NotifyErrorStatus(status);
+    client_ptr_factory_->InvalidateWeakPtrs();
   }
 }
 
diff --git a/media/gpu/windows/d3d12_video_encode_accelerator.h b/media/gpu/windows/d3d12_video_encode_accelerator.h
index e58f120a..770b5d4 100644
--- a/media/gpu/windows/d3d12_video_encode_accelerator.h
+++ b/media/gpu/windows/d3d12_video_encode_accelerator.h
@@ -11,6 +11,7 @@
 
 #include <wrl.h>
 
+#include <atomic>
 #include <vector>
 
 #include "base/containers/circular_deque.h"
@@ -124,7 +125,8 @@
   void EncodeTask(scoped_refptr<VideoFrame> frame,
                   const VideoEncoder::EncodeOptions& options);
 
-  void DoEncodeTask(const InputFrameRef& input_frame,
+  // Returns false if an error was encountered.
+  bool DoEncodeTask(const InputFrameRef& input_frame,
                     const BitstreamBuffer& bitstream_buffer);
 
   void TryEncodeFrames();
@@ -139,6 +141,8 @@
 
   void NotifyError(EncoderStatus status);
 
+  void NotifyErrorOnChildSequence(EncoderStatus status);
+
   // Invoked when the CommandBufferHelper is available.
   void OnCommandBufferHelperAvailable(GetCommandBufferHelperResult result);
 
@@ -183,7 +187,8 @@
   base::WeakPtr<Client> client_;
   std::unique_ptr<MediaLog> media_log_;
 
-  bool error_occurred_ = false;
+  // Whether an encoding error has occurred.
+  std::atomic<bool> error_occurred_ = false;
 
   // True if Destroy() has been called.
   bool destroy_requested_ GUARDED_BY_CONTEXT(child_sequence_checker_) = false;
diff --git a/media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc b/media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc
index de844fce..be1176f2 100644
--- a/media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc
+++ b/media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc
@@ -485,4 +485,107 @@
   Mock::VerifyAndClearExpectations(&client_);
 }
 
+// Verifies that when DoEncodeTask fails, TryEncodeFrames breaks out of the
+// loop and does not process subsequent frames.
+TEST_F(D3D12VideoEncodeAcceleratorTest, EncodeErrorStopsProcessingNextFrames) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc b/media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc
index de844fce..be1176f2 100644
--- a/media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc
+++ b/media/gpu/windows/d3d12_video_encode_accelerator_unittest.cc
@@ -485,4 +485,107 @@
   Mock::VerifyAndClearExpectations(&client_);
 }
 
+// Verifies that when DoEncodeTask fails, TryEncodeFrames breaks out of the
+// loop and does not process subsequent frames.
+TEST_F(D3D12VideoEncodeAcceleratorTest, EncodeErrorStopsProcessingNextFrames) {
+  auto* d3d12_video_encode_accelerator =
+      static_cast<D3D12VideoEncodeAccelerator*>(
+          video_encode_accelerator_.get());
+
+  // Set up a factory that makes Encode fail on the first call.
+  class FailFirstEncodeFactory : public D3D12VideoEncodeAccelerator::
+                                     VideoEncodeDelegateFactoryInterface {
+   public:
+    int encode_call_count_ = 0;
+
+    std::unique_ptr<D3D12VideoEncodeDelegate> CreateVideoEncodeDelegate(
+        ID3D12VideoDevice3* video_device,
+        VideoCodecProfile profile) override {
+      gpu::GpuDriverBugWorkarounds gpu_workarounds{};
+      auto encoder_delegate =
+          std::make_unique<NiceMock<MockVideoEncoderDelegate>>(
+              video_device, gpu_workarounds, profile);
+      ON_CALL(*encoder_delegate, Initialize(_))
+          .WillByDefault(Return(EncoderStatus::Codes::kOk));
+      ON_CALL(*encoder_delegate, GetMaxNumOfRefFrames())
+          .WillByDefault(Return(16));
+      ON_CALL(*encoder_delegate, GetMaxNumOfManualRefBuffers())
+          .WillByDefault(Return(0));
+      ON_CALL(*encoder_delegate, Encode(_, _, _, _))
+          .WillByDefault(
+              [this](D3D12PictureBuffer, const gfx::ColorSpace&,
+                     const BitstreamBuffer& bitstream_buffer,
+                     const VideoEncoder::EncodeOptions&)
+                  -> EncoderStatus::Or<D3D12VideoEncodeDelegate::EncodeResult> {
+                ++encode_call_count_;
+                if (encode_call_count_ == 1) {
+                  return EncoderStatus(
+                      EncoderStatus::Codes::kBadReferenceBuffer,
+                      "Simulated encode failure");
+                }
+                return D3D12VideoEncodeDelegate::EncodeResult{
+                    bitstream_buffer.id()};
+              });
+      return std::move(encoder_delegate);
+    }
+
+    VideoEncodeAccelerator::SupportedProfiles GetSupportedProfiles(
+        ID3D12VideoDevice3* video_device,
+        const std::vector<D3D12_VIDEO_ENCODER_CODEC>& codecs) override {
+      VideoEncodeAccelerator::SupportedProfile profile(kSupportedProfile,
+                                                       kSupportedSize, 30, 1);
+      profile.scalability_modes.push_back(SVCScalabilityMode::kL1T1);
+      profile.gpu_supported_pixel_formats.push_back(PIXEL_FORMAT_NV12);
+      profile.gpu_supported_pixel_formats.push_back(PIXEL_FORMAT_BGRA);
+      profile.supports_gpu_shared_images = true;
+      return {profile};
+    }
+  };
+
+  auto fail_factory = std::make_unique<FailFirstEncodeFactory>();
+  auto* fail_factory_ptr = fail_factory.get();
+  d3d12_video_encode_accelerator->SetEncoderFactoryForTesting(
+      std::move(fail_factory));
+
+  auto supported_profiles =
+      d3d12_video_encode_accelerator->GetSupportedProfiles();
+  ASSERT_FALSE(supported_profiles.empty());
+  auto profile = supported_profiles.front();
+  auto config = SupportedProfileToConfig(profile);
+
+  unsigned bitstream_buffer_count = 0;
+  size_t bitstream_buffer_size = 0;
+  EXPECT_CALL(*client_, RequireBitstreamBuffers(_, _, _))
+      .WillOnce(
+          [&](unsigned int count, const gfx::Size& size, size_t size_in_bytes) {
+            bitstream_buffer_count = count;
+            bitstream_buffer_size = size_in_bytes;
+          });
+  EXPECT_TRUE(d3d12_video_encode_accelerator
+                  ->Initialize(config, client_.get(), media_log_->Clone())
+                  .is_ok());
+  WaitForEncoderTasksToComplete();
+  Mock::VerifyAndClearExpectations(client_.get());
+
+  // Queue two frames and two bitstream buffers so TryEncodeFrames has two
+  // items to process in its loop.
+  for (unsigned i = 0; i < 2; ++i) {
+    BitstreamBuffer bitstream_buffer(
+        i, base::UnsafeSharedMemoryRegion::Create(bitstream_buffer_size),
+        bitstream_buffer_size);
+    d3d12_video_encode_accelerator->UseOutputBitstreamBuffer(
+        std::move(bitstream_buffer));
+  }
+  for (unsigned i = 0; i < 2; ++i) {
+    d3d12_video_encode_accelerator->Encode(CreateTestVideoFrame(), false);
+  }
+
+  EXPECT_CALL(*client_, NotifyErrorStatus(_)).Times(1);
+  WaitForEncoderTasksToComplete();
+  // Encode should have been called only once: the first call fails, and the
+  // loop should break without attempting to encode the second frame.
+  EXPECT_EQ(fail_factory_ptr->encode_call_count_, 1);
+  Mock::VerifyAndClearExpectations(client_.get());
+}
+
 }  // namespace media
diff --git a/media/gpu/windows/d3d12_video_encode_delegate_unittest.cc b/media/gpu/windows/d3d12_video_encode_delegate_unittest.cc
index 2a57aeb..b67eb474 100644
--- a/media/gpu/windows/d3d12_video_encode_delegate_unittest.cc
+++ b/media/gpu/windows/d3d12_video_encode_delegate_unittest.cc
@@ -333,4 +333,52 @@
   EXPECT_EQ(result_or_error.code(), EncoderStatus::Codes::kBadReferenceBuffer);
 }
 
+TEST_F(D3D12VideoEncodeDelegateTest,
+       EncodeWithOutOfRangeReferenceBufferIndexFails) {
+  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
+  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());
+
+  gfx::Size input_size = config.input_visible_size;
+  auto input_frame = CreateResource(input_size, config.input_format);
+  gfx::ColorSpace color_space = gfx::ColorSpace::CreateREC709();
+  constexpr size_t kPayloadSize = 1024;
+  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
+  BitstreamBuffer bitstream_buffer(0, shared_memory.Duplicate(), kPayloadSize);
+
+  VideoEncoder::EncodeOptions options;
+  // Use a single reference buffer with an index >= GetMaxNumOfManualRefBuffers.
+  options.reference_buffers.push_back(
+      static_cast<uint8_t>(encoder_delegate_->GetMaxNumOfManualRefBuffers()));
+
+  auto result_or_error = encoder_delegate_->Encode(input_frame, color_space,
+                                                   bitstream_buffer, options);
+
+  EXPECT_FALSE(result_or_error.has_value());
+  EXPECT_EQ(result_or_error.code(), EncoderStatus::Codes::kBadReferenceBuffer);
+}
+
+TEST_F(D3D12VideoEncodeDelegateTest,
+       EncodeWithOutOfRangeUpdateBufferIndexFails) {
+  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
+  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());
+
+  gfx::Size input_size = config.input_visible_size;
+  auto input_frame = CreateResource(input_size, config.input_format);
+  gfx::ColorSpace color_space = gfx::ColorSpace::CreateREC709();
+  constexpr size_t kPayloadSize = 1024;
+  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
+  BitstreamBuffer bitstream_buffer(0, shared_memory.Duplicate(), kPayloadSize);
+
+  VideoEncoder::EncodeOptions options;
+  // Set update_buffer to a value >= GetMaxNumOfRefFrames.
+  options.update_buffer =
+      static_cast<uint8_t>(encoder_delegate_->GetMaxNumOfRefFrames());
+
+  auto result_or_error = encoder_delegate_->Encode(input_frame, color_space,
+                                                   bitstream_buffer, options);
+
+  EXPECT_FALSE(result_or_error.has_value());
+  EXPECT_EQ(result_or_error.code(), EncoderStatus::Codes::kBadReferenceBuffer);
+}
+
 }  // namespace media
Loading diff…

Original Bug Report

reported by [email protected]

Potential GPU UAF via early return in D3D12 Video Encode Accelerator

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 without the security team.

Overview: A vulnerability in the D3D12 video encode path allows a command allocator to be reset while its command lists are still executing on the GPU. This occurs because an early error return skips crucial CPU synchronization, and the encoding loop immediately processes the next frame. A compromised renderer can trigger this by providing an invalid reference buffer index, potentially leading to GPU process compromise.

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_h264_delegate.cc
  • media/gpu/windows/d3d12_video_encode_h265_delegate.cc

Estimated timestamp from git blame: 2026-02-05

Technical Description

A potential GPU-side Use-After-Free (UAF) exists in the D3D12 video encode implementation on Windows. The issue stems from a synchronization failure when a renderer-triggered error causes an early return, allowing a D3D12 ID3D12CommandAllocator to be reset while the GPU is still executing commands backed by its memory.

The execution flow is as follows:

  1. D3D12VideoProcessorWrapper::ProcessFrames(): When a frame requires color space conversion, this method is called. It resets command_allocator_, records conversion commands, submits them to the GPU command queue, and signals a fence. Crucially, it does not block the CPU to wait for completion.
  2. Skipped CPU Synchronization: Under normal conditions, the CPU wait occurs later in the pipeline inside D3D12VideoEncoderWrapper::Encode() via a call to SignalAndWaitCPU(). However, if EncodeImpl() (in either the H.264 or H.265 delegates) encounters a validation error, it returns early. This skips the call to the encoder wrapper, meaning the CPU never waits for the video processor’s GPU work to finish.
  3. Loop Continuation: The outer loop in D3D12VideoEncodeAccelerator::TryEncodeFrames() calls DoEncodeTask(). If DoEncodeTask() encounters an error, it calls NotifyError() (which merely posts a task to a different thread) but does not break the while loop. The loop immediately proceeds to process the next frame.
  4. The UAF: The subsequent frame re-enters ProcessFrames(), which unconditionally executes command_allocator_->Reset(). Because the CPU never waited for the first frame, this resets the allocator while the GPU is still actively reading the previous command list, violating D3D12 rules and corrupting the command stream.

Potential Attack Steps

(Note: These are potential steps derived from static code analysis; our tooling agent does not have the ability to run a live proof-of-concept.)

A compromised renderer process could trigger this deterministically via Mojo:

  1. Initialize a D3D12 Video Encode Accelerator (e.g., H.265 profile, manual_reference_buffer_control = true).
  2. Send an Encode request for a frame that requires asynchronous shared image resolution. This blocks the TryEncodeFrames loop.
  3. Send an Encode request for Frame A. Frame A should require color space conversion (forcing the use of the video processor) and specify an invalid update_buffer index in its VideoEncodeOptions (e.g., >= max_num_ref_frames_). Mojo does not enforce strict limits on this field.
  4. Send an Encode request for Frame B (a normal frame).
  5. When the initial frame finishes resolving, TryEncodeFrames processes the queue back-to-back. Frame A submits video processing commands but errors out on the invalid update_buffer index, skipping the CPU wait. Frame B immediately enters ProcessFrames and resets the active command allocator.

Suggested Fix

  1. Halt Processing on Error: In D3D12VideoEncodeAccelerator::TryEncodeFrames(), the while loop should check if an error occurred during DoEncodeTask and break immediately, rather than continuing to the next frame. Changing DoEncodeTask to return a boolean indicating success could facilitate this.
  2. Safe Allocator Reset: D3D12VideoProcessorWrapper::ProcessFrames() should independently ensure its allocator is safe to reset, perhaps by waiting on its own fence (CPU-side) if the previous operation hasn’t completed, rather than relying entirely on downstream encoder logic to block the CPU.
  3. Mojo Input Validation: Validate bounds for fields like update_buffer and reference_buffers.size() earlier in the pipeline or directly within the Mojo traits mapping.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results 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