Medium watch CVSS 9.6 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
9.6
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.
Tracker540024134
Fix commit2d157885607b (chromium/src) +85/-26
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-28 (per vendor advisory)
Fix landed2026-08-04 (public git)
Disclosed2026-09-08
Patch gap35 days — fix public in source before the advisory

Background

H.265/HEVC SPS
the Sequence Parameter Set, a header carrying sequence-wide decode parameters (picture dimensions, bit depth, chroma format, coding-block sizes) that per the HEVC spec must stay constant across a coded video sequence.
CTB size (`ctb_log2_size_y`)
the coding-tree-block dimension derived from log2_min_luma_coding_block_size_minus3 and log2_diff_max_min_luma_coding_block_size, which controls how a picture is partitioned and thus how large the decoder’s per-picture data structures must be.
IRAP picture
an Intra Random Access Point picture, the only place a coded video sequence may legally introduce a new configuration; non-IRAP pictures (e.g. P-frames) must reuse the active SPS unchanged.
`validate_extended_bitstream()`
a H265NaluParser flag that gates stricter, spec-conformant bitstream validation, exposed to the decoder by the new accessor added in this patch.

Root Cause Analysis

The config-change detection in media/gpu/h265_decoder.cc (attributed by the commit message to H265Decoder::ProcessPPS; the @@ hunk header does not name the enclosing function) computed is_config_change from only a hand-picked subset of SPS-derived state — pic_size_, dpb_.max_num_pics(), profile_, bit_depth_, and chroma_sampling_. Fields such as the CTB size (ctb_log2_size_y) were never compared, so an SPS that changed only those omitted fields mid-sequence was not flagged as a configuration change and was allowed to proceed on a non-IRAP picture, where only color-space changes are permitted. This violated the HEVC invariant that SPS parameters are fixed within a coded video sequence: the decoder continued using per-picture buffers and geometry sized from the original SPS while decoding under the new, silently-accepted CTB configuration.

The fix stores the accepted SPS in a new active_sps_ member and, when validate_extended_bitstream() is true, sets is_config_change = !active_sps_ || *active_sps_ != *sps, comparing the entire struct via the newly defaulted H265SPS::operator== so any mid-sequence SPS modification is detected and rejected with kDecodeError. H265Decoder’s reset path also now clears active_sps_ so the comparison state does not leak across streams.

Key insight
The single mistake was detecting configuration changes by comparing a subset of SPS fields rather than the whole SPS, letting a changed CTB size slip through on a non-IRAP picture; the fix keeps the full active SPS and compares it in its entirety so any parameter change is caught.

Attack Path

  1. Establish a valid sequence The attacker gets the decoder to accept an initial IRAP picture with a valid SPS, fixing pic_size_, bit_depth_, chroma_sampling_, and the CTB size, and (post-fix) populating active_sps_.
  2. Inject a modified SPS mid-sequence A second SPS is supplied that changes only fields outside the old subset check — the regression test uses log2_min_luma_coding_block_size_minus3 = 1 to alter the CTB size — so the pre-patch is_config_change evaluates false.
  3. Decode a non-IRAP picture under the new config A P-frame (kFrame1) follows; because no config change was detected, decoding proceeds instead of returning kDecodeError, so the decoder operates with structures sized for the old CTB size but data described by the new one.
  4. Out-of-bounds write The size/geometry mismatch drives a write past an allocation during media decode (inferred; the specific sink, buffer, and offset are not shown in the diff, only that the patch prevents reaching this state).

Impact Assessment

An attacker who can get crafted H.265 content decoded (e.g. via a web page delivering media) can trigger an out-of-bounds write in the media decode path, in whichever process hosts H265Decoder. The advisory classifies this as an out-of-bounds write in Media; the preconditions are the ability to supply an H.265 bitstream and, for the strict full-SPS comparison branch, that validate_extended_bitstream() is active. Any escalation to memory corruption primitives or code execution beyond the OOB write itself is (inferred) and not demonstrated by the patch, and the advisory here reports no exploited-in-the-wild status.

Changed Functions

FunctionChangeNotes
if
media/gpu/h265_decoder.cc
modified

Files Changed

  • media/gpu/h265_decoder.h
  • media/gpu/h265_decoder.cc
  • media/gpu/h265_decoder_unittest.cc
  • media/parsers/h265_parser.cc
  • media/parsers/h265_nalu_parser.h
  • media/parsers/h265_parser.h

Audit Directions

  • Subset equality checks on parsed headers
    Audit other decoders (e.g. H264Decoder, VP9, AV1) for config-change detection that compares a hand-picked list of parameter-set fields instead of the full parsed struct, since omitted fields can change silently mid-sequence.
  • Buffer geometry keyed off un-revalidated parameters
    Look for per-picture allocations or index math derived from SPS/PPS quantities (CTB size, tile/slice grids, DPB sizing) that are computed once and reused across pictures without rechecking on every parameter-set update.
  • Flag-gated validation
    Check that stricter checks placed only behind flags like validate_extended_bitstream() are also correct on the default path, since here the pre-patch subset comparison is retained in the else branch and only the gated branch gains the full-SPS check.
From 2d157885607bfcd825e0c211ea81b8b7d9d7e73e Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <[email protected]>
Date: Mon, 03 Aug 2026 18:40:29 -0700
Subject: [PATCH] media: Reject all non-IRAP H.265 SPS configuration changes

H265Decoder::ProcessPPS checked only a subset of SPS fields when
detecting configuration changes, omitting parameters like CTB size
(ctb_log2_size_y).

Per HEVC spec, SPS parameters must not change within
a sequence (on non-IRAP pictures).

This change updates H265Decoder to store the active H265SPS and use
all its fields for config change detection,
ensuring any mid-sequence SPS modification is rejected.

Bug: 540024134
Change-Id: I26c70eb67eb24148bbfd0e39067b7b6933d7ae87
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8176990
Commit-Queue: Eugene Zemtsov <[email protected]>
Reviewed-by: Qiu, Jianlin <[email protected]>
Reviewed-by: Dale Curtis <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1673024}
---
diff --git a/media/gpu/h265_decoder.cc b/media/gpu/h265_decoder.cc
index 750a4ad..9aab489 100644
--- a/media/gpu/h265_decoder.cc
+++ b/media/gpu/h265_decoder.cc
@@ -202,6 +202,7 @@
   parser_.Reset();
   accelerator_->Reset();
 
+  active_sps_.reset();
   decoder_buffer_.reset();
   secure_handle_ = 0;
 
@@ -654,10 +655,15 @@
                             new_color_space != picture_color_space_;
   }
 
-  const bool is_config_change =
-      pic_size_ != new_pic_size || dpb_.max_num_pics() != sps->max_dpb_size ||
-      profile_ != new_profile || bit_depth_ != new_bit_depth ||
-      chroma_sampling_ != new_chroma_sampling;
+  bool is_config_change = false;
+  if (parser_.validate_extended_bitstream()) {
+    is_config_change = !active_sps_ || *active_sps_ != *sps;
+  } else {
+    is_config_change = pic_size_ != new_pic_size ||
+                       dpb_.max_num_pics() != sps->max_dpb_size ||
+                       profile_ != new_profile || bit_depth_ != new_bit_depth ||
+                       chroma_sampling_ != new_chroma_sampling;
+  }
 
   if (is_config_change) {
     // Only color space changes are allowed on non-IRAP pictures.
@@ -681,6 +687,7 @@
              << VideoChromaSamplingToString(new_chroma_sampling);
     profile_ = new_profile;
     bit_depth_ = new_bit_depth;
+    active_sps_ = std::make_unique<H265SPS>(*sps);
     pic_size_ = new_pic_size;
     chroma_sampling_ = new_chroma_sampling;
     picture_color_space_ = new_color_space;
diff --git a/media/gpu/h265_decoder.h b/media/gpu/h265_decoder.h
index 981663c..24c4961f 100644
--- a/media/gpu/h265_decoder.h
+++ b/media/gpu/h265_decoder.h
@@ -388,6 +388,8 @@
   VideoCodecProfile profile_;
   // Bit depth of input bitstream.
   uint8_t bit_depth_ = 0;
+  // Active SPS of input bitstream.
+  std::unique_ptr<H265SPS> active_sps_;
   // Chroma sampling format of input bitstream
   VideoChromaSampling chroma_sampling_ = VideoChromaSampling::kUnknown;
   // Video color space of input bitstream.
diff --git a/media/parsers/h265_nalu_parser.h b/media/parsers/h265_nalu_parser.h
index 5bef60a..dc2b9a2 100644
--- a/media/parsers/h265_nalu_parser.h
+++ b/media/parsers/h265_nalu_parser.h
@@ -146,6 +146,10 @@
   // from AdvanceToNextNALU().
   std::vector<SubsampleEntry> GetCurrentSubsamples();
 
+  bool validate_extended_bitstream() const {
+    return validate_extended_bitstream_;
+  }
+
  protected:
   H264BitReader br_;
 
diff --git a/media/parsers/h265_parser.cc b/media/parsers/h265_parser.cc
index 8d65703..7f505c7 100644
--- a/media/parsers/h265_parser.cc
+++ b/media/parsers/h265_parser.cc
@@ -193,14 +193,20 @@
 }
 
 H265SPS::H265SPS() = default;
-
+H265SPS::H265SPS(const H265SPS&) = default;
+H265SPS& H265SPS::operator=(const H265SPS&) = default;
 H265SPS::H265SPS(H265SPS&&) noexcept = default;
+H265SPS& H265SPS::operator=(H265SPS&&) noexcept = default;
 
 H265ProfileTierLevel::H265ProfileTierLevel() = default;
 
 H265VUIParameters::H265VUIParameters() = default;
-
+H265VUIParameters::H265VUIParameters(const H265VUIParameters&) = default;
+H265VUIParameters& H265VUIParameters::operator=(const H265VUIParameters&) =
+    default;
 H265VUIParameters::H265VUIParameters(H265VUIParameters&&) noexcept = default;
+H265VUIParameters& H265VUIParameters::operator=(H265VUIParameters&&) noexcept =
+    default;
 
 H265PPS::H265PPS() = default;
 
diff --git a/media/parsers/h265_parser.h b/media/parsers/h265_parser.h
index 2124a1e..7a34f834 100644
--- a/media/parsers/h265_parser.h
+++ b/media/parsers/h265_parser.h
@@ -132,7 +132,10 @@
 
 struct MEDIA_EXPORT H265VUIParameters {
   H265VUIParameters();
+  H265VUIParameters(const H265VUIParameters&);
+  H265VUIParameters& operator=(const H265VUIParameters&);
   H265VUIParameters(H265VUIParameters&&) noexcept;
+  H265VUIParameters& operator=(H265VUIParameters&&) noexcept;
   bool operator==(const H265VUIParameters&) const = default;
 
   // Syntax elements.
@@ -180,7 +183,10 @@
 
 struct MEDIA_EXPORT H265SPS {
   H265SPS();
+  H265SPS(const H265SPS&);
+  H265SPS& operator=(const H265SPS&);
   H265SPS(H265SPS&&) noexcept;
+  H265SPS& operator=(H265SPS&&) noexcept;
   bool operator==(const H265SPS&) const = default;
 
   // Syntax elements.
diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc
index 103981e6..68c549e 100644
--- a/media/gpu/h265_decoder_unittest.cc
+++ b/media/gpu/h265_decoder_unittest.cc
@@ -576,36 +576,70 @@
   EXPECT_CALL(*accelerator_, SubmitDecode(_)).Times(1);
   EXPECT_CALL(*accelerator_, OutputPicture(_)).Times(1);
   EXPECT_EQ(AcceleratedVideoDecoder::kRanOutOfStreamData, Decode());
-
-  // 2. Inject 10-bit SPS/PPS followed by an 8-bit P-frame (non-IRAP).
-  std::vector<uint8_t> ten_bit_sps_pps;
-  auto ten_bit_file = GetTestDataFilePath(k10BitFrame0);
-  std::vector<uint8_t> ten_bit_data;
-  CHECK(base::OptionalUnwrapTo(base::ReadFileToBytes(ten_bit_file),
-                               ten_bit_data));
-  // NALU at 0, type 32 (VPS)
-  // NALU at 28, type 33 (SPS)
-  // NALU at 73, type 34 (PPS)
-  // NALU at 84, type 39 (SEI)
-  // We want VPS + SPS + PPS.
-  base::Extend(ten_bit_sps_pps, base::span(ten_bit_data).first(84u));
+  EXPECT_TRUE(decoder_->Flush());
 
   std::vector<uint8_t> p_frame_data;
   auto p_frame_file = GetTestDataFilePath(kFrame1);
   CHECK(base::OptionalUnwrapTo(base::ReadFileToBytes(p_frame_file),
                                p_frame_data));
 
-  std::vector<uint8_t> malicious_bitstream = ten_bit_sps_pps;
-  base::Extend(malicious_bitstream, p_frame_data);
+  // 2. Inject 10-bit SPS/PPS (bit-depth config change) followed by P-frame
+  // (non-IRAP).
+  std::vector<uint8_t> ten_bit_sps_pps;
+  auto ten_bit_file = GetTestDataFilePath(k10BitFrame0);
+  std::vector<uint8_t> ten_bit_data;
+  CHECK(base::OptionalUnwrapTo(base::ReadFileToBytes(ten_bit_file),
+                               ten_bit_data));
+  base::Extend(ten_bit_sps_pps, base::span(ten_bit_data).first(84u));
 
-  auto buffer = DecoderBuffer::CopyFrom(malicious_bitstream);
+  std::vector<uint8_t> bit_depth_bitstream = ten_bit_sps_pps;
+  base::Extend(bit_depth_bitstream, p_frame_data);
+
+  auto buffer1 = DecoderBuffer::CopyFrom(bit_depth_bitstream);
   EXPECT_CALL(*accelerator_, SetStream(_, _));
-  decoder_->SetStream(1, buffer);
+  decoder_->SetStream(1, buffer1);
 
-  // 3. Verify that ConfigChange is NOT allowed on the P-frame.
+  // Verify bit-depth ConfigChange is NOT allowed on P-frame.
   EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, decoder_->Decode());
-  EXPECT_EQ(gfx::Size(320, 184), decoder_->GetPicSize());
-  EXPECT_EQ(8u, decoder_->GetBitDepth());
+
+  // Reset decoder state so we can test a second invalid config change stream.
+  decoder_->Reset();
+
+  // 3. Inject SPS with modified CTB size (CTB log size config change) followed
+  // by P-frame (non-IRAP).
+  H26xAnnexBBitstreamBuilder builder;
+  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 = 1;  // Changed CTB log size
+  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;
+
+  BuildPackedH265SPS(builder, sps);
+  builder.Flush();
+
+  std::vector<uint8_t> ctb_bitstream(builder.data().begin(),
+                                     builder.data().end());
+  base::Extend(ctb_bitstream, p_frame_data);
+
+  auto buffer2 = DecoderBuffer::CopyFrom(ctb_bitstream);
+  EXPECT_CALL(*accelerator_, SetStream(_, _));
+  decoder_->SetStream(2, buffer2);
+
+  // Verify CTB log size ConfigChange is NOT allowed on P-frame.
+  EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, decoder_->Decode());
 
   EXPECT_TRUE(decoder_->Flush());
 }
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 103981e6..68c549e 100644
--- a/media/gpu/h265_decoder_unittest.cc
+++ b/media/gpu/h265_decoder_unittest.cc
@@ -576,36 +576,70 @@
   EXPECT_CALL(*accelerator_, SubmitDecode(_)).Times(1);
   EXPECT_CALL(*accelerator_, OutputPicture(_)).Times(1);
   EXPECT_EQ(AcceleratedVideoDecoder::kRanOutOfStreamData, Decode());
-
-  // 2. Inject 10-bit SPS/PPS followed by an 8-bit P-frame (non-IRAP).
-  std::vector<uint8_t> ten_bit_sps_pps;
-  auto ten_bit_file = GetTestDataFilePath(k10BitFrame0);
-  std::vector<uint8_t> ten_bit_data;
-  CHECK(base::OptionalUnwrapTo(base::ReadFileToBytes(ten_bit_file),
-                               ten_bit_data));
-  // NALU at 0, type 32 (VPS)
-  // NALU at 28, type 33 (SPS)
-  // NALU at 73, type 34 (PPS)
-  // NALU at 84, type 39 (SEI)
-  // We want VPS + SPS + PPS.
-  base::Extend(ten_bit_sps_pps, base::span(ten_bit_data).first(84u));
+  EXPECT_TRUE(decoder_->Flush());
 
   std::vector<uint8_t> p_frame_data;
   auto p_frame_file = GetTestDataFilePath(kFrame1);
   CHECK(base::OptionalUnwrapTo(base::ReadFileToBytes(p_frame_file),
                                p_frame_data));
 
-  std::vector<uint8_t> malicious_bitstream = ten_bit_sps_pps;
-  base::Extend(malicious_bitstream, p_frame_data);
+  // 2. Inject 10-bit SPS/PPS (bit-depth config change) followed by P-frame
+  // (non-IRAP).
+  std::vector<uint8_t> ten_bit_sps_pps;
+  auto ten_bit_file = GetTestDataFilePath(k10BitFrame0);
+  std::vector<uint8_t> ten_bit_data;
+  CHECK(base::OptionalUnwrapTo(base::ReadFileToBytes(ten_bit_file),
+                               ten_bit_data));
+  base::Extend(ten_bit_sps_pps, base::span(ten_bit_data).first(84u));
 
-  auto buffer = DecoderBuffer::CopyFrom(malicious_bitstream);
+  std::vector<uint8_t> bit_depth_bitstream = ten_bit_sps_pps;
+  base::Extend(bit_depth_bitstream, p_frame_data);
+
+  auto buffer1 = DecoderBuffer::CopyFrom(bit_depth_bitstream);
   EXPECT_CALL(*accelerator_, SetStream(_, _));
-  decoder_->SetStream(1, buffer);
+  decoder_->SetStream(1, buffer1);
 
-  // 3. Verify that ConfigChange is NOT allowed on the P-frame.
+  // Verify bit-depth ConfigChange is NOT allowed on P-frame.
   EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, decoder_->Decode());
-  EXPECT_EQ(gfx::Size(320, 184), decoder_->GetPicSize());
-  EXPECT_EQ(8u, decoder_->GetBitDepth());
+
+  // Reset decoder state so we can test a second invalid config change stream.
+  decoder_->Reset();
+
+  // 3. Inject SPS with modified CTB size (CTB log size config change) followed
+  // by P-frame (non-IRAP).
+  H26xAnnexBBitstreamBuilder builder;
+  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 = 1;  // Changed CTB log size
+  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;
+
+  BuildPackedH265SPS(builder, sps);
+  builder.Flush();
+
+  std::vector<uint8_t> ctb_bitstream(builder.data().begin(),
+                                     builder.data().end());
+  base::Extend(ctb_bitstream, p_frame_data);
+
+  auto buffer2 = DecoderBuffer::CopyFrom(ctb_bitstream);
+  EXPECT_CALL(*accelerator_, SetStream(_, _));
+  decoder_->SetStream(2, buffer2);
+
+  // Verify CTB log size ConfigChange is NOT allowed on P-frame.
+  EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, decoder_->Decode());
 
   EXPECT_TRUE(decoder_->Flush());
 }
Loading diff…

Original Bug Report

reported by [email protected]

Potential VideoToolboxH265Accelerator state desync on SPS CTB size change inside non-IRAP frames

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: A potential synchronization issue in VideoToolboxH265Accelerator allows a stale CMVideoFormatDescription to be reused when Coding Tree Block (CTB) size changes on a non-IRAP frame. This occurs because format description regeneration is skipped for non-IRAP frames, even when a changed Sequence Parameter Set (SPS) is detected. Consequently, Apple’s hardware decoder may decode slices with addresses exceeding the session’s originally configured limits, potentially leading to out-of-bounds memory access in the macOS GPU process.

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_decompression_session_manager.mm

Estimated timestamp from git blame: 2023-09-20

Root Cause Analysis

In media/gpu/mac/video_toolbox_h265_accelerator.cc, SubmitDecode() only rebuilds the CMVideoFormatDescription format description (active_format_) on IRAP (keyframe) frames, even after detecting that the active SPS bytes have changed:

// media/gpu/mac/video_toolbox_h265_accelerator.cc:274-279
if (!active_format_ || (combined_nalu_data.size() && frame_is_keyframe_)) {
  combined_nalu_data.clear();
  if (!CreateFormat(pic)) {
    return Status::kFail;
  }
}

frame_is_keyframe_ is set from slice_hdr->irap_pic at line 182. The assumption that configuration changes can only occur at keyframes is enforced by H265Decoder::ProcessPPS() for some SPS fields, but not all:

// media/gpu/h265_decoder.cc:657-668
const bool is_config_change =
    pic_size_ != new_pic_size || dpb_.max_num_pics() != sps->max_dpb_size ||
    profile_ != new_profile || bit_depth_ != new_bit_depth ||
    chroma_sampling_ != new_chroma_sampling;

if (is_config_change) {
  // Only color space changes are allowed on non-IRAP pictures.
  if (curr_slice_hdr_ && !curr_slice_hdr_->irap_pic && !first_picture_) {
    DVLOG(1) << "A configuration change on a non-IRAP picture is not allowed.";
    return false;
  }
}

Importantly, ctb_log2_size_y / pic_size_in_ctbs_y are not checked by is_config_change. Two SPSes with identical dimensions, profile, bit-depth, and chroma sampling, but differing log2_min_luma_coding_block_size_minus3 or log2_diff_max_min_luma_coding_block_size can bypass this check on a non-IRAP frame without triggering a configuration change in H265Decoder.

Potential Trigger Path

(Note: These are potential steps as our tooling does not currently have runtime execution capabilities.)

  1. Stream Setup: The attacker serves an HEVC Annex-B stream containing two distinct SPS blocks sharing sps_id = 0:
    • SPS_A: Configured with a resolution of 1024x1024, ctb_log2_size_y = 6 (CTB size 64x64), resulting in pic_size_in_ctbs_y = 256 total CTBs.
    • SPS_B: Configured with the same resolution (1024x1024) but ctb_log2_size_y = 4 (CTB size 16x16), resulting in pic_size_in_ctbs_y = 4096 total CTBs.
  2. Initialize Session: The decoder processes an IDR (IRAP) frame utilizing SPS_A. The accelerator builds active_format_ from SPS_A, and the VideoToolboxDecompressionSessionManager instantiates a hardware decoding session optimized for a 256-CTB layout.
  3. Inject SPS_B: The stream transmits SPS_B, which overwrites sps_id = 0 in the parser’s cached state.
  4. Process non-IRAP Frame: The stream presents a non-IRAP TRAIL_R slice utilizing pps_id = 0 (pointing to sps_id = 0 / SPS_B).
    • H265Decoder::ProcessPPS() computes is_config_change = false because resolution and basic profiles match, allowing decoding to proceed on the non-IRAP frame.
    • VideoToolboxH265Accelerator::SubmitFrameMetadata() marks frame_is_keyframe_ = false.
  5. Craft OOB Slice Address: The TRAIL_R picture contains a second slice segment (first_slice_segment_in_pic_flag = 0) with slice_segment_address = 4095.
    • Chromium’s parser validates slice_segment_address against SPS_B’s limits (pic_size_in_ctbs_y - 1 = 4095) and permits the value.
  6. State Desynchronization: Inside SubmitDecode(), ExtractChangedParameterSetData() notices that the active SPS has changed, updates active_sps_data_[0] to SPS_B, and appends SPS_B to combined_nalu_data.
    • However, because frame_is_keyframe_ is false, CreateFormat() is not called. active_format_ remains configured with the stale SPS_A properties (256 CTBs max).
  7. OOB Write: The CMSampleBuffer containing the stale active_format_ is passed to the decompression session manager. The manager bypasses regeneration because the format description pointer has not changed. The hardware/software decoder processes a slice segment targeting CTB 4095 within a session configured only for 256 CTBs, potentially leading to out-of-bounds memory access in Apple’s closed-source decoder inside the macOS GPU process.

Suggested Fix

To prevent state desynchronization, VideoToolboxH265Accelerator should rebuild the CMVideoFormatDescription whenever a parameter set change is detected, regardless of whether the frame is a keyframe. This mirrors the safe behavior implemented in the H.264 sibling (VideoToolboxH264Accelerator):

// media/gpu/mac/video_toolbox_h265_accelerator.cc
if (!active_format_ || combined_nalu_data.size()) {
  combined_nalu_data.clear();
  if (!CreateFormat(pic)) {
    return Status::kFail;
  }
}

Evaluated with Chrome root at commit: 94d9235ebe3b7276e5284f0dc5d55577ff949908


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