CVE-2026-10911
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/gpu/vaapi/vaapi_wrapper.cc |
modified |
Files Changed
media/gpu/vaapi/BUILD.gnmedia/gpu/vaapi/vaapi_wrapper.cc
Patch
From caa9eb0fc5dacaa21a90eff43c093906e43d8c1a Mon Sep 17 00:00:00 2001 From: Hirokazu Honda <[email protected]> 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 <[email protected]> > Commit-Queue: Kai Ninomiya <[email protected]> > 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 <[email protected]> Reviewed-by: Nathan Hebert <[email protected]> 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;
Original Bug Report
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.ccmedia/mojo/mojom/video_frame_mojom_traits.ccmedia/mojo/services/mojo_video_encode_accelerator_service.ccmedia/gpu/chromeos/platform_video_frame_utils.ccmedia/gpu/buffer_validation.ccui/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
- Deserialization bypasses validation: In
media/mojo/mojom/video_frame_mojom_traits.cc, when aVideoFrameis deserialized and hits theis_shared_image_data()path, it extracts anExportedSharedImageand callsVideoFrame::WrapMappableSharedImage. Inmedia/base/video_frame.cc, this function blindly copies thestride,offset, andsizefrom theGpuMemoryBufferHandle’sNativePixmapHandleinto the frame’sColorPlaneLayout. Crucially, it never compares these values against the actual physical size of the DMA buffer FD, unlike themedia::GetFileSizecheck performed in theis_dmabuf_data()path. - Encode size check bypassed: In
media/mojo/services/mojo_video_encode_accelerator_service.cc,MojoVideoEncodeAcceleratorService::Encodeexplicitly bypassescoded_sizeconsistency checks if the frame is aMappableSharedImage(!frame->HasMappableSharedImage() && !frame->HasSharedImage()). - Ineffective defense-in-depth: Later in the pipeline,
CreateNativePixmapDmaBuf(inmedia/gpu/chromeos/platform_video_frame_utils.cc) callsCreateGpuMemoryBufferHandle, which invokesVerifyGpuMemoryBufferHandle. While this function correctly checks the dmabuf size usingGetFileSize, it is entirely wrapped in an#if DCHECK_IS_ON()block, leaving release builds unprotected. Furthermore, even in debug builds, a failure only triggers aDLOG_IF(WARNING)and does not reject the invalid handle. - Driver exploitation: The frame reaches
VaapiVideoEncodeAccelerator::CreateSurfacesForMappableSIEncoding, which extracts the unvalidatedNativePixmapand passes it toVaapiWrapper::CreateVASurfaceForPixmap. Inmedia/gpu/vaapi/vaapi_wrapper.cc(FillVADRMPRIMESurfaceDescriptor), the code queries the true dmabuf size vialseek(dma_buf_fd, 0, SEEK_END). However, it still blindly copies the attacker-controlledwidth,height,pitch(stride), andoffsetinto theVADRMPRIMESurfaceDescriptorand passes it tovaCreateSurfaces.
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.
- Compromise the Renderer: An attacker first gains arbitrary code execution in the Renderer process.
- Allocate a small buffer: The attacker allocates a legitimately sized, small DMA buffer.
- Forge Mojo Metadata: The attacker constructs a malicious
media::mojom::VideoFrameusing theSharedImageVideoFrameDatapath. They embed the valid FD but forge theExportedSharedImagemetadata to contain a massivecoded_size, along with enormousstrideandoffsetvalues for the planes. - Trigger Video Encode: The attacker sends this frame to the GPU process via WebRTC or WebCodecs hardware encoding paths (
media::mojom::VideoEncodeAccelerator::Encode). - 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.