CVE-2026-19164
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/parsers/h265_parser.cc |
modified | |
TEST_Fmedia/parsers/h265_parser_unittest.cc |
modified |
Files Changed
media/gpu/h265_decoder.ccmedia/parsers/h265_parser.ccmedia/parsers/h265_parser_unittest.cc
Patch
From 5dc7ba4f4e4770e36b567afbf457740e227d9edb Mon Sep 17 00:00:00 2001 From: Eugene Zemtsov <[email protected]> Date: Tue, 28 Jul 2026 10:01:55 -0700 Subject: [PATCH] media: Reject HEVC non-first slice segment when prior slice is missing Disallow non-first slice segments (first_slice_segment_in_pic_flag == 0) when no prior slice header exists (e.g. after mid-stream SPS/PPS updates). Enforce this in H265Parser::ParseSliceHeader() when validate_extended_bitstream_ is set, and in H265Decoder::PreprocessCurrentSlice() when curr_pic_ is null. Bug: 536470854 Change-Id: Ie7f159fbb9cdb229f516f0e46699a22aedefa184 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8160680 Reviewed-by: Dale Curtis <[email protected]> Commit-Queue: Eugene Zemtsov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1669582} --- diff --git a/media/gpu/h265_decoder.cc b/media/gpu/h265_decoder.cc index 1a1b0a4..750a4ad 100644 --- a/media/gpu/h265_decoder.cc +++ b/media/gpu/h265_decoder.cc @@ -703,6 +703,10 @@ return result; DCHECK(!curr_pic_); + } else if (!curr_pic_) { + DVLOG(1) << "Received slice segment with first_slice_segment_in_pic_flag " + << "equal to 0 without an active picture"; + return H265Accelerator::Status::kFail; } return H265Accelerator::Status::kOk; diff --git a/media/parsers/h265_parser.cc b/media/parsers/h265_parser.cc index 22d15ce..8d65703 100644 --- a/media/parsers/h265_parser.cc +++ b/media/parsers/h265_parser.cc @@ -1156,6 +1156,11 @@ std::min(shdr->temporal_id, sps->sps_max_sub_layers_minus1); if (!shdr->first_slice_segment_in_pic_flag) { + if (validate_extended_bitstream_ && !prior_shdr) { + DVLOG(1) << "First slice segment in picture must have " + << "first_slice_segment_in_pic_flag equal to 1"; + return kInvalidStream; + } if (pps->dependent_slice_segments_enabled_flag) READ_BOOL_OR_RETURN(&shdr->dependent_slice_segment_flag); READ_BITS_OR_RETURN(base::bits::Log2Ceiling(sps->pic_size_in_ctbs_y), diff --git a/media/parsers/h265_parser_unittest.cc b/media/parsers/h265_parser_unittest.cc index 5f0274c..2b97717 100644 --- a/media/parsers/h265_parser_unittest.cc +++ b/media/parsers/h265_parser_unittest.cc @@ -924,4 +924,28 @@ H265Parser::kOk); } +TEST_F(H265CrossSliceTest, RejectsNonFirstSliceSegmentWithoutPriorSliceHeader) { + H26xAnnexBBitstreamBuilder builder; + BuildSpsAndPps(builder); + AppendSecondSlice(builder, H265NALU::IDR_W_RADL); + builder.Flush(); + parser_.SetStream(builder.data()); + + H265NALU nalu; + int sps_id; + int pps_id; + ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk); + ASSERT_EQ(nalu.nal_unit_type, H265NALU::SPS_NUT); + ASSERT_EQ(parser_.ParseSPS(&sps_id), H265Parser::kOk); + ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk); + ASSERT_EQ(nalu.nal_unit_type, H265NALU::PPS_NUT); + ASSERT_EQ(parser_.ParsePPS(nalu, &pps_id), H265Parser::kOk); + + ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk); + ASSERT_EQ(nalu.nal_unit_type, H265NALU::IDR_W_RADL); + H265SliceHeader shdr; + EXPECT_EQ(parser_.ParseSliceHeader(nalu, &shdr, nullptr), + H265Parser::kInvalidStream); +} + } // namespace media
Regression Test / PoC
diff --git a/media/parsers/h265_parser_unittest.cc b/media/parsers/h265_parser_unittest.cc
index 5f0274c..2b97717 100644
--- a/media/parsers/h265_parser_unittest.cc
+++ b/media/parsers/h265_parser_unittest.cc
@@ -924,4 +924,28 @@
H265Parser::kOk);
}
+TEST_F(H265CrossSliceTest, RejectsNonFirstSliceSegmentWithoutPriorSliceHeader) {
+ H26xAnnexBBitstreamBuilder builder;
+ BuildSpsAndPps(builder);
+ AppendSecondSlice(builder, H265NALU::IDR_W_RADL);
+ builder.Flush();
+ parser_.SetStream(builder.data());
+
+ H265NALU nalu;
+ int sps_id;
+ int pps_id;
+ ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+ ASSERT_EQ(nalu.nal_unit_type, H265NALU::SPS_NUT);
+ ASSERT_EQ(parser_.ParseSPS(&sps_id), H265Parser::kOk);
+ ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+ ASSERT_EQ(nalu.nal_unit_type, H265NALU::PPS_NUT);
+ ASSERT_EQ(parser_.ParsePPS(nalu, &pps_id), H265Parser::kOk);
+
+ ASSERT_EQ(parser_.AdvanceToNextNALU(&nalu), H265Parser::kOk);
+ ASSERT_EQ(nalu.nal_unit_type, H265NALU::IDR_W_RADL);
+ H265SliceHeader shdr;
+ EXPECT_EQ(parser_.ParseSliceHeader(nalu, &shdr, nullptr),
+ H265Parser::kInvalidStream);
+}
+
} // namespace media
Original Bug Report
Potential High: H265Decoder skips ProcessPPS on first_slice_segment_in_pic_flag=0 leading to driver OOB write
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: A logic bypass in H265Decoder::Decode allows a malformed HEVC bitstream to skip surface reallocation while applying a newly parsed, larger SPS. This leads to submitting mismatched frame parameters (e.g. 8192x8192) to hardware-accelerated drivers against a stale-sized (e.g. 64x64) backing surface. The underlying graphics drivers then write out-of-bounds in GPU memory based on these maliciously large dimensions.
Affected files:
media/gpu/h265_decoder.ccmedia/parsers/h265_parser.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-09
1. Summary of the Issue (Meant for Human Triage)
A potential logic flaw in media/gpu/h265_decoder.cc allows a malformed/spec-illegal (but parser-accepted) HEVC bitstream to trigger mismatched picture dimensions between the high-level decoder state and the backend hardware-accelerated drivers.
Specifically, the decoder gates calls to ProcessPPS() (the only execution path that re-evaluates picture dimensions, issues kConfigChange, and manages hardware context re-allocations) on the condition that curr_slice_hdr_->first_slice_segment_in_pic_flag is non-zero. If an incoming slice has first_slice_segment_in_pic_flag set to 0, but is parsed under conditions where curr_pic_ == nullptr and last_slice_hdr_ == nullptr (which occurs naturally after a preceding mid-stream SPS/PPS overwrite calls FinishPrevFrameIfPresent()), ProcessPPS() is completely bypassed.
As a result, the decoder proceeds to allocate a picture surface from the stale, smaller surface pool (e.g., 64x64) but extracts the newly overwritten, larger SPS dimensions (e.g., 8192x8192) in StartNewFrame(). It then submits these mismatched parameters verbatim to the underlying accelerator delegate (VA-API, D3D11VA, or V4L2). Because driver-side structures calculate memory regions, macroblock buffers, and motion vector offsets from these newly parsed large dimensions while operating on the smaller physical backing surface, this causes an out-of-bounds (OOB) write in driver-allocated GPU memory.
This vulnerability is reachable remotely via WebCodecs or MSE (A-SERVER reachable) without requiring a compromised renderer. It affects Windows, macOS, Linux, and ChromeOS where the GPU or OOP-VD utility process is sandboxed (Android is unaffected as it utilizes the stateful MediaCodecVideoDecoder). This represents a High-severity (S1) memory corruption primitive.
2. Proof-of-Concept & Detailed Execution Flow
Note: The following are potential steps that trace the theoretical execution flow to trigger the vulnerability. Our tooling agent does not have the ability to run code, so this sequence relies on static analysis and codebase state machine verification.
Potential Attack Sequence
An attacker delivers a single Annex-B byte stream utilizing a sequence like this:
- SPS_NUT:
sps_seq_parameter_set_id = 0,pic_width_in_luma_samples = 64,pic_height_in_luma_samples = 64. - PPS_NUT:
pps_pic_parameter_set_id = 0,pps_seq_parameter_set_id = 0. - IDR_W_RADL:
first_slice_segment_in_pic_flag = 1,slice_pic_parameter_set_id = 0,slice_type = I. - SPS_NUT:
sps_seq_parameter_set_id = 0,pic_width_in_luma_samples = 8192,pic_height_in_luma_samples = 8192. - PPS_NUT:
pps_pic_parameter_set_id = 0,pps_seq_parameter_set_id = 0. - IDR_W_RADL:
first_slice_segment_in_pic_flag = 0,dependent_slice_segment_flag = 0,slice_segment_address = 1,slice_pic_parameter_set_id = 0,slice_type = I.
Code Execution Trace
- Initial Config (NALUs 1 & 2):
H265Decoder::Decode()parses SPS/PPS. Sincecurr_pps_id_ == -1, it callsProcessPPS(), updatingpic_size_ = {64, 64}and returningkConfigChange. The hardware decoder context and surface pool allocate at 64x64. - First Picture (NALU 3): Decoded successfully.
curr_pps_id_becomes0.curr_pic_references a 64x64 surface.last_slice_hdr_points to this slice header. - SPS Overwrite (NALU 4):
- Execution processes
H265NALU::SPS_NUT(media/gpu/h265_decoder.cc:450), callingFinishPrevFrameIfPresent(). FinishPrevFrameIfPresent()invokesFinishPicture(std::move(curr_pic_), std::move(last_slice_hdr_))(:1109).- The
std::moveimplicitly nullifies bothcurr_pic_andlast_slice_hdr_in the decoder state. - The parser reads the new SPS_NUT (NALU 4) and overwrites the active SPS entry
sps_id = 0with 8192x8192 dimensions (passing bounds check up to 16,888 ath265_parser.cc:626).
- Execution processes
- PPS Update (NALU 5): The parser parses the new PPS. At
media/gpu/h265_decoder.cc:471, the checkif (curr_pps_id_ == -1)evaluates tofalsebecausecurr_pps_id_is0.ProcessPPS()is skipped, meaningpic_size_remains at the stale64x64size and nokConfigChangeis returned. - Triggering the Desync (NALU 6):
- Parser Bypass: The slice header parser (
media/parsers/h265_parser.cc:1138) processes NALU 6. Sincedependent_slice_segment_flagis 0 (or defaults to 0) andprior_shdr(derived fromlast_slice_hdr_) isnullptr, the parser bypasses the!prior_shdrcheck for dependent slices (:1167). Crucially, becauseprior_shdrisnullptr, it also completely bypasses the cross-slice validation block atmedia/parsers/h265_parser.cc:1471. The parser accepts the illegal slice segment address against the new, larger SPS. - Gate Bypass: In
H265Decoder::Decode():Because// media/gpu/h265_decoder.cc:387 if (curr_slice_hdr_->first_slice_segment_in_pic_flag) { bool need_new_buffers = false; if (!ProcessPPS(...)) { ... } ... }first_slice_segment_in_pic_flag == 0,ProcessPPS()is completely bypassed, circumventing the protective check insideProcessPPS()against configuration changes on non-IRAP pictures (:658). - Stale Picture Allocation: The state-machine proceeds to
kEnsurePicture. Sincecurr_pic_ == nullptr, it callscurr_pic_ = accelerator_->CreateH265Picture();. This allocates a 64x64 surface from the stale-sized surface pool. - Mismatched Metadata Submission:
StartNewFrame()is called. It fetches the active SPS (which is now 8192x8192) but applies the stalevisible_rect_(64x64) to the picture (:1063). It then callsaccelerator_->SubmitFrameMetadata(sps, ..., curr_pic_).
- Parser Bypass: The slice header parser (
- Hardware Delegate Sinks (OOB Write):
- The hardware delegates copy the large SPS dimensions verbatim into hardware driver structs without cross-validating them against the physical backing surface.
- VA-API:
pic_param.pic_width_in_luma_samples = sps->pic_width_in_luma_samples;(h265_vaapi_video_decoder_delegate.cc:109). - D3D11:
(pic_param.params).PicWidthInMinCbsY = sps->pic_width_in_luma_samples >> min_cb_log2_size_y;(d3d11_h265_accelerator.cc:194). - These parameters are submitted via
vaRenderPicture()orID3D11VideoContext::SubmitDecoderBuffers()against the 64x64 surface. The GPU driver proceeds to use the 8192x8192 logical picture sizes to address memory, resulting in an out-of-bounds write in GPU-mapped driver memory.
Suggested Fix
The mismatch occurs because hardware-acceleration delegates blindly trust the sps dimensions passed into SubmitFrameMetadata without validating them against the backing surface size.
- Delegate Validation: In
H265VaapiVideoDecoderDelegate::SubmitFrameMetadata,D3D11H265Accelerator::SubmitFrameMetadata, andV4L2VideoDecoderDelegateH265::SubmitFrameMetadata, add a strict bounding check comparing the providedsps->pic_width_in_luma_samplesandpic_height_in_luma_samplesagainst the physical size of the providedpicsurface (e.g.,pic->visible_rect().size()orpic->AsVaapiH265Picture()->va_surface()->size()). Return a failure status if they misalign. - Decoder Logic Hardening: Modify
H265Decoder::Decodeso thatProcessPPShandles checking for pending configuration changes unconditionally when a new SPS/PPS is encountered, rather than relying exclusively on thefirst_slice_segment_in_pic_flaggate.
3. Technical Verification Details (Automated Audit Logs)
> Severity: High (S1)
> Brief Notes / Reasoning:
> The report accurately details a logic flaw in H265Decoder::Decode where an attacker-controlled bitstream can bypass surface reconfiguration (ProcessPPS()) by sending an IDR slice with first_slice_segment_in_pic_flag set to 0 immediately following a new SPS. The parser (media/parsers/h265_parser.cc:1158-1175) only validates prior_shdr for dependent slice segments, thereby accepting this spec-invalid structure.
>
> As verified, this desynchronization results in the accelerator delegate (h265_vaapi_video_decoder_delegate.cc or d3d11_h265_accelerator.cc) feeding the new, large SPS dimensions (e.g. 8192x8192) into the driver struct (e.g. DXVA_PicParams_HEVC.PicWidthInMinCbsY), while the actual backing surface and visible_rect_ remain at the stale size (e.g. 64x64). This produces a driver-side OOB write when the driver renders the 8192x8192 logical picture into a 64x64 surface.
>
> Per the severity guidelines and KB (GPU validating-layer gap / bitstream field passed unvalidated to HW-decode driver struct), a memory corruption primitive in the GPU process reachable from A-SERVER (WebCodecs/MSE) is High (S1) when sandboxed. H265Decoder is not used on Android (which uses MediaCodecVideoDecoder), avoiding the unsandboxed Critical (S0) tier. The highest reachable tier is the sandboxed Windows GPU process, making this High (S1) severity.
Verifiable Code Reachability & State Assumptions
std::movebehavior nullifieslast_slice_hdr_: Confirmed atmedia/gpu/h265_decoder.cc:1109.FinishPicture(std::move(curr_pic_), std::move(last_slice_hdr_))moves thestd::unique_ptr, guaranteeinglast_slice_hdr_isnullptrin subsequent processing.- Parser
prior_shdrbypass: Confirmed atmedia/parsers/h265_parser.cc:1471.if (prior_shdr && !shdr->first_slice_segment_in_pic_flag)dictates cross-slice picture validation. Becauseprior_shdris passed asnullptr(fromlast_slice_hdr_.get()), validation is skipped. - Dependent Slice Default: Confirmed at
media/parsers/h265_parser.h:394.bool dependent_slice_segment_flag = false;allows the attacker to omit it and seamlessly bypass!prior_shdrerror handling ath265_parser.cc:1167. ProcessPPSBypass: Confirmed atmedia/gpu/h265_decoder.cc:387. Theif (curr_slice_hdr_->first_slice_segment_in_pic_flag)evaluates to false, unconditionally skippingProcessPPS()and allowing the64x64state variables to persist despite the active8192x8192SPS.- Verbatim Sink Mapping: Confirmed at
media/gpu/vaapi/h265_vaapi_video_decoder_delegate.cc:109(FROM_SPS_TO_PP(pic_width_in_luma_samples)) andmedia/gpu/windows/d3d11_h265_accelerator.cc:194. The delegates make zero cross-checks against the underlyingVASurfaceID/ID3D11VideoDecoderOutputViewsize.
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.