CVE-2026-79004
Overview
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
Patch
From 4f2bb3f790c6898f6a4065e0e6d65c437826775d Mon Sep 17 00:00:00 2001 From: Ted Meyer <[email protected]> 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 <[email protected]> Auto-Submit: Ted (Chromium) Meyer <[email protected]> Commit-Queue: Frank Liberato <[email protected]> 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.