CVE-2026-13831
Overview
Background
- dma-buf
- a Linux/ChromeOS kernel buffer-sharing file descriptor used to pass GPU memory between processes; its allocation size can be queried with
lseek(fd, 0, SEEK_END). - `GpuMemoryBufferHandle` / `NativePixmapHandle`
- the IPC-transported description of shared GPU memory, carrying attacker-influenceable fields such as image dimensions, stride, and the backing fd.
- `vkGetImageMemoryRequirements`
- the Vulkan query that reports how many bytes a created
VkImageactually needs for its backing memory. - External memory import
- binding an imported dma-buf fd to a
VkImageviaVkImportMemoryFdInfoKHRandvkAllocateMemory, with anallocationSizederived from the driver’s requirements rather than from the fd’s real size.
Root Cause Analysis
When importing a dma-buf into a Vulkan image on Linux/ChromeOS, the GPU process trusted the attacker-supplied image dimensions from GpuMemoryBufferHandle and allocated/bound device memory sized by the driver’s vkGetImageMemoryRequirements without ever verifying that the imported fd was physically large enough to back that image. The old code path (InitializeSingleOrJointPlanes with a nullptr requirements pointer) imported the fd and bound it directly, so a small dma-buf bound to a large VkImage left the driver addressing memory beyond the fd’s real allocation.
The fix restructures initialization to create the VkImage first, call GetMemoryRequirements(0) to learn the required size, then measure the fd’s physical size with lseek(scoped_fd.get(), 0, SEEK_END); it rejects an un-seekable fd (fd_size < 0) and, when static_cast<uint64_t>(fd_size) < requirements.size, logs, records a crash dump, and (with kVulkanValidateDmabufSize enabled by default) calls Destroy() and returns false before any import. Only after passing validation does it perform AllocateAndBindMemory and, for VK_IMAGE_TILING_LINEAR, query the subresource layout, ensuring an undersized allocation is never bound.
lseek(SEEK_END) size check against vkGetImageMemoryRequirements and refuses import when the fd is too small.Attack Path
- Compromise a renderer The attacker gains code execution in a renderer able to send GPU IPC that creates shared images / Vulkan-backed buffers.
- Forge an undersized handle
They construct a
GpuMemoryBufferHandlewhoseNativePixmapHandleclaims large dimensions (e.g. 1024x1024) but whose backing fd is a tiny buffer (e.g. a 4 KB memfd, per the regression test). - Trigger Vulkan import
The GPU process calls the external-memory import path, which sizes the allocation from
vkGetImageMemoryRequirementsfor the large image and, pre-patch, binds the small fd without a size check. - Undersized binding
The large
VkImageis now backed by memory smaller than it needs, so GPU accesses to the image address bytes past the fd’s real allocation. - GPU out-of-bounds read/write Subsequent reads/writes to the image produce GPU-level out-of-bounds accesses in the GPU process, an information leak or corruption primitive (specific downstream primitive inferred).
Impact Assessment
GpuMemoryBufferHandle with mismatched dimensions and fd size. The advisory classifies this as insufficient validation of untrusted input in GPU; it reports no exploited-in-the-wild status, and escalation beyond the described OOB (e.g. sandbox escape) is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/vulkan/vulkan_image_linux.cc |
modified | |
TEST_Fgpu/vulkan/vulkan_image_unittest.cc |
modified | |
ifgpu/vulkan/vulkan_image_unittest.cc |
modified |
Files Changed
gpu/vulkan/vulkan_image_linux.ccgpu/vulkan/vulkan_image_unittest.cc
Audit Directions
- Trusting IPC-supplied buffer geometryAudit GPU import paths that size allocations or accesses from renderer-supplied dimensions/stride without cross-checking the backing fd/handle’s real capacity.
- Missing physical-size validation on shared fdsLook for dma-buf / shared-memory imports (Vulkan, GL, native pixmap) that bind memory without
lseek(SEEK_END)or equivalent size verification against required bytes. - Requirement-vs-supply mismatchesReview code that derives
allocationSizefrom driver requirements while the actual backing comes from an untrusted external source, ensuring the smaller of the two governs safety.
Patch
From 9185cf51e895d20790e4b93705c78bd3dd3f35a9 Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> Date: Wed, 27 May 2026 16:19:03 -0700 Subject: [PATCH] gpu: Validate dma-buf size against driver requirements during Vulkan import A compromised renderer could cause the GPU process to bind an undersized dma-buf to a large Vulkan image, leading to GPU-level out-of-bounds reads and writes. This occurred because the GPU process trusted attacker-supplied image dimensions during memory allocation without verifying the actual physical size of the imported file descriptor. This CL implements a targeted fix in VulkanImage initialization on Linux/ChromeOS: 1. Create the VkImage and query its actual memory requirements via vkGetImageMemoryRequirements before importing external memory. 2. Explicitly verify the physical size of the dma-buf FD using lseek(fd, 0, SEEK_END) and reject if it is smaller than the driver-reported requirement. 3. Manually perform memory allocation and layout setup steps for imported buffers to allow the validation check to occur at the correct stage of the Vulkan object lifecycle. This ensures that the GPU process never binds an undersized memory allocation to a Vulkan image, effectively mitigating the OOB access vulnerability while maintaining compatibility with Linux platforms where GpuMemoryBuffer metadata may be incomplete or unreliable. Test: vulkan_tests --gtest_filter=VulkanImageTest.RejectUndersizedDmaBuf Fixed: 513781328 Change-Id: Id079bfb0545728013b862c7a8cfc70f356bc5175 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7857471 Reviewed-by: Zhenyao Mo <[email protected]> Commit-Queue: Tzarial <[email protected]> Cr-Commit-Position: refs/heads/main@{#1637317} --- diff --git a/gpu/vulkan/vulkan_image_linux.cc b/gpu/vulkan/vulkan_image_linux.cc index 2ca3580..20f8ada 100644 --- a/gpu/vulkan/vulkan_image_linux.cc +++ b/gpu/vulkan/vulkan_image_linux.cc @@ -4,9 +4,14 @@ #include "gpu/vulkan/vulkan_image.h" +#include <sys/types.h> +#include <unistd.h> + #include <tuple> #include <vector> +#include "base/debug/dump_without_crashing.h" +#include "base/feature_list.h" #include "base/logging.h" #include "gpu/vulkan/vulkan_device_queue.h" #include "gpu/vulkan/vulkan_function_pointers.h" @@ -14,6 +19,10 @@ namespace gpu { +namespace { +BASE_FEATURE(kVulkanValidateDmabufSize, base::FEATURE_ENABLED_BY_DEFAULT); +} // namespace + // static std::unique_ptr<VulkanImage> VulkanImage::CreateWithExternalMemoryAndModifiers( VulkanDeviceQueue* device_queue, @@ -87,6 +96,42 @@ external_image_create_info.pNext = &modifier_info; } + device_queue_ = device_queue; + disjoint_planes_ = false; + + if (!CreateVkImage(size, format, usage, flags, image_tiling, + &external_image_create_info)) { + device_queue_ = nullptr; + return false; + } + + VkMemoryRequirements requirements = GetMemoryRequirements(0); + + // SECURITY: |gmb_handle| (including |size| and the fd) may originate from an + // untrusted process. Verify that the dma-buf is at least large enough to back + // an image of |size| before letting vkAllocateMemory import it with an + // allocationSize derived from vkGetImageMemoryRequirements(). + // dma-bufs support lseek(SEEK_END) to report their allocation size; treat an + // un-seekable fd as invalid since real dma-bufs always do. + off_t fd_size = lseek(scoped_fd.get(), 0, SEEK_END); + if (fd_size < 0) { + DLOG(ERROR) << "lseek() on dma-buf fd failed."; + Destroy(); + return false; + } + lseek(scoped_fd.get(), 0, SEEK_SET); + + if (static_cast<uint64_t>(fd_size) < requirements.size) { + LOG(ERROR) << "dma-buf (" << fd_size << " bytes) is too small for " + << size.width() << "x" << size.height() << " VkImage " + << "(requires " << requirements.size << " bytes)"; + base::debug::DumpWithoutCrashing(); + if (base::FeatureList::IsEnabled(kVulkanValidateDmabufSize)) { + Destroy(); + return false; + } + } + int memory_fd = scoped_fd.release(); VkImportMemoryFdInfoKHR import_memory_fd_info = { .sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR, @@ -109,18 +154,36 @@ import_memory_fd_info.pNext = &export_memory_info; } - VkMemoryRequirements* requirements = nullptr; - // TODO support multiple plane - auto result = InitializeSingleOrJointPlanes( - device_queue, size, format, usage, flags, image_tiling, - &external_image_create_info, &import_memory_fd_info, requirements); + // Allocate device memory and bind it to the image. On Linux, if + // |import_memory_fd_info| is provided, it will import the dma-buf FD. + auto result = AllocateAndBindMemory(0, &requirements, &import_memory_fd_info); // If vkAllocateMemory() returned successfully, the fd in scoped_fd should be // owned by vulkan, otherwise take the ownership of the fd back. if (result == kFailedBeforeAllocateMemory) { scoped_fd.reset(memory_fd); } - return result; + if (result != kSuccess) { + Destroy(); + return false; + } + + // Get subresource layout for images with VK_IMAGE_TILING_LINEAR. + // For images with VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT, the layout is + // initialized in InitializeWithExternalMemoryAndModifiers(). For + // VK_IMAGE_TILING_OPTIMAL the layout is not usable and + // vkGetImageSubresourceLayout() is illegal. + if (image_tiling == VK_IMAGE_TILING_LINEAR) { + const VkImageSubresource image_subresource = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .arrayLayer = 0, + }; + vkGetImageSubresourceLayout(device_queue_->GetVulkanDevice(), image_, + &image_subresource, &layouts_[0]); + } + + return true; } bool VulkanImage::InitializeWithExternalMemoryAndModifiers( diff --git a/gpu/vulkan/vulkan_image_unittest.cc b/gpu/vulkan/vulkan_image_unittest.cc index 64fddbe..89857ad 100644 --- a/gpu/vulkan/vulkan_image_unittest.cc +++ b/gpu/vulkan/vulkan_image_unittest.cc @@ -4,7 +4,13 @@ #include "gpu/vulkan/vulkan_image.h" +#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) +#include <sys/mman.h> +#include <unistd.h> +#endif + #include "base/logging.h" +#include "base/test/scoped_feature_list.h" #include "build/build_config.h" #include "gpu/config/gpu_info_collector.h" #include "gpu/config/gpu_test_config.h" @@ -120,4 +126,39 @@ } } +#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) +TEST_F(VulkanImageTest, RejectUndersizedDmaBuf) { + auto* device_queue = GetDeviceQueue(); + if (!device_queue) { + return; + } + + // 1. Create a small anonymous file (memfd) to simulate an undersized dma-buf. + int fd = memfd_create("undersized_dmabuf", 0); + ASSERT_GE(fd, 0); + base::ScopedFD scoped_fd(fd); + // Give it a tiny size (e.g., 4KB). + ASSERT_EQ(ftruncate(fd, 4096), 0); + + // 2. Construct a GpuMemoryBufferHandle claiming it's large (e.g., 1024x1024). + constexpr gfx::Size image_size(1024, 1024); + const int stride = image_size.width() * 4; // 4 bytes per pixel + const uint64_t size = static_cast<uint64_t>(stride) * image_size.height(); + + gfx::NativePixmapHandle native_pixmap_handle; + native_pixmap_handle.modifier = 0; // Linear tiling + native_pixmap_handle.planes.emplace_back(stride, 0, size, + base::ScopedFD(dup(fd))); + + gfx::GpuMemoryBufferHandle gmb_handle(std::move(native_pixmap_handle)); +
Regression Test / PoC
diff --git a/gpu/vulkan/vulkan_image_unittest.cc b/gpu/vulkan/vulkan_image_unittest.cc
index 64fddbe..89857ad 100644
--- a/gpu/vulkan/vulkan_image_unittest.cc
+++ b/gpu/vulkan/vulkan_image_unittest.cc
@@ -4,7 +4,13 @@
#include "gpu/vulkan/vulkan_image.h"
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+#include <sys/mman.h>
+#include <unistd.h>
+#endif
+
#include "base/logging.h"
+#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "gpu/config/gpu_info_collector.h"
#include "gpu/config/gpu_test_config.h"
@@ -120,4 +126,39 @@
}
}
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+TEST_F(VulkanImageTest, RejectUndersizedDmaBuf) {
+ auto* device_queue = GetDeviceQueue();
+ if (!device_queue) {
+ return;
+ }
+
+ // 1. Create a small anonymous file (memfd) to simulate an undersized dma-buf.
+ int fd = memfd_create("undersized_dmabuf", 0);
+ ASSERT_GE(fd, 0);
+ base::ScopedFD scoped_fd(fd);
+ // Give it a tiny size (e.g., 4KB).
+ ASSERT_EQ(ftruncate(fd, 4096), 0);
+
+ // 2. Construct a GpuMemoryBufferHandle claiming it's large (e.g., 1024x1024).
+ constexpr gfx::Size image_size(1024, 1024);
+ const int stride = image_size.width() * 4; // 4 bytes per pixel
+ const uint64_t size = static_cast<uint64_t>(stride) * image_size.height();
+
+ gfx::NativePixmapHandle native_pixmap_handle;
+ native_pixmap_handle.modifier = 0; // Linear tiling
+ native_pixmap_handle.planes.emplace_back(stride, 0, size,
+ base::ScopedFD(dup(fd)));
+
+ gfx::GpuMemoryBufferHandle gmb_handle(std::move(native_pixmap_handle));
+
+ // 3. Verify that InitializeFromGpuMemoryBufferHandle rejects it.
+ auto image = VulkanImage::CreateFromGpuMemoryBufferHandle(
+ device_queue, std::move(gmb_handle), image_size, VK_FORMAT_R8G8B8A8_UNORM,
+ VK_IMAGE_USAGE_SAMPLED_BIT, 0, VK_IMAGE_TILING_LINEAR,
+ VK_QUEUE_FAMILY_IGNORED);
+ EXPECT_FALSE(image);
+}
+#endif
+
} // namespace gpu