CVE-2026-87638
Overview
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_minus3andlog2_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
H265NaluParserflag 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.
Attack Path
- 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) populatingactive_sps_. - 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 = 1to alter the CTB size — so the pre-patchis_config_changeevaluates false. - Decode a non-IRAP picture under the new config
A P-frame (
kFrame1) follows; because no config change was detected, decoding proceeds instead of returningkDecodeError, so the decoder operates with structures sized for the old CTB size but data described by the new one. - 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
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
| Function | Change | Notes |
|---|---|---|
ifmedia/gpu/h265_decoder.cc |
modified |
Files Changed
media/gpu/h265_decoder.hmedia/gpu/h265_decoder.ccmedia/gpu/h265_decoder_unittest.ccmedia/parsers/h265_parser.ccmedia/parsers/h265_nalu_parser.hmedia/parsers/h265_parser.h
Audit Directions
- Subset equality checks on parsed headersAudit 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 parametersLook 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 validationCheck 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 theelsebranch and only the gated branch gains the full-SPS check.
Patch
From 2d157885607bfcd825e0c211ea81b8b7d9d7e73e Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
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 <eugene@chromium.org>
Reviewed-by: Qiu, Jianlin <jianlin.qiu@intel.com>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
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());
}
Regression Test / PoC
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());
}
Original Bug Report
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.ccmedia/gpu/h265_decoder.ccmedia/parsers/h265_parser.ccmedia/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.)
- 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 inpic_size_in_ctbs_y = 256total CTBs. - SPS_B: Configured with the same resolution (1024x1024) but
ctb_log2_size_y = 4(CTB size 16x16), resulting inpic_size_in_ctbs_y = 4096total CTBs.
- SPS_A: Configured with a resolution of 1024x1024,
- Initialize Session: The decoder processes an IDR (IRAP) frame utilizing SPS_A. The accelerator builds
active_format_from SPS_A, and theVideoToolboxDecompressionSessionManagerinstantiates a hardware decoding session optimized for a 256-CTB layout. - Inject SPS_B: The stream transmits SPS_B, which overwrites
sps_id = 0in the parser’s cached state. - Process non-IRAP Frame: The stream presents a non-IRAP
TRAIL_Rslice utilizingpps_id = 0(pointing tosps_id = 0/ SPS_B).H265Decoder::ProcessPPS()computesis_config_change = falsebecause resolution and basic profiles match, allowing decoding to proceed on the non-IRAP frame.VideoToolboxH265Accelerator::SubmitFrameMetadata()marksframe_is_keyframe_ = false.
- Craft OOB Slice Address: The
TRAIL_Rpicture contains a second slice segment (first_slice_segment_in_pic_flag = 0) withslice_segment_address = 4095.- Chromium’s parser validates
slice_segment_addressagainst SPS_B’s limits (pic_size_in_ctbs_y - 1 = 4095) and permits the value.
- Chromium’s parser validates
- State Desynchronization: Inside
SubmitDecode(),ExtractChangedParameterSetData()notices that the active SPS has changed, updatesactive_sps_data_[0]to SPS_B, and appends SPS_B tocombined_nalu_data.- However, because
frame_is_keyframe_isfalse,CreateFormat()is not called.active_format_remains configured with the stale SPS_A properties (256 CTBs max).
- However, because
- OOB Write: The
CMSampleBuffercontaining the staleactive_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.