CVE-2026-8529
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/filters/BUILD.gn |
modified | |
ifmedia/gpu/BUILD.gn |
modified | |
TEST_Fmedia/gpu/h265_decoder_unittest.cc |
modified | |
formedia/gpu/h265_decoder_unittest.cc |
modified |
Files Changed
media/filters/BUILD.gnmedia/gpu/BUILD.gnmedia/gpu/h265_decoder_unittest.cc
Patch
From 41eb2ddb1707ebf9296373d90c1e8a8d171cfa8f Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Mon, 16 Mar 2026 19:47:52 -0700
Subject: [PATCH] media: Reject H265 dependent slices across layer boundaries
When the nuh_layer_id of the current NAL unit is not equal to the
nuh_layer_id of the preceding NAL unit, dependent_slice_segment_flag
must be 0.
Previously, H265Parser did not enforce this constraint. This allowed
a base layer slice to incorrectly declare itself dependent on an
auxiliary layer slice, causing the parser to copy unvalidated slice
state into the base layer header.
This CL fixes the issue by explicitly tracking `nuh_layer_id` in
`H265SliceHeader` and returning `kInvalidStream` if a dependent
slice attempts to cross layer boundaries.
Bug: 490222151
Change-Id: I73c123873b5050fb5796ff5bd002e0c0e0401218
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7671861
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: Qiu, Jianlin <jianlin.qiu@intel.com>
Cr-Commit-Position: refs/heads/main@{#1600285}
---
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) {
diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc
index 0124e23..0660e23b 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
Regression Test / PoC
diff --git a/media/gpu/h265_decoder_unittest.cc b/media/gpu/h265_decoder_unittest.cc
index 0124e23..0660e23b 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.