CVE-2026-8529
Overview
Background
- Dependent slice segment
- an H.265 slice whose header omits most fields and inherits them from the preceding independent slice of the same picture.
- `nuh_layer_id`
- the NAL unit header field identifying which coding layer (e.g. base layer 0 vs an auxiliary alpha layer) a slice belongs to.
- `H265SliceHeader`
- the parsed slice-header struct whose fields after
slice_typeare copied as a block for dependent slices. - `kMaxLongTermRefPicSets`
- the parser’s array bound (32) for the long-term reference picture fields in
H265SliceHeader.
Root Cause Analysis
41eb2ddb1707 was always correctly mapped, but the earlier analysis was written from a truncated copy of its diff that left out the changed product source (media/parsers/h265_parser.cc and h265_parser.h) and showed only build/test files. This analysis is written from the complete diff.In pre-fix H265Parser::ParseSliceHeader, when shdr->dependent_slice_segment_flag is set and a prior_shdr exists (h265_parser.cc:1152-1156), the parser copies the tail of the previous slice header into the current one with memcpy starting at offsetof(H265SliceHeader, slice_type) (h265_parser.cc:1159-1164). The long-term reference arrays poc_lsb_lt, used_by_curr_pic_lt, delta_poc_msb_present_flag, delta_poc_msb_cycle_lt and the counts num_long_term_sps / num_long_term_pics sit after slice_type in the struct (h265_parser.h:405-410), so they are part of that copied block. Pre-fix the parser never required the prior and current slices to share a layer, so a base-layer (nuh_layer_id == 0) dependent slice could inherit slice state parsed under a different layer’s relaxed validation; the only layer-aware checks in this region (h265_parser.cc:1168-1172 and 1193-1197) merely constrain slice_type, not the long-term-ref counts.
The fix (commit 41eb2ddb…, chromium/src) records shdr->nuh_layer_id = nalu.nuh_layer_id (added after h265_parser.cc:1122), adds the field int nuh_layer_id = 0; before the copied block in H265SliceHeader (h265_parser.h, above the “everything after this is copied as a block” comment at lines 394-396), and, before the memcpy, returns kInvalidStream when shdr->nuh_layer_id != prior_shdr->nuh_layer_id (added diff, citing spec clause 7.4.7.1). This enforces the H.265 constraint that a dependent slice segment must share its predecessor’s layer, so unvalidated cross-layer slice state can no longer be propagated into a base-layer header.
nuh_layer_id differs from the prior slice’s, preventing unvalidated long-term-reference state from crossing layer boundaries.Attack Path
- Craft a multi-layer HEVC stream Provide a bitstream declaring an auxiliary alpha layer and an SPS with long-term reference pictures present. (inferred)
- Over-fill via the aux layer
Send an auxiliary-layer slice (
nuh_layer_id != 0) carrying more long-term reference pictures than base-layer validation would accept, stored as the prior slice. (inferred) - Cross the boundary
Send a base-layer (
nuh_layer_id == 0) dependent slice; pre-fix the parsermemcpys the aux-layer long-term-ref state into the base-layer header (h265_parser.cc:1159-1164). - Overflow fixed-size decoder arrays
Downstream decoding iterates over the inflated long-term-ref count and writes past fixed-size (
kMaxDpbSize) arrays, corrupting heap and stack. (inferred —media/gpu/h265_decoder.ccis not in the material)
Impact Assessment
media/gpu/h265_decoder.cc is not included here. The advisory reports no in-the-wild exploitation, and the reporter notes the path is relevant where auxiliary alpha layers are supported.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/parsers/h265_parser.cc |
modified | |
TEST_Fmedia/gpu/h265_decoder_unittest.cc |
modified | |
formedia/gpu/h265_decoder_unittest.cc |
modified | |
ifmedia/filters/BUILD.gn |
modified | |
ifmedia/gpu/BUILD.gn |
modified |
Files Changed
media/filters/BUILD.gnmedia/gpu/BUILD.gnmedia/gpu/h265_decoder_unittest.ccmedia/parsers/h265_parser.ccmedia/parsers/h265_parser.h
Audit Directions
- Block copies of parsed stateReview
memcpy/offsetof-based struct copies of parser output for fields that bypass per-field validation, especially across trust or layer boundaries. - Parser/decoder bound mismatchesFlag cases where a parser’s array capacity (
kMaxLongTermRefPicSets) exceeds the decoder’s fixed tracking-array size (kMaxDpbSize), which turns an over-count into an overflow. - Layer-id invariantsVerify multi-layer codec parsers enforce the spec constraints tying dependent/inherited structures to the same
nuh_layer_id.
Patch
diff --git a/media/parsers/h265_parser.cc b/media/parsers/h265_parser.cc
index cd4f8526..5974cc41 100644
--- a/media/parsers/h265_parser.cc
+++ b/media/parsers/h265_parser.cc
@@ -1120,6 +1120,7 @@
shdr->nalu_data = nalu.data.data();
shdr->nalu_size = nalu.data.size();
shdr->temporal_id = nalu.nuh_temporal_id_plus1 - 1;
+ shdr->nuh_layer_id = nalu.nuh_layer_id;
READ_BOOL_OR_RETURN(&shdr->first_slice_segment_in_pic_flag);
shdr->irap_pic = (shdr->nal_unit_type >= H265NALU::BLA_W_LP &&
@@ -1154,6 +1155,14 @@
DVLOG(1) << "Cannot parse dependent slice w/out prior slice data";
return kInvalidStream;
}
+
+ // 7.4.7.1
+ if (shdr->nuh_layer_id != prior_shdr->nuh_layer_id) {
+ DVLOG(1) << "Dependent slice segment must have the same nuh_layer_id "
+ << "as the prior slice";
+ return kInvalidStream;
+ }
+
// Copy everything in the structure starting at |slice_type| going forward.
// This is copying the dependent slice data that we do not parse below.
size_t skip_amount = offsetof(H265SliceHeader, slice_type);
diff --git a/media/parsers/h265_parser.h b/media/parsers/h265_parser.h
index ee07d751..700c43b1 100644
--- a/media/parsers/h265_parser.h
+++ b/media/parsers/h265_parser.h
@@ -391,6 +391,7 @@
int slice_pic_parameter_set_id = 0;
bool dependent_slice_segment_flag = false;
int slice_segment_address = 0;
+ int nuh_layer_id = 0;
// Do not move any of the above fields below or vice-versa, everything after
// this is copied as a block.
int slice_type = 0;
diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc
index 0124e23..0660e23 100644
--- a/media/gpu/h265_decoder_unittest.cc
+++ b/media/gpu/h265_decoder_unittest.cc
@@ -17,6 +17,8 @@
#include "base/memory/scoped_refptr.h"
#include "base/types/optional_util.h"
#include "media/base/test_data_util.h"
+#include "media/filters/h26x_annex_b_bitstream_builder.h"
+#include "media/gpu/h265_builder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -128,6 +130,7 @@
return format == VideoChromaSampling::k420;
}
void Reset() override {}
+ bool IsAlphaLayerSupported() override { return true; }
};
// Test H265Decoder by feeding different h265 frame sequences and make sure it
@@ -598,4 +601,154 @@
EXPECT_TRUE(decoder_->Flush());
}
+// This test verifies that dependent slices crossing layer boundaries
+// (different nuh_layer_id) are correctly rejected by the parser,
+// preventing unvalidated slice header state from being propagated.
+TEST_F(H265DecoderTest, DependentSliceLongTermRefPics) {
+ H26xAnnexBBitstreamBuilder builder;
+ // VPS
+ constexpr uint8_t kVpsWithAlpha[] = {
+ 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();
+
+ // 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;
+ sps.scaling_list_enabled_flag = false;
+ sps.amp_enabled_flag = false;
+ sps.sample_adaptive_offset_enabled_flag = false;
+ sps.pcm_enabled_flag = false;
+ sps.num_short_term_ref_pic_sets = 0;
+ sps.long_term_ref_pics_present_flag = true;
+ sps.num_long_term_ref_pics_sps = 0;
+ sps.sps_temporal_mvp_enabled_flag = false;
+ sps.strong_intra_smoothing_enabled_flag = false;
+ sps.vui_parameters_present_flag = false;
+ BuildPackedH265SPS(builder, sps);
+
+ // PPS
+ H265PPS pps = {};
+ pps.pps_pic_parameter_set_id = 0;
+ pps.pps_seq_parameter_set_id = 0;
+ pps.dependent_slice_segments_enabled_flag = true;
+ pps.output_flag_present_flag = false;
+ pps.num_extra_slice_header_bits = 0;
+ pps.sign_data_hiding_enabled_flag = false;
+ pps.cabac_init_present_flag = false;
+ pps.num_ref_idx_l0_default_active_minus1 = 0;
+ pps.num_ref_idx_l1_default_active_minus1 = 0;
+ pps.init_qp_minus26 = 0;
+ pps.constrained_intra_pred_flag = false;
+ pps.transform_skip_enabled_flag = false;
+ pps.cu_qp_delta_enabled_flag = false;
+ pps.pps_slice_chroma_qp_offsets_present_flag = false;
+ pps.pps_loop_filter_across_slices_enabled_flag = false;
+ pps.deblocking_filter_control_present_flag = false;
+ pps.pps_scaling_list_data_present_flag = false;
+ pps.lists_modification_present_flag = false;
+ pps.log2_parallel_merge_level_minus2 = 0;
+ pps.slice_segment_header_extension_present_flag = false;
+ BuildPackedH265PPS(builder, pps);
+
+ // NALU 1: Aux Layer (nuh_layer_id = 1)
+ 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, 1); // nuh_layer_id = 1
+ 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.AppendUE(32); // num_long_term_pics = 32!
+ for (int i = 0; i < 32; ++i) {
+ builder.AppendBits(8, i); // poc_lsb_lt
+ builder.AppendBool(
+ false); // used_by_curr_pic_lt_flag = false -> goes to poc_lt_foll_
+ builder.AppendBool(false); // delta_poc_msb_present_flag
+ }
+
+ builder.AppendSE(0); // slice_qp_delta
+ builder.AppendBool(true); // byte alignment bit
+ builder.Flush();
+
+ // NALU 2: Base Layer (nuh_layer_id = 0) with dependent slice
+ 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(false); // 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 (MISSING BEFORE!)
+ builder.AppendBool(true); // dependent_slice_segment_flag = true
+ builder.AppendBits(
+ 8, 1); // slice_segment_address = 1 (240 CTBs max, Log2Ceiling(240)=8)
+ builder.AppendBool(true); // byte alignment bit
+ builder.Flush();
+
+ auto buffer = DecoderBuffer::CopyFrom(builder.data());
+
+ // Set EXPECT_CALLs for decoding so we can reach the vulnerable function
+ 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));
+ EXPECT_CALL(*accelerator_, SubmitSlice(_, _, _, _, _, _, _, _, _, _, _, _))
+ .WillRepeatedly(Return(H265Decoder::H265Accelerator::Status::kOk));
+ EXPECT_CALL(*accelerator_, SubmitDecode(_))
+ .WillRepeatedly(Return(H265Decoder::H265Accelerator::Status::kOk));
+ EXPECT_CALL(*accelerator_, OutputPicture(_)).WillRepeatedly(Return(true));
+
+ decoder_->SetStream(1, buffer);
+
+ // Decode until config change (VPS/SPS/PPS)
+ auto res1 = decoder_->Decode();
+ EXPECT_EQ(AcceleratedVideoDecoder::kConfigChange, res1);
+
+ // Decode the frame (which would overflow the arrays if not rejected by the
+ // parser). It should be rejected as a decode error.
+ auto res2 = decoder_->Decode();
+ EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, res2);
+}
+
} // namespace media
diff --git a/media/filters/BUILD.gn b/media/filters/BUILD.gn
index 172eeba..49dfa44 100644
--- a/media/filters/BUILD.gn
+++ b/media/filters/BUILD.gn
@@ -246,7 +246,7 @@
}
}
- if (is_win || use_vaapi) {
+ if (is_win || use_vaapi || enable_hevc_parser_and_hw_decoder) {
sources += [
"h26x_annex_b_bitstream_builder.cc",
"h26x_annex_b_bitstream_builder.h",
diff --git a/media/gpu/BUILD.gn b/media/gpu/BUILD.gn
index 4dd63449c..1159d81e 100644
--- a/media/gpu/BUILD.gn
+++ b/media/gpu/BUILD.gn
@@ -425,12 +425,13 @@
"svc_layers.cc",
"svc_layers.h",
]
- if (enable_hevc_parser_and_hw_decoder) {
- sources += [
- "h265_builder.cc",
- "h265_builder.h",
- ]
- }
+ }
+
+ if (enable_hevc_parser_and_hw_decoder) {
+ sources += [
+ "h265_builder.cc",
+ "h265_builder.h",
+ ]
}
if (use_av1_hw_decoder) {
Regression Test / PoC
diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc
index 0124e23..0660e23 100644
--- a/media/gpu/h265_decoder_unittest.cc
+++ b/media/gpu/h265_decoder_unittest.cc
@@ -17,6 +17,8 @@
#include "base/memory/scoped_refptr.h"
#include "base/types/optional_util.h"
#include "media/base/test_data_util.h"
+#include "media/filters/h26x_annex_b_bitstream_builder.h"
+#include "media/gpu/h265_builder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -128,6 +130,7 @@
return format == VideoChromaSampling::k420;
}
void Reset() override {}
+ bool IsAlphaLayerSupported() override { return true; }
};
// Test H265Decoder by feeding different h265 frame sequences and make sure it
@@ -598,4 +601,154 @@
EXPECT_TRUE(decoder_->Flush());
}
+// This test verifies that dependent slices crossing layer boundaries
+// (different nuh_layer_id) are correctly rejected by the parser,
+// preventing unvalidated slice header state from being propagated.
+TEST_F(H265DecoderTest, DependentSliceLongTermRefPics) {
+ H26xAnnexBBitstreamBuilder builder;
+ // VPS
+ constexpr uint8_t kVpsWithAlpha[] = {
+ 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();
+
+ // 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;
+ sps.scaling_list_enabled_flag = false;
+ sps.amp_enabled_flag = false;
+ sps.sample_adaptive_offset_enabled_flag = false;
+ sps.pcm_enabled_flag = false;
+ sps.num_short_term_ref_pic_sets = 0;
+ sps.long_term_ref_pics_present_flag = true;
+ sps.num_long_term_ref_pics_sps = 0;
+ sps.sps_temporal_mvp_enabled_flag = false;
+ sps.strong_intra_smoothing_enabled_flag = false;
+ sps.vui_parameters_present_flag = false;
+ BuildPackedH265SPS(builder, sps);
+
+ // PPS
+ H265PPS pps = {};
+ pps.pps_pic_parameter_set_id = 0;
+ pps.pps_seq_parameter_set_id = 0;
+ pps.dependent_slice_segments_enabled_flag = true;
+ pps.output_flag_present_flag = false;
+ pps.num_extra_slice_header_bits = 0;
+ pps.sign_data_hiding_enabled_flag = false;
+ pps.cabac_init_present_flag = false;
+ pps.num_ref_idx_l0_default_active_minus1 = 0;
+ pps.num_ref_idx_l1_default_active_minus1 = 0;
+ pps.init_qp_minus26 = 0;
+ pps.constrained_intra_pred_flag = false;
+ pps.transform_skip_enabled_flag = false;
+ pps.cu_qp_delta_enabled_flag = false;
+ pps.pps_slice_chroma_qp_offsets_present_flag = false;
+ pps.pps_loop_filter_across_slices_enabled_flag = false;
+ pps.deblocking_filter_control_present_flag = false;
+ pps.pps_scaling_list_data_present_flag = false;
+ pps.lists_modification_present_flag = false;
+ pps.log2_parallel_merge_level_minus2 = 0;
+ pps.slice_segment_header_extension_present_flag = false;
+ BuildPackedH265PPS(builder, pps);
+
+ // NALU 1: Aux Layer (nuh_layer_id = 1)
+ 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, 1); // nuh_layer_id = 1
+ 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.AppendUE(32); // num_long_term_pics = 32!
+ for (int i = 0; i < 32; ++i) {
+ builder.AppendBits(8, i); // poc_lsb_lt
+ builder.AppendBool(
+ false); // used_by_curr_pic_lt_flag = false -> goes to poc_lt_foll_
+ builder.AppendBool(false); // delta_poc_msb_present_flag
+ }
+
+ builder.AppendSE(0); // slice_qp_delta
+ builder.AppendBool(true); // byte alignment bit
+ builder.Flush();
+
+ // NALU 2: Base Layer (nuh_layer_id = 0) with dependent slice
+ 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(false); // 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 (MISSING BEFORE!)
+ builder.AppendBool(true); // dependent_slice_segment_flag = true
+ builder.AppendBits(
+ 8, 1); // slice_segment_address = 1 (240 CTBs max, Log2Ceiling(240)=8)
+ builder.AppendBool(true); // byte alignment bit
+ builder.Flush();
+
+ auto buffer = DecoderBuffer::CopyFrom(builder.data());
+
+ // Set EXPECT_CALLs for decoding so we can reach the vulnerable function
+ 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));
+ EXPECT_CALL(*accelerator_, SubmitSlice(_, _, _, _, _, _, _, _, _, _, _, _))
+ .WillRepeatedly(Return(H265Decoder::H265Accelerator::Status::kOk));
+ EXPECT_CALL(*accelerator_, SubmitDecode(_))
+ .WillRepeatedly(Return(H265Decoder::H265Accelerator::Status::kOk));
+ EXPECT_CALL(*accelerator_, OutputPicture(_)).WillRepeatedly(Return(true));
+
+ decoder_->SetStream(1, buffer);
+
+ // Decode until config change (VPS/SPS/PPS)
+ auto res1 = decoder_->Decode();
+ EXPECT_EQ(AcceleratedVideoDecoder::kConfigChange, res1);
+
+ // Decode the frame (which would overflow the arrays if not rejected by the
+ // parser). It should be rejected as a decode error.
+ auto res2 = decoder_->Decode();
+ EXPECT_EQ(AcceleratedVideoDecoder::kDecodeError, res2);
+}
+
} // namespace media
Original Bug Report
Potential Heap and Stack Overflows in H265Decoder via Dependent Slice Segments
Flapjack (go/flapjack), an LLM-powered static analysis tool, has identified the following potential security issue.
Overview: A discrepancy between parser limits and internal array sizes in H265Decoder allows out-of-bounds writes. An attacker can bypass base-layer validation checks using auxiliary alpha layers and dependent slice segments. This leads to heap and stack buffer overflows, potentially resulting in GPU process RCE.
Affected files:
media/gpu/h265_decoder.ccmedia/parsers/h265_parser.ccmedia/gpu/h265_decoder.hmedia/parsers/h265_parser.h
Estimated timestamp from git blame: 2025-08-07
Summary
A critical vulnerability exists in Chrome’s H.265 video decoder implementation (media/gpu/h265_decoder.cc). A mismatch between the maximum allowed long-term reference pictures in the parser (kMaxLongTermRefPicSets = 32) and the fixed-size tracking arrays in the decoder (kMaxDpbSize = 16) can be exploited to cause severe heap and stack buffer overflows. An attacker can bypass the parser’s validation checks by utilizing auxiliary alpha layers and dependent slice segments, leading to arbitrary code execution in the sandboxed GPU process.
Root Cause Analysis
In media/parsers/h265_parser.cc, the ParseSliceHeader function reads the number of long-term reference pictures (num_long_term_pics). A bounds check ensures this value, combined with num_long_term_sps, does not exceed sps_max_dec_pic_buffering_minus1 (which is implicitly capped at 15 for base layers). However, this check is explicitly bypassed for non-base layers:
// media/parsers/h265_parser.cc:1241
if (nalu.nuh_layer_id == 0) {
TRUE_OR_RETURN(
shdr->num_long_term_pics <=
(sps->sps_max_dec_pic_buffering_minus1[clamped_temporal_id] - ...));
}
For an auxiliary alpha layer NALU (nuh_layer_id != 0), this check is skipped, allowing num_long_term_pics to reach the absolute maximum defined by kMaxLongTermRefPicSets (32).
When a subsequent base layer NALU (nuh_layer_id == 0) with dependent_slice_segment_flag == 1 is parsed, the parser copies the state from the previous slice header (prior_shdr) into the new slice header using memcpy:
// media/parsers/h265_parser.cc:1159
size_t skip_amount = offsetof(H265SliceHeader, slice_type);
UNSAFE_TODO(memcpy(reinterpret_cast<uint8_t*>(shdr) + skip_amount,
reinterpret_cast<uint8_t*>(prior_shdr) + skip_amount,
sizeof(H265SliceHeader) - skip_amount));
Because num_long_term_pics and its associated arrays are located after slice_type in the H265SliceHeader struct, they are blindly copied into the base layer header. The validation logic for dependent slices fails to re-evaluate the strict base-layer bounds check, allowing the base layer to inherit the unvalidated 32 LTRP count.
Exploitation Steps
An attacker would trigger this vulnerability using the following sequence:
- Provide a malicious bitstream with a VPS indicating an auxiliary alpha layer, and an SPS with
long_term_ref_pics_present_flag = 1. - Send an auxiliary alpha layer NALU (
nuh_layer_id != 0) containing up to 32 long-term reference pictures. The parser accepts this and stores it aslast_slice_hdr_. - Send a base layer NALU (
nuh_layer_id == 0) withdependent_slice_segment_flag == 1. The parser copies the 32 LTRP count into the base layer slice header. H265Decoder::StartNewFrameprocesses the base layer slice and callsCalcRefPicPocs.CalcRefPicPocsiterates up to 32 times, writing attacker-controlled data intopoc_lt_curr_orpoc_lt_foll_and their correspondingdelta_poc_msb_present_flagarrays. These arestd::arraymembers of sizekMaxDpbSize(16). This results in a 64-byte heap buffer overflow.- This overflow clobbers adjacent class members, most notably
ref_pic_list0_(astd::vector). By overwriting the internal pointers of this vector with controlled values, the attacker can manipulate arbitrary memory when vector operations (like.clear()or.push_back()) are later invoked. - Next,
StartNewFramecallsBuildRefPicLists, which allocates astd::array<scoped_refptr<H265Picture>, kMaxDpbSize> ref_pic_set_lt_follon the stack. - A loop iterates up to 32 times (based on
num_poc_lt_foll_), writingscoped_refptrobjects into this 16-element stack array, resulting in a severe stack buffer overflow.
Security Impact
The combination of heap corruption (vector hijacking leading to arbitrary scoped_refptr AddRef/Release operations) and stack corruption (overwriting return addresses with pointers to heap objects) provides highly reliable primitives for Remote Code Execution (RCE) within the GPU process. This issue is reachable on Apple platforms where IsAlphaLayerSupported() returns true.
Evaluated with Chrome root at commit: 818af3d59a508e1d651545a5bed0175ff7406aa3
Results from Flapjack so far have been promising, but it can be wrong in its deductions. At this time, it does not produce proof of concepts or fuzzer tests. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve Flapjack’s accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.