High watch CVSS 9.6 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Codecs
DescriptionInsufficient validation of untrusted input in Codecs
ComponentCodecs
Sub-componentMedia
Bug ClassInput Validation
Action tierwatch — dos-class only — no demonstrated corruption path
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker536470854
Fix commit5dc7ba4f4e47 (chromium/src) +33/-0
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-19 (per vendor advisory)
Fix landed2026-07-28 (public git)
Disclosed2026-08-06
Patch gap9 days — fix public in source before the advisory

Background

HEVC slice segments
An H.265 picture is coded as one or more slice segments; first_slice_segment_in_pic_flag is 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 optional prior_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 assume curr_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).

Key insight
The code assumed a non-first slice segment is always preceded by a first segment of the same picture, but never enforced it against untrusted streams. The fix adds that structural check in both the parser (prior_shdr required) and the decoder (curr_pic_ required).

Attack Path

  1. Deliver an HEVC stream An attacker serves HEVC content that is decoded through Chrome’s H265Decoder, for example via <video>, MSE or WebCodecs VideoDecoder (inferred delivery vectors; the diff shows only the parser and decoder).
  2. 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_flag is 0. It can also place such a segment right after a mid-stream SPS/PPS change.
  3. Parser accepts the header Before the patch, ParseSliceHeader parsed the non-first segment fields even though prior_shdr was null. The regression test now expects kInvalidStream here.
  4. Decoder proceeds without a picture Before the patch, PreprocessCurrentSlice returned kOk with curr_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

The bug lets a crafted HEVC bitstream push the hardware-accelerated decoder into an inconsistent state, with a slice processed without an active picture. 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

FunctionChangeNotes
if
media/parsers/h265_parser.cc
modified
TEST_F
media/parsers/h265_parser_unittest.cc
modified

Files Changed

  • media/gpu/h265_decoder.cc
  • media/parsers/h265_parser.cc
  • media/parsers/h265_parser_unittest.cc

Audit Directions

  • Cross-NALU state assumptions in codec decoders
    Review H264Decoder, VP9Decoder, AV1Decoder and 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 than DCHECKed.
  • Validation gated behind optional flags
    Find 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, as H265Decoder now does.
  • State reset on parameter-set changes
    Audit 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.
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • media/parsers/h265_parser.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-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:

  1. SPS_NUT: sps_seq_parameter_set_id = 0, pic_width_in_luma_samples = 64, pic_height_in_luma_samples = 64.
  2. PPS_NUT: pps_pic_parameter_set_id = 0, pps_seq_parameter_set_id = 0.
  3. IDR_W_RADL: first_slice_segment_in_pic_flag = 1, slice_pic_parameter_set_id = 0, slice_type = I.
  4. SPS_NUT: sps_seq_parameter_set_id = 0, pic_width_in_luma_samples = 8192, pic_height_in_luma_samples = 8192.
  5. PPS_NUT: pps_pic_parameter_set_id = 0, pps_seq_parameter_set_id = 0.
  6. 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

  1. Initial Config (NALUs 1 & 2): H265Decoder::Decode() parses SPS/PPS. Since curr_pps_id_ == -1, it calls ProcessPPS(), updating pic_size_ = {64, 64} and returning kConfigChange. The hardware decoder context and surface pool allocate at 64x64.
  2. First Picture (NALU 3): Decoded successfully. curr_pps_id_ becomes 0. curr_pic_ references a 64x64 surface. last_slice_hdr_ points to this slice header.
  3. SPS Overwrite (NALU 4):
    • Execution processes H265NALU::SPS_NUT (media/gpu/h265_decoder.cc:450), calling FinishPrevFrameIfPresent().
    • FinishPrevFrameIfPresent() invokes FinishPicture(std::move(curr_pic_), std::move(last_slice_hdr_)) (:1109).
    • The std::move implicitly nullifies both curr_pic_ and last_slice_hdr_ in the decoder state.
    • The parser reads the new SPS_NUT (NALU 4) and overwrites the active SPS entry sps_id = 0 with 8192x8192 dimensions (passing bounds check up to 16,888 at h265_parser.cc:626).
  4. PPS Update (NALU 5): The parser parses the new PPS. At media/gpu/h265_decoder.cc:471, the check if (curr_pps_id_ == -1) evaluates to false because curr_pps_id_ is 0. ProcessPPS() is skipped, meaning pic_size_ remains at the stale 64x64 size and no kConfigChange is returned.
  5. Triggering the Desync (NALU 6):
    • Parser Bypass: The slice header parser (media/parsers/h265_parser.cc:1138) processes NALU 6. Since dependent_slice_segment_flag is 0 (or defaults to 0) and prior_shdr (derived from last_slice_hdr_) is nullptr, the parser bypasses the !prior_shdr check for dependent slices (:1167). Crucially, because prior_shdr is nullptr, it also completely bypasses the cross-slice validation block at media/parsers/h265_parser.cc:1471. The parser accepts the illegal slice segment address against the new, larger SPS.
    • Gate Bypass: In H265Decoder::Decode():
      // media/gpu/h265_decoder.cc:387
      if (curr_slice_hdr_->first_slice_segment_in_pic_flag) {
        bool need_new_buffers = false;
        if (!ProcessPPS(...)) { ... }
        ...
      }
      
      Because first_slice_segment_in_pic_flag == 0, ProcessPPS() is completely bypassed, circumventing the protective check inside ProcessPPS() against configuration changes on non-IRAP pictures (:658).
    • Stale Picture Allocation: The state-machine proceeds to kEnsurePicture. Since curr_pic_ == nullptr, it calls curr_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 stale visible_rect_ (64x64) to the picture (:1063). It then calls accelerator_->SubmitFrameMetadata(sps, ..., curr_pic_).
  6. 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() or ID3D11VideoContext::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.

  1. Delegate Validation: In H265VaapiVideoDecoderDelegate::SubmitFrameMetadata, D3D11H265Accelerator::SubmitFrameMetadata, and V4L2VideoDecoderDelegateH265::SubmitFrameMetadata, add a strict bounding check comparing the provided sps->pic_width_in_luma_samples and pic_height_in_luma_samples against the physical size of the provided pic surface (e.g., pic->visible_rect().size() or pic->AsVaapiH265Picture()->va_surface()->size()). Return a failure status if they misalign.
  2. Decoder Logic Hardening: Modify H265Decoder::Decode so that ProcessPPS handles checking for pending configuration changes unconditionally when a new SPS/PPS is encountered, rather than relying exclusively on the first_slice_segment_in_pic_flag gate.

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::move behavior nullifies last_slice_hdr_: Confirmed at media/gpu/h265_decoder.cc:1109. FinishPicture(std::move(curr_pic_), std::move(last_slice_hdr_)) moves the std::unique_ptr, guaranteeing last_slice_hdr_ is nullptr in subsequent processing.
  • Parser prior_shdr bypass: Confirmed at media/parsers/h265_parser.cc:1471. if (prior_shdr && !shdr->first_slice_segment_in_pic_flag) dictates cross-slice picture validation. Because prior_shdr is passed as nullptr (from last_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_shdr error handling at h265_parser.cc:1167.
  • ProcessPPS Bypass: Confirmed at media/gpu/h265_decoder.cc:387. The if (curr_slice_hdr_->first_slice_segment_in_pic_flag) evaluates to false, unconditionally skipping ProcessPPS() and allowing the 64x64 state variables to persist despite the active 8192x8192 SPS.
  • Verbatim Sink Mapping: Confirmed at media/gpu/vaapi/h265_vaapi_video_decoder_delegate.cc:109 (FROM_SPS_TO_PP(pic_width_in_luma_samples)) and media/gpu/windows/d3d11_h265_accelerator.cc:194. The delegates make zero cross-checks against the underlying VASurfaceID / ID3D11VideoDecoderOutputView size.

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.

View on issue tracker