Overview

High
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
Tracker495819067
Fix commitcaa9eb0fc5da (chromium/src) +79/-17
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
media/gpu/vaapi/vaapi_wrapper.cc
modified

Files Changed

  • media/gpu/vaapi/BUILD.gn
  • media/gpu/vaapi/vaapi_wrapper.cc
From caa9eb0fc5dacaa21a90eff43c093906e43d8c1a Mon Sep 17 00:00:00 2001
From: Hirokazu Honda <hiroh@chromium.org>
Date: Mon, 18 May 2026 10:43:24 -0700
Subject: [PATCH] Reland "media/gpu/vaapiWrapper: Add native pixmap validation"

> This CL introduces more rigorous validation for plane metadata when
> populating VADRMPRIMESurfaceDescriptor:
>
> - Verifying that plane pitch is at least the minimum required stride.
> - Ensuring that the plane's memory range (offset + size) stays within
>   the bounds of the DMA-buf.
>
> Bug: 495819067
> Test: webrtc.RTCPeerConnection.*
> Change-Id: I5d27d3a21ab692367931406e9c345a4a61ee2bb6
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7834744
> Reviewed-by: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
> Commit-Queue: Kai Ninomiya <kainino@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1631660}

Bug: 495819067
Test: video_decode_accelerator_tests
Change-Id: Ie3b60a2e066c86b6bff7e85b115f90d9d8a3c259
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7855566
Commit-Queue: Hirokazu Honda <hiroh@chromium.org>
Reviewed-by: Nathan Hebert <nhebert@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632269}
---

diff --git a/media/gpu/vaapi/BUILD.gn b/media/gpu/vaapi/BUILD.gn
index 1d1e0a7..0dae8908 100644
--- a/media/gpu/vaapi/BUILD.gn
+++ b/media/gpu/vaapi/BUILD.gn
@@ -145,6 +145,7 @@
     "//base",
     "//build/config/linux/libdrm",
     "//media",
+    "//media/gpu:buffer_validation",
     "//media/gpu:common",
     "//media/gpu/chromeos:fourcc",
     "//ui/gfx/geometry",
diff --git a/media/gpu/vaapi/vaapi_wrapper.cc b/media/gpu/vaapi/vaapi_wrapper.cc
index 519517d1..b5bc79b 100644
--- a/media/gpu/vaapi/vaapi_wrapper.cc
+++ b/media/gpu/vaapi/vaapi_wrapper.cc
@@ -51,12 +51,14 @@
 #include "base/version.h"
 #include "build/build_config.h"
 #include "gpu/config/gpu_info.h"
+#include "media/base/format_utils.h"
 #include "media/base/limits.h"
 #include "media/base/media_switches.h"
 #include "media/base/platform_features.h"
 #include "media/base/video_codecs.h"
 #include "media/base/video_frame.h"
 #include "media/base/video_types.h"
+#include "media/gpu/buffer_validation.h"
 #include "media/gpu/chromeos/frame_resource.h"
 #include "media/gpu/macros.h"
 // Auto-generated for dlopen libva libraries
@@ -427,6 +429,66 @@
          base::FeatureList::IsEnabled(media::kGlobalVaapiLock);
 }
 
+bool ValidateAndGetPlaneInfo(const gfx::NativePixmap& pixmap,
+                             const media::VideoPixelFormat format,
+                             const gfx::Size& resolution,
+                             const int dma_buf_fd,
+                             const size_t plane_index,
+                             uint32_t& dmabuf_size,
+                             uint32_t& plane_offset,
+                             uint32_t& plane_pitch) {
+  size_t dmabuf_size_sz = 0;
+  if (!media::GetFileSize(dma_buf_fd, &dmabuf_size_sz)) {
+    LOG(ERROR) << "Failed to get the size of the dma-buf";
+    return false;
+  }
+  if (!base::IsValueInRangeForNumericType<uint32_t>(dmabuf_size_sz)) {
+    LOG(ERROR) << "Invalid data size: " << dmabuf_size_sz;
+    return false;
+  }
+  dmabuf_size = static_cast<uint32_t>(dmabuf_size_sz);
+
+  const size_t plane_offset_sz = pixmap.GetDmaBufOffset(plane_index);
+  if (!base::IsValueInRangeForNumericType<uint32_t>(plane_offset_sz)) {
+    LOG(ERROR) << "Invalid plane offset: " << plane_offset_sz;
+    return false;
+  }
+  plane_offset = static_cast<uint32_t>(plane_offset_sz);
+
+  plane_pitch = pixmap.GetDmaBufPitch(plane_index);
+  const size_t min_stride =
+      media::VideoFrame::RowBytes(plane_index, format, resolution.width());
+  if (base::saturated_cast<size_t>(plane_pitch) < min_stride) {
+    LOG(ERROR) << "Invalid stride for plane " << plane_index << ": "
+               << plane_pitch << " < " << min_stride;
+    return false;
+  }
+  const size_t plane_height =
+      media::VideoFrame::Rows(plane_index, format, resolution.height());
+  base::CheckedNumeric<size_t> min_plane_size =
+      base::CheckMul(plane_pitch, plane_height);
+  if (!min_plane_size.IsValid()) {
+    LOG(ERROR) << "Invalid plane size for plane " << plane_index;
+    return false;
+  }
+
+  base::CheckedNumeric<uint64_t> min_buffer_size =
+      base::CheckAdd(plane_offset, min_plane_size.ValueOrDie());
+  if (!min_buffer_size.IsValid()) {
+    LOG(ERROR) << "Invalid buffer size for plane " << plane_index;
+    return false;
+  }
+
+  if (min_buffer_size.ValueOrDie() >
+      base::checked_cast<uint64_t>(dmabuf_size)) {
+    LOG(ERROR) << "Plane " << plane_index << " is out of bounds: "
+               << static_cast<uint64_t>(min_buffer_size.ValueOrDie()) << " > "
+               << dmabuf_size;
+    return false;
+  }
+  return true;
+}
+
 bool FillVADRMPRIMESurfaceDescriptor(const gfx::NativePixmap& pixmap,
                                      VADRMPRIMESurfaceDescriptor& descriptor) {
   UNSAFE_TODO(memset(&descriptor, 0, sizeof(VADRMPRIMESurfaceDescriptor)));
@@ -443,6 +505,14 @@
     LOG(ERROR) << "Failed to get the DRM format from the buffer format";
     return false;
   }
+
+  const std::optional<media::VideoPixelFormat> format =
+      media::SharedImageFormatToVideoPixelFormat(shared_image_format);
+  if (!format) {
+    LOG(ERROR) << "Failed to get the VideoPixelFormat from the buffer format";
+    return false;
+  }
+
   if (num_planes > std::size(descriptor.objects)) {
     LOG(ERROR) << "Too many planes in the NativePixmap; got " << num_planes
                << " but the maximum number is "
@@ -476,32 +546,23 @@
       LOG(ERROR) << "Failed to get dmabuf from a NativePixmap";
       return false;
     }
-    const off_t data_size = lseek(dma_buf_fd, /*offset=*/0, SEEK_END);
-    if (data_size == static_cast<off_t>(-1)) {
-      PLOG(ERROR) << "Failed to get the size of the dma-buf";
-      return false;
-    }
-    if (lseek(dma_buf_fd, /*offset=*/0, SEEK_SET) == static_cast<off_t>(-1)) {
-      PLOG(ERROR) << "Failed to reset the file offset of the dma-buf";
+    uint32_t plane_offset = 0u;
+    uint32_t plane_pitch = 0u;
+    uint32_t dmabuf_size = 0u;
+    if (!ValidateAndGetPlaneInfo(pixmap, *format, size, dma_buf_fd, i,
+                                 dmabuf_size, plane_offset, plane_pitch)) {
       return false;
     }
 
     UNSAFE_TODO(descriptor.objects[i]).fd = dma_buf_fd;
-    UNSAFE_TODO(descriptor.objects[i]).size =
-        base::checked_cast<uint32_t>(data_size);
+    UNSAFE_TODO(descriptor.objects[i]).size = dmabuf_size;
     UNSAFE_TODO(descriptor.objects[i]).drm_format_modifier =
         pixmap.GetFormatModifier();
 
     UNSAFE_TODO(descriptor.layers[0].object_index[i]) =
         base::checked_cast<uint32_t>(i);
-    if (!base::IsValueInRangeForNumericType<uint32_t>(
-            pixmap.GetDmaBufOffset(i))) {
-      LOG(ERROR) << "The offset for plane " << i << " is out-of-range";
-      return false;
-    }
-    UNSAFE_TODO(descriptor.layers[0].offset[i]) =
-        base::checked_cast<uint32_t>(pixmap.GetDmaBufOffset(i));
-    UNSAFE_TODO(descriptor.layers[0].pitch[i]) = pixmap.GetDmaBufPitch(i);
+    UNSAFE_TODO(descriptor.layers[0].offset[i]) = plane_offset;
+    UNSAFE_TODO(descriptor.layers[0].pitch[i]) = plane_pitch;
   }
 
   return true;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU OOB access via unvalidated dmabuf dimensions in SharedImageVideoFrameData

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can supply inconsistent dmabuf dimensions and strides via SharedImageVideoFrameData during video encoding. The GPU process fails to validate these dimensions against the actual dmabuf file size in release builds before passing them to the VA-API driver. This can lead to out-of-bounds memory access by the driver or GPU hardware, potentially allowing a GPU sandbox escape.

Affected files:

  • media/gpu/vaapi/vaapi_wrapper.cc
  • media/mojo/mojom/video_frame_mojom_traits.cc
  • media/mojo/services/mojo_video_encode_accelerator_service.cc
  • media/gpu/chromeos/platform_video_frame_utils.cc
  • media/gpu/buffer_validation.cc
  • ui/gfx/mojom/native_handle_types_mojom_traits.cc

Estimated timestamp from git blame: 2026-01-21

Description

There is a potential vulnerability in the GPU process where it fails to properly validate the dimensions, strides, and offsets of a DMA buffer (dmabuf) provided by the renderer via a SharedImageVideoFrameData Mojo message. While the is_dmabuf_data deserialization path rigorously checks that the provided planes fit within the actual file descriptor size, the is_shared_image_data path lacks these critical checks.

This allows a compromised renderer to pass a small, valid dmabuf but forge excessively large stride, offset, and dimension metadata. This metadata propagates through the media stack and is ultimately passed to third-party VA-API drivers (such as Intel iHD or Mesa Gallium) via vaCreateSurfaces. If the driver or GPU hardware trusts these dimensions, it will perform out-of-bounds memory accesses, potentially leading to memory corruption within the GPU process and a renderer-to-GPU sandbox escape.

Technical Details

  1. Deserialization bypasses validation: In media/mojo/mojom/video_frame_mojom_traits.cc, when a VideoFrame is deserialized and hits the is_shared_image_data() path, it extracts an ExportedSharedImage and calls VideoFrame::WrapMappableSharedImage. In media/base/video_frame.cc, this function blindly copies the stride, offset, and size from the GpuMemoryBufferHandle’s NativePixmapHandle into the frame’s ColorPlaneLayout. Crucially, it never compares these values against the actual physical size of the DMA buffer FD, unlike the media::GetFileSize check performed in the is_dmabuf_data() path.
  2. Encode size check bypassed: In media/mojo/services/mojo_video_encode_accelerator_service.cc, MojoVideoEncodeAcceleratorService::Encode explicitly bypasses coded_size consistency checks if the frame is a MappableSharedImage (!frame->HasMappableSharedImage() && !frame->HasSharedImage()).
  3. Ineffective defense-in-depth: Later in the pipeline, CreateNativePixmapDmaBuf (in media/gpu/chromeos/platform_video_frame_utils.cc) calls CreateGpuMemoryBufferHandle, which invokes VerifyGpuMemoryBufferHandle. While this function correctly checks the dmabuf size using GetFileSize, it is entirely wrapped in an #if DCHECK_IS_ON() block, leaving release builds unprotected. Furthermore, even in debug builds, a failure only triggers a DLOG_IF(WARNING) and does not reject the invalid handle.
  4. Driver exploitation: The frame reaches VaapiVideoEncodeAccelerator::CreateSurfacesForMappableSIEncoding, which extracts the unvalidated NativePixmap and passes it to VaapiWrapper::CreateVASurfaceForPixmap. In media/gpu/vaapi/vaapi_wrapper.cc (FillVADRMPRIMESurfaceDescriptor), the code queries the true dmabuf size via lseek(dma_buf_fd, 0, SEEK_END). However, it still blindly copies the attacker-controlled width, height, pitch (stride), and offset into the VADRMPRIMESurfaceDescriptor and passes it to vaCreateSurfaces.

Potential Attacker Steps

Note: Fortify LLM agent does not currently have the ability to run code, so these are suggested steps to trigger the vulnerability based on static analysis.

  1. Compromise the Renderer: An attacker first gains arbitrary code execution in the Renderer process.
  2. Allocate a small buffer: The attacker allocates a legitimately sized, small DMA buffer.
  3. Forge Mojo Metadata: The attacker constructs a malicious media::mojom::VideoFrame using the SharedImageVideoFrameData path. They embed the valid FD but forge the ExportedSharedImage metadata to contain a massive coded_size, along with enormous stride and offset values for the planes.
  4. Trigger Video Encode: The attacker sends this frame to the GPU process via WebRTC or WebCodecs hardware encoding paths (media::mojom::VideoEncodeAccelerator::Encode).
  5. Achieve OOB Access: The GPU process accepts the frame, bypasses dimension checks, and instructs the VA-API driver to create a surface using the massive dimensions backed by the small FD. The driver configures the GPU (or uses a CPU fallback) to process the frame, resulting in an out-of-bounds read/write into GPU process memory, which can be leveraged for a sandbox escape.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker