High watch CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in Media
DescriptionOut of bounds write in Media
ComponentMedia
Sub-componentMedia
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker497821764
Fix commitb4b39ce12995 (chromium/src) +38/-1
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-03-31 (public git)
Disclosed2026-05-12
Patch gap42 days — fix public in source before the advisory

Background

D3D12 video encode delegates
per-codec classes (d3d12_video_encode_av1_delegate, ..._h264_delegate, ..._h265_delegate) in the GPU process that drive hardware video encoding via Direct3D 12.
Manual reference buffers
client-selectable indices (options.reference_buffers, options.update_buffer) naming which previously-encoded frames the encoder should reference/update for the current frame.
`GetMaxNumOfManualRefBuffers()`
the delegate’s declared upper bound on how many/which manual reference slots the encoder supports.
`list0_reference_frames_` / `reference_frame_manager_`
the backing arrays/manager indexed by those reference-buffer values when constructing the encoder’s picture parameters.

Root Cause Analysis

In the manual-reference path, the delegates copied options.reference_buffers and options.update_buffer from the caller’s EncodeOptions into the per-frame descriptors without validating that each index was within the encoder’s supported range. Those indices are subsequently used to look up reference frames (e.g. H.265 iterates reference_buffers[i] into reference_frame_manager_.GetReferenceFrameId(...)), so an index at or above GetMaxNumOfManualRefBuffers(), or a reference_buffers list longer than list0_reference_frames_, drives an out-of-range access when the picture parameters are populated. The H.265 delegate previously relied only on a late CHECK_LE(reference_buffers.size(), list0_reference_frames_.size()) just before the lookup loop, which aborts rather than rejecting gracefully and did not bound the individual index values at all.

The fix adds early validation in each codec’s delegate: a loop rejecting any ref_idx >= GetMaxNumOfManualRefBuffers() and (for H.264/H.265) a reference_buffers.size() > list0_reference_frames_.size() check, each returning EncoderStatus::Codes::kBadReferenceBuffer before the values are used; the now-redundant late CHECK_LE in H.265 is removed. The enclosing function names are not shown in the hunk headers.

Key insight
The core mistake was trusting client-supplied manual reference/update buffer indices and counts without bounds-checking them against GetMaxNumOfManualRefBuffers()/list0_reference_frames_ before indexing reference-frame structures; the fix validates every index and the list size up front and returns kBadReferenceBuffer instead of proceeding (or CHECK-aborting) into an out-of-range access.

Attack Path

  1. Submit crafted encode options A client of the GPU-process video encoder populates options.reference_buffers (or options.update_buffer) with an index >= GetMaxNumOfManualRefBuffers(), or more entries than list0_reference_frames_.size() (inferred: renderer/client control of EncodeOptions).
  2. Enter the manual-reference branch Execution takes the else branch that uses manual references, copying the attacker’s indices into reference_buffers/update_buffer without validation (pre-patch).
  3. Out-of-range index use The indices/count are used to build picture parameters and look up reference frames (e.g. H.265 loops reference_buffers[i] into the reference-frame manager), causing the out-of-bounds write the advisory describes.
  4. Post-patch The added validation loops/size check return kBadReferenceBuffer before any lookup, so the malformed request is rejected rather than indexing out of bounds.

Impact Assessment

The advisory classifies this as an out of bounds write in Media, rated high. An attacker able to control hardware-encode options on Windows (D3D12 encoding) could supply out-of-range manual reference/update buffer indices and trigger an out-of-bounds write in the GPU process. Preconditions are reaching the D3D12 AV1/H.264/H.265 manual-reference encode path with crafted EncodeOptions; any escalation of the OOB write to code execution or sandbox escape is not demonstrated by the diff and is (inferred).

Changed Functions

FunctionChangeNotes
for
media/gpu/windows/d3d12_video_encode_av1_delegate.cc
modified
for
media/gpu/windows/d3d12_video_encode_h264_delegate.cc
modified
for
media/gpu/windows/d3d12_video_encode_h265_delegate.cc
modified

Files Changed

  • media/gpu/windows/d3d12_video_encode_av1_delegate.cc
  • media/gpu/windows/d3d12_video_encode_h264_delegate.cc
  • media/gpu/windows/d3d12_video_encode_h265_delegate.cc

Audit Directions

  • Client-supplied indices into fixed arrays
    Flag any use of options.reference_buffers/update_buffer-style client indices to subscript reference-frame or descriptor arrays without a prior >= max/size check.
  • Late `CHECK` as the only bound
    Review codepaths whose only guard is a downstream CHECK_LE/DCHECK just before the indexing loop; prefer early validated rejection (kBadReferenceBuffer) and verify no index is used before it.
  • Cross-codec duplication
    When one codec delegate adds a bounds check, audit sibling delegates (AV1/H.264/H.265/VP9) for the identical unvalidated manual-reference pattern, since these files mirror each other.
From b4b39ce12995f6ca6e5580e3e503a36594cd8e8c Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <[email protected]>
Date: Tue, 31 Mar 2026 15:53:34 -0700
Subject: [PATCH] media: Validate reference buffer indices in D3D12 encoding

This change ensures all manual reference and update buffer indices
fall within supported bounds before proceeding with encoding.

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

diff --git a/media/gpu/windows/d3d12_video_encode_av1_delegate.cc b/media/gpu/windows/d3d12_video_encode_av1_delegate.cc
index 9c2d0e3..05dae4f31 100644
--- a/media/gpu/windows/d3d12_video_encode_av1_delegate.cc
+++ b/media/gpu/windows/d3d12_video_encode_av1_delegate.cc
@@ -1002,6 +1002,20 @@
     UINT input_frame_subresource,
     const VideoEncoder::EncodeOptions& options,
     const gfx::ColorSpace& input_color_space) {
+  for (uint8_t ref_idx : options.reference_buffers) {
+    if (ref_idx >= GetMaxNumOfManualRefBuffers()) {
+      return {EncoderStatus::Codes::kBadReferenceBuffer,
+              "Manual reference buffer index exceeds that is supported by "
+              "encoder"};
+    }
+  }
+
+  if (options.update_buffer.has_value() &&
+      options.update_buffer.value() >= GetMaxNumOfManualRefBuffers()) {
+    return {EncoderStatus::Codes::kBadReferenceBuffer,
+            "Update buffer index exceeds that is supported by encoder"};
+  }
+
   input_arguments_.SequenceControlDesc.Flags =
       D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_NONE;
   input_arguments_.SequenceControlDesc.RateControl =
diff --git a/media/gpu/windows/d3d12_video_encode_h264_delegate.cc b/media/gpu/windows/d3d12_video_encode_h264_delegate.cc
index df6703f..b28c640 100644
--- a/media/gpu/windows/d3d12_video_encode_h264_delegate.cc
+++ b/media/gpu/windows/d3d12_video_encode_h264_delegate.cc
@@ -394,6 +394,18 @@
       destroy_buffer = 1;
     }
   } else {
+    for (uint8_t ref_idx : options.reference_buffers) {
+      if (ref_idx >= GetMaxNumOfManualRefBuffers()) {
+        return {EncoderStatus::Codes::kBadReferenceBuffer,
+                "Manual reference buffer index exceeds that is supported by "
+                "encoder"};
+      }
+    }
+    if (options.reference_buffers.size() > list0_reference_frames_.size()) {
+      return {EncoderStatus::Codes::kBadReferenceBuffer,
+              "Number of manual reference buffers exceeds that is supported by "
+              "encoder"};
+    }
     reference_buffers = options.reference_buffers;
     update_buffer = options.update_buffer;
   }
diff --git a/media/gpu/windows/d3d12_video_encode_h265_delegate.cc b/media/gpu/windows/d3d12_video_encode_h265_delegate.cc
index 3592d00..5365aaf0 100644
--- a/media/gpu/windows/d3d12_video_encode_h265_delegate.cc
+++ b/media/gpu/windows/d3d12_video_encode_h265_delegate.cc
@@ -319,6 +319,18 @@
       destroy_buffer = 1;
     }
   } else {
+    for (uint8_t ref_idx : options.reference_buffers) {
+      if (ref_idx >= GetMaxNumOfManualRefBuffers()) {
+        return {EncoderStatus::Codes::kBadReferenceBuffer,
+                "Manual reference buffer index exceeds that is supported by "
+                "encoder"};
+      }
+    }
+    if (options.reference_buffers.size() > list0_reference_frames_.size()) {
+      return {EncoderStatus::Codes::kBadReferenceBuffer,
+              "Number of manual reference buffers exceeds that is supported by "
+              "encoder"};
+    }
     reference_buffers = options.reference_buffers;
     update_buffer = options.update_buffer;
   }
@@ -376,7 +388,6 @@
     pic_params_.pList0ReferenceFrames = nullptr;
   } else {
     pic_params_.FrameType = D3D12_VIDEO_ENCODER_FRAME_TYPE_HEVC_P_FRAME;
-    CHECK_LE(reference_buffers.size(), list0_reference_frames_.size());
     for (size_t i = 0; i < reference_buffers.size(); i++) {
       std::optional<uint32_t> descriptor_index =
           reference_frame_manager_.GetReferenceFrameId(reference_buffers[i]);
Loading diff…

Original Bug Report

reported by [email protected]

Potential Renderer-to-GPU sandbox escape via OOB access in D3D12 AV1 encode driver

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The D3D12 AV1 video encoding delegate fails to validate reference buffer indices provided by an untrusted renderer before passing them to the hardware driver. By supplying out-of-bounds indices, a compromised renderer can trigger an out-of-bounds array access within the D3D12 GPU driver. This driver-level memory corruption could potentially lead to a sandbox escape from the Renderer to the GPU process.

Affected files:

  • media/gpu/windows/d3d12_video_encode_av1_delegate.cc
  • media/gpu/windows/d3d12_video_encode_delegate.cc

Estimated timestamp from git blame: 2025-12-17

Summary

A vulnerability exists in the D3D12 AV1 video encoding implementation in Chrome (media/gpu/windows/d3d12_video_encode_av1_delegate.cc). When manual reference buffer control is enabled, the delegate processes reference indices supplied by the renderer via Mojo (VideoEncoder::EncodeOptions::reference_buffers). While Chrome correctly validates the number of reference buffers, it completely fails to validate the values of the indices themselves before passing them to the underlying Independent Hardware Vendor (IHV) D3D12 driver.

According to the D3D12 AV1 specification, reference indices must strictly point to valid Decoded Picture Buffer (DPB) slots, which range from 0 to 7. Because the values from the untrusted renderer (which can be up to 255) are copied directly into the driver’s command struct without bounds checking, an attacker can reliably trigger an out-of-bounds array access within the third-party GPU driver.

Technical Details

  1. Mojo IPC: A compromised renderer acquires a media.mojom.VideoEncodeAccelerator interface. It calls Encode(), passing a media.mojom.VideoEncodeOptions struct. The renderer populates the reference_buffers array with an out-of-bounds index (e.g., [200]) and crucially leaves the optional update_buffer field empty.
  2. Size Validation Passes: In the GPU process, D3D12VideoEncodeDelegate::Encode (media/gpu/windows/d3d12_video_encode_delegate.cc:276) correctly checks that the size of the reference_buffers array does not exceed the maximum allowed (typically 8 for AV1). This check passes since the array size is only 1.
  3. Missing Value Validation: The execution flow reaches D3D12VideoEncodeAV1Delegate::HandleManualReferences (media/gpu/windows/d3d12_video_encode_av1_delegate.cc:1449). This function copies the unvalidated index (200) directly into the picture_params_ struct, which is mapped directly to the D3D12 API’s D3D12_VIDEO_ENCODER_AV1_PICTURE_CONTROL_CODEC_DATA.
    // Set primary reference frame to the first reference buffer index.
    picture_params_.PrimaryRefFrame = options.reference_buffers[0];
    
    // ... we pad the remaining ReferenceIndices[] with `options.reference_buffers[0]` ...
    for (size_t i = 0; i < 7ull; i++) {
      base::span(picture_params_.ReferenceIndices)[i] =
          options.reference_buffers.size() > i ? options.reference_buffers[i]
                                               : options.reference_buffers[0];
    }
    
  4. Driver Invocation: The picture_params_ struct is subsequently passed to the D3D12 driver via ID3D12VideoEncodeCommandList::EncodeFrame. The driver expects these indices to be within [0, 7]. By providing 200, the driver performs an out-of-bounds read/write on its internal DPB tracking arrays.
  5. Bypassing Chrome Safety Checks: Later in Chrome’s execution flow, D3D12VideoEncodeAV1Delegate::RefreshDPBAndDescriptors() is called. Because the attacker deliberately omitted options.update_buffer, picture_params_.RefreshFrameFlags was evaluated to 0 in step 3.
    void D3D12VideoEncodeAV1Delegate::RefreshDPBAndDescriptors() {
      // ...
      if (picture_params_.RefreshFrameFlags == 0) {
        return; // Early exit cleanly bypasses the CHECK below.
      }
      uint8_t refreshed_dpb_idx = std::countr_zero((picture_params_.RefreshFrameFlags));
      CHECK_LT(refreshed_dpb_idx, max_num_ref_frames_);
      // ...
    }
    
    This early return elegantly bypasses a CHECK_LT assertion that would have otherwise safely crashed the GPU process on invalid inputs, ensuring the driver-level exploit executes cleanly.

Potential Impact

The impact depends on the specific hardware driver implementation (e.g., Intel, AMD, NVIDIA). At minimum, this results in a GPU process crash. However, memory corruption at the driver level could potentially be exploited to achieve arbitrary code execution within the highly privileged GPU process, representing a full sandbox escape from the Renderer.

(Note: These are potential steps based on source code analysis; our tooling has not executed a live proof-of-concept.)

Suggested Fix

In D3D12VideoEncodeAV1Delegate::HandleManualReferences (or centrally in D3D12VideoEncodeDelegate::Encode), validate that every element within the options.reference_buffers array is strictly less than the maximum allowed number of reference frames (GetMaxNumOfManualRefBuffers()).

// In D3D12VideoEncodeAV1Delegate::HandleManualReferences:
for (uint8_t ref_idx : options.reference_buffers) {
  if (ref_idx >= GetMaxNumOfManualRefBuffers()) {
    // Return an error or crash cleanly.
    return;
  }
}

(Note: The H.265 delegate already performs a similar validation check via reference_frame_manager_.GetReferenceFrameId(reference_buffers[i]).)

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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