Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactNumeric truncation error in Media
DescriptionNumeric truncation error in Media
ComponentMedia
Sub-componentMedia/gpu/v4l2
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker536423794
Fix commit71c52c59bfb6 (chromium/src) +17/-2
CWECWE-197
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-08-07 (public git)
Disclosed2026-09-08
Patch gap32 days — fix public in source before the advisory

Background

H.265 PPS tiling
The Picture Parameter Set of an H.265/HEVC stream can partition a picture into a grid of tiles whose per-column and per-row sizes are carried in the column_width_minus1 and row_height_minus1 arrays.
V4L2 HEVC UAPI
The Linux V4L2 stateless decoder kernel interface (v4l2_pps) stores the tile dimension arrays as __u8 (8-bit unsigned) elements, a narrower type than the parsed media PPS uses.
`base::checked_cast<__u8>()` / `base::IsValueInRangeForNumericType<__u8>()`
Chromium numeric helpers that respectively convert with a range assertion and test whether a value fits in a target integer type without truncation.
Numeric truncation
Assigning a wider integer to a narrower type silently discards high-order bits, so a large parsed dimension becomes an unrelated small value in the field passed to the kernel.

Root Cause Analysis

In the enclosing PPS-submission path (inferred: V4L2VideoDecoderDelegateH265::SubmitFrameMetadata), the pre-patch code iterated over the tile columns (for (int i = 0; i <= pps->num_tile_columns_minus1; ++i)) and tile rows and directly assigned each parsed pps->column_width_minus1[i] / pps->row_height_minus1[i] into the __u8 array elements of v4l2_pps. Because the source fields are a wider integer type than the destination __u8, any value that does not fit in 8 bits was silently truncated by the implicit conversion, so the value programmed into the V4L2 HEVC UAPI structure could differ arbitrarily from the value the bitstream actually declared. The invariant violated is that a dimension copied into the kernel-facing structure must faithfully represent the parsed value (or the frame must be rejected), not a truncated remainder.

The fix adds a base::IsValueInRangeForNumericType<__u8>() guard before each assignment that returns Status::kFail when the value would not fit, and replaces the raw assignment with base::checked_cast<__u8>(); the range check runs first, so an out-of-range dimension now aborts submission instead of being narrowed. The diff shows only the addition of these bounds checks and the safe-cast; it does not itself demonstrate any downstream dereference or memory-safety sink.

Key insight
The single mistake is copying a wider parsed integer into a narrower __u8 UAPI field without a range check, allowing silent numeric truncation; the fix rejects the frame when the value exceeds the __u8 range and otherwise converts it through a checked cast.

Attack Path

  1. Craft malicious H.265 stream An attacker supplies an H.265/HEVC video whose PPS declares tile column_width_minus1 or row_height_minus1 entries with values that do not fit in a __u8 (greater than 255).
  2. Values reach the delegate unguarded The parsed PPS values are iterated and, pre-patch, assigned straight into the __u8 v4l2_pps.column_width_minus1[i] / v4l2_pps.row_height_minus1[i] fields, where the implicit conversion truncates them.
  3. Truncated dimensions are submitted to the kernel The v4l2_pps structure carrying the corrupted, truncated tile dimensions is passed onward to the V4L2 stateless decoder, so the kernel receives a tile geometry inconsistent with what the bitstream described.
  4. Downstream misuse of the corrupted geometry (inferred) — Any decoder logic (in the GPU process or the kernel driver) that relies on these tile dimensions could act on the truncated values; the specific sink, primitive, or memory-safety consequence is not shown by this diff.

Impact Assessment

An attacker who can get a crafted H.265 stream decoded on a V4L2-backed platform can cause truncated tile dimensions to be programmed into the V4L2 HEVC UAPI structure, corrupting the tile geometry handed to the stateless decoder in the GPU process. The classified impact is a numeric truncation (logic) error in the Media component; any escalation to out-of-bounds access, memory corruption, or beyond is not demonstrated by the patch and is (inferred). Preconditions are the ability to have attacker-controlled H.The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. The advisory states no exploited-in-the-wild status or severity beyond the truncation classification.

Changed Functions

FunctionChangeNotes
for
media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc
modified

Files Changed

  • media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc

Audit Directions

  • Wide-to-narrow UAPI copies
    Review other places where parsed bitstream fields (H.264/H.265/VP9/AV1 SPS/PPS) are copied into fixed-width __u8/__u16 kernel UAPI structs, and confirm each is guarded by a range check or base::checked_cast rather than an implicit conversion.
  • Loop-bounded array assignments
    Audit loops bounded by num_tile_columns_minus1 / num_tile_rows_minus1 and similar parsed counts across the V4L2 delegates, checking that both the count and each per-element value are validated before writing into kernel-facing arrays.
  • `UNSAFE_TODO` assignment sites
    Search for UNSAFE_TODO(...) array writes fed by parsed stream values, since these mark raw accesses where type-narrowing or bounds validation may still be missing.
From 71c52c59bfb6daa32a033e4658ebfd45cbcb0855 Mon Sep 17 00:00:00 2001
From: Hirokazu Honda <hiroh@chromium.org>
Date: Fri, 07 Aug 2026 09:26:39 -0700
Subject: [PATCH] media/gpu/v4l2: Validate H.265 tile dimensions range for V4L2 PPS

In V4L2VideoDecoderDelegateH265::SubmitFrameMetadata(),
column_width_minus1 and row_height_minus1 from the parsed H.265 PPS
are copied into v4l2_pps. The V4L2 HEVC UAPI stores these fields as
__u8 arrays, whereas the parsed H.265 PPS fields use larger integer
types.

Add bounds checks with base::IsValueInRangeForNumericType<__u8>()
and use base::checked_cast<__u8>() to safely convert the values and
prevent integer overflow/truncation.

Bug: 536423794
Test: v4l2_video_decode_accelerator_tests
Change-Id: Ia46d684549da7571c96bda9243826370e1096ea7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8223082
Commit-Queue: Nathan Hebert <nhebert@chromium.org>
Auto-Submit: Hirokazu Honda <hiroh@chromium.org>
Reviewed-by: Nathan Hebert <nhebert@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1675750}
---
diff --git a/media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc b/media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc
index 1c152cc0..9e8e0505 100644
--- a/media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc
+++ b/media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc
@@ -295,8 +295,15 @@
                   std::remove_reference_t<decltype(pps->column_width_minus1)>>,
           "column_width_minus1 arrays must be same size");
       for (int i = 0; i <= pps->num_tile_columns_minus1; ++i) {
+        if (!base::IsValueInRangeForNumericType<__u8>(
+                pps->column_width_minus1[i])) {
+          DVLOG(1) << "column_width_minus1[" << i
+                   << "]=" << pps->column_width_minus1[i]
+                   << " exceeds V4L2 UAPI __u8 range";
+          return Status::kFail;
+        }
         UNSAFE_TODO(v4l2_pps.column_width_minus1[i]) =
-            pps->column_width_minus1[i];
+            base::checked_cast<__u8>(pps->column_width_minus1[i]);
       }
 
       static_assert(
@@ -305,7 +312,15 @@
                   std::remove_reference_t<decltype(pps->row_height_minus1)>>,
           "row_height_minus1 arrays must be same size");
       for (int i = 0; i <= pps->num_tile_rows_minus1; ++i) {
-        UNSAFE_TODO(v4l2_pps.row_height_minus1[i]) = pps->row_height_minus1[i];
+        if (!base::IsValueInRangeForNumericType<__u8>(
+                pps->row_height_minus1[i])) {
+          DVLOG(1) << "row_height_minus1[" << i
+                   << "]=" << pps->row_height_minus1[i]
+                   << " exceeds V4L2 UAPI __u8 range";
+          return Status::kFail;
+        }
+        UNSAFE_TODO(v4l2_pps.row_height_minus1[i]) =
+            base::checked_cast<__u8>(pps->row_height_minus1[i]);
       }
     }
   }
Loading diff…

Original Bug Report

reported by aw...@chromium.org

Potential V4L2 HEVC delegate narrowing truncation of tile dimensions before VIDIOC_S_EXT_CTRLS

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: The V4L2 HEVC accelerator delegate silently truncates PPS tile size parameters from int to __u8 when copying metadata to the V4L2 control structure. This causes inconsistent tile geometries to be sent to kernel drivers via VIDIOC_S_EXT_CTRLS, risking driver-side and GPU-memory corruption.

Affected files:

  • media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc
  • media/parsers/h265_parser.h
  • media/parsers/h265_parser.cc

Estimated timestamp from git blame: 2023-04-18

1. Summary of the Issue (Meant for Human Triage)

A potential narrowing truncation vulnerability exists in the Chromium V4L2 stateless HEVC video decoder delegate (V4L2VideoDecoderDelegateH265). The H.265 parser correctly parses Picture Parameter Set (PPS) explicit tile dimensions (column_width_minus1 and row_height_minus1) into std::array<int, N>. These values are bounded by the Sequence Parameter Set (SPS) dimensions, meaning they can legitimately reach values up to 1054 for large (e.g., 8K) resolutions with 16x16 Coding Tree Blocks (CTBs).

However, when the V4L2 delegate copies this metadata to the Linux kernel UAPI structure v4l2_ctrl_hevc_pps to configure the hardware decoder, the destination fields are typed as __u8[]. Because the copy uses a direct assignment without a range check, any tile width or height value ≥ 256 is silently truncated to its lowest 8 bits. The overall picture dimensions sent in the paired SPS control (v4l2_ctrl_hevc_sps) are typed as __u16 and are not truncated.

Consequently, an attacker-controlled web video stream can cause the V4L2 driver to receive an internally inconsistent state: an SPS describing a wide picture, paired with a PPS describing tile boundaries that sum to a fraction of that width. The VIDIOC_S_EXT_CTRLS ioctl succeeds because the kernel’s generic validation (std_validate_compound()) does not cross-check the sum of the tile widths against the SPS width. This mismatch propagates to the hardware driver (e.g., MediaTek mtk-vcodec, Hantro G2, or Rockchip), which calculates tile boundaries based on the truncated data. When the hardware CABAC engine subsequently decodes slice data that was validly encoded against the un-truncated widths, it will desynchronize and read/write out of bounds within the driver-allocated, GPU-resident scratch buffers.

2. Proof-of-Concept & Detailed Execution Flow

(Note: These are potential execution steps deduced from static analysis; our tooling agent does not have the ability to run code natively to demonstrate a live exploit.)

Potential Attacker Steps & Code Flow

  1. Attacker Bitstream Construction: An attacker serves a malicious Annex-B HEVC stream (via an HTML5 <video>, MSE, or WebCodecs). They construct an SPS defining pic_width_in_luma_samples = 8192 and a minimum CTB size of 16 (yielding 512 CTBs horizontally). They then construct a PPS referencing this SPS with tiles_enabled_flag = 1, uniform_spacing_flag = 0, and exactly two tile columns (num_tile_columns_minus1 = 1).
  2. Parser Read: media/parsers/h265_parser.cc parses the bitstream. It computes sps->pic_width_in_ctbs_y = 512.
  3. Tile Width Validation: At media/parsers/h265_parser.cc:974-981, the parser reads the explicit tile width provided by the attacker:
    for (int i = 0; i < pps->num_tile_columns_minus1; ++i) {
      READ_UE_OR_RETURN(&pps->column_width_minus1[i]);
      IN_RANGE_OR_RETURN(
          pps->column_width_minus1[i], 0,
          pps->column_width_minus1[pps->num_tile_columns_minus1] - 1);
      pps->column_width_minus1[pps->num_tile_columns_minus1] -=
          pps->column_width_minus1[i] + 1;
    }
    
    The attacker encodes the first tile width as 510. The IN_RANGE_OR_RETURN validates that 510 is between 0 and 510 (pic_width_in_ctbs_y - 1 - 1). The parser securely writes 510 and 0 to its std::array<int, 19> column_width_minus1.
  4. Delegate Narrowing Cast: In the sandboxed kHardwareVideoDecoding utility process on a ChromeOS ARM device, V4L2VideoDecoderDelegateH265::SubmitFrameMetadata copies the parser state to the kernel UAPI struct (v4l2_ctrl_hevc_pps):
    // media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc:297-300
    for (int i = 0; i <= pps->num_tile_columns_minus1; ++i) {
      UNSAFE_TODO(v4l2_pps.column_width_minus1[i]) =
          pps->column_width_minus1[i];
    }
    
    The array v4l2_pps.column_width_minus1 is defined in linux/v4l2-controls.h as __u8 column_width_minus1[20]. Assigning the int value 510 to this __u8 array silently truncates it to 254 (510 % 256).
  5. Ioctl Submission: The delegate sends the truncated controls to the driver via VIDIOC_S_EXT_CTRLS at media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc:503.
  6. Driver Desync & Memory Corruption: The V4L2 driver uses the truncated tile geometry (summing to 256 CTBs) to allocate and program hardware tile-boundary registers. When the V4L2 delegate subsequently pushes the slice data (encoded correctly against the 511-CTB tile), the CABAC engine desynchronizes from the hardware boundaries, resulting in out-of-bounds reads and writes in the GPU-mapped scratch buffers.

Suggested Fix

To fix this vulnerability, V4L2VideoDecoderDelegateH265::SubmitFrameMetadata should explicitly validate that the parsed column_width_minus1 and row_height_minus1 values can safely fit within the V4L2 UAPI 8-bit types. If they exceed std::numeric_limits<uint8_t>::max(), the delegate should safely abort the decode process by returning a kDecodeError status (or false), preventing the truncated data from ever reaching VIDIOC_S_EXT_CTRLS.

3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)

> The vulnerability accurately identifies a narrowing truncation from int to __u8 in V4L2VideoDecoderDelegateH265::SubmitFrameMetadata. The parser bounds column_width_minus1 by pic_width_in_ctbs_y (up to 1056), but the V4L2 struct stores it as __u8, silently truncating values ≥256. This leads to inconsistent tile geometry being passed to the hardware driver via ioctl. Medium (S2) is correct per severity guidelines: it represents a GPU validating-layer gap where an unchecked value reaches the driver, but the consequence is driver-specific and GPU-memory-resident (no host-memory write primitive shown). Furthermore, the affected code runs on ChromeOS/Linux, typically in the sandboxed OOP-VD utility process, confirming the one-step discount from the GPU process tier.

Prior Validator Critic Notes

  • H.265 Parser Bounds: Checked at media/parsers/h265_parser.cc:976-978. The parser correctly checks bounds according to the H.265 spec (relative to pic_width_in_ctbs_y), but it is not aware of the Linux kernel UAPI __u8 restriction.
  • VA-API Sibling Delegate: Checked at media/gpu/vaapi/h265_vaapi_video_decoder_delegate.cc:156-178. VA-API uses uint16_t for column_width_minus1[], so truncation does not occur on that platform, confirming this is a V4L2-specific issue.
  • Static Assertions: Checked in media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc:292-296. The static_assert validates array element count equality, but fails to check the width or type compatibility of the elements, allowing the int to __u8 narrowing conversion to compile without error under current warnings/flags.
  • Device Resolution Configuration Checks: The resolution changes during mid-stream decoding run through V4L2StatelessVideoDecoderBackend::ApplyResolution() but do not trigger secondary validation of the tile widths against the original configuration capabilities.

Code Flow Map

  • media/parsers/h265_parser.h:309: std::array<int, kMaxNumTileColumnWidth> column_width_minus1 = {};
  • media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc:298-299: UNSAFE_TODO(v4l2_pps.column_width_minus1[i]) = pps->column_width_minus1[i];
  • linux/v4l2-controls.h:2199: __u8 column_width_minus1[20];
  • media/gpu/v4l2/v4l2_video_decoder_delegate_h265.cc:503: if (device_->Ioctl(VIDIOC_S_EXT_CTRLS, &ext_ctrls) != 0) {

Environmental Assumptions

  • Target platform: ChromeOS ARM with V4L2 stateless HEVC enabled.
  • Build targets: #if BUILDFLAG(IS_CHROMEOS) enabled in v4l2_video_decoder_delegate_h265.cc indicating active production code for CrOS overlays. kUseOutOfProcessVideoDecoding is generally enabled by default on ChromeOS via media/base/media_switches.cc:1438, placing this within the kHardwareVideoDecoding sandboxed utility process.

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