Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Codecs
DescriptionInsufficient validation of untrusted input in Codecs
ComponentCodecs
Bug ClassLogic Error
Tracker511713779
Fix commite1c30db0cf70 (chromium/src) +169/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
media/gpu/BUILD.gn
modified
component
media/gpu/BUILD.gn
modified
if
media/parsers/h264_parser.cc
modified
erase_if
media/parsers/h264_parser.cc
modified
TEST
media/parsers/h264_parser_unittest.cc
modified
if
media/parsers/h265_parser.cc
modified
EraseIf
media/parsers/h265_parser.cc
modified

Files Changed

  • media/gpu/BUILD.gn
  • media/parsers/BUILD.gn
  • media/parsers/h264_parser.cc
  • media/parsers/h264_parser_unittest.cc
  • media/parsers/h265_parser.cc
From e1c30db0cf70257ba3a14b98a40195d2cbd8d571 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <[email protected]>
Date: Fri, 15 May 2026 12:45:33 -0700
Subject: [PATCH] media: Invalidate dependent PPS when SPS is overwritten

In the H.264 and H.265 parsers, overwriting an existing SPS left
dependent PPS active. This could lead to an inconsistent state where
a PPS bypasses validation checks against the new SPS geometry,
potentially causing out-of-bounds math in downstream hardware delegates.

This change fixes the issue by erasing dependent PPSes when their parent
SPS is overwritten, safely failing the decoder on subsequent slices
until a new, valid PPS is provided.

Bug: 511713779
Change-Id: I5f6e82af1f013c57fb70de638780d0058ca507a2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7846848
Auto-Submit: Eugene Zemtsov <[email protected]>
Reviewed-by: Dale Curtis <[email protected]>
Commit-Queue: Eugene Zemtsov <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1631472}
---

diff --git a/media/gpu/BUILD.gn b/media/gpu/BUILD.gn
index a6a1e975e..281a03d 100644
--- a/media/gpu/BUILD.gn
+++ b/media/gpu/BUILD.gn
@@ -53,6 +53,7 @@
     "//media/gpu/test/*",
     "//media/gpu/vaapi/*",
     "//media/mojo/*",
+    "//media/parsers:unit_tests",
     "//remoting/codec:encoder",
     "//third_party/blink/renderer/modules:unit_tests",
     "//third_party/blink/renderer/modules/mediarecorder",
@@ -448,8 +449,8 @@
 }
 
 if (is_linux || is_chromeos) {
-  # The buffer validation functionality is in its own component so that it can be
-  # depended on without pulling the entire //media/gpu target.
+  # The buffer validation functionality is in its own component so that it can
+  # be depended on without pulling the entire //media/gpu target.
   component("buffer_validation") {
     defines = [ "IS_MEDIA_GPU_BUFFER_VALIDATION_IMPL" ]
     sources = [
diff --git a/media/parsers/BUILD.gn b/media/parsers/BUILD.gn
index 2ab86a9..f8c3376 100644
--- a/media/parsers/BUILD.gn
+++ b/media/parsers/BUILD.gn
@@ -11,7 +11,8 @@
   sources = [ "parse_jpeg.rs" ]
   cxx_bindings = [ "parse_jpeg.rs" ]
 
-  # Required by Chromium policy because cxx generates unsafe code under the hood.
+  # Required by Chromium policy because cxx generates unsafe code under the
+  # hood.
   allow_unsafe = true
 }
 
@@ -109,6 +110,7 @@
   deps = [
     "//base/test:test_support",
     "//media:test_support",
+    "//media/gpu",
     "//testing/gtest",
     "//third_party/libgav1:libgav1_parser",
   ]
diff --git a/media/parsers/h264_parser.cc b/media/parsers/h264_parser.cc
index 583c75f..76efff2 100644
--- a/media/parsers/h264_parser.cc
+++ b/media/parsers/h264_parser.cc
@@ -1078,6 +1078,18 @@
 
   // If an SPS with the same id already exists, replace it.
   *sps_id = sps->seq_parameter_set_id;
+
+  if (validate_extended_bitstream_) {
+    auto it = active_SPSes_.find(*sps_id);
+    if (it == active_SPSes_.end() || *(it->second) != *sps) {
+      // Invalidate dependent PPSes since their validations against the old SPS
+      // are no longer guaranteed to hold under the new SPS.
+      std::erase_if(active_PPSes_, [id = *sps_id](const auto& pair) {
+        return pair.second->seq_parameter_set_id == id;
+      });
+    }
+  }
+
   active_SPSes_[*sps_id] = std::move(sps);
 
   return kOk;
diff --git a/media/parsers/h264_parser_unittest.cc b/media/parsers/h264_parser_unittest.cc
index a8953cf0..acb125c 100644
--- a/media/parsers/h264_parser_unittest.cc
+++ b/media/parsers/h264_parser_unittest.cc
@@ -17,6 +17,8 @@
 #include "base/strings/string_number_conversions.h"
 #include "media/base/subsample_entry.h"
 #include "media/base/test_data_util.h"
+#include "media/filters/h26x_annex_b_bitstream_builder.h"
+#include "media/gpu/h264_builder.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/functional/overload.h"
 #include "ui/gfx/geometry/rect.h"
@@ -489,4 +491,71 @@
   }
 }
 
+TEST(H264ParserTest, SpsOverwritesInvalidatesPps) {
+  H264SPS sps;
+  sps.profile_idc = 100;
+  sps.level_idc = 13;
+  sps.chroma_format_idc = 1;
+  sps.log2_max_frame_num_minus4 = 5;
+  sps.log2_max_pic_order_cnt_lsb_minus4 = 6;
+  sps.max_num_ref_frames = 4;
+  sps.pic_width_in_mbs_minus1 = 19;
+  sps.pic_height_in_map_units_minus1 = 11;
+
+  H264PPS pps;
+  pps.entropy_coding_mode_flag = true;
+  pps.weighted_bipred_idc = 2;
+  pps.chroma_qp_index_offset = -2;
+  pps.deblocking_filter_control_present_flag = true;
+  pps.transform_8x8_mode_flag = true;
+  pps.second_chroma_qp_index_offset = -2;
+
+  H26xAnnexBBitstreamBuilder bitstream_builder(
+      /*insert_emulation_prevention_bytes=*/true);
+  BuildPackedH264SPS(bitstream_builder, sps);
+  BuildPackedH264PPS(bitstream_builder, sps, pps);
+
+  // Re-append the SPS to simulate an overwrite.
+  BuildPackedH264SPS(bitstream_builder, sps);
+
+  // Now change the SPS to simulate an overwrite with different parameters.
+  sps.pic_width_in_mbs_minus1 = 20;
+  BuildPackedH264SPS(bitstream_builder, sps);
+
+  H264Parser parser;
+  parser.SetStream(bitstream_builder.data());
+
+  H264NALU nalu;
+  EXPECT_EQ(parser.AdvanceToNextNALU(&nalu), H264Parser::Result::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H264NALU::kSPS);
+  int sps_id;
+  EXPECT_EQ(parser.ParseSPS(&sps_id), H264Parser::Result::kOk);
+
+  EXPECT_EQ(parser.AdvanceToNextNALU(&nalu), H264Parser::Result::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H264NALU::kPPS);
+  int pps_id;
+  EXPECT_EQ(parser.ParsePPS(&pps_id), H264Parser::Result::kOk);
+
+  EXPECT_NE(parser.GetPPS(pps_id), nullptr);
+
+  // Parse the second SPS (identical to the first).
+  EXPECT_EQ(parser.AdvanceToNextNALU(&nalu), H264Parser::Result::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H264NALU::kSPS);
+  int new_sps_id;
+  EXPECT_EQ(parser.ParseSPS(&new_sps_id), H264Parser::Result::kOk);
+  EXPECT_EQ(new_sps_id, sps_id);
+
+  // The PPS should NOT be invalidated because the SPS hasn't changed.
+  EXPECT_NE(parser.GetPPS(pps_id), nullptr);
+
+  // Parse the third SPS (different from the first).
+  EXPECT_EQ(parser.AdvanceToNextNALU(&nalu), H264Parser::Result::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H264NALU::kSPS);
+  EXPECT_EQ(parser.ParseSPS(&new_sps_id), H264Parser::Result::kOk);
+  EXPECT_EQ(new_sps_id, sps_id);
+
+  // The PPS should be invalidated because the SPS has changed.
+  EXPECT_EQ(parser.GetPPS(pps_id), nullptr);
+}
+
 }  // namespace media
diff --git a/media/parsers/h265_parser.cc b/media/parsers/h265_parser.cc
index 2b41700..5ac981c 100644
--- a/media/parsers/h265_parser.cc
+++ b/media/parsers/h265_parser.cc
@@ -883,6 +883,18 @@
 
   // If an SPS with the same id already exists, replace it.
   *sps_id = sps->sps_seq_parameter_set_id;
+
+  if (validate_extended_bitstream_) {
+    auto it = active_sps_.find(*sps_id);
+    if (it == active_sps_.end() || *(it->second) != *sps) {
+      // Invalidate dependent PPSes since their validations against the old SPS
+      // are no longer guaranteed to hold under the new SPS.
+      base::EraseIf(active_pps_, [id = *sps_id](const auto& pair) {
+        return pair.second->pps_seq_parameter_set_id == id;
+      });
+    }
+  }
+
   active_sps_[*sps_id] = std::move(sps);
 
   return res;
diff --git a/media/parsers/h265_parser.h b/media/parsers/h265_parser.h
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 a8953cf0..acb125c 100644
--- a/media/parsers/h264_parser_unittest.cc
+++ b/media/parsers/h264_parser_unittest.cc
@@ -17,6 +17,8 @@
 #include "base/strings/string_number_conversions.h"
 #include "media/base/subsample_entry.h"
 #include "media/base/test_data_util.h"
+#include "media/filters/h26x_annex_b_bitstream_builder.h"
+#include "media/gpu/h264_builder.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/functional/overload.h"
 #include "ui/gfx/geometry/rect.h"
@@ -489,4 +491,71 @@
   }
 }
 
+TEST(H264ParserTest, SpsOverwritesInvalidatesPps) {
+  H264SPS sps;
+  sps.profile_idc = 100;
+  sps.level_idc = 13;
+  sps.chroma_format_idc = 1;
+  sps.log2_max_frame_num_minus4 = 5;
+  sps.log2_max_pic_order_cnt_lsb_minus4 = 6;
+  sps.max_num_ref_frames = 4;
+  sps.pic_width_in_mbs_minus1 = 19;
+  sps.pic_height_in_map_units_minus1 = 11;
+
+  H264PPS pps;
+  pps.entropy_coding_mode_flag = true;
+  pps.weighted_bipred_idc = 2;
+  pps.chroma_qp_index_offset = -2;
+  pps.deblocking_filter_control_present_flag = true;
+  pps.transform_8x8_mode_flag = true;
+  pps.second_chroma_qp_index_offset = -2;
+
+  H26xAnnexBBitstreamBuilder bitstream_builder(
+      /*insert_emulation_prevention_bytes=*/true);
+  BuildPackedH264SPS(bitstream_builder, sps);
+  BuildPackedH264PPS(bitstream_builder, sps, pps);
+
+  // Re-append the SPS to simulate an overwrite.
+  BuildPackedH264SPS(bitstream_builder, sps);
+
+  // Now change the SPS to simulate an overwrite with different parameters.
+  sps.pic_width_in_mbs_minus1 = 20;
+  BuildPackedH264SPS(bitstream_builder, sps);
+
+  H264Parser parser;
+  parser.SetStream(bitstream_builder.data());
+
+  H264NALU nalu;
+  EXPECT_EQ(parser.AdvanceToNextNALU(&nalu), H264Parser::Result::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H264NALU::kSPS);
+  int sps_id;
+  EXPECT_EQ(parser.ParseSPS(&sps_id), H264Parser::Result::kOk);
+
+  EXPECT_EQ(parser.AdvanceToNextNALU(&nalu), H264Parser::Result::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H264NALU::kPPS);
+  int pps_id;
+  EXPECT_EQ(parser.ParsePPS(&pps_id), H264Parser::Result::kOk);
+
+  EXPECT_NE(parser.GetPPS(pps_id), nullptr);
+
+  // Parse the second SPS (identical to the first).
+  EXPECT_EQ(parser.AdvanceToNextNALU(&nalu), H264Parser::Result::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H264NALU::kSPS);
+  int new_sps_id;
+  EXPECT_EQ(parser.ParseSPS(&new_sps_id), H264Parser::Result::kOk);
+  EXPECT_EQ(new_sps_id, sps_id);
+
+  // The PPS should NOT be invalidated because the SPS hasn't changed.
+  EXPECT_NE(parser.GetPPS(pps_id), nullptr);
+
+  // Parse the third SPS (different from the first).
+  EXPECT_EQ(parser.AdvanceToNextNALU(&nalu), H264Parser::Result::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H264NALU::kSPS);
+  EXPECT_EQ(parser.ParseSPS(&new_sps_id), H264Parser::Result::kOk);
+  EXPECT_EQ(new_sps_id, sps_id);
+
+  // The PPS should be invalidated because the SPS has changed.
+  EXPECT_EQ(parser.GetPPS(pps_id), nullptr);
+}
+
 }  // namespace media
diff --git a/media/parsers/h265_parser_unittest.cc b/media/parsers/h265_parser_unittest.cc
index 62e0060..4e2efb5c 100644
--- a/media/parsers/h265_parser_unittest.cc
+++ b/media/parsers/h265_parser_unittest.cc
@@ -13,6 +13,8 @@
 #include "base/files/memory_mapped_file.h"
 #include "base/logging.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/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/functional/overload.h"
 
@@ -634,4 +636,64 @@
   EXPECT_NE(H265Parser::kOk, parser.ParseVPS(&unused_vps_id));
 }
 
+TEST_F(H265ParserTest, SpsOverwritesInvalidatesPps) {
+  H265SPS sps = {};
+  sps.profile_tier_level.general_profile_idc = 1;
+  sps.pic_width_in_luma_samples = 1280;
+  sps.pic_height_in_luma_samples = 720;
+  sps.log2_max_pic_order_cnt_lsb_minus4 = 4;
+  sps.log2_diff_max_min_luma_coding_block_size = 3;
+  sps.log2_diff_max_min_luma_transform_block_size = 3;
+
+  H265PPS pps = {};
+  pps.pps_pic_parameter_set_id = 0;
+  pps.pps_seq_parameter_set_id = 0;
+
+  H26xAnnexBBitstreamBuilder builder;
+  BuildPackedH265SPS(builder, sps);
+  BuildPackedH265PPS(builder, pps);
+
+  // Re-append the SPS to simulate an overwrite.
+  BuildPackedH265SPS(builder, sps);
+
+  // Now change the SPS to simulate an overwrite with different parameters.
+  sps.pic_width_in_luma_samples = 1920;
+  BuildPackedH265SPS(builder, sps);
+  builder.Flush();
+
+  parser_.SetStream(builder.data());
+
+  H265NALU nalu;
+  EXPECT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H265NALU::SPS_NUT);
+  int sps_id;
+  EXPECT_EQ(parser_.ParseSPS(&sps_id), H265Parser::kOk);
+
+  EXPECT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H265NALU::PPS_NUT);
+  int pps_id;
+  EXPECT_EQ(parser_.ParsePPS(nalu, &pps_id), H265Parser::kOk);
+
+  EXPECT_NE(parser_.GetPPS(pps_id), nullptr);
+
+  // Parse the second SPS (identical to the first).
+  EXPECT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H265NALU::SPS_NUT);
+  int new_sps_id;
+  EXPECT_EQ(parser_.ParseSPS(&new_sps_id), H265Parser::kOk);
+  EXPECT_EQ(new_sps_id, sps_id);
+
+  // The PPS should NOT be invalidated because the SPS hasn't changed.
+  EXPECT_NE(parser_.GetPPS(pps_id), nullptr);
+
+  // Parse the third SPS (different from the first).
+  EXPECT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+  EXPECT_EQ(nalu.nal_unit_type, H265NALU::SPS_NUT);
+  EXPECT_EQ(parser_.ParseSPS(&new_sps_id), H265Parser::kOk);
+  EXPECT_EQ(new_sps_id, sps_id);
+
+  // The PPS should be invalidated because the SPS has changed.
+  EXPECT_EQ(parser_.GetPPS(pps_id), nullptr);
+}
+
 }  // namespace media
Loading diff…

Original Bug Report

reported by [email protected]

Potential out-of-bounds in GPU drivers/kernel due to H265 SPS replacement bypassing PPS validation

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A vulnerability in the H.265 parser allows an attacker to bypass Picture Parameter Set (PPS) validation by replacing an active Sequence Parameter Set (SPS) with a new one that alters Coding Tree Block (CTB) geometry. Because the decoder fails to detect this specific configuration change, an inconsistent SPS/PPS pair is sent to hardware delegates. This results in integer underflows in VAAPI (sending malformed data to drivers) and passes invalid invariants directly to the Linux kernel in V4L2, representing a potential GPU-to-kernel sandbox escape.

Affected files:

  • media/parsers/h265_parser.cc
  • media/gpu/h265_decoder.cc
  • media/gpu/vaapi/h265_vaapi_video_decoder_delegate.cc
  • media/gpu/windows/d3d11_h265_accelerator.cc
  • media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc

Estimated timestamp from git blame: 2026-03-25

Root Cause

In Chromium’s H.265 implementation, H265Parser::ParsePPS() cross-validates several fields against the Sequence Parameter Set (SPS) currently active at the time of parsing. For example, it enforces that pps->num_tile_columns_minus1 is less than sps->pic_width_in_ctbs_y (media/parsers/h265_parser.cc:947).

However, H265Parser::ParseSPS() allows a new SPS with the same sps_seq_parameter_set_id to overwrite an existing one without invalidating or re-validating any Picture Parameter Sets (PPS) that already reference that ID (media/parsers/h265_parser.cc:886).

If an attacker provides an initial SPS/PPS pair that is valid, they can subsequently replace the SPS with a new one that has identical overall picture dimensions (luma samples) but a completely different CTB geometry (e.g., a much larger CTB size, resulting in a tiny pic_width_in_ctbs_y). This renders the existing PPS invalid under the new SPS, effectively bypassing the parser’s initial boundary checks.

Missing Re-validation in H265Decoder

When processing a subsequent slice, H265Decoder::ProcessPPS() checks for configuration changes to decide if the decoder needs to be flushed. It calculates is_config_change by comparing pic_size_, profile_, bit_depth_, chroma_sampling_, and max_dpb_size.

Because the attacker carefully kept the overall pic_size_ identical, is_config_change evaluates to false. Consequently, the decoder does not trigger a flush or re-configuration. H265Decoder::StartNewFrame() then proceeds to ship the inconsistent, unvalidated SPS/PPS pair to the hardware accelerator delegate.

Impact on GPU Drivers and Kernel

This validation bypass results in malformed data being passed to platform-specific video decoding delegates:

  1. VAAPI (media/gpu/vaapi/h265_vaapi_video_decoder_delegate.cc): If pps->uniform_spacing_flag is true, the delegate calculates column_width_minus1 (lines 157-164). Because sps->pic_width_in_ctbs_y is now smaller than pps->num_tile_columns_minus1 + 1, the integer division evaluates to 0. The subsequent subtraction (0 - 0 - 1) results in -1. This -1 wraps around to 0xFFFF when stored in the uint16_t array pic_param.column_width_minus1. Submitting a column width of 65535 CTBs to the vendor-supplied User Mode Driver (UMD) will likely cause immediate out-of-bounds memory accesses in the driver.
  2. V4L2 stateless (media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc): On ChromeOS ARM devices, inconsistent SPS and PPS control structures (where num_tile_columns_minus1 >= sps->pic_width_in_ctbs_y) are copied into v4l2_ctrl_hevc_pps and sent directly to the Linux kernel via the VIDIOC_S_EXT_CTRLS ioctl. The kernel’s V4L2 stateless H.265 drivers rely entirely on the userspace client (Chrome) to enforce spec invariants. Providing contradictory structures will cause the kernel to read/write out-of-bounds when computing physical tile memory offsets, representing a potential GPU-sandbox to Kernel privilege escalation.

Potential Steps to Trigger

(Note: These are suggested steps; we do not currently have a working Proof of Concept that executes code.)

  1. Create an H.265 bitstream embedded in a <video> tag or via MSE.
  2. Insert an SPS_NUT NALU (sps_id=0) with pic_width_in_luma_samples=1920 and a small CTB size, making pic_width_in_ctbs_y very large (e.g., 100).
  3. Insert a PPS_NUT NALU referencing sps_id=0, setting tiles_enabled_flag=1 and num_tile_columns_minus1=5.
  4. Insert a second SPS_NUT NALU (sps_id=0) with pic_width_in_luma_samples=1920 but a maximum CTB size, making pic_width_in_ctbs_y very small (e.g., 1).
  5. Insert a Slice NALU referencing the PPS to force the decoder to process the inconsistent state and pass the malformed parameters to the hardware delegate.

Suggested Fix

In H265Parser::ParseSPS(), when an SPS is replaced, iterate through active_pps_ and either remove or invalidate any PPS that references the replaced sps_seq_parameter_set_id. Alternatively, H265Decoder::ProcessPPS() could be updated to explicitly check for changes in CTB geometry (e.g., sps->pic_width_in_ctbs_y, sps->ctb_log2_size_y) when calculating is_config_change, forcing a decoder flush and proper re-validation.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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