High patch-now ✔︎ verified CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Codecs
DescriptionHeap buffer overflow in Codecs
ComponentCodecs
Sub-componentMedia
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (confirmed) — factors set by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: none for citations. The decoder-side kMaxDpbSize overflow and the value 32 for kMaxLongTermRefPicSets come only from the reporter/unit test (h265_decoder.cc absent); the analysis correctly marks the overflow as inferred. Reachability limited to alpha-layer-capable platforms per reporter.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker490222151
Fix commit41eb2ddb1707 (chromium/src) +171/-7
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-06 (per vendor advisory)
Fix landed2026-03-17 (public git)
Disclosed2026-05-12
Patch gap56 days — fix public in source before the advisory

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_type are 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

Re-analyzed on 2026-10-03. The fix commit 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.

Key insight
A dependent slice was allowed to inherit a prior slice’s header from a different layer; the fix rejects dependent slices whose nuh_layer_id differs from the prior slice’s, preventing unvalidated long-term-reference state from crossing layer boundaries.

Attack Path

  1. Craft a multi-layer HEVC stream Provide a bitstream declaring an auxiliary alpha layer and an SPS with long-term reference pictures present. (inferred)
  2. 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)
  3. Cross the boundary Send a base-layer (nuh_layer_id == 0) dependent slice; pre-fix the parser memcpys the aux-layer long-term-ref state into the base-layer header (h265_parser.cc:1159-1164).
  4. 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.cc is not in the material)

Impact Assessment

Per the advisory this is a high-severity heap buffer overflow in Chrome’s codecs, reached through H.265 decoding in the GPU process. The reporter describes resulting heap and stack corruption and potential GPU-process RCE; that escalation and the decoder-side array writes are (inferred) because 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

FunctionChangeNotes
if
media/parsers/h265_parser.cc
modified
TEST_F
media/gpu/h265_decoder_unittest.cc
modified
for
media/gpu/h265_decoder_unittest.cc
modified
if
media/filters/BUILD.gn
modified
if
media/gpu/BUILD.gn
modified

Files Changed

  • media/filters/BUILD.gn
  • media/gpu/BUILD.gn
  • media/gpu/h265_decoder_unittest.cc
  • media/parsers/h265_parser.cc
  • media/parsers/h265_parser.h

Audit Directions

  • Block copies of parsed state
    Review memcpy / offsetof-based struct copies of parser output for fields that bypass per-field validation, especially across trust or layer boundaries.
  • Parser/decoder bound mismatches
    Flag 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 invariants
    Verify multi-layer codec parsers enforce the spec constraints tying dependent/inherited structures to the same nuh_layer_id.
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) {
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 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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • media/parsers/h265_parser.cc
  • media/gpu/h265_decoder.h
  • media/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:

  1. Provide a malicious bitstream with a VPS indicating an auxiliary alpha layer, and an SPS with long_term_ref_pics_present_flag = 1.
  2. 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 as last_slice_hdr_.
  3. Send a base layer NALU (nuh_layer_id == 0) with dependent_slice_segment_flag == 1. The parser copies the 32 LTRP count into the base layer slice header.
  4. H265Decoder::StartNewFrame processes the base layer slice and calls CalcRefPicPocs.
  5. CalcRefPicPocs iterates up to 32 times, writing attacker-controlled data into poc_lt_curr_ or poc_lt_foll_ and their corresponding delta_poc_msb_present_flag arrays. These are std::array members of size kMaxDpbSize (16). This results in a 64-byte heap buffer overflow.
  6. This overflow clobbers adjacent class members, most notably ref_pic_list0_ (a std::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.
  7. Next, StartNewFrame calls BuildRefPicLists, which allocates a std::array<scoped_refptr<H265Picture>, kMaxDpbSize> ref_pic_set_lt_foll on the stack.
  8. A loop iterates up to 32 times (based on num_poc_lt_foll_), writing scoped_refptr objects 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.

View on issue tracker