Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Media
DescriptionImproper input validation in Media
ComponentMedia
Bug ClassLogic Error
Tracker536428615
Fix commitad64b2c8fdc0 (chromium/src) +146/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

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

Files Changed

  • media/parsers/h264_parser.cc
  • media/parsers/h264_parser_unittest.cc
From ad64b2c8fdc096eb8ce2e555b70a7f44bee0e207 Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
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 <dalecurtis@chromium.org>
Reviewed-by: Eugene Zemtsov <eugene@chromium.org>
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
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 aw...@chromium.org

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