Medium watch CVSS 8.8 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Media
DescriptionImproper input validation in Media
ComponentMedia
Sub-componentH265
Bug ClassInput Validation
Action tierwatch — ub-class only — no demonstrated corruption path
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.
Tracker536444242
Fix commit7af6810a344b (chromium/src) +132/-0
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-19 (per vendor advisory)
Fix landed2026-07-22 (public git)
Disclosed2026-08-25
Patch gap34 days — fix public in source before the advisory

Background

H.265/HEVC parameter sets
SPS (Sequence Parameter Set) and PPS (Picture Parameter Set) carry decode configuration; in Blink/Chrome’s GPU decoder they are parsed by H265Parser and forwarded to the hardware accelerator via ProcessSPS/ProcessPPS.
`nuh_layer_id` / alpha layer
the NAL unit header layer id; a non-zero nuh_layer_id denotes a non-base (e.g. alpha) layer, handled in a separate branch of H265Decoder::Decode().
`FinishPrevFrameIfPresent()`
completes and submits any picture currently being assembled before decoder state that would affect it is changed.
`CHECK_ACCELERATOR_RESULT`
macro that evaluates an accelerator-returning call and propagates a decode error/failure status instead of continuing.

Root Cause Analysis

In H265Decoder::Decode(), the branch that handles NAL units with a non-zero nuh_layer_id processed H265NALU::SPS_NUT and H265NALU::PPS_NUT by immediately calling parser_.ParseSPS/parser_.ParsePPS and forwarding the parameter set to the accelerator, without first finishing any in-progress picture. If an alpha-layer SPS or PPS arrived mid-picture, the parameter set for the accelerator was updated while a picture was still pending, so the decoder violated the invariant that a picture must be finished before its governing parameter sets are replaced. Per the patch description, this could cause state collisions or unvalidated parameter-set updates in hardware accelerators (such as VideoToolbox on macOS).

The fix inserts CHECK_ACCELERATOR_RESULT(FinishPrevFrameIfPresent()) at the top of both the SPS_NUT and PPS_NUT cases in that branch, so the current picture is completed (or a decode error is surfaced) before the alpha-layer parameter set is parsed and forwarded.

The added test AlphaLayerSpsPpsMidPicture asserts the ordering: the base-layer slice’s SubmitDecode/OutputPicture run before the alpha-layer ProcessSPS/ProcessPPS.

Key insight
The alpha-layer (non-zero nuh_layer_id) SPS/PPS handling skipped FinishPrevFrameIfPresent(), letting parameter sets be updated mid-picture; the fix calls FinishPrevFrameIfPresent() (guarded by CHECK_ACCELERATOR_RESULT) before parsing the alpha-layer parameter set.

Attack Path

  1. Craft a multi-layer HEVC stream The attacker builds an H.265 bitstream with an alpha layer enabled (VPS with alpha), a base-layer picture, and alpha-layer SPS/PPS NAL units (nuh_layer_id = 1).
  2. Interleave parameter sets mid-picture The stream places the alpha-layer SPS/PPS so they arrive while a base-layer picture is still pending in H265Decoder::Decode().
  3. Reach the vulnerable branch Decoding routes the non-zero-nuh_layer_id SPS_NUT/PPS_NUT into the branch that, pre-patch, parses and forwards the parameter set without finishing the current picture.
  4. Corrupt accelerator state (inferred) Updating parameter sets before the picture completes causes state collisions / unvalidated parameter-set updates in the hardware accelerator (e.g. VideoToolbox), the improper-input-validation condition the patch prevents.

Impact Assessment

An attacker who gets a victim to decode a malicious H.265 stream (e.g. via a web page using hardware-accelerated video) can drive the decoder to push alpha-layer parameter-set updates to the hardware accelerator mid-picture, producing state collisions or unvalidated parameter updates in the GPU process / platform accelerator. The precondition is that hardware H.265 decoding with alpha-layer support is in use (the patch names VideoToolbox on macOS). The advisory classifies this as improper input validation in Media; no exploited-in-the-wild status is stated, and any memory-safety consequence beyond the described state collision is not demonstrated by the patch (inferred).

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

Audit Directions

  • Parameter-set updates without frame completion
    Audit all codec decoders (H.264/AV1/VP9 as well as H.265) for SPS/PPS/config-header handlers that update decoder or accelerator state without first calling their equivalent of FinishPrevFrameIfPresent().
  • Layer-specific code branches
    Review non-base-layer / scalability / alpha-layer branches for missing checks that the base-layer path performs, since duplicated branches often drift.
  • Accelerator forwarding order
    Look for places that forward parsed data to a hardware accelerator (ProcessSPS/ProcessPPS/SubmitFrameMetadata) and verify ordering invariants relative to in-flight picture submission.
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);
+
+  EXPECT_EQ(AcceleratedVideoDecoder::kConfigChange, decoder_->Decode());
+  EXPECT_EQ(AcceleratedVideoDecoder::kRanOutOfStreamData, decoder_->Decode());
+}
+
 }  // namespace media
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