CVE-2026-19164
Overview
Background
- HEVC slice segments
- An H.265 picture is coded as one or more slice segments;
first_slice_segment_in_pic_flagis 1 only on the first segment of a picture. Later (non-first) segments, and especially dependent segments, rely on state set up by an earlier segment of the same picture. - `H265Parser::ParseSliceHeader`
- The bitstream parser (
media/parsers) that decodes a slice header. It receives an optionalprior_shdr, the previous slice header of the same picture, which non-first segments can inherit from. - `H265Decoder::PreprocessCurrentSlice`
- The accelerated-decoder state machine step (
media/gpu) that prepares for each slice. A first slice starts a new picture (curr_pic_), and later slices assumecurr_pic_already exists. - `validate_extended_bitstream_`
- A parser flag that turns on extra cross-slice bitstream-conformance checks. The diff does not show where it is set.
Root Cause Analysis
Neither the parser nor the decoder rejected a slice segment with first_slice_segment_in_pic_flag == 0 when no earlier segment of the picture existed. This can happen at stream start, or after a mid-stream SPS/PPS update that ends the previous picture. In ParseSliceHeader, the !shdr->first_slice_segment_in_pic_flag branch went on to read dependent_slice_segment_flag and the segment address even when prior_shdr was null. In PreprocessCurrentSlice, only the first-slice branch handled picture setup. A non-first slice with curr_pic_ null fell through to H265Accelerator::Status::kOk, so later decoding steps ran without an active picture.
The fix adds two rejections. The parser returns kInvalidStream for a non-first segment with no prior_shdr when validate_extended_bitstream_ is set. The decoder unconditionally returns H265Accelerator::Status::kFail for a non-first segment when curr_pic_ is null.
The patch does not show the exact downstream fault the missing check allowed. The advisory classifies the bug only as insufficient validation, and any memory-safety consequence (e.g. using a null or stale picture or slice state) is (inferred).
prior_shdr required) and the decoder (curr_pic_ required).Attack Path
- Deliver an HEVC stream
An attacker serves HEVC content that is decoded through Chrome’s
H265Decoder, for example via<video>, MSE or WebCodecsVideoDecoder(inferred delivery vectors; the diff shows only the parser and decoder). - Send SPS/PPS, then an orphan segment
As in the regression test, the stream contains a valid SPS and PPS followed by an IDR slice whose
first_slice_segment_in_pic_flagis 0. It can also place such a segment right after a mid-stream SPS/PPS change. - Parser accepts the header
Before the patch,
ParseSliceHeaderparsed the non-first segment fields even thoughprior_shdrwas null. The regression test now expectskInvalidStreamhere. - Decoder proceeds without a picture
Before the patch,
PreprocessCurrentSlicereturnedkOkwithcurr_pic_null, and later slice processing ran against missing picture state. The concrete result, such as a crash or memory misuse in the GPU-side decoder, is not shown (inferred).
Impact Assessment
H265Decoder lives in media/gpu, so this is most likely the GPU or media-decoding process (inferred). Reaching it needs a platform and build where HEVC is decoded through this accelerated decoder, since Chrome’s HEVC support is generally hardware-backed (inferred). Ordinary web content that can deliver media to that decoder can plausibly reach it without user interaction beyond loading the page (inferred). The parser-side check only applies when validate_extended_bitstream_ is enabled, but the decoder-side check is unconditional. The advisory states “Insufficient validation of untrusted input in Codecs” with no in-the-wild status. Any memory-corruption or code-execution outcome is (inferred).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
Audit Directions
- Cross-NALU state assumptions in codec decodersReview
H264Decoder,VP9Decoder,AV1Decoderand the platform accelerators for slice, tile or frame units that assume an earlier unit of the same picture set up state (curr_pic_, prior slice header, reference lists). Check that each assumption is validated rather thanDCHECKed. - Validation gated behind optional flagsFind conformance checks that only run when a flag like
validate_extended_bitstream_is set, and confirm that every consumer that skips the flag has its own equivalent guard downstream, asH265Decodernow does. - State reset on parameter-set changesAudit how mid-stream SPS/PPS/VPS updates reset picture and slice state, and whether the next unit is required to be a picture start. Orphan continuation units after a reset are a recurring source of decoder state confusion.
Patch
From 5dc7ba4f4e4770e36b567afbf457740e227d9edb Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
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 <dalecurtis@chromium.org>
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
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.