CVE-2026-113332
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pgpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc |
modified | |
ifgpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_image/external_vk_image_backing.cctesting/buildbot/filters/linux.swiftshader.tsan.gl_tests_passthrough.filtergpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc
Patch
From 5bafa058980e82da579a5ffa3ff541d8db1bdfad Mon Sep 17 00:00:00 2001
From: Tigran Bantikyan <bantikyan@chromium.org>
Date: Fri, 18 Sep 2026 14:46:36 -0700
Subject: [PATCH] [gpu] Check initial pixel upload result in ExternalVkImageBacking
ExternalVkImageBacking::Create() marks the backing as cleared
unconditionally when initial pixel data is provided, even if the upload
via UploadToVkImage() fails (e.g. staging buffer OOM under memory
pressure, or a lost device). This bypasses the IsCleared() check,
potentially exposing uninitialized VRAM allocator residue during
readback.
This CL checks the upload result before calling SetCleared(). On failure
Create() returns nullptr rather than handing back a backing that claims
to be initialized. AngleVulkanImageBacking::Initialize() already does
this, as do GLTextureImageBacking and EGLImageBacking since
crrev.com/c/7909296.
Landing a regression test for the failure path wasn't feasible since
forcing the upload to fail needs patching process-global Vulkan function
pointers, which won't work well in any parallel test setup. However, I
was able to verify the fix locally.
TAG=agy
Bug: 517031871
Change-Id: I857d95033e0d3a9643e9fb6db0e82ccbcaf37c63
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8420004
Reviewed-by: Brandon Jones <bajones@chromium.org>
Commit-Queue: Tigran Bantikyan <bantikyan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1701101}
---
diff --git a/gpu/command_buffer/service/shared_image/external_vk_image_backing.cc b/gpu/command_buffer/service/shared_image/external_vk_image_backing.cc
index 08f1a99..609874d 100644
--- a/gpu/command_buffer/service/shared_image/external_vk_image_backing.cc
+++ b/gpu/command_buffer/service/shared_image/external_vk_image_backing.cc
@@ -254,7 +254,10 @@
return nullptr;
}
SkPixmap pixmap(image_info, pixel_data.data(), image_info.minRowBytes());
- backing->UploadToVkImage({pixmap});
+ if (!backing->UploadToVkImage({pixmap})) {
+ DLOG(ERROR) << "Failed to upload initial pixel data to VkImage";
+ return nullptr;
+ }
// Mark the backing as cleared.
backing->SetCleared();
diff --git a/testing/buildbot/filters/linux.swiftshader.tsan.gl_tests_passthrough.filter b/testing/buildbot/filters/linux.swiftshader.tsan.gl_tests_passthrough.filter
index f796aed..8a826c5 100644
--- a/testing/buildbot/filters/linux.swiftshader.tsan.gl_tests_passthrough.filter
+++ b/testing/buildbot/filters/linux.swiftshader.tsan.gl_tests_passthrough.filter
@@ -1,5 +1,6 @@
# crbug.com/462477487
-ExternalVkImageBackingFactoryWithFormatTest.Basic/*
+-ExternalVkImageBackingFactoryWithFormatTest.InitialPixelUpload*/*
-ExternalVkImageBackingFactoryWithFormatTest.ReadbackToMemory/*
-ExternalVkImageBackingFactoryWithFormatTest.Upload/*
diff --git a/gpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc
index a44b5f01..14c8597a 100644
--- a/gpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc
@@ -34,6 +34,7 @@
#include "third_party/skia/include/gpu/ganesh/GrBackendSemaphore.h"
#include "third_party/skia/include/gpu/ganesh/SkImageGanesh.h"
#include "third_party/skia/include/private/chromium/GrPromiseImageTexture.h"
+#include "ui/gfx/skia_span_util.h"
#include "ui/gl/buildflags.h"
#if BUILDFLAG(USE_DAWN)
@@ -590,6 +591,100 @@
}
}
+TEST_P(ExternalVkImageBackingFactoryWithFormatTest, InitialPixelUploadSuccess) {
+ const auto format = get_format();
+ if (!format.is_single_plane()) {
+ GTEST_SKIP() << "Initial pixel data only supported for single-plane";
+ }
+
+ const gfx::Size size(16, 16);
+ const auto color_space = gfx::ColorSpace::CreateSRGB();
+ const gpu::SharedImageUsageSet usage =
+ SHARED_IMAGE_USAGE_DISPLAY_READ | SHARED_IMAGE_USAGE_CPU_UPLOAD;
+ const SharedImageInfo si_info{format,
+ size,
+ color_space,
+ kTopLeft_GrSurfaceOrigin,
+ kPremul_SkAlphaType,
+ usage,
+ "TestLabel"};
+
+ std::vector<SkBitmap> bitmaps = AllocateRedBitmaps(format, size);
+ const auto pixel_data = gfx::SkPixmapToSpan(bitmaps[0].pixmap());
+
+ auto mailbox = Mailbox::Generate();
+ auto backing = backing_factory_->CreateSharedImage(
+ mailbox, si_info, /*is_thread_safe=*/false, pixel_data);
+ EXPECT_NE(backing, nullptr);
+ if (!backing) {
+ return;
+ }
+ EXPECT_TRUE(backing->IsCleared());
+
+ std::unique_ptr<SharedImageRepresentationFactoryRef> shared_image_ref =
+ shared_image_manager_.Register(std::move(backing), &memory_type_tracker_);
+ EXPECT_TRUE(shared_image_ref);
+
+ VerifyPixelsWithReadbackGanesh(mailbox, bitmaps);
+}
+
+TEST_P(ExternalVkImageBackingFactoryWithFormatTest,
+ InitialPixelUploadInvalidSize) {
+ const auto format = get_format();
+ if (!format.is_single_plane()) {
+ GTEST_SKIP() << "Initial pixel data only supported for single-plane";
+ }
+
+ const gfx::Size size(16, 16);
+ const auto color_space = gfx::ColorSpace::CreateSRGB();
+ const gpu::SharedImageUsageSet usage =
+ SHARED_IMAGE_USAGE_DISPLAY_READ | SHARED_IMAGE_USAGE_CPU_UPLOAD;
+ const SharedImageInfo si_info{format,
+ size,
+ color_space,
+ kTopLeft_GrSurfaceOrigin,
+ kPremul_SkAlphaType,
+ usage,
+ "TestLabel"};
+
+ std::vector<SkBitmap> bitmaps = AllocateRedBitmaps(format, size);
+ const auto pixel_data = gfx::SkPixmapToSpan(bitmaps[0].pixmap());
+
+ // Provide pixel data with invalid size.
+ auto mailbox = Mailbox::Generate();
+ auto backing = backing_factory_->CreateSharedImage(
+ mailbox, si_info, /*is_thread_safe=*/false,
+ pixel_data.first(pixel_data.size() - 1));
+ EXPECT_EQ(backing, nullptr);
+}
+
+TEST_P(ExternalVkImageBackingFactoryWithFormatTest,
+ InitialPixelUploadMultiplanarNotSupported) {
+ const auto format = get_format();
+ if (format.is_single_plane()) {
+ GTEST_SKIP() << "Test only covers multiplanar formats";
+ }
+
+ const gfx::Size size(16, 16);
+ const auto color_space = gfx::ColorSpace::CreateSRGB();
+ const gpu::SharedImageUsageSet usage =
+ SHARED_IMAGE_USAGE_DISPLAY_READ | SHARED_IMAGE_USAGE_CPU_UPLOAD;
+ const SharedImageInfo si_info{format,
+ size,
+ color_space,
+ kTopLeft_GrSurfaceOrigin,
+ kPremul_SkAlphaType,
+ usage,
+ "TestLabel"};
+
+ std::vector<uint8_t> pixel_data(format.EstimatedSizeInBytes(size), 0x12);
+ auto mailbox = Mailbox::Generate();
+ auto backing = backing_factory_->CreateSharedImage(
+ mailbox, si_info, /*is_thread_safe=*/false,
+ base::span<const uint8_t>(pixel_data));
+ EXPECT_EQ(backing, nullptr);
+}
+
std::string TestParamToString(
const testing::TestParamInfo<viz::SharedImageFormat>& param_info) {
return param_info.param.ToTestParamString();
Regression Test / PoC
diff --git a/gpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc
index a44b5f01..14c8597a 100644
--- a/gpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/external_vk_image_backing_factory_unittest.cc
@@ -34,6 +34,7 @@
#include "third_party/skia/include/gpu/ganesh/GrBackendSemaphore.h"
#include "third_party/skia/include/gpu/ganesh/SkImageGanesh.h"
#include "third_party/skia/include/private/chromium/GrPromiseImageTexture.h"
+#include "ui/gfx/skia_span_util.h"
#include "ui/gl/buildflags.h"
#if BUILDFLAG(USE_DAWN)
@@ -590,6 +591,100 @@
}
}
+TEST_P(ExternalVkImageBackingFactoryWithFormatTest, InitialPixelUploadSuccess) {
+ const auto format = get_format();
+ if (!format.is_single_plane()) {
+ GTEST_SKIP() << "Initial pixel data only supported for single-plane";
+ }
+
+ const gfx::Size size(16, 16);
+ const auto color_space = gfx::ColorSpace::CreateSRGB();
+ const gpu::SharedImageUsageSet usage =
+ SHARED_IMAGE_USAGE_DISPLAY_READ | SHARED_IMAGE_USAGE_CPU_UPLOAD;
+ const SharedImageInfo si_info{format,
+ size,
+ color_space,
+ kTopLeft_GrSurfaceOrigin,
+ kPremul_SkAlphaType,
+ usage,
+ "TestLabel"};
+
+ std::vector<SkBitmap> bitmaps = AllocateRedBitmaps(format, size);
+ const auto pixel_data = gfx::SkPixmapToSpan(bitmaps[0].pixmap());
+
+ auto mailbox = Mailbox::Generate();
+ auto backing = backing_factory_->CreateSharedImage(
+ mailbox, si_info, /*is_thread_safe=*/false, pixel_data);
+ EXPECT_NE(backing, nullptr);
+ if (!backing) {
+ return;
+ }
+ EXPECT_TRUE(backing->IsCleared());
+
+ std::unique_ptr<SharedImageRepresentationFactoryRef> shared_image_ref =
+ shared_image_manager_.Register(std::move(backing), &memory_type_tracker_);
+ EXPECT_TRUE(shared_image_ref);
+
+ VerifyPixelsWithReadbackGanesh(mailbox, bitmaps);
+}
+
+TEST_P(ExternalVkImageBackingFactoryWithFormatTest,
+ InitialPixelUploadInvalidSize) {
+ const auto format = get_format();
+ if (!format.is_single_plane()) {
+ GTEST_SKIP() << "Initial pixel data only supported for single-plane";
+ }
+
+ const gfx::Size size(16, 16);
+ const auto color_space = gfx::ColorSpace::CreateSRGB();
+ const gpu::SharedImageUsageSet usage =
+ SHARED_IMAGE_USAGE_DISPLAY_READ | SHARED_IMAGE_USAGE_CPU_UPLOAD;
+ const SharedImageInfo si_info{format,
+ size,
+ color_space,
+ kTopLeft_GrSurfaceOrigin,
+ kPremul_SkAlphaType,
+ usage,
+ "TestLabel"};
+
+ std::vector<SkBitmap> bitmaps = AllocateRedBitmaps(format, size);
+ const auto pixel_data = gfx::SkPixmapToSpan(bitmaps[0].pixmap());
+
+ // Provide pixel data with invalid size.
+ auto mailbox = Mailbox::Generate();
+ auto backing = backing_factory_->CreateSharedImage(
+ mailbox, si_info, /*is_thread_safe=*/false,
+ pixel_data.first(pixel_data.size() - 1));
+ EXPECT_EQ(backing, nullptr);
+}
+
+TEST_P(ExternalVkImageBackingFactoryWithFormatTest,
+ InitialPixelUploadMultiplanarNotSupported) {
+ const auto format = get_format();
+ if (format.is_single_plane()) {
+ GTEST_SKIP() << "Test only covers multiplanar formats";
+ }
+
+ const gfx::Size size(16, 16);
+ const auto color_space = gfx::ColorSpace::CreateSRGB();
+ const gpu::SharedImageUsageSet usage =
+ SHARED_IMAGE_USAGE_DISPLAY_READ | SHARED_IMAGE_USAGE_CPU_UPLOAD;
+ const SharedImageInfo si_info{format,
+ size,
+ color_space,
+ kTopLeft_GrSurfaceOrigin,
+ kPremul_SkAlphaType,
+ usage,
+ "TestLabel"};
+
+ std::vector<uint8_t> pixel_data(format.EstimatedSizeInBytes(size), 0x12);
+ auto mailbox = Mailbox::Generate();
+ auto backing = backing_factory_->CreateSharedImage(
+ mailbox, si_info, /*is_thread_safe=*/false,
+ base::span<const uint8_t>(pixel_data));
+ EXPECT_EQ(backing, nullptr);
+}
+
std::string TestParamToString(
const testing::TestParamInfo<viz::SharedImageFormat>& param_info) {
return param_info.param.ToTestParamString();
Original Bug Report
Uninitialized GPU memory read in ExternalVkImageBacking::Create on upload failure
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: ExternalVkImageBacking::Create discards the boolean return value of UploadToVkImage() and unconditionally marks the backing as cleared. If the initial pixel data upload fails (e.g., under memory pressure or context loss), the image backing is registered as fully initialized despite wrapping uninitialized VkDeviceMemory. A compromised renderer can potentially exploit this to bypass safety checks and read back uninitialized graphics memory containing cross-origin data.
Affected files:
gpu/command_buffer/service/shared_image/external_vk_image_backing.cc
Estimated timestamp from git blame: 2021-11-19
Root Cause Analysis
In ExternalVkImageBacking::Create (located in gpu/command_buffer/service/shared_image/external_vk_image_backing.cc), when a non-empty pixel_data span is provided, the code attempts to upload it to the newly created Vulkan image using UploadToVkImage(). However, the return value of this function call is discarded, and the backing is unconditionally marked as cleared:
if (!pixel_data.empty()) {
auto image_info = backing->AsSkImageInfo();
if (pixel_data.size() != image_info.computeMinByteSize()) {
DLOG(ERROR) << "Invalid initial pixel data size";
return nullptr;
}
SkPixmap pixmap(image_info, pixel_data.data(), image_info.minRowBytes());
backing->UploadToVkImage({pixmap}); // <--- Boolean return value is discarded
// Mark the backing as cleared.
backing->SetCleared(); // <--- Unconditionally marks backing as cleared
backing->latest_content_ = kInVkImage;
}
UploadToVkImage() returns a boolean indicating success and has several documented failure paths. For example, it calls GrDirectContext::updateBackendTexture(), which can return false under memory pressure (e.g., when staging buffer allocation fails in GrVkGpu::onWritePixels) or when the graphics context is lost/abandoned.
Since the underlying VkImage is allocated before the upload attempt, if UploadToVkImage() fails, the image backing continues to exist and wraps a fully-allocated but uninitialized VkDeviceMemory allocation. Because SetCleared() is called unconditionally, the system registers the backing as initialized and cleared, which bypasses safety checks designed to prevent reading uninitialized memory.
Potential Vulnerability Flow and Reachability
(Note: These are suggested and potential steps based on static analysis, as our tooling does not have the ability to execute code or run a working Proof of Concept.)
- Allocation: A compromised renderer initiates a SharedImage creation with initial pixel data via the Mojo IPC channel using
mojom::DeferredSharedImageRequest::kCreateSharedImageWithDatawith read usages (e.g.,SHARED_IMAGE_USAGE_GLES2_READ | SHARED_IMAGE_USAGE_RASTER_READ). - Backing Dispatch: The GPU process selects the
ExternalVkImageBackingFactory(active under Ganesh-Vulkan) and allocates the underlyingVkImageusing raw, uninitialized driver memory containing residual GPU data from prior allocations. - Failed Upload: Under simulated GPU memory pressure or context disturbance, the initial staging buffer allocation or upload fails inside
UploadToVkImage(), returningfalse. - Falsified State: The return value is ignored by
ExternalVkImageBacking::Create.SetCleared()is called, setting the backing’s internal statecleared_rect_to the full size of the image, marking it as completely initialized. - Readback & Leak: The compromised renderer immediately triggers a readback of the shared image (e.g., via
ReadbackARGBImagePixelsINTERNALImmediate). The safety checkIsCleared()inCopySharedImageHelper::ReadPixels(ingpu/command_buffer/service/copy_shared_image_helper.cc) evaluates totruebecause of the falsified state. The readback proceeds, copying the uninitialized device memory containing cross-origin graphical data into renderer-accessible shared memory.
Suggested Remediation
Modify ExternalVkImageBacking::Create to check the return value of UploadToVkImage() and return nullptr if the initial upload fails:
if (!backing->UploadToVkImage({pixmap})) {
DLOG(ERROR) << "Failed to upload initial pixel data";
return nullptr;
}
backing->SetCleared();
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.