CVE-2026-10966
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/gpu/BUILD.gn |
modified | |
componentmedia/gpu/BUILD.gn |
modified | |
ifmedia/parsers/h264_parser.cc |
modified | |
erase_ifmedia/parsers/h264_parser.cc |
modified | |
TESTmedia/parsers/h264_parser_unittest.cc |
modified | |
ifmedia/parsers/h265_parser.cc |
modified | |
EraseIfmedia/parsers/h265_parser.cc |
modified |
Files Changed
media/gpu/BUILD.gnmedia/parsers/BUILD.gnmedia/parsers/h264_parser.ccmedia/parsers/h264_parser_unittest.ccmedia/parsers/h265_parser.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccmedia/gpu/h265_decoder.ccmedia/gpu/vaapi/h265_vaapi_video_decoder_delegate.ccmedia/gpu/windows/d3d11_h265_accelerator.ccmedia/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:
- VAAPI (
media/gpu/vaapi/h265_vaapi_video_decoder_delegate.cc): Ifpps->uniform_spacing_flagis true, the delegate calculatescolumn_width_minus1(lines 157-164). Becausesps->pic_width_in_ctbs_yis now smaller thanpps->num_tile_columns_minus1 + 1, the integer division evaluates to 0. The subsequent subtraction (0 - 0 - 1) results in-1. This-1wraps around to0xFFFFwhen stored in theuint16_tarraypic_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. - V4L2 stateless (
media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc): On ChromeOS ARM devices, inconsistent SPS and PPS control structures (wherenum_tile_columns_minus1 >= sps->pic_width_in_ctbs_y) are copied intov4l2_ctrl_hevc_ppsand sent directly to the Linux kernel via theVIDIOC_S_EXT_CTRLSioctl. 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.)
- Create an H.265 bitstream embedded in a
<video>tag or via MSE. - Insert an
SPS_NUTNALU (sps_id=0) withpic_width_in_luma_samples=1920and a small CTB size, makingpic_width_in_ctbs_yvery large (e.g., 100). - Insert a
PPS_NUTNALU referencingsps_id=0, settingtiles_enabled_flag=1andnum_tile_columns_minus1=5. - Insert a second
SPS_NUTNALU (sps_id=0) withpic_width_in_luma_samples=1920but a maximum CTB size, makingpic_width_in_ctbs_yvery small (e.g., 1). - 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.