Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Media
DescriptionImproper input validation in Media
ComponentMedia
Bug ClassLogic Error
Tracker536444242
Fix commit7af6810a344b (chromium/src) +132/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
media/gpu/h265_decoder.cc
modified
TEST_F
media/gpu/h265_decoder_unittest.cc
modified
for
media/gpu/h265_decoder_unittest.cc
modified

Files Changed

  • media/gpu/h265_decoder.cc
  • media/gpu/h265_decoder_unittest.cc
From 7af6810a344b2cf35e47ec0c2f833ad0adfedf90 Mon Sep 17 00:00:00 2001
From: Dale Curtis <[email protected]>
Date: Tue, 21 Jul 2026 17:12:10 -0700
Subject: [PATCH] h265: Finish previous frame before updating alpha-layer SPS/PPS

When an alpha-layer (non-zero nuh_layer_id) SPS or PPS NAL unit is
encountered during H.265 decoding, FinishPrevFrameIfPresent() was not
being called prior to parsing the parameter set and forwarding it to the
accelerator. If an alpha-layer SPS or PPS arrived mid-picture, updating
parameter sets before completing the current picture could cause state
collisions or unvalidated parameter set updates in hardware accelerators
(such as VideoToolbox on macOS).

This change adds CHECK_ACCELERATOR_RESULT(FinishPrevFrameIfPresent()) to
the case H265NALU::SPS_NUT and case H265NALU::PPS_NUT branches within
the non-zero nuh_layer_id handling block in H265Decoder::Decode().

Also adds a unit test coverage in H265DecoderTest verifying that
FinishPrevFrameIfPresent() completes the pending picture before
processing the alpha-layer SPS/PPS.

TAG=agy
CONV=4eee7864-0106-476f-b647-78eba7f8c730

Bug: 536444242
Change-Id: I8d0eeb6b7221cd8ae4636c6e79782288d198131b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8128206
Auto-Submit: Dale Curtis <[email protected]>
Reviewed-by: Qiu, Jianlin <[email protected]>
Commit-Queue: Qiu, Jianlin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1665919}
---

diff --git a/media/gpu/h265_decoder.cc b/media/gpu/h265_decoder.cc
index 3aff91d0..43f6711d 100644
--- a/media/gpu/h265_decoder.cc
+++ b/media/gpu/h265_decoder.cc
@@ -311,6 +311,7 @@
             break;
           }
           case H265NALU::SPS_NUT: {
+            CHECK_ACCELERATOR_RESULT(FinishPrevFrameIfPresent());
             int sps_id;
             par_res = parser_.ParseSPS(&sps_id);
             if (par_res != H265Parser::kOk) {
@@ -320,6 +321,7 @@
             break;
           }
           case H265NALU::PPS_NUT: {
+            CHECK_ACCELERATOR_RESULT(FinishPrevFrameIfPresent());
             int pps_id;
             par_res = parser_.ParsePPS(*curr_nalu_, &pps_id);
             if (par_res != H265Parser::kOk) {
diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc
index 0660e23b..a8fcc52 100644
--- a/media/gpu/h265_decoder_unittest.cc
+++ b/media/gpu/h265_decoder_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "media/gpu/h265_decoder.h"
 
+#include <array>
 #include <cstring>
 #include <memory>
 #include <string>
@@ -126,6 +127,14 @@
   MOCK_METHOD2(SetStream,
                Status(base::span<const uint8_t> stream,
                       const DecryptConfig* decrypt_config));
+  MOCK_METHOD(void,
+              ProcessSPS,
+              (const H265SPS* sps, base::span<const uint8_t> data),
+              (override));
+  MOCK_METHOD(void,
+              ProcessPPS,
+              (const H265PPS* pps, base::span<const uint8_t> data),
+              (override));
   bool IsChromaSamplingSupported(VideoChromaSampling format) override {
     return format == VideoChromaSampling::k420;
   }
@@ -751,4 +760,125 @@
   EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, res2);
 }
 
+TEST_F(H265DecoderTest, AlphaLayerSpsPpsMidPicture) {
+  H26xAnnexBBitstreamBuilder builder;
+
+  // VPS with alpha layer enabled
+  constexpr auto kVpsWithAlpha = std::to_array<uint8_t>({
+      0x40, 0x01, 0x0c, 0x11, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00,
+      0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00,
+      0x3e, 0x19, 0x40, 0xbf, 0x3e, 0x08, 0x00, 0x08, 0x30, 0x20,
+      0xa4, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xc5, 0x20,
+  });
+  builder.AppendBits(32, 0x00000001);  // start code
+  builder.Flush();
+  for (uint8_t b : kVpsWithAlpha) {
+    builder.AppendBits(8, b);
+  }
+  builder.Flush();
+
+  // Base layer SPS
+  H265SPS sps = {};
+  sps.sps_video_parameter_set_id = 0;
+  sps.sps_max_sub_layers_minus1 = 0;
+  sps.sps_temporal_id_nesting_flag = true;
+  sps.profile_tier_level.general_profile_idc = 1;
+  sps.profile_tier_level.general_level_idc = 120;
+  sps.sps_seq_parameter_set_id = 0;
+  sps.chroma_format_idc = 1;
+  sps.pic_width_in_luma_samples = 320;
+  sps.pic_height_in_luma_samples = 184;
+  sps.log2_min_luma_coding_block_size_minus3 = 0;
+  sps.log2_diff_max_min_luma_coding_block_size = 1;
+  sps.log2_min_luma_transform_block_size_minus2 = 0;
+  sps.log2_diff_max_min_luma_transform_block_size = 0;
+  sps.max_transform_hierarchy_depth_inter = 0;
+  sps.max_transform_hierarchy_depth_intra = 0;
+  sps.log2_max_pic_order_cnt_lsb_minus4 = 4;
+  sps.sps_max_dec_pic_buffering_minus1[0] = 1;
+  sps.sps_max_num_reorder_pics[0] = 0;
+  sps.sps_max_latency_increase_plus1[0] = 0;
+  BuildPackedH265SPS(builder, sps);
+
+  // Base layer PPS
+  H265PPS pps = {};
+  pps.pps_pic_parameter_set_id = 0;
+  pps.pps_seq_parameter_set_id = 0;
+  BuildPackedH265PPS(builder, pps);
+
+  // Base layer Slice (nuh_layer_id = 0)
+  builder.AppendBits(32, 0x00000001);  // start code
+  builder.Flush();
+  builder.AppendBits(1, 0);                  // forbidden_zero_bit
+  builder.AppendBits(6, H265NALU::CRA_NUT);  // nal_unit_type
+  builder.AppendBits(6, 0);                  // nuh_layer_id = 0
+  builder.AppendBits(3, 1);                  // nuh_temporal_id_plus1 = 1
+
+  builder.AppendBool(true);   // first_slice_segment_in_pic_flag
+  builder.AppendBool(false);  // no_output_of_prior_pics_flag (for IRAP)
+  builder.AppendUE(0);        // slice_pic_parameter_set_id
+  builder.AppendUE(2);        // slice_type = I (2)
+  builder.AppendBits(8, 0);   // slice_pic_order_cnt_lsb
+  builder.AppendBool(false);  // short_term_ref_pic_set_sps_flag
+  builder.AppendUE(0);        // num_negative_pics
+  builder.AppendUE(0);        // num_positive_pics
+  builder.AppendSE(0);        // slice_qp_delta
+  builder.AppendBool(true);   // byte alignment bit
+  builder.Flush();
+
+  // Alpha layer SPS (nuh_layer_id = 1)
+  H26xAnnexBBitstreamBuilder alpha_sps_builder;
+  BuildPackedH265SPS(alpha_sps_builder, sps);
+  std::vector<uint8_t> alpha_sps_data(alpha_sps_builder.data().begin(),
+                                      alpha_sps_builder.data().end());
+  alpha_sps_data[5] = 0x09;  // nuh_layer_id = 1
+  builder.AppendBits(32, 0x00000001);
+  builder.Flush();
+  for (size_t i = 4; i < alpha_sps_data.size(); ++i) {
+    builder.AppendBits(8, alpha_sps_data[i]);
+  }
+  builder.Flush();
+
+  // Alpha layer PPS (nuh_layer_id = 1)
+  H26xAnnexBBitstreamBuilder alpha_pps_builder;
+  BuildPackedH265PPS(alpha_pps_builder, pps);
+  std::vector<uint8_t> alpha_pps_data(alpha_pps_builder.data().begin(),
+                                      alpha_pps_builder.data().end());
+  alpha_pps_data[5] = 0x09;  // nuh_layer_id = 1
+  builder.AppendBits(32, 0x00000001);
+  builder.Flush();
+  for (size_t i = 4; i < alpha_pps_data.size(); ++i) {
+    builder.AppendBits(8, alpha_pps_data[i]);
+  }
+  builder.Flush();
+
+  auto buffer = DecoderBuffer::CopyFrom(builder.data());
+
+  EXPECT_CALL(*accelerator_, SetStream(_, _))
+      .WillRepeatedly(Return(H265Decoder::H265Accelerator::Status::kOk));
+  EXPECT_CALL(*accelerator_, CreateH265Picture()).WillRepeatedly([]() {
+    return base::MakeRefCounted<H265Picture>();
+  });
+  EXPECT_CALL(*accelerator_, SubmitFrameMetadata(_, _, _, _, _, _, _, _))
+      .WillRepeatedly(Return(H265Decoder::H265Accelerator::Status::kOk));
+
+  {
+    InSequence sequence;
+    EXPECT_CALL(*accelerator_, ProcessSPS(_, _));
+    EXPECT_CALL(*accelerator_, ProcessPPS(_, _));
+    EXPECT_CALL(*accelerator_, SubmitSlice(_, _, _, _, _, _, _, _, _, _, _, _))
+        .WillOnce(Return(H265Decoder::H265Accelerator::Status::kOk));
+    EXPECT_CALL(*accelerator_, SubmitDecode(_))
+        .WillOnce(Return(H265Decoder::H265Accelerator::Status::kOk));
+    EXPECT_CALL(*accelerator_, OutputPicture(_)).WillOnce(Return(true));
+    EXPECT_CALL(*accelerator_, ProcessSPS(_, _));
+    EXPECT_CALL(*accelerator_, ProcessPPS(_, _));
+  }
+
+  decoder_->SetStream(1, buffer);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc
index 0660e23b..a8fcc52 100644
--- a/media/gpu/h265_decoder_unittest.cc
+++ b/media/gpu/h265_decoder_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "media/gpu/h265_decoder.h"
 
+#include <array>
 #include <cstring>
 #include <memory>
 #include <string>
@@ -126,6 +127,14 @@
   MOCK_METHOD2(SetStream,
                Status(base::span<const uint8_t> stream,
                       const DecryptConfig* decrypt_config));
+  MOCK_METHOD(void,
+              ProcessSPS,
+              (const H265SPS* sps, base::span<const uint8_t> data),
+              (override));
+  MOCK_METHOD(void,
+              ProcessPPS,
+              (const H265PPS* pps, base::span<const uint8_t> data),
+              (override));
   bool IsChromaSamplingSupported(VideoChromaSampling format) override {
     return format == VideoChromaSampling::k420;
   }
@@ -751,4 +760,125 @@
   EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, res2);
 }
 
+TEST_F(H265DecoderTest, AlphaLayerSpsPpsMidPicture) {
+  H26xAnnexBBitstreamBuilder builder;
+
+  // VPS with alpha layer enabled
+  constexpr auto kVpsWithAlpha = std::to_array<uint8_t>({
+      0x40, 0x01, 0x0c, 0x11, 0xff, 0xff, 0x01, 0x60, 0x00, 0x00,
+      0x03, 0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00,
+      0x3e, 0x19, 0x40, 0xbf, 0x3e, 0x08, 0x00, 0x08, 0x30, 0x20,
+      0xa4, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0xc5, 0x20,
+  });
+  builder.AppendBits(32, 0x00000001);  // start code
+  builder.Flush();
+  for (uint8_t b : kVpsWithAlpha) {
+    builder.AppendBits(8, b);
+  }
+  builder.Flush();
+
+  // Base layer SPS
+  H265SPS sps = {};
+  sps.sps_video_parameter_set_id = 0;
+  sps.sps_max_sub_layers_minus1 = 0;
+  sps.sps_temporal_id_nesting_flag = true;
+  sps.profile_tier_level.general_profile_idc = 1;
+  sps.profile_tier_level.general_level_idc = 120;
+  sps.sps_seq_parameter_set_id = 0;
+  sps.chroma_format_idc = 1;
+  sps.pic_width_in_luma_samples = 320;
+  sps.pic_height_in_luma_samples = 184;
+  sps.log2_min_luma_coding_block_size_minus3 = 0;
+  sps.log2_diff_max_min_luma_coding_block_size = 1;
+  sps.log2_min_luma_transform_block_size_minus2 = 0;
+  sps.log2_diff_max_min_luma_transform_block_size = 0;
+  sps.max_transform_hierarchy_depth_inter = 0;
+  sps.max_transform_hierarchy_depth_intra = 0;
+  sps.log2_max_pic_order_cnt_lsb_minus4 = 4;
+  sps.sps_max_dec_pic_buffering_minus1[0] = 1;
+  sps.sps_max_num_reorder_pics[0] = 0;
+  sps.sps_max_latency_increase_plus1[0] = 0;
+  BuildPackedH265SPS(builder, sps);
+
+  // Base layer PPS
+  H265PPS pps = {};
+  pps.pps_pic_parameter_set_id = 0;
+  pps.pps_seq_parameter_set_id = 0;
+  BuildPackedH265PPS(builder, pps);
+
+  // Base layer Slice (nuh_layer_id = 0)
+  builder.AppendBits(32, 0x00000001);  // start code
+  builder.Flush();
+  builder.AppendBits(1, 0);                  // forbidden_zero_bit
+  builder.AppendBits(6, H265NALU::CRA_NUT);  // nal_unit_type
+  builder.AppendBits(6, 0);                  // nuh_layer_id = 0
+  builder.AppendBits(3, 1);                  // nuh_temporal_id_plus1 = 1
+
+  builder.AppendBool(true);   // first_slice_segment_in_pic_flag
+  builder.AppendBool(false);  // no_output_of_prior_pics_flag (for IRAP)
+  builder.AppendUE(0);        // slice_pic_parameter_set_id
+  builder.AppendUE(2);        // slice_type = I (2)
+  builder.AppendBits(8, 0);   // slice_pic_order_cnt_lsb
+  builder.AppendBool(false);  // short_term_ref_pic_set_sps_flag
+  builder.AppendUE(0);        // num_negative_pics
+  builder.AppendUE(0);        // num_positive_pics
+  builder.AppendSE(0);        // slice_qp_delta
+  builder.AppendBool(true);   // byte alignment bit
+  builder.Flush();
+
+  // Alpha layer SPS (nuh_layer_id = 1)
+  H26xAnnexBBitstreamBuilder alpha_sps_builder;
+  BuildPackedH265SPS(alpha_sps_builder, sps);
+  std::vector<uint8_t> alpha_sps_data(alpha_sps_builder.data().begin(),
+                                      alpha_sps_builder.data().end());
+  alpha_sps_data[5] = 0x09;  // nuh_layer_id = 1
+  builder.AppendBits(32, 0x00000001);
+  builder.Flush();
+  for (size_t i = 4; i < alpha_sps_data.size(); ++i) {
+    builder.AppendBits(8, alpha_sps_data[i]);
+  }
+  builder.Flush();
+
+  // Alpha layer PPS (nuh_layer_id = 1)
+  H26xAnnexBBitstreamBuilder alpha_pps_builder;
+  BuildPackedH265PPS(alpha_pps_builder, pps);
+  std::vector<uint8_t> alpha_pps_data(alpha_pps_builder.data().begin(),
+                                      alpha_pps_builder.data().end());
+  alpha_pps_data[5] = 0x09;  // nuh_layer_id = 1
+  builder.AppendBits(32, 0x00000001);
+  builder.Flush();
+  for (size_t i = 4; i < alpha_pps_data.size(); ++i) {
+    builder.AppendBits(8, alpha_pps_data[i]);
+  }
+  builder.Flush();
+
+  auto buffer = DecoderBuffer::CopyFrom(builder.data());
+
+  EXPECT_CALL(*accelerator_, SetStream(_, _))
+      .WillRepeatedly(Return(H265Decoder::H265Accelerator::Status::kOk));
+  EXPECT_CALL(*accelerator_, CreateH265Picture()).WillRepeatedly([]() {
+    return base::MakeRefCounted<H265Picture>();
+  });
+  EXPECT_CALL(*accelerator_, SubmitFrameMetadata(_, _, _, _, _, _, _, _))
+      .WillRepeatedly(Return(H265Decoder::H265Accelerator::Status::kOk));
+
+  {
+    InSequence sequence;
+    EXPECT_CALL(*accelerator_, ProcessSPS(_, _));
+    EXPECT_CALL(*accelerator_, ProcessPPS(_, _));
+    EXPECT_CALL(*accelerator_, SubmitSlice(_, _, _, _, _, _, _, _, _, _, _, _))
+        .WillOnce(Return(H265Decoder::H265Accelerator::Status::kOk));
+    EXPECT_CALL(*accelerator_, SubmitDecode(_))
+        .WillOnce(Return(H265Decoder::H265Accelerator::Status::kOk));
+    EXPECT_CALL(*accelerator_, OutputPicture(_)).WillOnce(Return(true));
+    EXPECT_CALL(*accelerator_, ProcessSPS(_, _));
+    EXPECT_CALL(*accelerator_, ProcessPPS(_, _));
+  }
+
+  decoder_->SetStream(1, buffer);
+
+  EXPECT_EQ(AcceleratedVideoDecoder::kConfigChange, decoder_->Decode());
+  EXPECT_EQ(AcceleratedVideoDecoder::kRanOutOfStreamData, decoder_->Decode());
+}
+
 }  // namespace media
Loading diff…

Original Bug Report

reported by [email protected]

Potential macOS GPU driver validation bypass via VideoToolbox H.265 SPS cache collision

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: The VideoToolbox H.265 accelerator caches SPS bytes by ID without layer namespacing, allowing an alpha-layer SPS to overwrite the base-layer SPS mid-frame. Slices validated against a large base-layer SPS are passed to the macOS driver paired with the overwritten small alpha-layer format description. This potentially passes out-of-bounds slice segment addresses to the Apple hardware decoder.

Affected files:

  • media/gpu/mac/video_toolbox_h265_accelerator.cc
  • media/gpu/h265_decoder.cc
  • media/parsers/h265_parser.cc
  • media/gpu/mac/video_toolbox_h265_accelerator.h

Estimated timestamp from git blame: 2023-09-20

1. Summary of the Issue (Meant for Human Triage)

In the VideoToolbox H.265 hardware video decoder on macOS, a state desynchronization vulnerability exists between the Chromium-side slice header parser and the format description passed to the native Apple driver. The VideoToolboxH265Accelerator caches Sequence Parameter Set (SPS) raw bytes in a map (seen_sps_data_) keyed solely by the sps_seq_parameter_set_id, without namespacing or disambiguating by the NALU layer ID (nuh_layer_id).

Furthermore, the auxiliary alpha-layer SPS parsing branch in H265Decoder::Decode() does not trigger FinishPrevFrameIfPresent(), a crucial frame-flushing mechanism that is enforced on the base layer. This discrepancy allows an attacker to supply an auxiliary/alpha layer SPS mid-picture that shares the same ID as the active base-layer SPS. The raw bytes of the alpha-layer SPS completely overwrite the base-layer SPS bytes in the accelerator’s cache.

When the frame is finalized, the accelerator uses the overwritten, smaller alpha-layer SPS to construct the CMVideoFormatDescription. The native Apple decoder (VTDecompressionSessionDecodeFrame) then receives slice headers that were previously validated by Chromium against the larger base-layer SPS. Because the format description passed to the driver specifies much smaller dimensions, fields like slice_segment_address can bypass parser protections and appear completely out-of-bounds to the underlying macOS GPU process, potentially leading to driver-side memory corruption.


2. Proof-of-Concept & Detailed Execution Flow

Note: The following steps describe a potential execution flow based on static analysis. Our tooling agent does not yet have the ability to run code, but this sequence demonstrates how an attacker could hypothetically trigger the vulnerability.

An attacker serving a malicious H.265 bitstream (via WebCodecs, MSE, or <video src> on macOS where hardware HEVC is enabled by default) can craft a sequence to force a cache collision:

  1. Bitstream Initialization: The stream starts with a Video Parameter Set (VPS) indicating aux_alpha_layer_id = 1.
  2. Base-Layer SPS parsing: The stream provides a base-layer SPS (SPS_A) with nuh_layer_id = 0, sps_seq_parameter_set_id = 0, and large spatial dimensions (e.g., pic_size_in_ctbs_y = 16384). The decoder processes it in H265Decoder::Decode (media/gpu/h265_decoder.cc:450-457), calling FinishPrevFrameIfPresent(). The raw bytes are cached in the accelerator via VideoToolboxH265Accelerator::ProcessSPS (media/gpu/mac/video_toolbox_h265_accelerator.cc:64-65): seen_sps_data_[0] = SPS_A bytes.
  3. PPS parsing: A base-layer PPS (PPS_A) referencing sps_id = 0 is parsed and cached.
  4. First Base-Layer Slice: An IDR slice (Slice 0) with first_slice_segment_in_pic_flag = 1 starts a new frame. It is submitted via accelerator_->SubmitSlice, which captures the active parameter set IDs (frame_sps_ids_.insert(0)) and records the slice into frame_slice_data_.
  5. Attacker Payload Slice: A second IDR slice (Slice 1) with first_slice_segment_in_pic_flag = 0 carries an artificially large slice_segment_address (e.g., 16383).
  6. Parser Validation: During H265Parser::ParseSliceHeader (media/parsers/h265_parser.cc:1161-1164), the slice is bounds-checked against the active SPS (SPS_A). IN_RANGE_OR_RETURN(shdr->slice_segment_address, 0, sps->pic_size_in_ctbs_y - 1) succeeds because SPS_A allows sizes up to 16384. This slice is appended to frame_slice_data_.
  7. Alpha-Layer SPS Overwrite Payload: The stream provides an alpha-layer SPS (SPS_B) with nuh_layer_id = 1, using the same sps_seq_parameter_set_id = 0, but specifying minimal dimensions (e.g., pic_size_in_ctbs_y = 1).
  8. Missing Frame Flush: In H265Decoder::Decode, the alpha-layer bypass branch is taken (media/gpu/h265_decoder.cc:264-340). Execution hits the H265NALU::SPS_NUT case (media/gpu/h265_decoder.cc:313-319). Crucially, this branch lacks a call to FinishPrevFrameIfPresent().
  9. Cache Collision: accelerator_->ProcessSPS is called with SPS_B. seen_sps_data_[0] is silently overwritten with the small SPS_B bytes.
  10. Format Description Creation: The stream ends (kEOStream). FinishPrevFrameIfPresent flushes the frame, calling accelerator_->SubmitDecode(). This triggers CreateFormat() (media/gpu/mac/video_toolbox_h265_accelerator.cc:274-276).
  11. Apple Driver Initialization: CreateFormat calls ExtractParameterSetData, looking up ID 0 in seen_sps_data_. It retrieves the corrupted/overwritten SPS_B bytes. It passes these to CMVideoFormatDescriptionCreateFromHEVCParameterSets (media/gpu/mac/video_toolbox_h265_accelerator.cc:130-137), initializing active_format_ to a highly restrictive 1-CTB format description.
  12. Driver Handoff: SubmitDecode creates a CMSampleBuffer binding the restrictive active_format_ with the previously accumulated frame_slice_data_ (which contains Slice 1 with slice_segment_address = 16383). This is dispatched to the native Apple driver via VTDecompressionSessionDecodeFrame.
  13. Driver-Side Exploitation Consequence: Assuming the opaque native Apple VideoToolbox driver relies on the Format Description bounds without independently re-validating the slice segment addresses on-the-fly, the driver will perform an out-of-bounds write using the 16383 index into a 1-CTB configuration, resulting in memory corruption within the sandboxed macOS GPU process.

3. Technical Verification Details (Automated Audit Logs)

> The report accurately identifies a driver validation bypass in VideoToolboxH265Accelerator. > > 1. seen_sps_data_ (in the accelerator) and active_sps_ (in the parser) are keyed solely by sps_seq_parameter_set_id, without nuh_layer_id namespacing. > 2. In H265Decoder::Decode(), the alpha-layer SPS parsing branch (unlike the base-layer branch) does not call FinishPrevFrameIfPresent(). > 3. An attacker can supply an alpha-layer SPS with the same ID as the base-layer SPS mid-frame. This overwrites the cached seen_sps_data_ and active_sps_ with the smaller alpha SPS bytes. > 4. Prior slices in the frame were already parsed, and their fields (such as slice_segment_address) were validated against the larger base-layer SPS. > 5. During SubmitDecode(), the overwritten (smaller) SPS is used to construct the CMVideoFormatDescription. > 6. This mismatched format description is passed alongside the slice data to VTDecompressionSessionDecodeFrame, effectively passing an out-of-bounds slice_segment_address (and other bounded fields) to the underlying hardware decoder. > > The consequence depends on opaque Apple driver behavior (whether VideoToolbox gracefully errors or performs an out-of-bounds write). Because the macOS GPU process is sandboxed, an actual memory corruption would be High (S1) severity (sandboxed RCE). Given the opaque and unproven driver-side memory corruption, downgrading to Medium (S2) is correct per the GPU validating-layer gap severity guidelines. This issue is A-SERVER reachable via web bitstreams since HEVC is default-enabled on macOS.

Codebase Investigator Output:

  • The investigator confirmed VideoToolboxH265Accelerator::ProcessSPS keying at media/gpu/mac/video_toolbox_h265_accelerator.cc:57-66 uses solely sps->sps_seq_parameter_set_id with no layer-awareness.
  • The investigator confirmed the alpha-layer branch (media/gpu/h265_decoder.cc:313-321) explicitly lacks FinishPrevFrameIfPresent(), whereas the base layer branch (media/gpu/h265_decoder.cc:450-457) correctly invokes it.
  • The investigator confirmed IN_RANGE_OR_RETURN bounds checking at media/parsers/h265_parser.cc:1161-1164 evaluates against the currently active SPS (sps->pic_size_in_ctbs_y).
  • The investigator confirmed ExtractParameterSetData passes the mapped data to CMVideoFormatDescriptionCreateFromHEVCParameterSets at media/gpu/mac/video_toolbox_h265_accelerator.cc:130-137.

Suggested Fix:

  1. Modify VideoToolboxH265Accelerator::seen_sps_data_ (and related structures for VPS/PPS) to namespace keys by both sps_seq_parameter_set_id and nuh_layer_id (e.g., using a struct or packed integer (nuh_layer_id << 16) | sps_id).
  2. Add CHECK_ACCELERATOR_RESULT(FinishPrevFrameIfPresent()) to the alpha-layer SPS and PPS decoding branches (H265NALU::SPS_NUT and H265NALU::PPS_NUT around line 313 in media/gpu/h265_decoder.cc) to enforce the same frame boundaries as the base layer.

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


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