Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Media
DescriptionOut of bounds read in Media
ComponentMedia
Bug ClassOOB
Tracker536428988
Fix commit4f2bb3f790c6 (chromium/src) +23/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

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

Files Changed

  • media/gpu/vaapi/vaapi_wrapper.cc
From 4f2bb3f790c6898f6a4065e0e6d65c437826775d Mon Sep 17 00:00:00 2001
From: Ted Meyer <tmathmeyer@chromium.org>
Date: Mon, 27 Jul 2026 09:56:32 -0700
Subject: [PATCH] Use ValidateAndGetPlaneInfo for external buffers

A previous patch introduced this helper and made use of it in the
FillVADRMPRIMESurfaceDescriptor function, but we should also be using it
in the FillVASurfaceAttribExternalBuffers for similar purposes.

Fixed: 536428988
Change-Id: I1bcc16a56c1170ab99c5eed37c9119a51d5ec4db
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8144537
Reviewed-by: Frank Liberato <liberato@chromium.org>
Auto-Submit: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
Commit-Queue: Frank Liberato <liberato@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1668750}
---

diff --git a/media/gpu/vaapi/vaapi_wrapper.cc b/media/gpu/vaapi/vaapi_wrapper.cc
index 22a791f..94a78b9a 100644
--- a/media/gpu/vaapi/vaapi_wrapper.cc
+++ b/media/gpu/vaapi/vaapi_wrapper.cc
@@ -600,33 +600,38 @@
                << std::size(va_attrib_extbuf.pitches);
     return false;
   }
-  for (size_t i = 0; i < num_planes; ++i) {
-    UNSAFE_TODO(va_attrib_extbuf.pitches[i]) = pixmap.GetDmaBufPitch(i);
-    UNSAFE_TODO(va_attrib_extbuf.offsets[i]) =
-        base::checked_cast<uint32_t>(pixmap.GetDmaBufOffset(i));
-    DVLOG(4) << "plane " << i
-             << ": pitch: " << UNSAFE_TODO(va_attrib_extbuf.pitches[i])
-             << " offset: " << UNSAFE_TODO(va_attrib_extbuf.offsets[i]);
-  }
-  va_attrib_extbuf.num_planes = base::checked_cast<uint32_t>(num_planes);
 
+  // This legacy DRM_PRIME path collapses all planes onto fd[0] with
+  // num_buffers=1, so every plane's (offset + pitch*rows) must fit inside
+  // fd[0] regardless of what per-plane fd the pixmap carries. Validate each
+  // plane against fd[0].
   const int dma_buf_fd = pixmap.GetDmaBufFd(0);
   if (dma_buf_fd < 0) {
     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";
+
+  const auto format =
+      media::SharedImageFormatToVideoPixelFormat(shared_image_format);
+  if (!format) {
+    LOG(ERROR) << "Failed to get the VideoPixelFormat from the buffer format";
     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";
-    return false;
+
+  uint32_t fd0_size = 0u;
+  for (size_t i = 0; i < num_planes; ++i) {
+    uint32_t plane_offset = 0u;
+    uint32_t plane_pitch = 0u;
+    if (!ValidateAndGetPlaneInfo(pixmap, *format, size,
+                                 /*dma_buf_fd=*/dma_buf_fd, i, fd0_size,
+                                 plane_offset, plane_pitch)) {
+      return false;
+    }
+    UNSAFE_TODO(va_attrib_extbuf.pitches[i]) = plane_pitch;
+    UNSAFE_TODO(va_attrib_extbuf.offsets[i]) = plane_offset;
   }
-  // If the data size doesn't fit in a uint32_t, we probably have bigger
-  // problems.
-  va_attrib_extbuf.data_size = base::checked_cast<uint32_t>(data_size);
+  va_attrib_extbuf.num_planes = base::checked_cast<uint32_t>(num_planes);
+  va_attrib_extbuf.data_size = fd0_size;
 
   // We only have to pass the first file descriptor to a driver. A VA-API driver
   // shall create a VASurface from the single fd correctly.
Loading diff…

Original Bug Report

reported by aw...@chromium.org

Potential multi-fd collapse in VaapiWrapper allows GPU OOB read

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 FillVASurfaceAttribExternalBuffers function collapses multiple plane file descriptors onto fd[0] but copies per-plane offsets without validating them against fd[0]’s size. A compromised renderer can bypass upstream validators and force this unpatched branch using modifier = kNoModifier. This potentially leads to an out-of-bounds read of GPU memory inside the sandboxed GPU process.

Affected files:

  • media/gpu/vaapi/vaapi_wrapper.cc
  • media/gpu/buffer_validation.cc
  • media/mojo/mojom/video_frame_mojom_traits.cc

Estimated timestamp from git blame: 2022-06-17

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

A potential vulnerability exists in the VA-API video encode path where a compromised renderer can force an out-of-bounds (OOB) memory read in the sandboxed GPU process (Linux/ChromeOS). The underlying issue is an incomplete fix for b/495839810. The original fix introduced ValidateAndGetPlaneInfo to check buffer sizes and plane bounds, but it was only applied to the FillVADRMPRIMESurfaceDescriptor function. Its sibling, FillVASurfaceAttribExternalBuffers (the legacy VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME branch), remains unpatched.

FillVASurfaceAttribExternalBuffers copies renderer-supplied per-plane offset and pitch values into the VA-API attributes structure but collapses all planes onto a single file descriptor (fd[0]). Upstream validators, such as VerifyGpuMemoryBufferHandle in media/gpu/buffer_validation.cc, validate each plane’s offset and size strictly against its own respective plane file descriptor.

A compromised renderer can potentially bypass these upstream checks by providing a valid small file descriptor (FD) for plane 0 and a valid large FD for plane 1, along with a large offset. It can then reliably steer execution into the unpatched FillVASurfaceAttribExternalBuffers branch by setting the format modifier to gfx::NativePixmapHandle::kNoModifier. When the VA-API driver imports only fd[0] (the small buffer) but interprets the offset for the second plane using the unvalidated, out-of-bounds offset, it performs an OOB read of adjacent GPU memory. This sensitive cross-origin GPU data could then be leaked into the encoded bitstream returned to the renderer.

Note: The steps described below are a potential execution path. Our tooling agent does not have the ability to run code or execute a live proof-of-concept.

Suggested Fix: To fix the vulnerability, FillVASurfaceAttribExternalBuffers must loop through and validate every plane’s offset and pitch against the file size of fd[0]. This can be achieved by calling ValidateAndGetPlaneInfo(pixmap, *format, size, dma_buf_fd, i, dmabuf_size, plane_offset, plane_pitch) for every plane i (from 0 to num_planes - 1), passing the first file descriptor dma_buf_fd = pixmap.GetDmaBufFd(0) as the argument for all iterations.

2. Proof-of-Concept & Detailed Execution Flow

The potential execution flow from the attacker’s entry point to the code sink is traced below:

  1. Attacker Setup: A compromised renderer on ChromeOS or Linux allocates two distinct real dmabufs (Direct Memory Access buffers): bo_small (e.g., ~4 KiB) and bo_large (e.g., ~1 MiB).
  2. Mojo Connection: The renderer binds the media.mojom.VideoEncodeAcceleratorProvider interface and invokes CreateVideoEncodeAccelerator(), followed by Initialize() with storage_type set to kGpuMemoryBuffer. This sets native_input_mode_ = true (media/gpu/vaapi/vaapi_video_encode_accelerator.cc:322).
  3. Payload Construction: The renderer prepares an Encode IPC message. It forges a media.mojom.VideoFrame with SharedImageVideoFrameData and explicitly sets is_mappable = true.
  4. Handle Forgery: Inside the data, it crafts an ExportedSharedImage containing a buffer_handle of type gfx::NATIVE_PIXMAP. The attacker configures the NativePixmapHandle with modifier = gfx::NativePixmapHandle::kNoModifier (0x00ffffffffffffffULL).
  5. Plane Forgery: The attacker sets plane[0] to point to the small buffer (fd = bo_small, offset = 0, size = 4096) and plane[1] to point to the large buffer with a large offset (fd = bo_large, offset = 524288, size = 2048).
  6. Deserialization & First Validator Bypass: The GPU process deserializes the frame via StructTraits in media/mojo/mojom/video_frame_mojom_traits.cc:414-461. Because is_mappable is true, it extracts the handle and calls media::VerifyGpuMemoryBufferHandle (video_frame_mojom_traits.cc:449).
  7. In media/gpu/buffer_validation.cc:96-150, VerifyGpuMemoryBufferHandle iterates through the planes and validates bounds per plane against each plane’s own file descriptor using GetFileSize(plane.fd.get(), &file_size_in_bytes).
    • For plane[0]: 0 + 4096 <= lseek(bo_small, 0, SEEK_END) (Passes).
    • For plane[1]: 524288 + 2048 <= lseek(bo_large, 0, SEEK_END) (Passes). The validator completes successfully without verifying if multiple planes use distinct FDs or cross-checking offsets[1] against plane[0].fd’s size.
  8. Service Handling: The VideoFrame is created. MojoVideoEncodeAcceleratorService::Encode accepts the frame because frame->HasMappableSharedImage() returns true (media/mojo/services/mojo_video_encode_accelerator_service.cc:204).
  9. Second Validator Bypass: Execution reaches VaapiVideoEncodeAccelerator::EncodeTask. The frame passes the storage-type gate (vaapi_video_encode_accelerator.cc:591-595) and proceeds to CreateSurfacesForMappableSIEncoding. At line 641, CreateNativePixmapDmaBuf(&frame) extracts the underlying native buffer, deeply copying the forged FDs, offsets, and modifier. It calls VerifyGpuMemoryBufferHandle a second time (media/gpu/chromeos/platform_video_frame_utils.cc:545), which passes for the same per-plane reason. A gfx::NativePixmapDmaBuf is instantiated.
  10. Branch Steering: vaapi_wrapper_->CreateVASurfaceForPixmap(std::move(pixmap)) is invoked. Inside VaapiWrapper::CreateVASurfaceForPixmap (media/gpu/vaapi/vaapi_wrapper.cc:2608), the branch condition is evaluated:
    const bool use_drm_prime_2 = 
        (...) && pixmap->GetFormatModifier() != gfx::NativePixmapHandle::kNoModifier;
    
    Since the attacker set the modifier to kNoModifier, use_drm_prime_2 evaluates to false, steering execution into the else block calling the unpatched FillVASurfaceAttribExternalBuffers (vaapi_wrapper.cc:2624).
  11. Multi-FD Collapse: In FillVASurfaceAttribExternalBuffers (vaapi_wrapper.cc:576), a for loop blindly copies the offsets:
    for (size_t i = 0; i < num_planes; ++i) {
      UNSAFE_TODO(va_attrib_extbuf.pitches[i]) = pixmap.GetDmaBufPitch(i);
      UNSAFE_TODO(va_attrib_extbuf.offsets[i]) = base::checked_cast<uint32_t>(pixmap.GetDmaBufOffset(i)); 
    }
    
    The function retrieves only the first plane’s FD (bo_small) and calculates data_size based solely on it:
    const int dma_buf_fd = pixmap.GetDmaBufFd(0);
    const off_t data_size = lseek(dma_buf_fd, /*offset=*/0, SEEK_END);
    
  12. OOB Trigger: The structure is finalized with num_buffers = 1u and buffers = &fd pointing solely to bo_small (vaapi_wrapper.cc:633-635), without calling ValidateAndGetPlaneInfo or ensuring offsets[i] < data_size.
  13. The VA-API driver is called via vaCreateSurfaces with VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME. It imports the single buffer (bo_small). When sampling the NV12 UV plane, the driver calculates the read address using the base mapped address of bo_small plus offsets[1] (524288). This causes a read ~520 KiB past its bounds into adjacent GPU-mapped memory, leaking sensitive data into the encoded bitstream.

3. Technical Verification Details (Automated Audit Logs)

> The vulnerability report describes a valid memory-safety issue (GPU-memory-resident OOB read) reachable from a compromised renderer. The report correctly identifies that FillVASurfaceAttribExternalBuffers collapses all plane FDs onto fd[0] but copies per-plane offsets without validating them against fd[0]’s size. Upstream validators (like VerifyGpuMemoryBufferHandle) only check plane[i].offset + plane[i].size <= file_size(plane[i].fd), allowing an attacker to pass validation by supplying a large FD for plane[1] while passing a small FD for plane[0]. The renderer can force execution of the unpatched FillVASurfaceAttribExternalBuffers branch by setting modifier = kNoModifier in the forged NativePixmapHandle. This causes the VAAPI driver to read OOB from the small BO. As the vulnerability requires a compromised renderer, occurs in the sandboxed GPU process (VAAPI is Linux/ChromeOS only), and results in a GPU-memory-resident OOB read (rather than a write), it falls under the Medium severity (S2) category per the GPU validating-layer gap rule for read-only consequences.

The validator confirmed through source code investigation that:

  • FillVASurfaceAttribExternalBuffers at media/gpu/vaapi/vaapi_wrapper.cc:576-640 executes without verifying the alignment of plane offsets relative to the size of fd[0], nor does it invoke ValidateAndGetPlaneInfo (which was applied exclusively to FillVADRMPRIMESurfaceDescriptor to fix b/495839810).
  • The upstream validation logic inside VerifyGpuMemoryBufferHandle (media/gpu/buffer_validation.cc:96-148) evaluates bounds constraints solely on a per-plane basis using GetFileSize(plane.fd.get(), &file_size_in_bytes). The validator confirmed that this logic is entirely blind to the subsequent multi-FD collapse inside FillVASurfaceAttribExternalBuffers.
  • The execution path is reachable via SharedImageVideoFrameData with is_mappable = true. The validator confirmed that MojoVideoEncodeAcceleratorService::Encode and VaapiVideoEncodeAccelerator::EncodeTask storage-type gates accept Mappable SharedImage frames.
  • The format modifier is a user-supplied variable within gfx::NativePixmapHandle, serialized over IPC (ui/gfx/mojom/native_handle_types.mojom). The validator confirmed that ClientSharedImage::CloneGpuMemoryBufferHandle successfully deep-copies the FDs, offsets, and modifier without dropping them. Setting modifier = kNoModifier guarantees use_drm_prime_2 = false, steering into the unpatched sink.

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