High watch CVSS 9.6 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Media
DescriptionImproper input validation in Media
ComponentMedia
Bug ClassInput Validation
Action tierwatch — ub-class only — no demonstrated corruption path
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.
Tracker536428615
Fix commitad64b2c8fdc0 (chromium/src) +146/-4
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-19 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

Background

`H264Parser`
Chromium’s H.264 Annex-B bitstream parser in media/parsers/h264_parser.cc. Its parsed SPS/PPS/slice-header structs feed Chromium’s accelerated (hardware) decoders, which pass many fields straight to platform video drivers.
Reference picture list modification
slice-header syntax (modification_of_pic_nums_idc with abs_diff_pic_num_minus1 or long_term_pic_num) that reorders the reference lists. H.264 §7.4.3.1 limits these values to MaxPicNum (at most 65536) and MaxLongTermFrameIdx (at most 15).
Decoded reference picture marking (MMCO)
memory_mgmnt_control_operation commands 1–6 with operands (difference_of_pic_nums_minus1, long_term_pic_num, long_term_frame_idx, max_long_term_frame_idx_plus1) that manage the DPB. H.264 §7.4.3.3 limits these to 65535, 15, 15 and 16 respectively.
`IN_RANGE_IF_OR_RETURN(value, min, max, cond)`
a parser macro that returns an error (kInvalidStream, as the unit test expects) when cond is true and value is outside [min, max]. Here cond is the member validate_extended_bitstream_.

Root Cause Analysis

Several slice-header fields were read with READ_UE_OR_RETURN (unsigned Exp-Golomb, which can decode to very large values) and stored without any range check. In the reference-list modification loop (enclosing function not named in the hunk), abs_diff_pic_num_minus1 for modification_of_pic_nums_idc 0/1 and long_term_pic_num for idc 2 were accepted as is. In the dec-ref-pic-marking loop (also not named in the hunk), difference_of_pic_nums_minus1, long_term_pic_num, long_term_frame_idx and max_long_term_frame_idx_plus1 were likewise unbounded. The only existing check there was memory_mgmnt_control_operation > 6. As the commit says, these out-of-spec values could then be passed “blindly … out to drivers” by the hardware-decode paths that use the parsed header.

The fix adds IN_RANGE_IF_OR_RETURN after each read, using the spec maxima (65535, 15, 15, 15, 16). When validate_extended_bitstream_ is set, a non-conforming slice header now makes the parse fail instead of producing a header with out-of-range values. The new unit test uses a default-constructed H264Parser and expects kInvalidStream for each out-of-range value, which suggests the validation is active by default in that configuration.

Key insight
Fields decoded from attacker-controlled Exp-Golomb codes were trusted without the spec’s range limits, so values far beyond MaxPicNum or the 16-entry DPB could reach downstream decoders and drivers. The fix enforces the H.264 §7.4.3.1 and §7.4.3.3 bounds at parse time.

Attack Path

  1. Serve a crafted H.264 stream An attacker supplies H.264 content, for example through <video>, Media Source Extensions or WebCodecs VideoDecoder, containing a P slice whose header carries out-of-range ref_pic_list_modification or MMCO operands. The unit test builds exactly such headers.
  2. Parser accepts the header Before the fix, H264Parser::ParseSliceHeader (via the modification and marking sub-parsers) returned kOk with, for example, long_term_frame_idx = 16 or abs_diff_pic_num_minus1 = 65536.
  3. Values reach the accelerated decoder Chromium’s hardware decode path uses the parsed slice header and, per the commit message, passes these fields to platform drivers. This needs a hardware-accelerated H.264 decoder to be in use for the stream (inferred).
  4. Driver processes invalid indices A driver that assumes conforming ranges (for example using long_term_frame_idx to index a 16-entry DPB) may behave out of bounds. The patch does not show any specific sink (inferred).

Impact Assessment

Ordinary web content can feed a malformed H.264 stream to Chromium’s parser and, before the fix, have out-of-spec reference-management values passed on to the hardware decoding stack. The advisory calls this “Improper input validation in Media” and gives no in-the-wild status. The concrete consequence depends on the platform video driver: memory corruption or a crash in the GPU/media process or driver is plausible but not shown by the patch (inferred). The new checks only run when validate_extended_bitstream_ is true.

Changed Functions

FunctionChangeNotes
if
media/parsers/h264_parser.cc
modified
if
media/parsers/h264_parser_unittest.cc
modified
TEST
media/parsers/h264_parser_unittest.cc
modified

Files Changed

  • media/parsers/h264_parser.cc
  • media/parsers/h264_parser_unittest.cc

Audit Directions

  • Unbounded `READ_UE_OR_RETURN` / `READ_SE_OR_RETURN` fields
    Sweep h264_parser.cc, h265_parser.cc, h266_parser.cc and the VP9/AV1 header parsers for decoded values stored without a spec-range check, especially those used as array indices or DPB slot numbers downstream.
  • Validation gated on a flag
    Check where validate_extended_bitstream_ (and similar flags) are false, and whether any accelerator path (VA-API, V4L2, D3D11/D3D12, VideoToolbox, MediaCodec) relies on the parser for safety while using a parser with validation turned off.
  • Values copied into driver structures
    Audit the *Accelerator classes that copy slice-header fields into driver structures (for example VA or V4L2 slice parameters) for missing local clamps, since driver code is outside Chromium’s control and often assumes conforming input.
From ad64b2c8fdc096eb8ce2e555b70a7f44bee0e207 Mon Sep 17 00:00:00 2001
From: Dale Curtis <[email protected]>
Date: Tue, 21 Jul 2026 15:07:38 -0700
Subject: [PATCH] Fix a few different validation issues with h264 parsing

This adds range validation for a few different fields to avoid
blindly passing potentially bad data out to drivers.

Fixed: 536428615
Change-Id: I8d945cc477ddca47b57dc28df7f66beb4442b106
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8128120
Commit-Queue: Dale Curtis <[email protected]>
Reviewed-by: Eugene Zemtsov <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1665828}
---
diff --git a/media/parsers/h264_parser.cc b/media/parsers/h264_parser.cc
index 2dd3ffd0..8e2ce92 100644
--- a/media/parsers/h264_parser.cc
+++ b/media/parsers/h264_parser.cc
@@ -1200,10 +1200,18 @@
       case 0:
       case 1:
         READ_UE_OR_RETURN(&pic_num_mod->abs_diff_pic_num_minus1);
+        // H.264 Section 7.4.3.1: abs_diff_pic_num_minus1 shall be in
+        // [0, MaxPicNum - 1], MaxPicNum <= 65536.
+        IN_RANGE_IF_OR_RETURN(pic_num_mod->abs_diff_pic_num_minus1, 0, 65535,
+                              validate_extended_bitstream_);
         break;
 
       case 2:
         READ_UE_OR_RETURN(&pic_num_mod->long_term_pic_num);
+        // H.264 Section 7.4.3.1: long_term_pic_num in [0, MaxLongTermFrameIdx],
+        // MaxLongTermFrameIdx <= max_num_ref_frames - 1 <= 15.
+        IN_RANGE_IF_OR_RETURN(pic_num_mod->long_term_pic_num, 0, 15,
+                              validate_extended_bitstream_);
         break;
 
       case 3:
@@ -1345,18 +1353,40 @@
           break;
 
         if (marking->memory_mgmnt_control_operation == 1 ||
-            marking->memory_mgmnt_control_operation == 3)
+            marking->memory_mgmnt_control_operation == 3) {
           READ_UE_OR_RETURN(&marking->difference_of_pic_nums_minus1);
+          // H.264 Section 7.4.3.3: difference_of_pic_nums_minus1 shall be in
+          // [0, MaxPicNum - 1], MaxPicNum <= 65536.
+          IN_RANGE_IF_OR_RETURN(marking->difference_of_pic_nums_minus1, 0,
+                                65535, validate_extended_bitstream_);
+        }
 
-        if (marking->memory_mgmnt_control_operation == 2)
+        if (marking->memory_mgmnt_control_operation == 2) {
           READ_UE_OR_RETURN(&marking->long_term_pic_num);
+          // H.264 Section 7.4.3.3: long_term_pic_num in [0,
+          // MaxLongTermFrameIdx], MaxLongTermFrameIdx <= max_num_ref_frames - 1
+          // <= 15.
+          IN_RANGE_IF_OR_RETURN(marking->long_term_pic_num, 0, 15,
+                                validate_extended_bitstream_);
+        }
 
         if (marking->memory_mgmnt_control_operation == 3 ||
-            marking->memory_mgmnt_control_operation == 6)
+            marking->memory_mgmnt_control_operation == 6) {
           READ_UE_OR_RETURN(&marking->long_term_frame_idx);
+          // H.264 Section 7.4.3.3: long_term_frame_idx in [0,
+          // MaxLongTermFrameIdx], MaxLongTermFrameIdx <= max_num_ref_frames - 1
+          // <= 15.
+          IN_RANGE_IF_OR_RETURN(marking->long_term_frame_idx, 0, 15,
+                                validate_extended_bitstream_);
+        }
 
-        if (marking->memory_mgmnt_control_operation == 4)
+        if (marking->memory_mgmnt_control_operation == 4) {
           READ_UE_OR_RETURN(&marking->max_long_term_frame_idx_plus1);
+          // H.264 Section 7.4.3.3: max_long_term_frame_idx_plus1 in [0,
+          // max_num_ref_frames], max_num_ref_frames <= 16.
+          IN_RANGE_IF_OR_RETURN(marking->max_long_term_frame_idx_plus1, 0, 16,
+                                validate_extended_bitstream_);
+        }
 
         if (marking->memory_mgmnt_control_operation > 6)
           return kInvalidStream;
diff --git a/media/parsers/h264_parser_unittest.cc b/media/parsers/h264_parser_unittest.cc
index 59364e7..1f37f51be 100644
--- a/media/parsers/h264_parser_unittest.cc
+++ b/media/parsers/h264_parser_unittest.cc
@@ -536,6 +536,118 @@
 
     EXPECT_EQ(H264Parser::kInvalidStream, parser.ParseSEI(&sei));
   }
+
+  // SliceHeader: DecRefPicMarking and RefPicListModification range checks.
+  {
+    H264SPS sps;
+    sps.profile_idc = 66;  // Baseline
+    sps.level_idc = 10;
+    sps.log2_max_frame_num_minus4 = 0;
+    sps.pic_order_cnt_type = 0;
+    sps.log2_max_pic_order_cnt_lsb_minus4 = 0;
+    sps.max_num_ref_frames = 4;
+    sps.pic_width_in_mbs_minus1 = 1;
+    sps.pic_height_in_map_units_minus1 = 1;
+    sps.frame_mbs_only_flag = true;
+
+    H264PPS pps;
+    pps.pic_parameter_set_id = 0;
+    pps.seq_parameter_set_id = 0;
+
+    // Helper lambda to build a slice header with custom modification and
+    // marking ops.
+    auto build_slice = [&](uint32_t ref_mod_idc, uint32_t ref_mod_val,
+                           uint32_t mmco, uint32_t mmco_val1,
+                           uint32_t mmco_val2 = 0) {
+      H26xAnnexBBitstreamBuilder builder(
+          /*insert_emulation_prevention_bytes=*/true);
+      BuildPackedH264SPS(builder, sps);
+      BuildPackedH264PPS(builder, sps, pps);
+
+      builder.BeginNALU(H264NALU::kNonIDRSlice, 1);
+      builder.AppendUE(0);       // first_mb_in_slice
+      builder.AppendUE(0);       // slice_type (P slice)
+      builder.AppendUE(0);       // pic_parameter_set_id
+      builder.AppendBits(4, 0);  // frame_num
+      builder.AppendBits(4, 0);  // pic_order_cnt_lsb
+
+      builder.AppendBool(false);  // num_ref_idx_active_override_flag
+      if (ref_mod_idc != 3) {
+        builder.AppendBool(true);  // ref_pic_list_modification_flag_l0
+        builder.AppendUE(ref_mod_idc);
+        builder.AppendUE(ref_mod_val);
+        builder.AppendUE(3);  // end of modifications
+      } else {
+        builder.AppendBool(false);  // ref_pic_list_modification_flag_l0
+      }
+
+      if (mmco != 0) {
+        builder.AppendBool(true);  // adaptive_ref_pic_marking_mode_flag
+        builder.AppendUE(mmco);
+        if (mmco == 1 || mmco == 3) {
+          builder.AppendUE(mmco_val1);
+        }
+        if (mmco == 2) {
+          builder.AppendUE(mmco_val1);
+        }
+        if (mmco == 3 || mmco == 6) {
+          builder.AppendUE(mmco == 3 ? mmco_val2 : mmco_val1);
+        }
+        if (mmco == 4) {
+          builder.AppendUE(mmco_val1);
+        }
+        builder.AppendUE(0);  // end of MMCO
+      } else {
+        builder.AppendBool(false);  // adaptive_ref_pic_marking_mode_flag
+      }
+
+      builder.AppendSE(0);  // slice_qp_delta
+      builder.FinishNALU();
+      return std::vector<uint8_t>(builder.data().begin(), builder.data().end());
+    };
+
+    auto parse_slice = [&](const std::vector<uint8_t>& stream_data) {
+      H264Parser p;
+      p.SetStream(stream_data);
+      H264NALU n;
+      int sps_id_out, pps_id_out;
+      EXPECT_EQ(H264Parser::kOk, p.AdvanceToNextNALU(&n));  // SPS
+      EXPECT_EQ(H264Parser::kOk, p.ParseSPS(&sps_id_out));
+      EXPECT_EQ(H264Parser::kOk, p.AdvanceToNextNALU(&n));  // PPS
+      EXPECT_EQ(H264Parser::kOk, p.ParsePPS(&pps_id_out));
+      EXPECT_EQ(H264Parser::kOk, p.AdvanceToNextNALU(&n));  // Slice
+      H264SliceHeader sh;
+      return p.ParseSliceHeader(n, &sh);
+    };
+
+    // Valid slice header.
+    EXPECT_EQ(H264Parser::kOk, parse_slice(build_slice(3, 0, 0, 0)));
+
+    // MMCO 6: long_term_frame_idx = 16 (invalid, max 15).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(3, 0, 6, 16)));
+
+    // MMCO 2: long_term_pic_num = 16 (invalid, max 15).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(3, 0, 2, 16)));
+
+    // MMCO 1: difference_of_pic_nums_minus1 = 65536 (invalid, max 65535).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(3, 0, 1, 65536)));
+
+    // MMCO 4: max_long_term_frame_idx_plus1 = 17 (invalid, max 16).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(3, 0, 4, 17)));
+
+    // RefPicListModification 2: long_term_pic_num = 16 (invalid, max 15).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(2, 16, 0, 0)));
+
+    // RefPicListModification 0: abs_diff_pic_num_minus1 = 65536 (invalid, max
+    // 65535).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(0, 65536, 0, 0)));
+  }
 }
 
 TEST(H264ParserTest, SpsOverwritesInvalidatesPps) {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/parsers/h264_parser_unittest.cc b/media/parsers/h264_parser_unittest.cc
index 59364e7..1f37f51be 100644
--- a/media/parsers/h264_parser_unittest.cc
+++ b/media/parsers/h264_parser_unittest.cc
@@ -536,6 +536,118 @@
 
     EXPECT_EQ(H264Parser::kInvalidStream, parser.ParseSEI(&sei));
   }
+
+  // SliceHeader: DecRefPicMarking and RefPicListModification range checks.
+  {
+    H264SPS sps;
+    sps.profile_idc = 66;  // Baseline
+    sps.level_idc = 10;
+    sps.log2_max_frame_num_minus4 = 0;
+    sps.pic_order_cnt_type = 0;
+    sps.log2_max_pic_order_cnt_lsb_minus4 = 0;
+    sps.max_num_ref_frames = 4;
+    sps.pic_width_in_mbs_minus1 = 1;
+    sps.pic_height_in_map_units_minus1 = 1;
+    sps.frame_mbs_only_flag = true;
+
+    H264PPS pps;
+    pps.pic_parameter_set_id = 0;
+    pps.seq_parameter_set_id = 0;
+
+    // Helper lambda to build a slice header with custom modification and
+    // marking ops.
+    auto build_slice = [&](uint32_t ref_mod_idc, uint32_t ref_mod_val,
+                           uint32_t mmco, uint32_t mmco_val1,
+                           uint32_t mmco_val2 = 0) {
+      H26xAnnexBBitstreamBuilder builder(
+          /*insert_emulation_prevention_bytes=*/true);
+      BuildPackedH264SPS(builder, sps);
+      BuildPackedH264PPS(builder, sps, pps);
+
+      builder.BeginNALU(H264NALU::kNonIDRSlice, 1);
+      builder.AppendUE(0);       // first_mb_in_slice
+      builder.AppendUE(0);       // slice_type (P slice)
+      builder.AppendUE(0);       // pic_parameter_set_id
+      builder.AppendBits(4, 0);  // frame_num
+      builder.AppendBits(4, 0);  // pic_order_cnt_lsb
+
+      builder.AppendBool(false);  // num_ref_idx_active_override_flag
+      if (ref_mod_idc != 3) {
+        builder.AppendBool(true);  // ref_pic_list_modification_flag_l0
+        builder.AppendUE(ref_mod_idc);
+        builder.AppendUE(ref_mod_val);
+        builder.AppendUE(3);  // end of modifications
+      } else {
+        builder.AppendBool(false);  // ref_pic_list_modification_flag_l0
+      }
+
+      if (mmco != 0) {
+        builder.AppendBool(true);  // adaptive_ref_pic_marking_mode_flag
+        builder.AppendUE(mmco);
+        if (mmco == 1 || mmco == 3) {
+          builder.AppendUE(mmco_val1);
+        }
+        if (mmco == 2) {
+          builder.AppendUE(mmco_val1);
+        }
+        if (mmco == 3 || mmco == 6) {
+          builder.AppendUE(mmco == 3 ? mmco_val2 : mmco_val1);
+        }
+        if (mmco == 4) {
+          builder.AppendUE(mmco_val1);
+        }
+        builder.AppendUE(0);  // end of MMCO
+      } else {
+        builder.AppendBool(false);  // adaptive_ref_pic_marking_mode_flag
+      }
+
+      builder.AppendSE(0);  // slice_qp_delta
+      builder.FinishNALU();
+      return std::vector<uint8_t>(builder.data().begin(), builder.data().end());
+    };
+
+    auto parse_slice = [&](const std::vector<uint8_t>& stream_data) {
+      H264Parser p;
+      p.SetStream(stream_data);
+      H264NALU n;
+      int sps_id_out, pps_id_out;
+      EXPECT_EQ(H264Parser::kOk, p.AdvanceToNextNALU(&n));  // SPS
+      EXPECT_EQ(H264Parser::kOk, p.ParseSPS(&sps_id_out));
+      EXPECT_EQ(H264Parser::kOk, p.AdvanceToNextNALU(&n));  // PPS
+      EXPECT_EQ(H264Parser::kOk, p.ParsePPS(&pps_id_out));
+      EXPECT_EQ(H264Parser::kOk, p.AdvanceToNextNALU(&n));  // Slice
+      H264SliceHeader sh;
+      return p.ParseSliceHeader(n, &sh);
+    };
+
+    // Valid slice header.
+    EXPECT_EQ(H264Parser::kOk, parse_slice(build_slice(3, 0, 0, 0)));
+
+    // MMCO 6: long_term_frame_idx = 16 (invalid, max 15).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(3, 0, 6, 16)));
+
+    // MMCO 2: long_term_pic_num = 16 (invalid, max 15).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(3, 0, 2, 16)));
+
+    // MMCO 1: difference_of_pic_nums_minus1 = 65536 (invalid, max 65535).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(3, 0, 1, 65536)));
+
+    // MMCO 4: max_long_term_frame_idx_plus1 = 17 (invalid, max 16).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(3, 0, 4, 17)));
+
+    // RefPicListModification 2: long_term_pic_num = 16 (invalid, max 15).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(2, 16, 0, 0)));
+
+    // RefPicListModification 0: abs_diff_pic_num_minus1 = 65536 (invalid, max
+    // 65535).
+    EXPECT_EQ(H264Parser::kInvalidStream,
+              parse_slice(build_slice(0, 65536, 0, 0)));
+  }
 }
 
 TEST(H264ParserTest, SpsOverwritesInvalidatesPps) {
Loading diff…

Original Bug Report

reported by [email protected]

Potential missing bounds check for H.264 long_term_frame_idx leads to driver OOB

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: The H.264 bitstream parser lacks a bounds check for the long_term_frame_idx field when parsing memory management control operations. This allows an attacker-controlled, unvalidated large integer to propagate directly into bounds-bearing hardware-decode driver structures. Processing these structures can result in potential out-of-bounds memory accesses in the sandboxed GPU process or kernel-level drivers.

Affected files:

  • media/parsers/h264_parser.cc
  • media/gpu/h264_decoder.cc
  • media/gpu/windows/d3d11_h264_accelerator.cc
  • media/gpu/v4l2/v4l2_video_decoder_delegate_h264.cc

Estimated timestamp from git blame: 2012-05-14

1. Summary of the Issue (Meant for Human Triage)

A potential high-severity security vulnerability has been identified in Chromium’s H.264 bitstream parser and hardware accelerator decoder delegates. An unvalidated bitstream field is propagated directly to hardware-accelerated video decoding driver APIs without bounds checking.

Specifically, the H.264 slice-header field dec_ref_pic_marking().long_term_frame_idx (per ITU-T H.264 §7.4.3.3) is parsed using READ_UE_OR_RETURN without any subsequent range-bounding check (such as IN_RANGE_OR_RETURN). Under the H.264 specification, this field must be bounded within [0, MaxLongTermFrameIdx], where MaxLongTermFrameIdx is at most 15. However, the parser accepts values up to 0x7FFFFFFF.

This unvalidated value is stored verbatim in the reference H264Picture objects inside the Decoded Picture Buffer (DPB). During frame submission, the accelerator delegates read this value and pass it directly to the OS-level hardware video decoding APIs:

  • On Windows (D3D11 / DXVA): It is narrow-cast to a USHORT (allowing control up to 0xFFFF) and copied into the DXVA_PicParams_H264.FrameNumList array, which is then submitted via ID3D11VideoContext::SubmitDecoderBuffers.
  • On ChromeOS (V4L2): The full, un-narrowed 31-bit value is stored as a __u32 inside v4l2_h264_dpb_entry.pic_num and sent to stateless hardware video decoding kernel drivers via ioctl(VIDIOC_S_EXT_CTRLS, V4L2_CID_STATELESS_H264_DECODE_PARAMS).

Because the downstream proprietary GPU and kernel drivers rely on these fields for indexing internally managed reference picture tables, passing unchecked, out-of-bounds indices from sandboxed user-space processes exposes a severe risk of driver-level or kernel-level memory corruption.

Note: The steps provided below are potential suggested steps; our tooling agent does not yet have the ability to run code or execute a live proof of concept.

2. Proof-of-Concept & Detailed Execution Flow

Below is the suggested step-by-step trace of how an attacker could potentially trigger this vulnerability, detailing the exact data flow from untrusted input to the bounds-bearing driver sink:

  1. An attacker crafts a malicious Annex-B H.264 video bitstream containing an unvalidated long_term_frame_idx value (up to 0x7FFFFFFF).
  2. The attacker delivers this video to a victim using an HTML <video> tag or the WebCodecs API (VideoDecoder.decode()).
  3. The Chromium browser or renderer process routes this bitstream to the H264Parser.
  4. In media/parsers/h264_parser.cc, the parser parses a memory management control operation (MMCO) via ParseDecRefPicMarking.
  5. If the MMCO is 3 or 6, READ_UE_OR_RETURN(&marking->long_term_frame_idx) is called (line 1356).
  6. The READ_UE_OR_RETURN macro processes an unsigned exponential-Golomb value. It limits the number of leading zeros to 31, allowing values up to 0x7FFFFFFF (media/parsers/bit_reader_macros.h).
  7. Unlike other H.264 parser fields (e.g., first_mb_in_slice at line 1433), there is no IN_RANGE_OR_RETURN macro bounding long_term_frame_idx to the specification-mandated range [0, MaxLongTermFrameIdx].
  8. This parsed slice header is passed to H264Decoder::HandleMemoryManagementOps in media/gpu/h264_decoder.cc.
  9. If MMCO is 3 (line 895) or 6 (line 943), the unvalidated ref_pic_marking->long_term_frame_idx is copied verbatim directly to the H264Picture::long_term_frame_idx field (e.g., pic->long_term_frame_idx = ref_pic_marking->long_term_frame_idx).
  10. The H264Picture object containing this tainted value resides in the Decoded Picture Buffer (dpb_).
  11. Before submitting the frame metadata to the accelerator, H264Decoder::UpdatePicNums (line 423) is called.
  12. At line 431, pic->long_term_pic_num is assigned the tainted value from pic->long_term_frame_idx.
  13. The decoder then calls SubmitFrameMetadata on the active hardware accelerator delegate.
  14. On ChromeOS (V4L2): The function V4L2VideoDecoderDelegateH264::SubmitFrameMetadata translates the DPB to the V4L2 representation (H264DPBToV4L2DPB).
  15. In media/gpu/v4l2/v4l2_video_decoder_delegate_h264.cc (lines 204-205), entry.frame_num = pic->long_term_pic_num; (narrowed to __u16) and entry.pic_num = pic->long_term_frame_idx; (preserved as a full 0x7FFFFFFF 32-bit __u32 value) are copied into the v4l2_h264_dpb_entry struct.
  16. At line 611, the priv_->v4l2_decode_param structure containing the tainted DPB is submitted to the kernel driver via ioctl(VIDIOC_S_EXT_CTRLS, V4L2_CID_STATELESS_H264_DECODE_PARAMS).
  17. The V4L2_CID_STATELESS_H264_DECODE_PARAMS bounds-bearing structure uses dpb[i].pic_num and frame_num for driver indexing, resulting in an out-of-bounds access within the kernel-space V4L2 driver.
  18. On Windows (D3D11/DXVA): The function D3D11H264Accelerator::SubmitFrameMetadata maps the DPB into the DXVA structure.
  19. In media/gpu/windows/d3d11_h264_accelerator.cc (lines 122-125), frame_num_list_[i] is set to our_ref_pic->long_term_pic_num, casting the large integer to a USHORT (up to 0xFFFF).
  20. At lines 291-292, frame_num_list_ is copied into DXVA_PicParams_H264.FrameNumList via memcpy.
  21. At line 306, params_buffer.Commit() pushes the D3D11 buffer description containing the tainted FrameNumList onto the video_buffers_ queue (ScopedD3D11DecoderBuffer::Commit).
  22. Later, when SubmitSlice is executed, the D3D11VideoDecoderWrapperImpl::SubmitDecoderBuffers() API is called (media/gpu/windows/d3d11_video_decoder_wrapper.cc:188-218).
  23. The video_context_->SubmitDecoderBuffers(...) call passes the tainted structure to the D3D11 hardware driver in the GPU process.
  24. The D3D11 hardware driver uses the out-of-bounds FrameNumList[i] value, leading to out-of-bounds memory access within the driver/GPU process.

Suggested Fix

Apply an IN_RANGE_OR_RETURN check immediately after parsing the field in media/parsers/h264_parser.cc (similar to the fix applied in b/500028989 for first_mb_in_slice). The parser should ensure the parsed long_term_frame_idx strictly bounded.

        if (marking->memory_mgmnt_control_operation == 3 ||
            marking->memory_mgmnt_control_operation == 6) {
          READ_UE_OR_RETURN(&marking->long_term_frame_idx);
          // Add appropriate bounds check based on max_long_term_frame_idx_plus1
          // or absolute H.264 specification limits (e.g., 0 to 15).
          // IN_RANGE_OR_RETURN(marking->long_term_frame_idx, 0, ...);
        }

3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)

> Critic model verdict (2026-07-18 05:02:51): > * Severity: High (S1) > * Brief Notes / Reasoning: > The report accurately identifies a missing bounds check in H.264 bitstream parsing that leads to unvalidated data reaching hardware video decode drivers. >
> 1. Root Cause: In media/parsers/h264_parser.cc, the dec_ref_pic_marking().long_term_frame_idx field is read via READ_UE_OR_RETURN but lacks an IN_RANGE_OR_RETURN check, allowing an attacker to supply values up to 0x7FFFFFFF. The H.264 spec max is 15. > 2. Trigger Path: H264Decoder::HandleMemoryManagementOps (media/gpu/h264_decoder.cc) copies this unvalidated value directly to H264Picture::long_term_frame_idx. UpdatePicNums() assigns it to long_term_pic_num. When SubmitFrameMetadata() is called, accelerator delegates read this tainted DPB state. In D3D11H264Accelerator, the value is cast to USHORT (giving control over the lower 16 bits) and copied into DXVA_PicParams_H264.FrameNumList. In V4L2VideoDecoderDelegateH264, it is placed into v4l2_h264_dpb_entry.pic_num as a __u32. Both are submitted directly to the OS/kernel drivers. > 3. Severity Justification (S1 / High): The knowledge base explicitly defines this exact pattern as High (S1): ‘Bitstream field passed unvalidated to a HW-decode driver struct: a media/parsers/h26x_parser.cc field read via READ_UE_OR_RETURN with no IN_RANGE_OR_RETURN, copied verbatim by an accelerator delegate… into DXVA_PicParams_* / V4L2_CID_*’. The KB also explicitly notes: ‘Do NOT reject for no ASAN report — the corruption is driver-side. PoC = the bad value at the delegate copy step.’ > 4. Ceilings/Modifiers: The GPU process is sandboxed on Windows and ChromeOS. Android uses MediaCodecVideoDecoder and does not instantiate H264Decoder, so it is not unsandboxed (S0). Therefore, this is memory corruption in a sandboxed process, confirming the High (S1) rating despite the report’s self-rating of Medium.

Prior Automated Validation Assertions:

  • Parser (no semantic bound):

    // media/parsers/h264_parser.cc:1354-1356
    if (marking->memory_mgmnt_control_operation == 3 ||
        marking->memory_mgmnt_control_operation == 6)
      READ_UE_OR_RETURN(&marking->long_term_frame_idx);
    

    READ_UE_OR_RETURN accepts up to _num_bits_processed == 31 → *out = (1u<<31)-1 = 0x7FFFFFFF. Per ITU-T H.264 §7.4.3.3 / §8.2.5.4, long_term_frame_idx must be bounded.

  • Decoder (verbatim copy, no re-check):

    // media/gpu/h264_decoder.cc:925-943  (MMCO=6)
    case 6: {
      ...
      pic->ref = true;
      pic->long_term = true;
      pic->long_term_frame_idx = ref_pic_marking->long_term_frame_idx;  // :942 — no bound
      break;
    }
    
  • D3D11 sink (§M4.8 verbatim copy):

    // media/gpu/windows/d3d11_h264_accelerator.cc:117-125
    ref_frame_list_[i].AssociatedFlag = our_ref_pic->long_term;         // = 1
    ...
    frame_num_list_[i] =
        ref_frame_list_[i].AssociatedFlag
            ? our_ref_pic->long_term_pic_num                             // 0x7FFFFFFF → USHORT narrow → 0xFFFF
            : our_ref_pic->frame_num;
    

    frame_num_list_ is USHORT[16].

  • V4L2 sink (no narrowing, full 31-bit value):

    // media/gpu/v4l2/v4l2_video_decoder_delegate_h264.cc:200-205
    struct v4l2_h264_dpb_entry& entry = priv_->v4l2_decode_param.dpb[i++];
    ...
    if (pic->long_term) {
      entry.frame_num = pic->long_term_pic_num;   // __u16 ← 0x7FFFFFFF (narrows)
      entry.pic_num   = pic->long_term_frame_idx; // __u32 ← 0x7FFFFFFF (NO narrowing)
    }
    

Evaluated with Chrome root at commit: b96d2ec58f4f5f92b540a723966b199d6e9951b4


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker