Overview

Medium
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Media
DescriptionInsufficient validation of untrusted input in Media
ComponentMedia
Bug ClassLogic Error
Tracker484547633
Fix commitd0a80fe50c1e (chromium/src) +23/-90
CISA KEVNot listed
Creditedxmzyshypnc
Disclosed2026-04-07

Changed Functions

FunctionChangeNotes
for
media/base/video_frame_layout.cc
modified
if
media/base/video_frame_layout.cc
modified
for
media/mojo/mojom/video_frame_mojom_traits.cc
modified
TEST_F
media/mojo/mojom/video_frame_mojom_traits_unittest.cc
modified
for
media/mojo/mojom/video_frame_mojom_traits_unittest.cc
modified

Files Changed

  • media/base/video_frame_layout.cc
  • media/base/video_frame_layout_unittest.cc
  • media/mojo/mojom/media_types.mojom
  • media/mojo/mojom/video_frame_mojom_traits.cc
  • media/mojo/mojom/video_frame_mojom_traits_unittest.cc
From d0a80fe50c1e778fdb9e2d5283e7e5ad193bffcd Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <[email protected]>
Date: Mon, 02 Mar 2026 17:17:06 -0800
Subject: [PATCH] media: Prevent passing of frames with absurdly large strides via Mojo

- Make strides unsigned in mojo, as they are in VideoFrame
- Validate that each plane's footprint (offset + stride * rows)
    fits in the buffer.
- Remove support for interleaved planes. IMC4 pixel format.

Bug: 484547633, 378046071
Change-Id: I8e5dbebddc434041bd7c31c2b16c2b5963314061
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7614704
Reviewed-by: Xiaohan Wang <[email protected]>
Reviewed-by: Dale Curtis <[email protected]>
Commit-Queue: Eugene Zemtsov <[email protected]>
Reviewed-by: Matthew Denton <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1592880}
---

diff --git a/media/base/video_frame_layout.cc b/media/base/video_frame_layout.cc
index 5120fc36..3d6ff38f 100644
--- a/media/base/video_frame_layout.cc
+++ b/media/base/video_frame_layout.cc
@@ -190,7 +190,8 @@
     return false;
   }
 
-  for (const auto& plane : planes_) {
+  for (size_t plane_idx = 0; plane_idx < planes_.size(); ++plane_idx) {
+    const auto& plane = planes_[plane_idx];
     if (plane.offset > data_size || plane.size > data_size) {
       return false;
     }
@@ -201,6 +202,14 @@
     if (!plane_end.IsValid() || plane_end.ValueOrDie() > data_size) {
       return false;
     }
+
+    size_t rows = VideoFrame::Rows(plane_idx, format_, coded_size_.height());
+    // Offset + stride * rows: furthermost byte that can be reasonably read
+    // during copying or conversion of the plane.
+    auto read_end = base::CheckMul(plane.stride, rows) + plane.offset;
+    if (!read_end.IsValid() || read_end.ValueOrDie() > data_size) {
+      return false;
+    }
   }
 
   return true;
diff --git a/media/base/video_frame_layout_unittest.cc b/media/base/video_frame_layout_unittest.cc
index 7b83496..e4351ab 100644
--- a/media/base/video_frame_layout_unittest.cc
+++ b/media/base/video_frame_layout_unittest.cc
@@ -318,8 +318,8 @@
   auto coded_size = gfx::Size(320, 180);
 
   std::vector<size_t> strides = {384, 192, 192};
-  std::vector<size_t> offsets = {0, 200, 300};
-  std::vector<size_t> sizes = {200, 100, 100};
+  std::vector<size_t> offsets = {0, 70000, 90000};
+  std::vector<size_t> sizes = {70000, 20000, 20000};
   std::vector<ColorPlaneLayout> planes(strides.size());
   for (size_t i = 0; i < strides.size(); i++) {
     planes[i].stride = strides[i];
diff --git a/media/mojo/mojom/media_types.mojom b/media/mojo/mojom/media_types.mojom
index d5e2654..1b88d88 100644
--- a/media/mojo/mojom/media_types.mojom
+++ b/media/mojo/mojom/media_types.mojom
@@ -488,7 +488,7 @@
 
   // Stride and offsets for each plane. Offsets are relative to the start
   // of |frame_data|.
-  array<int32> strides;
+  array<uint32> strides;
   array<uint32> offsets;
 };
 
diff --git a/media/mojo/mojom/video_frame_mojom_traits.cc b/media/mojo/mojom/video_frame_mojom_traits.cc
index 058a12b4..e97b860 100644
--- a/media/mojo/mojom/video_frame_mojom_traits.cc
+++ b/media/mojo/mojom/video_frame_mojom_traits.cc
@@ -39,7 +39,7 @@
 
 base::ReadOnlySharedMemoryRegion CreateRegion(const media::VideoFrame& frame,
                                               std::vector<uint32_t>& offsets,
-                                              std::vector<int32_t>& strides) {
+                                              std::vector<uint32_t>& strides) {
   TRACE_EVENT0("media", "VideoFrameDataPtr::CreateRegion");
   size_t num_planes = media::VideoFrame::NumPlanes(frame.format());
   DCHECK_LE(num_planes, 3u);
@@ -105,7 +105,7 @@
       input->storage_type() == media::VideoFrame::STORAGE_UNOWNED_MEMORY ||
       input->storage_type() == media::VideoFrame::STORAGE_OWNED_MEMORY) {
     std::vector<uint32_t> offsets;
-    std::vector<int32_t> strides;
+    std::vector<uint32_t> strides;
     auto region = CreateRegion(*input, offsets, strides);
     if (!region.IsValid()) {
       DLOG(ERROR) << "Failed to create region from VideoFrame";
@@ -272,7 +272,7 @@
     mojo::ArrayDataView<uint32_t> offsets;
     shared_memory_data.GetOffsetsDataView(&offsets);
 
-    mojo::ArrayDataView<int32_t> strides;
+    mojo::ArrayDataView<uint32_t> strides;
     shared_memory_data.GetStridesDataView(&strides);
 
     base::ReadOnlySharedMemoryMapping mapping = region.Map();
@@ -288,7 +288,6 @@
     }
 
     auto mapped_region = mapping.GetMemoryAsSpan<uint8_t>();
-    std::array<base::span<const uint8_t>, 3> plane_data;
     std::vector<media::ColorPlaneLayout> planes(num_planes);
     for (size_t i = 0; i < num_planes; i++) {
       if (offsets[i] > mapped_region.size()) {
@@ -300,16 +299,8 @@
 
       planes[i].stride = strides[i];
       planes[i].offset = base::strict_cast<size_t>(offsets[i]);
-      const size_t space_till_mapping_end = mapping.size() - offsets[i];
-      const size_t calculated_plane_size =
+      planes[i].size =
           media::VideoFrame::Rows(i, format, coded_size.height()) * strides[i];
-
-      // TODO(crbug.com/378046071) For H.264 content Widevine outputs planes
-      // in IMC4 pixel format. Since Y and V planes in IMC4 overlap,
-      // the distance to the next plane can't be used to determent the size of
-      // the current plane.
-      planes[i].size = std::min(calculated_plane_size, space_till_mapping_end);
-      plane_data[i] = mapped_region.subspan(offsets[i], planes[i].size);
     }
 
     auto layout = media::VideoFrameLayout::CreateWithPlanes(format, coded_size,
@@ -319,6 +310,12 @@
       return false;
     }
 
+    std::array<base::span<const uint8_t>, 3> plane_data;
+    for (size_t i = 0; i < num_planes; i++) {
+      plane_data[i] = mapped_region.subspan(layout->planes()[i].offset,
+                                            layout->planes()[i].size);
+    }
+
     if (media::IsYuvPlanar(format) && media::IsOpaque(format)) {
       frame = media::VideoFrame::WrapExternalYuvDataWithLayout(
           *layout, visible_rect, natural_size, plane_data[0], plane_data[1],
diff --git a/media/mojo/mojom/video_frame_mojom_traits_unittest.cc b/media/mojo/mojom/video_frame_mojom_traits_unittest.cc
index b5c92fd..755fade 100644
--- a/media/mojo/mojom/video_frame_mojom_traits_unittest.cc
+++ b/media/mojo/mojom/video_frame_mojom_traits_unittest.cc
@@ -195,79 +195,6 @@
   }
 }
 
-TEST_F(VideoFrameStructTraitsTest, InterleavedPlanes) {
-  constexpr VideoFrame::StorageType storage_type = VideoFrame::STORAGE_SHMEM;
-  constexpr VideoPixelFormat format = PIXEL_FORMAT_I420;
-  constexpr gfx::Size kCodedSize(100, 100);
-  constexpr gfx::Rect kVisibleRect(kCodedSize);
-  constexpr gfx::Size kNaturalSize = kCodedSize;
-  constexpr base::TimeDelta kTimestamp;
-
-  scoped_refptr<media::VideoFrame> frame;
-
-  std::vector<size_t> strides = VideoFrame::ComputeStrides(format, kCodedSize);
-  ASSERT_EQ(strides[1], strides[2]);
-
-  size_t aggregate_size = 0;
-  std::array<size_t, 3> sizes = {};
-  for (size_t i = 0; i < strides.size(); ++i) {
-    sizes[i] =
-        media::VideoFrame::Rows(i, format, kCodedSize.height()) * strides[i];
-    aggregate_size += sizes[i];
-  }
-  auto region = base::WritableSharedMemoryRegion::Create(aggregate_size);
-  ASSERT_TRUE(region.IsValid());
-  auto mapping = region.MapAt(0, aggregate_size);
-
-  auto [y_plane, uv_plane] =
-      mapping.GetMemoryAsSpan<uint8_t>().split_at(sizes[0]);
-  std::ranges::fill(y_plane, 1);
-
-  // Setup memory layout where U and V planes occupy the same space, but have
-  // interleaving U and V rows. This is achieved by doubling the stride.
-  size_t normal_stride = strides[1];
-  size_t uv_stride = normal_stride * 2;
-
-  int yu_rows = media::VideoFrame::Rows(1, format, kCodedSize.height());
-  auto uv_plane2 = uv_plane;  // Loop below is destructive.
-  for (int i = 0; i < yu_rows; ++i) {
-    const auto [u, v] = uv_plane2.take_first(uv_stride).split_at(normal_stride);
-    std::ranges::fill(u, 2);
-    std::ranges::fill(v, 3);
-  }
-
-  frame = media::VideoFrame::WrapExternalYuvData(
-      format, kCodedSize, kVisibleRect, kNaturalSize, strides[0], uv_stride,
-      uv_stride, y_plane, uv_plane, uv_plane.subspan(normal_stride),
-      kTimestamp);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/base/video_frame_layout_unittest.cc b/media/base/video_frame_layout_unittest.cc
index 7b83496..e4351ab 100644
--- a/media/base/video_frame_layout_unittest.cc
+++ b/media/base/video_frame_layout_unittest.cc
@@ -318,8 +318,8 @@
   auto coded_size = gfx::Size(320, 180);
 
   std::vector<size_t> strides = {384, 192, 192};
-  std::vector<size_t> offsets = {0, 200, 300};
-  std::vector<size_t> sizes = {200, 100, 100};
+  std::vector<size_t> offsets = {0, 70000, 90000};
+  std::vector<size_t> sizes = {70000, 20000, 20000};
   std::vector<ColorPlaneLayout> planes(strides.size());
   for (size_t i = 0; i < strides.size(); i++) {
     planes[i].stride = strides[i];
diff --git a/media/mojo/mojom/video_frame_mojom_traits_unittest.cc b/media/mojo/mojom/video_frame_mojom_traits_unittest.cc
index b5c92fd..755fade 100644
--- a/media/mojo/mojom/video_frame_mojom_traits_unittest.cc
+++ b/media/mojo/mojom/video_frame_mojom_traits_unittest.cc
@@ -195,79 +195,6 @@
   }
 }
 
-TEST_F(VideoFrameStructTraitsTest, InterleavedPlanes) {
-  constexpr VideoFrame::StorageType storage_type = VideoFrame::STORAGE_SHMEM;
-  constexpr VideoPixelFormat format = PIXEL_FORMAT_I420;
-  constexpr gfx::Size kCodedSize(100, 100);
-  constexpr gfx::Rect kVisibleRect(kCodedSize);
-  constexpr gfx::Size kNaturalSize = kCodedSize;
-  constexpr base::TimeDelta kTimestamp;
-
-  scoped_refptr<media::VideoFrame> frame;
-
-  std::vector<size_t> strides = VideoFrame::ComputeStrides(format, kCodedSize);
-  ASSERT_EQ(strides[1], strides[2]);
-
-  size_t aggregate_size = 0;
-  std::array<size_t, 3> sizes = {};
-  for (size_t i = 0; i < strides.size(); ++i) {
-    sizes[i] =
-        media::VideoFrame::Rows(i, format, kCodedSize.height()) * strides[i];
-    aggregate_size += sizes[i];
-  }
-  auto region = base::WritableSharedMemoryRegion::Create(aggregate_size);
-  ASSERT_TRUE(region.IsValid());
-  auto mapping = region.MapAt(0, aggregate_size);
-
-  auto [y_plane, uv_plane] =
-      mapping.GetMemoryAsSpan<uint8_t>().split_at(sizes[0]);
-  std::ranges::fill(y_plane, 1);
-
-  // Setup memory layout where U and V planes occupy the same space, but have
-  // interleaving U and V rows. This is achieved by doubling the stride.
-  size_t normal_stride = strides[1];
-  size_t uv_stride = normal_stride * 2;
-
-  int yu_rows = media::VideoFrame::Rows(1, format, kCodedSize.height());
-  auto uv_plane2 = uv_plane;  // Loop below is destructive.
-  for (int i = 0; i < yu_rows; ++i) {
-    const auto [u, v] = uv_plane2.take_first(uv_stride).split_at(normal_stride);
-    std::ranges::fill(u, 2);
-    std::ranges::fill(v, 3);
-  }
-
-  frame = media::VideoFrame::WrapExternalYuvData(
-      format, kCodedSize, kVisibleRect, kNaturalSize, strides[0], uv_stride,
-      uv_stride, y_plane, uv_plane, uv_plane.subspan(normal_stride),
-      kTimestamp);
-  auto ro_region =
-      base::WritableSharedMemoryRegion::ConvertToReadOnly(std::move(region));
-  frame->BackWithSharedMemory(&ro_region);
-
-  EXPECT_TRUE(frame);
-  EXPECT_EQ(frame->storage_type(), storage_type);
-  EXPECT_TRUE(RoundTrip(&frame));
-  EXPECT_TRUE(frame);
-  EXPECT_EQ(frame->format(), format);
-  EXPECT_EQ(frame->coded_size(), kCodedSize);
-
-  auto plane_1 = frame->GetVisiblePlaneData(1);
-  auto plane_2 = frame->GetVisiblePlaneData(2);
-  // Bytes between the visible edge and the full stride are not considered part
-  // of the visible plane, and may not be accessible through the above spans.
-  const size_t row_bytes_1 =
-      VideoFrame::RowBytes(1, format, kCodedSize.width());
-  const size_t row_bytes_2 =
-      VideoFrame::RowBytes(2, format, kCodedSize.width());
-  for (int i = 0; i < yu_rows; ++i) {
-    const auto [u, v] = uv_plane.take_first(uv_stride).split_at(normal_stride);
-    EXPECT_EQ(plane_1.subspan(i * frame->stride(1), row_bytes_1),
-              u.first(row_bytes_1));
-    EXPECT_EQ(plane_2.subspan(i * frame->stride(2), row_bytes_2),
-              v.first(row_bytes_2));
-  }
-}
-
 TEST_F(VideoFrameStructTraitsTest, InvalidOffsets) {
   constexpr auto kFormat = PIXEL_FORMAT_I420;
Loading diff…

Original Bug Report

reported by [email protected]

VideoFrame Mojo deserialization accepts negative stride β†’ OOB read in video encoders

Steps to reproduce the problem

== Environment == Tested on: Chromium trunk (Linux x64), ASAN build Affected platforms: All (Linux, macOS, Windows, ChromeOS, Android) The vulnerable code is platform-independent.

== Build Configuration (ASAN) == is_asan = true is_debug = false is_component_build = false target_cpu = “x64”

== Reproduction via ASAN Unit Test ==

The vulnerability is in the Mojo deserialization of SharedMemory-backed VideoFrames. A unit test that exercises the real deserialization code path with a crafted negative stride value triggers heap-buffer-overflow under ASAN.

Steps:

  1. Build media_unittests with ASAN: autoninja -C out/ASAN media_unittests

  2. Run the NegativeStride test: ASAN_OPTIONS=“detect_odr_violation=0:halt_on_error=1”
    out/ASAN/media_unittests
    –gtest_filter="NegativeStride"
    –single-process-tests

  3. Observe ASAN heap-buffer-overflow report.

== Reproduction via MojoJS PoC (requires –enable-blink-features=MojoJS) ==

  1. Apply the attached poc_patch.diff to add a stride probe after deserialization (needed because headless Linux has no GPU encoder backend; the probe simulates the same memory access pattern as VpxVideoEncoder).

  2. Serve poc_negative_stride.html on a local HTTP server: python3 -m http.server 8787

  3. Run Chrome with ASAN: ./out/ASAN/chrome
    –enable-blink-features=MojoJS,MojoJSTest
    –no-sandbox –disable-gpu-sandbox
    –headless=new –ozone-platform=headless –disable-gpu
    –use-fake-device-for-media-stream
    –enable-features=UseOutOfProcessVideoEncoding
    –enable-logging=stderr
    “http://127.0.0.1:8787/poc_negative_stride.html”

  4. Observe SEGV_MAPERR crash at the deserialization site.

== What the PoC does == The PoC uses MojoJS to call VideoEncodeAccelerator.Encode() with a VideoFrame whose strides[] are set to -1 (0xFFFFFFFF as int32_t). The frame is sent via Mojo IPC and deserialized in the browser/GPU process. The negative stride passes all existing validation checks and produces a VideoFrame with stride = SIZE_MAX, which causes backward out-of-bounds reads when any consumer (VPX encoder, AV1 encoder, libyuv, etc.) iterates over pixel rows.

Problem Description

== Summary == media/mojo/mojom/video_frame_mojom_traits.cc:301 assigns an int32_t stride value from Mojo IPC directly to a size_t field without checking for negative values. A compromised renderer can send stride = -1, which becomes SIZE_MAX (0xFFFFFFFFFFFFFFFF) after implicit sign extension. Downstream consumers (video encoders, libyuv) truncate this to int(-1) and use it for pointer arithmetic, causing backward out-of-bounds heap reads.

== Root Cause == In StructTraits<VideoFrameDataView>::Read(), SharedMemory path:

planes[i].stride = strides[i]; // int32_t β†’ size_t, no negative check

The existing FitsInContiguousBufferOfSize() check does NOT validate stride. It only checks (plane.offset + plane.size <= data_size). The plane.size is computed as: Rows(i, format, height) * strides[i] When strides[i] = -1, this multiplies size_t * int32_t(-1), causing integer overflow that wraps to a small value. std::min() then clamps it further. So plane.size and plane.offset are both reasonable β†’ the check passes. But planes[i].stride = SIZE_MAX is never validated.

Notably, the DMA-buf deserialization path in the SAME FILE (line ~196) correctly uses base::IsValueInRangeForNumericType<size_t>(data.stride()) to reject negative values. The SharedMemory path is missing this check β€” this is clearly an oversight.

== Affected Consumers == Any code that calls VideoFrame::stride() and uses it for pixel row traversal:

  • VpxVideoEncoder (vpx_video_encoder.cc:306-308): stride[VPX_PLANE_Y] = frame.stride(kY) β†’ vpx_image_t::stride is int[4], SIZE_MAX truncates to -1 β†’ libvpx OOB
  • Av1VideoEncoder (av1_video_encoder.cc): same pattern with aom_image_t
  • OpenH264VideoEncoder: same pattern
  • libyuv color conversion: stride passed as int parameter
  • WebMediaPlayerMSCompositor: stride used for pixel copy

== Security Impact ==

  • Type: Out-of-bounds read (heap-buffer-overflow / heap underflow)
  • Attack surface: Compromised renderer β†’ browser/GPU process via Mojo IPC
  • Threat model: Standard Chrome “compromised renderer” model. A renderer exploit (e.g., V8 bug) can craft arbitrary Mojo messages including negative strides.
  • Impact: Cross-process information disclosure (reading heap data before the VideoFrame buffer) or process crash (SEGV on unmapped page).
  • No MojoJS required for real exploitation β€” MojoJS is used only for PoC convenience.

== ASAN Crash == heap-buffer-overflow on address 0x7dc7994ef1ff READ of size 1 at 0x7dc7994ef1ff thread T0 0x7dc7994ef1ff is located 1 bytes before 19072-byte region [0x7dc7994ef200,0x7dc7994f3c80)

== Suggested Fix == Add a negative stride check before the assignment: if (strides[i] < 0) { DLOG(ERROR) << “Negative stride at plane " << i; return false; } planes[i].stride = static_cast<size_t>(strides[i]);

This matches the existing DMA-buf path validation pattern.

Additional Comments

== Environment == Chromium commit: b3acbdcb7bbe7eb076ed9509d3c5f2e10587a5b7 (2026-02-12, trunk/main branch) Build configs: Linux x64, both Default (non-ASAN) and ASAN builds (see args.gn details in attached bug_report.md Β§8)

== Why a PoC patch is needed ==

The PoC patch (poc_patch.diff) adds a ~30-line read-only probe to video_frame_mojom_traits.cc, immediately after the deserialized VideoFrame is created. This patch is needed because:

  1. Our test server is headless Linux with no GPU. The natural consumer of the poisoned VideoFrame is VpxVideoEncoder (or AV1/OpenH264/VideoToolbox encoders), but VideoEncodeAccelerator requires a hardware backend (VAAPI/V4L2/VideoToolbox) to initialize. On our server, VEA.Initialize() fails β†’ Encode() is never called β†’ the poisoned frame is never consumed.

  2. The vulnerability itself (int32_t β†’ size_t assignment at line 301) is fully exercised WITHOUT the patch. The deserialization succeeds, FitsInContiguousBufferOfSize() is bypassed (integer overflow), and the VideoFrame is created with stride = SIZE_MAX. The patch merely performs the same memory access that the encoder would perform, to trigger ASAN detection in our GPU-less environment.

  3. On macOS or any machine with a GPU, the patch is unnecessary β€” the real VPX/AV1/VideoToolbox encoder will consume the frame and crash naturally.

== What the patch simulates and why it’s a valid reproduction ==

The patch mirrors the exact code path in vpx_video_encoder.cc:

Patch code VpxVideoEncoder real code ────────────────────────────────── ────────────────────────────────── y_plane = frame->visible_data(kY) planes[Y] = frame.visible_data(kY) (line 301) y_stride = frame->stride(kY) stride[Y] = frame.stride(kY) (line 307) *(y_plane + y_stride) libvpx: planes[Y] + row * stride[Y] (vpx_codec_encode)

The type conversion chain is identical: frame->stride(kY) returns size_t = SIZE_MAX β†’ assigned to int variable β†’ truncated to -1 β†’ pointer arithmetic: y_plane + (-1) β†’ backward OOB read

== Why the OOB access is reachable without any security checks in between ==

From the deserialization point (line 301) to the consumer access, there are NO intervening security checks on the stride value:

  1. Line 301: planes[i].stride = strides[i] β€” int32(-1) β†’ size_t(SIZE_MAX), no check
  2. Line 306: FitsInContiguousBufferOfSize() β€” checks (offset + size ≀ buffer_size) only. The “size” is computed as Rows * strides[i], which overflows (size_t * int32_t(-1) wraps), then std::min() clamps it to a small value. The stride field itself is NEVER validated.
  3. Line 334: frame->BackWithOwnedSharedMemory() β€” stores the mapping, no stride check
  4. VideoFrame::stride() β€” a simple getter, returns planes_[i].stride directly
  5. VpxVideoEncoder::SetupStandardYuvPlanes() β€” copies stride to vpx_image_t::stride (int), no range check
  6. vpx_codec_encode() β€” uses stride for row traversal, no bounds check

The DMA-buf deserialization path in the SAME file (line ~196) correctly validates: if (!base::IsValueInRangeForNumericType<size_t>(data.stride())) return false; The SharedMemory path is missing this check β€” clearly an oversight.

== Attached files ==

  • poc_negative_stride.html β€” MojoJS PoC (sends negative stride via VEA Mojo)
  • poc_patch.diff β€” Read-only probe patch for GPU-less reproduction
  • poc_patch_report.md β€” Detailed analysis report
  • bug_report_VideoFrame_NegativeStride.md β€” Full technical report with crash logs
  • asan_unittest_crash.log β€” Complete ASAN unit test output
  • fix_negative_stride.diff β€” Suggested fix (add negative stride check)

Summary

VideoFrame Mojo deserialization accepts negative stride β†’ OOB read in video encoders

Custom Questions

Type of crash:

Browser/GPU process crash (the VideoFrame deserialization and video encoding happen in the browser or GPU process, not the renderer/tab process). When UseOutOfProcessVideoEncoding is enabled, the crash occurs in the dedicated video encoding utility process. Otherwise it occurs in the GPU process. The renderer (tab) process is the attacker β€” it sends the malicious Mojo IPC message. The crash is cross-process.

Crash state:

== ASAN Crash (media_unittests –gtest_filter=NegativeStride) ==

==========================================================//

// ==1484417==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7dc7994ef1ff at pc 0x561e67f6deee bp 0x7ffd6ed28f90 sp 0x7ffd6ed28f88 READ of size 1 at 0x7dc7994ef1ff thread T0 #0 0x561e67f6deed in media::VideoFrameStructTraitsTest_NegativeStride_Test::TestBody() media/mojo/mojom/video_frame_mojom_traits_unittest.cc:496:17 #1 0x561e6399763b in testing::Test::Run() #2 0x561e6399a7fa in testing::TestInfo::Run()

0x7dc7994ef1ff is located 1 bytes before 19072-byte region [0x7dc7994ef200,0x7dc7994f3c80) allocated by thread T0 here: #0 0x561e60decc5d in operator new[](unsigned long) #1 0x561e67f6db23 in media::VideoFrameStructTraitsTest_NegativeStride_Test::TestBody()

SUMMARY: AddressSanitizer: heap-buffer-overflow media/mojo/mojom/video_frame_mojom_traits_unittest.cc:496:17 in media::VideoFrameStructTraitsTest_NegativeStride_Test::TestBody()

== Non-ASAN Crash (MojoJS PoC in headless Chrome) ==

Received signal 11 SEGV_MAPERR 7f0d51061fff #4 0x7f0d64589e07 mojo::StructTraits<>::Read() [../../media/mojo/mojom/video_frame_mojom_traits.cc:357:36] #5 VideoEncodeAcceleratorStubDispatch::AcceptWithResponder()

Crash address: 0x7f0d51061fff (1 byte before shared memory mapping region) Signal: SIGSEGV (SEGV_MAPERR) β€” access to unmapped page

== Key type conversion trace == stride input: int32_t(-1) = 0xFFFFFFFF after assign: size_t = 0xFFFFFFFFFFFFFFFF (SIZE_MAX = 18446744073709551615) encoder cast: (int)SIZE_MAX = -1 pointer math: y_plane + (-1) β†’ 1 byte before buffer start β†’ heap underflow

Reporter credit:

xmzyshypnc

Additional Data

Category: Security
Chrome Channel: Stable
Regression: N/A \

View on issue tracker