CVE-2026-79004
Overview
Background
- VA-API external buffers
VASurfaceAttribExternalBuffersdescribes caller-owned memory (here a dma-buf) to a VA-API driver: per-planepitches[]andoffsets[],num_planes, the backing fd(s), and the totaldata_size. The driver then reads or writes pixel data at those offsets.- `NativePixmap` dma-buf metadata
- a
NativePixmapcarries a per-plane fd, offset (GetDmaBufOffset) and pitch (GetDmaBufPitch). These values are metadata attached to the buffer and the dma-buf may not actually be large enough for them. - Legacy DRM_PRIME single-fd path
- as the new comment explains,
FillVASurfaceAttribExternalBufferspasses onlyfd[0]withnum_buffers=1, so every plane’soffset + pitch*rowsmust fall insidefd[0]whatever per-plane fd the pixmap reports. - `ValidateAndGetPlaneInfo`
- a helper added by an earlier patch and already used by
FillVADRMPRIMESurfaceDescriptor. From its use here, it validates one plane’s offset/pitch for a given pixel format and size against a dma-buf fd and returns the checkedplane_offset,plane_pitchand the fd size (fd0_size). Its body is not in this diff.
Root Cause Analysis
In media/gpu/vaapi/vaapi_wrapper.cc, the code that fills va_attrib_extbuf (named FillVASurfaceAttribExternalBuffers in the commit message; the hunk itself does not show the function signature) copied each plane’s GetDmaBufPitch(i) and GetDmaBufOffset(i) straight into pitches[i]/offsets[i]. The only size information it gathered was the total size of fd[0] via lseek(..., SEEK_END), which it stored in data_size. Nothing checked that a plane’s offset plus its pitch times its row count actually fits within fd[0]. Because this path collapses all planes onto fd[0], a pixmap whose plane offsets or pitches exceed the dma-buf, or that point past fd[0] for a plane really backed by a different fd, made the VA-API driver access memory beyond the end of the buffer.
The fix converts the SharedImageFormat to a VideoPixelFormat (failing if unsupported) and calls ValidateAndGetPlaneInfo for each plane against dma_buf_fd (= fd[0]). It returns false before any attribute is filled if a plane does not fit, and uses the validated plane_offset/plane_pitch and the helper-derived fd0_size instead of raw pixmap values and the lseek result. This is a tightening: the same validation was already applied to the sibling FillVADRMPRIMESurfaceDescriptor path.
NativePixmap without bounding them against the one dma-buf (fd[0]) it actually passes to the driver. The fix routes every plane through the same ValidateAndGetPlaneInfo bounds check that the DRM PRIME descriptor path already used.Attack Path
- Inconsistent pixmap
a
NativePixmapreaches the VA-API wrapper with plane offsets/pitches that, measured againstfd[0], extend past the end of that dma-buf. For example, the planes may really live in separate fds or the metadata may be oversized. How such a pixmap is produced (e.g. by a less-trusted client supplying buffer metadata) is not shown in the diff (inferred). - Surface import via external buffers the GPU/media code imports the pixmap as a VA surface through the legacy external-buffers path on a Linux/ChromeOS build using VA-API hardware video acceleration.
- Unchecked attributes handed to the driver
before the fix, the raw
pitches[]/offsets[]were written intova_attrib_extbufwith onlyfd[0]attached and no per-plane bounds check. - Out-of-bounds access
when the driver decodes into, encodes from or processes the surface, it reads plane data beyond the end of
fd[0], which is the out-of-bounds read the advisory reports. Disclosure of adjacent memory or a crash is a possible consequence (inferred).
Impact Assessment
NativePixmap whose plane layout does not fit fd[0]. Whether web content alone can produce such a pixmap or whether a compromised renderer or other client is needed is not shown by the patch (inferred). Information disclosure from GPU-process or device memory is a plausible consequence (inferred). No exploitation-in-the-wild status is given.Changed Functions
| Function | Change | Notes |
|---|---|---|
formedia/gpu/vaapi/vaapi_wrapper.cc |
modified | |
ifmedia/gpu/vaapi/vaapi_wrapper.cc |
modified |
Files Changed
media/gpu/vaapi/vaapi_wrapper.cc
Audit Directions
- Single-fd collapse of multi-plane bufferslook for other dma-buf import paths (VA-API, V4L2, EGL/
EGL_LINUX_DMA_BUF, Vulkan external memory) that pass only plane 0’s fd but use per-plane offsets taken from the pixmap without checking them against that fd’s size. - Sibling paths missing a hardening helperwhen a validation helper such as
ValidateAndGetPlaneInfois added to one function, grep for the other functions that build the same driver structures fromGetDmaBufOffset/GetDmaBufPitchand check that they use it too. - Total-size checks without per-plane boundsa valid overall
data_size(e.g. fromlseek) does not prove thatoffset + pitch*rowsfits for each plane, so audit code that records the buffer size but never compares plane extents against it.
Patch
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.
Original Bug Report
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.ccmedia/gpu/buffer_validation.ccmedia/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:
- Attacker Setup: A compromised renderer on ChromeOS or Linux allocates two distinct real dmabufs (Direct Memory Access buffers):
bo_small(e.g., ~4 KiB) andbo_large(e.g., ~1 MiB). - Mojo Connection: The renderer binds the
media.mojom.VideoEncodeAcceleratorProviderinterface and invokesCreateVideoEncodeAccelerator(), followed byInitialize()withstorage_typeset tokGpuMemoryBuffer. This setsnative_input_mode_ = true(media/gpu/vaapi/vaapi_video_encode_accelerator.cc:322). - Payload Construction: The renderer prepares an
EncodeIPC message. It forges amedia.mojom.VideoFramewithSharedImageVideoFrameDataand explicitly setsis_mappable = true. - Handle Forgery: Inside the data, it crafts an
ExportedSharedImagecontaining abuffer_handleof typegfx::NATIVE_PIXMAP. The attacker configures theNativePixmapHandlewithmodifier = gfx::NativePixmapHandle::kNoModifier(0x00ffffffffffffffULL). - Plane Forgery: The attacker sets
plane[0]to point to the small buffer (fd = bo_small,offset = 0,size = 4096) andplane[1]to point to the large buffer with a large offset (fd = bo_large,offset = 524288,size = 2048). - Deserialization & First Validator Bypass: The GPU process deserializes the frame via
StructTraitsinmedia/mojo/mojom/video_frame_mojom_traits.cc:414-461. Becauseis_mappableis true, it extracts the handle and callsmedia::VerifyGpuMemoryBufferHandle(video_frame_mojom_traits.cc:449). - In
media/gpu/buffer_validation.cc:96-150,VerifyGpuMemoryBufferHandleiterates through the planes and validates bounds per plane against each plane’s own file descriptor usingGetFileSize(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-checkingoffsets[1]againstplane[0].fd’s size.
- For
- Service Handling: The
VideoFrameis created.MojoVideoEncodeAcceleratorService::Encodeaccepts the frame becauseframe->HasMappableSharedImage()returnstrue(media/mojo/services/mojo_video_encode_accelerator_service.cc:204). - Second Validator Bypass: Execution reaches
VaapiVideoEncodeAccelerator::EncodeTask. The frame passes the storage-type gate (vaapi_video_encode_accelerator.cc:591-595) and proceeds toCreateSurfacesForMappableSIEncoding. At line 641,CreateNativePixmapDmaBuf(&frame)extracts the underlying native buffer, deeply copying the forged FDs, offsets, and modifier. It callsVerifyGpuMemoryBufferHandlea second time (media/gpu/chromeos/platform_video_frame_utils.cc:545), which passes for the same per-plane reason. Agfx::NativePixmapDmaBufis instantiated. - Branch Steering:
vaapi_wrapper_->CreateVASurfaceForPixmap(std::move(pixmap))is invoked. InsideVaapiWrapper::CreateVASurfaceForPixmap(media/gpu/vaapi/vaapi_wrapper.cc:2608), the branch condition is evaluated:Since the attacker set the modifier toconst bool use_drm_prime_2 = (...) && pixmap->GetFormatModifier() != gfx::NativePixmapHandle::kNoModifier;kNoModifier,use_drm_prime_2evaluates tofalse, steering execution into theelseblock calling the unpatchedFillVASurfaceAttribExternalBuffers(vaapi_wrapper.cc:2624). - Multi-FD Collapse: In
FillVASurfaceAttribExternalBuffers(vaapi_wrapper.cc:576), aforloop blindly copies the offsets:The function retrieves only the first plane’s FD (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)); }bo_small) and calculatesdata_sizebased solely on it:const int dma_buf_fd = pixmap.GetDmaBufFd(0); const off_t data_size = lseek(dma_buf_fd, /*offset=*/0, SEEK_END); - OOB Trigger: The structure is finalized with
num_buffers = 1uandbuffers = &fdpointing solely tobo_small(vaapi_wrapper.cc:633-635), without callingValidateAndGetPlaneInfoor ensuringoffsets[i] < data_size. - The VA-API driver is called via
vaCreateSurfaceswithVA_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 ofbo_smallplusoffsets[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:
FillVASurfaceAttribExternalBuffersatmedia/gpu/vaapi/vaapi_wrapper.cc:576-640executes without verifying the alignment of plane offsets relative to the size offd[0], nor does it invokeValidateAndGetPlaneInfo(which was applied exclusively toFillVADRMPRIMESurfaceDescriptorto 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 usingGetFileSize(plane.fd.get(), &file_size_in_bytes). The validator confirmed that this logic is entirely blind to the subsequent multi-FD collapse insideFillVASurfaceAttribExternalBuffers. - The execution path is reachable via
SharedImageVideoFrameDatawithis_mappable = true. The validator confirmed thatMojoVideoEncodeAcceleratorService::EncodeandVaapiVideoEncodeAccelerator::EncodeTaskstorage-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 thatClientSharedImage::CloneGpuMemoryBufferHandlesuccessfully deep-copies the FDs, offsets, and modifier without dropping them. Settingmodifier = kNoModifierguaranteesuse_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.