Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Media
DescriptionInsufficient validation of untrusted input in Media
ComponentMedia
Bug ClassLogic Error
Tracker519693032
Fix commit5a3e59883dbf (chromium/src) +325/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
media/base/win/mf_helpers.cc
modified
MFHelpersAlignmentTest
media/base/win/mf_helpers_unittest.cc
modified
if
media/base/win/mf_helpers_unittest.cc
modified
GetSupportedProfiles
media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
modified

Files Changed

  • media/base/win/mf_helpers.cc
  • media/base/win/mf_helpers_unittest.cc
  • media/gpu/BUILD.gn
  • media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
From 5a3e59883dbf977e24391b57340381d63b7e073e Mon Sep 17 00:00:00 2001
From: Sangbaek Park <sangbaekpark@google.com>
Date: Tue, 16 Jun 2026 16:16:11 -0700
Subject: [PATCH] media: 4:2:0 alignment validation in MF VideoEncodeAccelerator

Added the 4:2:0 subsampled format alignment validation in
QueueInput() so that unaligned visible rects are securely and correctly
rejected.

Unit tests added: : {
MediaFoundationVideoEncodeAcceleratorAlignmentTest.*,
MFHelpersAlignmentTest.*
}

Bug: 519693032
Change-Id: Ica81fd85c10ebb9de394c88a2f7fadf33295e961
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7905334
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Sangbaek Park <sangbaekpark@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1647964}
---

diff --git a/media/base/win/mf_helpers.cc b/media/base/win/mf_helpers.cc
index a5d937dc..4e3cfa2 100644
--- a/media/base/win/mf_helpers.cc
+++ b/media/base/win/mf_helpers.cc
@@ -919,6 +919,20 @@
 
   HRESULT hr;
   if (need_perform_copy) {
+    if (frame->format() == PIXEL_FORMAT_NV12 ||
+        frame->format() == PIXEL_FORMAT_I420 ||
+        frame->format() == PIXEL_FORMAT_YV12 ||
+        frame->format() == PIXEL_FORMAT_NV21) {
+      const gfx::Rect& visible_rect = frame->visible_rect();
+      if (visible_rect.x() % 2 != 0 || visible_rect.y() % 2 != 0 ||
+          visible_rect.width() % 2 != 0 || visible_rect.height() % 2 != 0) {
+        DLOG(ERROR) << "Source visible_rect is not properly aligned for 4:2:0 "
+                       "subsampled format.";
+        return nullptr;
+      }
+    } else {
+      NOTREACHED();
+    }
     D3D11_TEXTURE2D_DESC desc;
     input_texture->GetDesc(&desc);
     desc.Width = static_cast<UINT>(frame->visible_rect().width());
@@ -1183,6 +1197,21 @@
                                            nullptr, &shared_handle);
   }
   if (FAILED(hr) || is_texture_array) {
+    if (frame->format() == PIXEL_FORMAT_NV12 ||
+        frame->format() == PIXEL_FORMAT_I420 ||
+        frame->format() == PIXEL_FORMAT_YV12 ||
+        frame->format() == PIXEL_FORMAT_NV21) {
+      const gfx::Rect& visible_rect = frame->visible_rect();
+      if (visible_rect.x() % 2 != 0 || visible_rect.y() % 2 != 0 ||
+          visible_rect.width() % 2 != 0 || visible_rect.height() % 2 != 0) {
+        RETURN_ON_FAILURE_WITH_CALLBACK(
+            E_INVALIDARG,
+            "Source visible_rect is not properly aligned for "
+            "4:2:0 subsampled format.");
+      }
+    } else {
+      NOTREACHED();
+    }
     TRACE_EVENT0("media", "CopyTextureOnCreateSharedHandleFailed");
     texture_desc.Usage = D3D11_USAGE_DEFAULT;
     texture_desc.BindFlags =
diff --git a/media/base/win/mf_helpers_unittest.cc b/media/base/win/mf_helpers_unittest.cc
index d39e2bc5..b8391d8 100644
--- a/media/base/win/mf_helpers_unittest.cc
+++ b/media/base/win/mf_helpers_unittest.cc
@@ -80,5 +80,68 @@
   EXPECT_EQ(output_desc.Height, static_cast<UINT>(visible_rect.height()));
 }
 
+class MFHelpersAlignmentTest
+    : public ::testing::TestWithParam<VideoPixelFormat> {};
+
+TEST_P(MFHelpersAlignmentTest,
+       CreateSampleFromTextureRejectsUnalignedVisibleRect) {
+  Microsoft::WRL::ComPtr<ID3D11Device> device;
+  Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;
+  HRESULT hr =
+      D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr,
+                        0, D3D11_SDK_VERSION, &device, nullptr, &context);
+  if (FAILED(hr)) {
+    // Fallback to WARP if hardware is not available.
+    hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, nullptr,
+                           0, D3D11_SDK_VERSION, &device, nullptr, &context);
+    if (FAILED(hr)) {
+      GTEST_SKIP() << "D3D11 device creation failed";
+    }
+  }
+
+  D3D11_TEXTURE2D_DESC desc = {};
+  desc.Width = 1920;
+  desc.Height = 1088;
+  desc.MipLevels = 1;
+  desc.ArraySize = 1;
+  desc.Format = DXGI_FORMAT_NV12;
+  desc.SampleDesc.Count = 1;
+  desc.Usage = D3D11_USAGE_DEFAULT;
+  desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
+
+  Microsoft::WRL::ComPtr<ID3D11Texture2D> input_texture;
+  hr = device->CreateTexture2D(&desc, nullptr, &input_texture);
+  ASSERT_TRUE(SUCCEEDED(hr));
+
+  // Create a video frame with an unaligned visible rect.
+  gfx::Size coded_size(1922, 1082);
+  gfx::Rect visible_rect(1, 1, 1920, 1080);
+  gfx::Size natural_size(1920, 1080);
+  VideoPixelFormat format = GetParam();
+  scoped_refptr<VideoFrame> frame = VideoFrame::CreateFrame(
+      format, coded_size, visible_rect, natural_size, base::TimeDelta());
+
+  if (!frame) {
+    // VideoFrame::CreateFrame natively validates alignment for some formats
+    // (e.g., I420) and might return null, in which case we safely skip.
+    GTEST_SKIP() << "Cannot create unaligned frame natively for format "
+                 << format;
+  }
+
+  // Create the sample and perform the copy.
+  Microsoft::WRL::ComPtr<IMFSample> sample = CreateSampleFromTexture(
+      device, frame, input_texture, /*need_perform_copy=*/true);
+
+  // Because the visible_rect is unaligned, CreateSampleFromTexture should fail.
+  EXPECT_FALSE(sample);
+}
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         MFHelpersAlignmentTest,
+                         ::testing::Values(PIXEL_FORMAT_NV12,
+                                           PIXEL_FORMAT_I420,
+                                           PIXEL_FORMAT_YV12,
+                                           PIXEL_FORMAT_NV21));
+
 }  // namespace
 }  // namespace media
diff --git a/media/gpu/BUILD.gn b/media/gpu/BUILD.gn
index bd7ed419..1ce3e8d1 100644
--- a/media/gpu/BUILD.gn
+++ b/media/gpu/BUILD.gn
@@ -697,6 +697,7 @@
       "windows/d3d12_video_encode_delegate_unittest.cc",
       "windows/d3d12_video_encode_delegate_unittest.h",
       "windows/d3d12_video_encode_h264_delegate_unittest.cc",
+      "windows/media_foundation_video_encode_accelerator_win_unittest.cc",
       "windows/mf_audio_encoder_unittest.cc",
       "windows/mf_video_processor_accelerator_unittest.cc",
       "windows/scoped_d3d_buffers_unittest.cc",
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 b9eebb46..d8eb5c36 100644
--- a/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
+++ b/media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
@@ -346,6 +346,18 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 }
 
+void MediaFoundationVideoEncodeAccelerator::InitializeForTesting(
+    Client* client,
+    std::unique_ptr<MediaLog> media_log,
+    const gfx::Size& input_visible_size,
+    scoped_refptr<DXGIDeviceManager> dxgi_device_manager) {
+  client_ = client;
+  media_log_ = std::move(media_log);
+  state_ = kEncoding;
+  input_visible_size_ = input_visible_size;
+  dxgi_device_manager_ = std::move(dxgi_device_manager);
+}
+
 VideoEncodeAccelerator::SupportedProfiles
 MediaFoundationVideoEncodeAccelerator::GetSupportedProfiles() {
   TRACE_EVENT0("gpu,startup",
@@ -444,8 +456,9 @@
   low_latency_mode_ = config.require_low_delay;
   drop_frame_thresh_percentage_ = config.drop_frame_thresh_percentage;
 
-  if (config.HasTemporalLayer())
+  if (config.HasTemporalLayer()) {
     num_temporal_layers_ = config.spatial_layers.front().num_of_temporal_layers;
+  }
 
   input_since_keyframe_count_ = 0;
   zero_layer_counter_ = 0;
@@ -818,6 +831,22 @@
     scoped_refptr<media::VideoFrame> frame,
     const VideoEncoder::EncodeOptions& options,
     bool discard_output) {
+  if (frame && (frame->format() == PIXEL_FORMAT_NV12 ||
+                frame->format() == PIXEL_FORMAT_I420 ||
+                frame->format() == PIXEL_FORMAT_YV12 ||
+                frame->format() == PIXEL_FORMAT_NV21)) {
+    const gfx::Rect& visible_rect = frame->visible_rect();
+    if (visible_rect.x() % 2 != 0 || visible_rect.y() % 2 != 0 ||
+        visible_rect.width() % 2 != 0 || visible_rect.height() % 2 != 0) {
+      NotifyErrorStatus({EncoderStatus::Codes::kInvalidInputFrame,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/base/win/mf_helpers_unittest.cc b/media/base/win/mf_helpers_unittest.cc
index d39e2bc5..b8391d8 100644
--- a/media/base/win/mf_helpers_unittest.cc
+++ b/media/base/win/mf_helpers_unittest.cc
@@ -80,5 +80,68 @@
   EXPECT_EQ(output_desc.Height, static_cast<UINT>(visible_rect.height()));
 }
 
+class MFHelpersAlignmentTest
+    : public ::testing::TestWithParam<VideoPixelFormat> {};
+
+TEST_P(MFHelpersAlignmentTest,
+       CreateSampleFromTextureRejectsUnalignedVisibleRect) {
+  Microsoft::WRL::ComPtr<ID3D11Device> device;
+  Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;
+  HRESULT hr =
+      D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr,
+                        0, D3D11_SDK_VERSION, &device, nullptr, &context);
+  if (FAILED(hr)) {
+    // Fallback to WARP if hardware is not available.
+    hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, nullptr,
+                           0, D3D11_SDK_VERSION, &device, nullptr, &context);
+    if (FAILED(hr)) {
+      GTEST_SKIP() << "D3D11 device creation failed";
+    }
+  }
+
+  D3D11_TEXTURE2D_DESC desc = {};
+  desc.Width = 1920;
+  desc.Height = 1088;
+  desc.MipLevels = 1;
+  desc.ArraySize = 1;
+  desc.Format = DXGI_FORMAT_NV12;
+  desc.SampleDesc.Count = 1;
+  desc.Usage = D3D11_USAGE_DEFAULT;
+  desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
+
+  Microsoft::WRL::ComPtr<ID3D11Texture2D> input_texture;
+  hr = device->CreateTexture2D(&desc, nullptr, &input_texture);
+  ASSERT_TRUE(SUCCEEDED(hr));
+
+  // Create a video frame with an unaligned visible rect.
+  gfx::Size coded_size(1922, 1082);
+  gfx::Rect visible_rect(1, 1, 1920, 1080);
+  gfx::Size natural_size(1920, 1080);
+  VideoPixelFormat format = GetParam();
+  scoped_refptr<VideoFrame> frame = VideoFrame::CreateFrame(
+      format, coded_size, visible_rect, natural_size, base::TimeDelta());
+
+  if (!frame) {
+    // VideoFrame::CreateFrame natively validates alignment for some formats
+    // (e.g., I420) and might return null, in which case we safely skip.
+    GTEST_SKIP() << "Cannot create unaligned frame natively for format "
+                 << format;
+  }
+
+  // Create the sample and perform the copy.
+  Microsoft::WRL::ComPtr<IMFSample> sample = CreateSampleFromTexture(
+      device, frame, input_texture, /*need_perform_copy=*/true);
+
+  // Because the visible_rect is unaligned, CreateSampleFromTexture should fail.
+  EXPECT_FALSE(sample);
+}
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         MFHelpersAlignmentTest,
+                         ::testing::Values(PIXEL_FORMAT_NV12,
+                                           PIXEL_FORMAT_I420,
+                                           PIXEL_FORMAT_YV12,
+                                           PIXEL_FORMAT_NV21));
+
 }  // namespace
 }  // namespace media
diff --git a/media/gpu/windows/media_foundation_video_encode_accelerator_win_unittest.cc b/media/gpu/windows/media_foundation_video_encode_accelerator_win_unittest.cc
new file mode 100644
index 0000000..70059b5e57
--- /dev/null
+++ b/media/gpu/windows/media_foundation_video_encode_accelerator_win_unittest.cc
@@ -0,0 +1,196 @@
+// 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/media_foundation_video_encode_accelerator_win.h"
+
+#include <d3d11.h>
+#include <wrl/client.h>
+
+#include <memory>
+
+#include "base/functional/callback_helpers.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/test/task_environment.h"
+#include "base/win/scoped_handle.h"
+#include "components/viz/common/resources/shared_image_format.h"
+#include "gpu/command_buffer/client/test_shared_image_interface.h"
+#include "media/base/encoder_status.h"
+#include "media/base/media_log.h"
+#include "media/base/media_util.h"
+#include "media/base/video_codecs.h"
+#include "media/base/video_frame.h"
+#include "media/base/win/dxgi_device_manager.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/size.h"
+#include "ui/gfx/gpu_memory_buffer_handle.h"
+
+namespace media {
+
+namespace {
+
+class MockEncoderClient : public VideoEncodeAccelerator::Client {
+ public:
+  void RequireBitstreamBuffers(unsigned int input_count,
+                               const gfx::Size& input_coded_size,
+                               size_t output_buffer_size) override {}
+  void BitstreamBufferReady(int32_t bitstream_buffer_id,
+                            const BitstreamBufferMetadata& metadata) override {}
+  void NotifyErrorStatus(const EncoderStatus& status) override {
+    status_ = status;
+  }
+  void NotifyEncoderInfoChange(const VideoEncoderInfo& info) override {}
+
+  EncoderStatus GetLastStatus() const { return status_; }
+
+ private:
+  EncoderStatus status_ = EncoderStatus::Codes::kOk;
+};
+
+class TestMediaFoundationVideoEncodeAccelerator
+    : public MediaFoundationVideoEncodeAccelerator {
+ public:
+  using MediaFoundationVideoEncodeAccelerator::InitializeForTesting;
+
+  TestMediaFoundationVideoEncodeAccelerator(
+      const gpu::GpuPreferences& gpu_preferences,
+      const gpu::GpuDriverBugWorkarounds& gpu_workarounds,
+      CHROME_LUID luid)
+      : MediaFoundationVideoEncodeAccelerator(gpu_preferences,
+                                              gpu_workarounds,
+                                              luid) {}
+  ~TestMediaFoundationVideoEncodeAccelerator() override = default;
+};
+
+}  // namespace
+
+class MediaFoundationVideoEncodeAcceleratorTest : public ::testing::Test {
+ protected:
+  void SetUpFakeEncoder(TestMediaFoundationVideoEncodeAccelerator* encoder,
+                        VideoEncodeAccelerator::Client* client,
+                        Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device) {
+    encoder->InitializeForTesting(
+        client, std::make_unique<NullMediaLog>(), gfx::Size(1920, 1080),
+        DXGIDeviceManager::Create(CHROME_LUID{0, 0}, d3d11_device.Get()));
+  }
+
+  base::test::SingleThreadTaskEnvironment task_environment_;
+};
+
+class MediaFoundationVideoEncodeAcceleratorAlignmentTest
+    : public MediaFoundationVideoEncodeAcceleratorTest,
+      public ::testing::WithParamInterface<VideoPixelFormat> {};
+
+TEST_P(MediaFoundationVideoEncodeAcceleratorAlignmentTest,
+       RejectUnalignedVisibleRect) {
+  // Step 1: Initialize a D3D11 device. Try hardware first, then fallback to
+  // WARP (software) if unavailable.
+  Microsoft::WRL::ComPtr<ID3D11Device> d3d11_device;
+  Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;
+  HRESULT hr =
+      D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr,
+                        0, D3D11_SDK_VERSION, &d3d11_device, nullptr, &context);
+  if (FAILED(hr)) {
+    hr =
+        D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, nullptr, 0,
+                          D3D11_SDK_VERSION, &d3d11_device, nullptr, &context);
+    if (FAILED(hr)) {
+      GTEST_SKIP() << "D3D11 device creation failed";
+    }
+  }
+
+  // Step 2: Set up a fake MediaFoundationVideoEncodeAccelerator with a mock
+  // client.
+  auto encoder = base::WrapUnique(new TestMediaFoundationVideoEncodeAccelerator(
+      gpu::GpuPreferences(), gpu::GpuDriverBugWorkarounds(),
+      CHROME_LUID{0, 0}));
+
+  MockEncoderClient client;
+  SetUpFakeEncoder(encoder.get(), &client, d3d11_device);
+
+  // Step 3: Define an intentionally unaligned visible_rect (odd
+  // coordinates/dimensions) for a 4:2:0 format.
+  gfx::Rect visible_rect(1, 1, 1919, 1079);
+  gfx::Size natural_size(1919, 1079);
+  VideoPixelFormat format = GetParam();
+  scoped_refptr<VideoFrame> frame;
+
+  if (format == PIXEL_FORMAT_NV12) {
+    // Step 4a: For NV12, simulate a GPU-backed frame. Create a shared D3D11
+    // texture and wrap it in a SharedImage and VideoFrame.
+    D3D11_TEXTURE2D_DESC desc = {};
+    desc.Width = 1920;
+    desc.Height = 1080;
+    desc.MipLevels = 1;
+    desc.ArraySize = 1;
+    desc.Format = DXGI_FORMAT_NV12;
+    desc.SampleDesc.Count = 1;
+    desc.Usage = D3D11_USAGE_DEFAULT;
+    desc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
+    desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE |
+                     D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
+
+    Microsoft::WRL::ComPtr<ID3D11Texture2D> texture;
+    hr = d3d11_device->CreateTexture2D(&desc, nullptr, &texture);
+    ASSERT_HRESULT_SUCCEEDED(hr);
+
+    Microsoft::WRL::ComPtr<IDXGIResource1> dxgi_resource;
+    hr = texture.As(&dxgi_resource);
+    ASSERT_HRESULT_SUCCEEDED(hr);
+
+    HANDLE shared_handle;
+    hr = dxgi_resource->CreateSharedHandle(nullptr, DXGI_SHARED_RESOURCE_READ,
+                                           nullptr, &shared_handle);
+    ASSERT_HRESULT_SUCCEEDED(hr);
+
+    gfx::GpuMemoryBufferHandle gmb_handle{
+        gfx::DXGIHandle(base::win::ScopedHandle(shared_handle))};
+    gmb_handle.type = gfx::GpuMemoryBufferType::DXGI_SHARED_HANDLE;
+
+    auto test_sii = base::MakeRefCounted<gpu::TestSharedImageInterface>();
+    auto shared_image = test_sii->CreateSharedImage(
+        {viz::MultiPlaneFormat::kNV12, gfx::Size(1920, 1080), gfx::ColorSpace(),
+         gpu::SHARED_IMAGE_USAGE_DISPLAY_READ,
+         "MediaFoundationVideoEncodeAcceleratorTest"},
+        gpu::kNullSurfaceHandle, gfx::BufferUsage::GPU_READ,
+        std::move(gmb_handle));
+
+    frame = VideoFrame::WrapMappableSharedImage(
+        std::move(shared_image), test_sii->GenVerifiedSyncToken(),
+        base::DoNothing(), visible_rect, natural_size, base::TimeDelta());
+  } else {
+    // Step 4b: For memory-backed frames (I420, YV12, NV21), use
+    // CreateZeroInitializedFrame instead of D3D texture mocks.
+    frame = VideoFrame::CreateZeroInitializedFrame(
+        format, gfx::Size(1920, 1080), visible_rect, natural_size,
+        base::TimeDelta());
+  }
+
+  if (!frame) {
+    // Step 5: Check if the frame was successfully created.
+    // VideoFrame::CreateZeroInitializedFrame natively validates alignment and
+    // might return null, in which case we safely skip.
+    GTEST_SKIP() << "Cannot create unaligned frame natively for format "
+                 << format;
+  }
+
+  encoder->Encode(frame, false);
+  task_environment_.RunUntilIdle();
+
+  // Step 6: Attempt to encode the frame. Because the visible_rect is unaligned,
+  // the encoder should immediately reject the frame and safely report
+  // kInvalidInputFrame via the client.
+  EXPECT_EQ(client.GetLastStatus().code(),
+            EncoderStatus::Codes::kInvalidInputFrame);
+}
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         MediaFoundationVideoEncodeAcceleratorAlignmentTest,
+                         ::testing::Values(PIXEL_FORMAT_NV12,
+                                           PIXEL_FORMAT_I420,
+                                           PIXEL_FORMAT_YV12,
+                                           PIXEL_FORMAT_NV21));
+
+}  // namespace media
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU VRAM leak in MFVEA PerformD3DCopy due to unaligned NV12 visible_rect

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: In MediaFoundationVideoEncodeAccelerator on Windows, a compromised renderer can supply an unaligned visible_rect for NV12 video frames. This results in an invalid source box for D3D11 CopySubresourceRegion, causing the copy operation to silently fail. Consequently, the uninitialized destination texture is encoded and returned to the renderer, potentially leaking cross-origin GPU VRAM.

Affected files:

  • media/gpu/windows/media_foundation_video_encode_accelerator_win.cc

Estimated timestamp from git blame: 2024-06-25

Root Cause Analysis

In MediaFoundationVideoEncodeAccelerator on Windows, when the frame size matches the configured size, the accelerator attempts to copy the frame’s texture to avoid concurrent usage glitches. This is performed via PerformD3DCopy inside media/gpu/windows/media_foundation_video_encode_accelerator_win.cc:

  1. Uninitialized Texture Allocation (InitializeD3DCopying): The destination texture copied_d3d11_texture_ is allocated with nullptr as the pInitialData argument, which leaves its contents uninitialized (stale GPU memory):

    // media/gpu/windows/media_foundation_video_encode_accelerator_win.cc
    HRESULT hr = texture_device->CreateTexture2D(&copy_desc, nullptr,
                                                 &copied_d3d11_texture);
    
  2. Unaligned Box Construction and Copy (PerformD3DCopy): The code checks that visible_rect is within the bounds of the input texture’s dimensions. However, it does not check that the coordinates are 2x2 aligned for chroma subsampling. It then constructs a D3D11_BOX directly using these coordinates:

    D3D11_BOX src_box = {static_cast<UINT>(visible_rect.x()),
                         static_cast<UINT>(visible_rect.y()),
                         0,
                         static_cast<UINT>(visible_rect.right()),
                         static_cast<UINT>(visible_rect.bottom()),
                         1};
    device_context->CopySubresourceRegion(copied_d3d11_texture_.Get(), 0, 0, 0,
                                          0, input_texture, 0, &src_box);
    
  3. Silent Failure and Information Leak: For DXGI_FORMAT_NV12 textures, the D3D11 runtime requires the box dimensions and offsets to align to the format’s 2x2 subsampling grid. If the renderer provides odd-aligned coordinates (e.g., x = 1, y = 1), the CopySubresourceRegion call is rejected by the D3D11 runtime (D3D11_MESSAGE_ID_COPYSUBRESOURCEREGION_INVALIDSOURCEBOX) and silently no-ops.

    Since there is no return-value check or fallback handling, the uninitialized copied_d3d11_texture_ is wrapped into an IMFSample and passed to the MFT hardware encoder via ProcessInput. The encoder compresses this uninitialized GPU memory and returns the bitstream containing stale GPU graphics data back to the renderer.

Suggested Attack Steps

Note: The following are potential steps as these findings are based on static analysis and have not been validated with a dynamic proof-of-concept.

  1. From a compromised renderer, initialize a VideoEncodeAccelerator with format PIXEL_FORMAT_NV12 and a standard target resolution (e.g., 1920x1080).
  2. Allocate a slightly larger multi-planar NV12 SharedImage (e.g., 1922x1082) backed by a DXGI shared resource handle.
  3. Request an encode with a visible_rect containing odd coordinates (e.g., gfx::Rect(1, 1, 1920, 1080)).
  4. The Mojo deserializer verifies that visible_rect is contained within the coded_size of 1922x1082 but does not enforce any alignment requirements, letting the request proceed.
  5. PerformD3DCopy builds the unaligned D3D11_BOX and executes the copy, which silently fails on NV12. The stale VRAM is then encoded and returned to the renderer via BitstreamBufferReady.

Suggested Fix

To remediate this issue, validate that the coordinates of visible_rect are properly aligned to the subsampling requirements of the video format before performing the copy.

For PIXEL_FORMAT_NV12, enforce that visible_rect.x(), visible_rect.y(), visible_rect.width(), and visible_rect.height() are aligned on even boundaries, or return E_INVALIDARG if they are not. Alternatively, zero-initialize the texture on allocation or clear it when a copy fails to avoid leaking stale memory.

Evaluated with Chrome root at commit: 9ebf4302210513a012c901d87a2668b3aadf8cc1


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