CVE-2026-11079
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fmedia/gpu/h264_decoder_unittest.cc |
modified | |
ifmedia/parsers/h264_parser.cc |
modified |
Files Changed
media/gpu/h264_decoder_unittest.ccmedia/parsers/h264_parser.cc
Patch
From 94ad12b2d1dadb6f4728d4864b2828f4e015267f Mon Sep 17 00:00:00 2001 From: Ted Meyer <[email protected]> Date: Wed, 08 Apr 2026 13:23:34 -0700 Subject: [PATCH] Harden h264 parser to prevent sending OOB data Address missing h264 spec section 7.4.3 - the first_mb_in_slice should always be in range [0, PicSizeInMbs - 1]. This change just hardens the decoders to prevent an out-of-bounds value making it to the hardware driver, in case those drivers don't check things themselves. Fixed: 500028989 Change-Id: I624243d798b2eee4d6de6070502ff96f0c284bb1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737743 Auto-Submit: Ted (Chromium) Meyer <[email protected]> Commit-Queue: Ted (Chromium) Meyer <[email protected]> Reviewed-by: Eugene Zemtsov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1611752} --- diff --git a/media/gpu/h264_decoder_unittest.cc b/media/gpu/h264_decoder_unittest.cc index a4aa9d3..83e640c 100644 --- a/media/gpu/h264_decoder_unittest.cc +++ b/media/gpu/h264_decoder_unittest.cc @@ -815,6 +815,43 @@ ASSERT_TRUE(decoder_->Flush()); } +// POC: Unvalidated first_mb_in_slice in subsequent slices reaches the +// accelerator. +// +// H264Decoder::PreprocessCurrentSlice() only validates first_mb_in_slice==0 +// when IsNewPrimaryCodedPicture() returns true. For the second (and later) +// slice of a multi-slice picture, IsNewPrimaryCodedPicture() returns false +// and the value is forwarded to accelerator_->SubmitSlice() with no upper +// bound check. The H.264 spec (7.4.3) requires first_mb_in_slice < +// PicSizeInMbs; the H.265 parser enforces the equivalent condition. +// +// This test feeds a hand-crafted Annex-B stream: +// SPS : 320x240 (PicSizeInMbs = 20*15 = 300) +// PPS +// IDR slice 0 : first_mb_in_slice = 0 (passes the new-picture check) +// IDR slice 1 : first_mb_in_slice = 65535 (same frame_num/idr_pic_id -> +// IsNewPrimaryCodedPicture()=false) +TEST_F(H264DecoderTest, UnvalidatedFirstMbInSliceReachesAccelerator) { + static constexpr uint8_t kStream[] = { + 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1e, 0xda, 0x05, 0x07, + 0xe4, 0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x38, 0x80, 0x00, 0x00, + 0x00, 0x01, 0x65, 0x88, 0x84, 0xd5, 0x55, 0x40, 0x00, 0x00, 0x00, + 0x01, 0x65, 0x00, 0x00, 0x80, 0x00, 0x08, 0x84, 0xd5, 0x55, 0x40, + }; + + decoder_->SetStream(0, DecoderBuffer::CopyFrom(base::span(kStream))); + + // First Decode() processes the SPS and reports a config change. + ASSERT_EQ(AcceleratedVideoDecoder::kConfigChange, decoder_->Decode()); + EXPECT_EQ(gfx::Size(320, 240), decoder_->GetPicSize()); + + EXPECT_CALL(*accelerator_, SubmitSlice(_, _, _, _, _, _, _, _)) + .WillRepeatedly(Return(H264Decoder::H264Accelerator::Status::kOk)); + + // Second Decode() processes PPS + both IDR slices of the same picture. + ASSERT_EQ(AcceleratedVideoDecoder::kDecodeError, decoder_->Decode()); +} + TEST_F(H264DecoderTest, ModifyReferencePicList_CompactionTime) { // Seed the decoder DPB and `curr_pic_` with a long term ref pic. scoped_refptr<H264Picture> ref_pic = base::MakeRefCounted<H264Picture>(); diff --git a/media/parsers/h264_parser.cc b/media/parsers/h264_parser.cc index 8ae6bb03..c4efb1f 100644 --- a/media/parsers/h264_parser.cc +++ b/media/parsers/h264_parser.cc @@ -1414,11 +1414,29 @@ if (!sps->frame_mbs_only_flag) { READ_BOOL_OR_RETURN(&shdr->field_pic_flag); if (shdr->field_pic_flag) { + // Note that per-spec, the field_pic_flag should be used as a denominator + // when calculating frame_height while checking pic_size_in_mbs below. + // If interlaced streams ever become supported, additional arithmetic will + // need to be added to the calculation of `frame_height_in_mbs`. DVLOG(1) << "Interlaced streams not supported"; return kUnsupportedStream; } } + // H.264 spec 7.4.3: first_mb_in_slice shall be in [0, PicSizeInMbs - 1]. + // Without this check the value flows unvalidated into + // VASliceParameterBufferH264.first_mb_in_slice and is used by the VA-API + // driver as a write offset into the decode surface. + { + const int frame_height_in_mbs = (2 - sps->frame_mbs_only_flag) * + (sps->pic_height_in_map_units_minus1 + 1); + base::CheckedNumeric<int> pic_size = sps->pic_width_in_mbs_minus1 + 1; + pic_size *= frame_height_in_mbs; + TRUE_OR_RETURN(pic_size.IsValid()); + const int pic_size_in_mbs = pic_size.ValueOrDie(); + IN_RANGE_OR_RETURN(shdr->first_mb_in_slice, 0, pic_size_in_mbs - 1); + } + if (shdr->idr_pic_flag) { READ_UE_OR_RETURN(&shdr->idr_pic_id); IN_RANGE_OR_RETURN(shdr->idr_pic_id, 0, 65535);
Regression Test / PoC
diff --git a/media/gpu/h264_decoder_unittest.cc b/media/gpu/h264_decoder_unittest.cc
index a4aa9d3..83e640c 100644
--- a/media/gpu/h264_decoder_unittest.cc
+++ b/media/gpu/h264_decoder_unittest.cc
@@ -815,6 +815,43 @@
ASSERT_TRUE(decoder_->Flush());
}
+// POC: Unvalidated first_mb_in_slice in subsequent slices reaches the
+// accelerator.
+//
+// H264Decoder::PreprocessCurrentSlice() only validates first_mb_in_slice==0
+// when IsNewPrimaryCodedPicture() returns true. For the second (and later)
+// slice of a multi-slice picture, IsNewPrimaryCodedPicture() returns false
+// and the value is forwarded to accelerator_->SubmitSlice() with no upper
+// bound check. The H.264 spec (7.4.3) requires first_mb_in_slice <
+// PicSizeInMbs; the H.265 parser enforces the equivalent condition.
+//
+// This test feeds a hand-crafted Annex-B stream:
+// SPS : 320x240 (PicSizeInMbs = 20*15 = 300)
+// PPS
+// IDR slice 0 : first_mb_in_slice = 0 (passes the new-picture check)
+// IDR slice 1 : first_mb_in_slice = 65535 (same frame_num/idr_pic_id ->
+// IsNewPrimaryCodedPicture()=false)
+TEST_F(H264DecoderTest, UnvalidatedFirstMbInSliceReachesAccelerator) {
+ static constexpr uint8_t kStream[] = {
+ 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1e, 0xda, 0x05, 0x07,
+ 0xe4, 0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x38, 0x80, 0x00, 0x00,
+ 0x00, 0x01, 0x65, 0x88, 0x84, 0xd5, 0x55, 0x40, 0x00, 0x00, 0x00,
+ 0x01, 0x65, 0x00, 0x00, 0x80, 0x00, 0x08, 0x84, 0xd5, 0x55, 0x40,
+ };
+
+ decoder_->SetStream(0, DecoderBuffer::CopyFrom(base::span(kStream)));
+
+ // First Decode() processes the SPS and reports a config change.
+ ASSERT_EQ(AcceleratedVideoDecoder::kConfigChange, decoder_->Decode());
+ EXPECT_EQ(gfx::Size(320, 240), decoder_->GetPicSize());
+
+ EXPECT_CALL(*accelerator_, SubmitSlice(_, _, _, _, _, _, _, _))
+ .WillRepeatedly(Return(H264Decoder::H264Accelerator::Status::kOk));
+
+ // Second Decode() processes PPS + both IDR slices of the same picture.
+ ASSERT_EQ(AcceleratedVideoDecoder::kDecodeError, decoder_->Decode());
+}
+
TEST_F(H264DecoderTest, ModifyReferencePicList_CompactionTime) {
// Seed the decoder DPB and `curr_pic_` with a long term ref pic.
scoped_refptr<H264Picture> ref_pic = base::MakeRefCounted<H264Picture>();
Original Bug Report
Potential OOB write in GPU process via unvalidated H.264 first_mb_in_slice in VA-API
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 security team.
Overview: Chromium’s H.264 parser and decoder fail to validate the first_mb_in_slice field for non-initial slices of a picture. When hardware acceleration via VA-API is used, this unvalidated value is passed to the GPU driver. This can potentially lead to an out-of-bounds DMA write in the GPU or Utility process.
Affected files:
media/gpu/h264_decoder.ccmedia/parsers/h264_parser.ccmedia/gpu/vaapi/h264_vaapi_video_decoder_delegate.cc
Estimated timestamp from git blame: 2018-07-17
Description
A potential high-severity memory corruption vulnerability exists in Chromium’s hardware-accelerated H.264 video decoding pipeline. The first_mb_in_slice field within H.264 slice headers is not correctly validated against the picture’s dimensions for multi-slice frames. When processing subsequent slices in a picture, an arbitrarily large value can be passed directly to the VA-API driver, which may result in an out-of-bounds (OOB) memory write by the hardware decoder.
Root Cause Analysis
- Lack of bounds check in parsing: In
media/parsers/h264_parser.cc,H264Parser::ParseSliceHeaderreads thefirst_mb_in_slicefield from the bitstream usingREAD_UE_OR_RETURN. However, it never validates that this value is within the bounds of the total macroblocks in the picture defined by the Sequence Parameter Set (SPS). - Validation bypass in decoding: In
media/gpu/h264_decoder.cc,H264Decoder::PreprocessCurrentSlicecontains a validation check (slice_hdr->first_mb_in_slice != 0). However, this check is nested inside anif (IsNewPrimaryCodedPicture(...))block. For the second and subsequent slices of a multi-slice picture, this condition evaluates tofalse, causing the decoder to skip the check entirely. - Propagation to Hardware: The unvalidated value is passed to the VA-API delegate in
media/gpu/vaapi/h264_vaapi_video_decoder_delegate.cc. TheSubmitSlicefunction copies the value directly into theVASliceParameterBufferH264struct (SHDRToSP(first_mb_in_slice)). - OOB Write: This struct is submitted to the underlying VA-API driver (e.g., Intel iHD or Mesa). Hardware drivers typically use
first_mb_in_sliceto compute the memory offset within the destination GPU surface for the decoded macroblock data. An abnormally large value can cause the GPU DMA to write out-of-bounds, corrupting the address space of the GPU process (Linux Desktop) or the sandboxed Utility process (ChromeOS OOP-VD).
Suggested Attacker Steps
Note: These are suggested/potential steps, as our tooling agent doesn’t yet have the ability to run code to verify a working exploit.
- Craft a malicious H.264 video stream.
- Define a Sequence Parameter Set (SPS) with a small resolution (e.g., 320x240, yielding 300 macroblocks).
- Within an IDR frame, provide a valid first slice with
first_mb_in_slice = 0. - Provide a second slice for the same frame with a malformed, massive
first_mb_in_slicevalue (e.g.,65535). - Embed this video stream into a web page using an HTML5
<video>tag or the WebCodecs API. - When a victim views the page on a VA-API enabled system, the unvalidated field will potentially trigger an out-of-bounds write during hardware decoding.
Suggested Fix
Add a bounds check to ensure first_mb_in_slice is strictly less than the total number of macroblocks in the picture. This can be done in H264Decoder::ProcessCurrentSlice (or PreprocessCurrentSlice outside the IsNewPrimaryCodedPicture block) since the SPS and picture dimensions are known at that stage.
For example, calculate the picture size in macroblocks (based on sps->pic_width_in_mbs_minus1 and sps->pic_height_in_map_units_minus1) and add a check:
if (slice_hdr->first_mb_in_slice >= PicSizeInMbs) {
return H264Accelerator::Status::kFail;
}
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
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.