CVE-2026-9900
Overview
Files Changed
src/libANGLE/renderer/vulkan/MemoryObjectVk.cppsrc/tests/capture_replay_tests/capture_replay_expectations.txtsrc/tests/gl_tests/CompressedTextureFormatsTest.cppsrc/tests/gl_tests/VulkanExternalImageTest.cpp
Patch
From 189b249fba4d8fb331f4ac145b8c2754f7ef5f01 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Tue, 21 Apr 2026 11:19:43 -0400
Subject: [PATCH] Vulkan: Fix import of non-renderable image
When an image is imported with EXT_external_objects (or it's ANGLE
variant), the format the actual Vulkan image that was created is passed
in. ANGLE has no choice but to match that format, and cannot fall back
to a renderable one.
Bug: chromium:497637277
Bug: angleproject:42264819
Change-Id: I1b80a7a96dc68f3ac9b200db9c69d8243cd03773
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7780913
Reviewed-by: Charlie Lao <cclao@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/renderer/vulkan/MemoryObjectVk.cpp b/src/libANGLE/renderer/vulkan/MemoryObjectVk.cpp
index 043148b..8987ea7 100644
--- a/src/libANGLE/renderer/vulkan/MemoryObjectVk.cpp
+++ b/src/libANGLE/renderer/vulkan/MemoryObjectVk.cpp
@@ -180,8 +180,11 @@
{
vk::Renderer *renderer = contextVk->getRenderer();
+ // The format of the image is dictated by |internalFormat|, we can't fall back to a renderable
+ // format if any, because the image must match the external one.
const vk::Format &vkFormat = renderer->getFormat(internalFormat);
- angle::FormatID actualFormatID = vkFormat.getActualRenderableImageFormatID();
+ angle::FormatID actualFormatID =
+ vkFormat.getActualImageFormatID(vk::ImageFormatSupport::SampleOnly);
VkExternalMemoryImageCreateInfo externalMemoryImageCreateInfo = {};
externalMemoryImageCreateInfo.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO;
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index 5df1eab..75e6ddb 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -248,6 +248,7 @@
# VVL error
42264614 LINUX : VulkanExternalImageTestES31.UninitializedOnGLImportAndStorageWrite/* = SKIP_FOR_CAPTURE
+42264614 LINUX : VulkanExternalImageTest.NonRenderableWithFlags/* = SKIP_FOR_CAPTURE
# CoherentBufferTracker deadlocks and flaky crashes
42266529 LINUX : BufferDataTestES3.* = SKIP_FOR_CAPTURE
diff --git a/src/tests/gl_tests/CompressedTextureFormatsTest.cpp b/src/tests/gl_tests/CompressedTextureFormatsTest.cpp
index dc0fb4d..0900d16 100644
--- a/src/tests/gl_tests/CompressedTextureFormatsTest.cpp
+++ b/src/tests/gl_tests/CompressedTextureFormatsTest.cpp
@@ -632,7 +632,7 @@
{
// ETC2/EAC formats always pass validation on ES3 contexts but in some cases fail in drivers
// because their emulation is not implemented for OpenGL renderer.
- // https://crbug.com/angleproject/6300
+ // https://anglebug.com/42264819
if (mAlwaysOnES3)
{
ANGLE_SKIP_TEST_IF(getClientMajorVersion() >= 3 &&
diff --git a/src/tests/gl_tests/VulkanExternalImageTest.cpp b/src/tests/gl_tests/VulkanExternalImageTest.cpp
index e76bb2f..05f66bc 100644
--- a/src/tests/gl_tests/VulkanExternalImageTest.cpp
+++ b/src/tests/gl_tests/VulkanExternalImageTest.cpp
@@ -51,7 +51,8 @@
{VK_FORMAT_R8_UNORM, GL_ALPHA8_EXT}, // ALPHA_8
{VK_FORMAT_R8_UNORM, GL_LUMINANCE8_EXT}, // LUMINANCE_8
{VK_FORMAT_R8G8_UNORM, GL_RG8_EXT}, // RG_88
- {VK_FORMAT_R8G8B8A8_UNORM, GL_RGB8_OES}, // RGBX_8888
+ {VK_FORMAT_R8G8B8_UNORM, GL_RGB8_OES}, // RGB_888
+ {VK_FORMAT_R8G8B8A8_UNORM, GL_RGBX8_ANGLE}, // RGBX_8888
};
struct OpaqueFdTraits
@@ -519,12 +520,6 @@
helper.initialize(isSwiftshader, enableDebugLayers);
for (const ImageFormatPair &format : kChromeFormats)
{
- // https://crbug.com/angleproject/5046
- if ((format.vkFormat == VK_FORMAT_R4G4B4A4_UNORM_PACK16) && IsIntel())
- {
- continue;
- }
-
if (!Traits::CanCreateImage(helper, format.vkFormat, VK_IMAGE_TYPE_2D,
VK_IMAGE_TILING_OPTIMAL, createFlags, usageFlags))
{
@@ -542,6 +537,12 @@
continue;
}
+ if (format.internalFormat == GL_RGBX8_ANGLE &&
+ !IsGLExtensionEnabled("GL_ANGLE_rgbx_internal_format"))
+ {
+ continue;
+ }
+
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory deviceMemory = VK_NULL_HANDLE;
VkDeviceSize deviceMemorySize = 0;
@@ -1416,6 +1417,127 @@
enableDebugLayers());
}
+// Test importing a non-renderable texture, using GL_ANGLE_memory_object_flags.
+TEST_P(VulkanExternalImageTest, NonRenderableWithFlags)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_memory_object_fd"));
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_semaphore_fd"));
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_memory_object_flags"));
+
+ using Traits = OpaqueFdTraits;
+
+ ASSERT(EnsureGLExtensionEnabled(Traits::MemoryObjectExtension()));
+ ASSERT(EnsureGLExtensionEnabled(Traits::SemaphoreExtension()));
+
+ // The format that is used by this test, RGBA4, is not necessarily renderable. This test
+ // ensures ANGLE does not attempt to fall back to RGBA8.
+ VkImageCreateFlags createFlags = kDefaultImageCreateFlags;
+ VkImageUsageFlags usageFlags = VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+ VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
+
+ AdjustCreateFlags(true, &createFlags);
+
+ VulkanHelper helper;
+ helper.initialize(isSwiftshader(), enableDebugLayers());
+
+ const VkFormat format = VK_FORMAT_R4G4B4A4_UNORM_PACK16;
+ ANGLE_SKIP_TEST_IF(!Traits::CanCreateImage(helper, format, VK_IMAGE_TYPE_2D,
+ VK_IMAGE_TILING_OPTIMAL, createFlags, usageFlags));
+ ANGLE_SKIP_TEST_IF(!Traits::CanCreateSemaphore(helper));
+
+ VkSemaphore vkAcquireSemaphore = VK_NULL_HANDLE;
+ VkResult result = Traits::CreateSemaphore(&helper, &vkAcquireSemaphore);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_TRUE(vkAcquireSemaphore != VK_NULL_HANDLE);
+
+ VkSemaphore vkReleaseSemaphore = VK_NULL_HANDLE;
+ result = Traits::CreateSemaphore(&helper, &vkReleaseSemaphore);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_TRUE(vkReleaseSemaphore != VK_NULL_HANDLE);
+
+ typename Traits::Handle acquireSemaphoreHandle = Traits::InvalidHandle();
+ result = Traits::ExportSemaphore(&helper, vkAcquireSemaphore, &acquireSemaphoreHandle);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_NE(acquireSemaphoreHandle, Traits::InvalidHandle());
+
+ typename Traits::Handle releaseSemaphoreHandle = Traits::InvalidHandle();
+ result = Traits::ExportSemaphore(&helper, vkReleaseSemaphore, &releaseSemaphoreHandle);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_NE(releaseSemaphoreHandle, Traits::InvalidHandle());
+
+ VkImage image = VK_NULL_HANDLE;
+ VkDeviceMemory deviceMemory = VK_NULL_HANDLE;
+ VkDeviceSize deviceMemorySize = 0;
+
+ VkExtent3D extent = {kWidth, kHeight, 1};
+ result = Traits::CreateImage2D(&helper, format, createFlags, usageFlags, nullptr, extent,
+ &image, &deviceMemory, &deviceMemorySize);
+ EXPECT_EQ(result, VK_SUCCESS);
+
+ // Initialize the image
+ const std::vector<uint16_t> kPixels(kWidth * kHeight, 0x730F);
+ helper.writePixels(image, VK_IMAGE_LAYOUT_UNDEFINED, VK_FORMAT_R4G4B4A4_UNORM_PACK16, {0, 0, 0},
+ extent, kPixels.data(), kWidth * kHeight * sizeof(uint16_t));
+
+ typename Traits::Handle memoryHandle = Traits::InvalidHandle();
+ result = Traits::ExportMemory(&helper, deviceMemory, &memoryHandle);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_NE(memoryHandle, Traits::InvalidHandle());
+
+ {
+ GLMemoryObject memoryObject;
+ GLint dedicatedMemory = GL_TRUE;
+ glMemoryObjectParameterivEXT(memoryObject, GL_DEDICATED_MEMORY_OBJECT_EXT,
+ &dedicatedMemory);
+ Traits::ImportMemory(memoryObject, deviceMemorySize, memoryHandle);
+
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexStorageMemFlags2DANGLE(GL_TEXTURE_2D, 1, GL_RGBA4, kWidth, kHeight, memoryObject, 0,
+ createFlags, usageFlags, nullptr);
+
+ GLSemaphore glAcquireSemaphore;
+ Traits::ImportSemaphore(glAcquireSemaphore, acquireSemaphoreHandle);
+
+ // Note: writePixels leaves the image in TRANSFER_DST_OPTIMAL layout.
+ helper.releaseImageAndSignalSemaphore(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+ vkAcquireSemaphore);
+
+ const GLuint barrierTexture = texture;
+ const GLenum textureSrcLayout = GL_LAYOUT_SHADER_READ_ONLY_EXT;
+ glWaitSemaphoreEXT(glAcquireSemaphore, 0, nullptr, 1, &barrierTexture, &textureSrcLayout);
+
+ // Sample from the texture and verify color.
Regression Test / PoC
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index 5df1eab..75e6ddb 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -248,6 +248,7 @@
# VVL error
42264614 LINUX : VulkanExternalImageTestES31.UninitializedOnGLImportAndStorageWrite/* = SKIP_FOR_CAPTURE
+42264614 LINUX : VulkanExternalImageTest.NonRenderableWithFlags/* = SKIP_FOR_CAPTURE
# CoherentBufferTracker deadlocks and flaky crashes
42266529 LINUX : BufferDataTestES3.* = SKIP_FOR_CAPTURE
diff --git a/src/tests/gl_tests/CompressedTextureFormatsTest.cpp b/src/tests/gl_tests/CompressedTextureFormatsTest.cpp
index dc0fb4d..0900d16 100644
--- a/src/tests/gl_tests/CompressedTextureFormatsTest.cpp
+++ b/src/tests/gl_tests/CompressedTextureFormatsTest.cpp
@@ -632,7 +632,7 @@
{
// ETC2/EAC formats always pass validation on ES3 contexts but in some cases fail in drivers
// because their emulation is not implemented for OpenGL renderer.
- // https://crbug.com/angleproject/6300
+ // https://anglebug.com/42264819
if (mAlwaysOnES3)
{
ANGLE_SKIP_TEST_IF(getClientMajorVersion() >= 3 &&
diff --git a/src/tests/gl_tests/VulkanExternalImageTest.cpp b/src/tests/gl_tests/VulkanExternalImageTest.cpp
index e76bb2f..05f66bc 100644
--- a/src/tests/gl_tests/VulkanExternalImageTest.cpp
+++ b/src/tests/gl_tests/VulkanExternalImageTest.cpp
@@ -51,7 +51,8 @@
{VK_FORMAT_R8_UNORM, GL_ALPHA8_EXT}, // ALPHA_8
{VK_FORMAT_R8_UNORM, GL_LUMINANCE8_EXT}, // LUMINANCE_8
{VK_FORMAT_R8G8_UNORM, GL_RG8_EXT}, // RG_88
- {VK_FORMAT_R8G8B8A8_UNORM, GL_RGB8_OES}, // RGBX_8888
+ {VK_FORMAT_R8G8B8_UNORM, GL_RGB8_OES}, // RGB_888
+ {VK_FORMAT_R8G8B8A8_UNORM, GL_RGBX8_ANGLE}, // RGBX_8888
};
struct OpaqueFdTraits
@@ -519,12 +520,6 @@
helper.initialize(isSwiftshader, enableDebugLayers);
for (const ImageFormatPair &format : kChromeFormats)
{
- // https://crbug.com/angleproject/5046
- if ((format.vkFormat == VK_FORMAT_R4G4B4A4_UNORM_PACK16) && IsIntel())
- {
- continue;
- }
-
if (!Traits::CanCreateImage(helper, format.vkFormat, VK_IMAGE_TYPE_2D,
VK_IMAGE_TILING_OPTIMAL, createFlags, usageFlags))
{
@@ -542,6 +537,12 @@
continue;
}
+ if (format.internalFormat == GL_RGBX8_ANGLE &&
+ !IsGLExtensionEnabled("GL_ANGLE_rgbx_internal_format"))
+ {
+ continue;
+ }
+
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory deviceMemory = VK_NULL_HANDLE;
VkDeviceSize deviceMemorySize = 0;
@@ -1416,6 +1417,127 @@
enableDebugLayers());
}
+// Test importing a non-renderable texture, using GL_ANGLE_memory_object_flags.
+TEST_P(VulkanExternalImageTest, NonRenderableWithFlags)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_memory_object_fd"));
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_semaphore_fd"));
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_memory_object_flags"));
+
+ using Traits = OpaqueFdTraits;
+
+ ASSERT(EnsureGLExtensionEnabled(Traits::MemoryObjectExtension()));
+ ASSERT(EnsureGLExtensionEnabled(Traits::SemaphoreExtension()));
+
+ // The format that is used by this test, RGBA4, is not necessarily renderable. This test
+ // ensures ANGLE does not attempt to fall back to RGBA8.
+ VkImageCreateFlags createFlags = kDefaultImageCreateFlags;
+ VkImageUsageFlags usageFlags = VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+ VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
+
+ AdjustCreateFlags(true, &createFlags);
+
+ VulkanHelper helper;
+ helper.initialize(isSwiftshader(), enableDebugLayers());
+
+ const VkFormat format = VK_FORMAT_R4G4B4A4_UNORM_PACK16;
+ ANGLE_SKIP_TEST_IF(!Traits::CanCreateImage(helper, format, VK_IMAGE_TYPE_2D,
+ VK_IMAGE_TILING_OPTIMAL, createFlags, usageFlags));
+ ANGLE_SKIP_TEST_IF(!Traits::CanCreateSemaphore(helper));
+
+ VkSemaphore vkAcquireSemaphore = VK_NULL_HANDLE;
+ VkResult result = Traits::CreateSemaphore(&helper, &vkAcquireSemaphore);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_TRUE(vkAcquireSemaphore != VK_NULL_HANDLE);
+
+ VkSemaphore vkReleaseSemaphore = VK_NULL_HANDLE;
+ result = Traits::CreateSemaphore(&helper, &vkReleaseSemaphore);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_TRUE(vkReleaseSemaphore != VK_NULL_HANDLE);
+
+ typename Traits::Handle acquireSemaphoreHandle = Traits::InvalidHandle();
+ result = Traits::ExportSemaphore(&helper, vkAcquireSemaphore, &acquireSemaphoreHandle);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_NE(acquireSemaphoreHandle, Traits::InvalidHandle());
+
+ typename Traits::Handle releaseSemaphoreHandle = Traits::InvalidHandle();
+ result = Traits::ExportSemaphore(&helper, vkReleaseSemaphore, &releaseSemaphoreHandle);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_NE(releaseSemaphoreHandle, Traits::InvalidHandle());
+
+ VkImage image = VK_NULL_HANDLE;
+ VkDeviceMemory deviceMemory = VK_NULL_HANDLE;
+ VkDeviceSize deviceMemorySize = 0;
+
+ VkExtent3D extent = {kWidth, kHeight, 1};
+ result = Traits::CreateImage2D(&helper, format, createFlags, usageFlags, nullptr, extent,
+ &image, &deviceMemory, &deviceMemorySize);
+ EXPECT_EQ(result, VK_SUCCESS);
+
+ // Initialize the image
+ const std::vector<uint16_t> kPixels(kWidth * kHeight, 0x730F);
+ helper.writePixels(image, VK_IMAGE_LAYOUT_UNDEFINED, VK_FORMAT_R4G4B4A4_UNORM_PACK16, {0, 0, 0},
+ extent, kPixels.data(), kWidth * kHeight * sizeof(uint16_t));
+
+ typename Traits::Handle memoryHandle = Traits::InvalidHandle();
+ result = Traits::ExportMemory(&helper, deviceMemory, &memoryHandle);
+ EXPECT_EQ(result, VK_SUCCESS);
+ EXPECT_NE(memoryHandle, Traits::InvalidHandle());
+
+ {
+ GLMemoryObject memoryObject;
+ GLint dedicatedMemory = GL_TRUE;
+ glMemoryObjectParameterivEXT(memoryObject, GL_DEDICATED_MEMORY_OBJECT_EXT,
+ &dedicatedMemory);
+ Traits::ImportMemory(memoryObject, deviceMemorySize, memoryHandle);
+
+ GLTexture texture;
+ glBindTexture(GL_TEXTURE_2D, texture);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexStorageMemFlags2DANGLE(GL_TEXTURE_2D, 1, GL_RGBA4, kWidth, kHeight, memoryObject, 0,
+ createFlags, usageFlags, nullptr);
+
+ GLSemaphore glAcquireSemaphore;
+ Traits::ImportSemaphore(glAcquireSemaphore, acquireSemaphoreHandle);
+
+ // Note: writePixels leaves the image in TRANSFER_DST_OPTIMAL layout.
+ helper.releaseImageAndSignalSemaphore(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
+ vkAcquireSemaphore);
+
+ const GLuint barrierTexture = texture;
+ const GLenum textureSrcLayout = GL_LAYOUT_SHADER_READ_ONLY_EXT;
+ glWaitSemaphoreEXT(glAcquireSemaphore, 0, nullptr, 1, &barrierTexture, &textureSrcLayout);
+
+ // Sample from the texture and verify color.
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Texture2D(), essl1_shaders::fs::Texture2D());
+ drawQuad(program, std::string(essl1_shaders::PositionAttrib()), 0.0f);
+ EXPECT_PIXEL_COLOR_NEAR(0, 0, GLColor(127, 63, 0, 255), 16);
+ EXPECT_GL_NO_ERROR();
+
+ GLSemaphore glReleaseSemaphore;
+ Traits::ImportSemaphore(glReleaseSemaphore, releaseSemaphoreHandle);
+
+ const GLenum textureDstLayout = GL_LAYOUT_TRANSFER_SRC_EXT;
+ glSignalSemaphoreEXT(glReleaseSemaphore, 0, nullptr, 1, &barrierTexture, &textureDstLayout);
+
+ helper.waitSemaphoreAndAcquireImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+ vkReleaseSemaphore);
+ }
+
+ EXPECT_GL_NO_ERROR();
+
+ vkDeviceWaitIdle(helper.getDevice());
+ vkDestroyImage(helper.getDevice(), image, nullptr);
+ vkDestroySemaphore(helper.getDevice(), vkAcquireSemaphore, nullptr);
+ vkDestroySemaphore(helper.getDevice(), vkReleaseSemaphore, nullptr);
+ vkFreeMemory(helper.getDevice(), deviceMemory, nullptr);
+}
+
template <typename Traits>
void RunUninitializedOnGLImportTest(bool useMemoryObjectFlags,
std::function<GLenum(GLuint)> useTexture,
@@ -1616,7 +1738,7 @@
}
// Test that texture storage created from VkImage memory can be imported as uninitialized in GL and
-// then used as sampler. Because the image is initialized, sampled results would be garbage, so
+// then used as sampler. Because the image is uninitialized, sampled results would be garbage, so
// this test is primarily ensuring no validation errors are generated.
TEST_P(VulkanExternalImageTest, UninitializedOnGLImportAndSample)
{
diff --git a/src/tests/test_utils/VulkanHelper.cpp b/src/tests/test_utils/VulkanHelper.cpp
index 5135cb2..d40da4a 100644
--- a/src/tests/test_utils/VulkanHelper.cpp
+++ b/src/tests/test_utils/VulkanHelper.cpp
@@ -1274,9 +1274,12 @@
const void *pixels,
size_t pixelsSize)
{
- ASSERT(imageFormat == VK_FORMAT_B8G8R8A8_UNORM || imageFormat == VK_FORMAT_R8G8B8A8_UNORM);
+ ASSERT(imageFormat == VK_FORMAT_B8G8R8A8_UNORM || imageFormat == VK_FORMAT_R8G8B8A8_UNORM ||
+ imageFormat == VK_FORMAT_R4G4B4A4_UNORM_PACK16);
ASSERT(imageExtent.depth == 1);
- ASSERT(pixelsSize == 4 * imageExtent.width * imageExtent.height);
+
+ uint32_t pixelSize = imageFormat == VK_FORMAT_R4G4B4A4_UNORM_PACK16 ? 2 : 4;
+ ASSERT(pixelsSize == pixelSize * imageExtent.width * imageExtent.height);
VkBufferCreateInfo bufferCreateInfo = {
/* .sType = */ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
diff --git a/src/tests/test_utils/angle_test_instantiate.cpp b/src/tests/test_utils/angle_test_instantiate.cpp
index 7c802b1..43d7ec6 100644
--- a/src/tests/test_utils/angle_test_instantiate.cpp
+++ b/src/tests/test_utils/angle_test_instantiate.cpp
@@ -264,7 +264,7 @@
// for desktop Linux when USE_OZONE && USE_X11 are both defined results in incorrect tests'
// expectations. We should also rework them and make IsOzone less vague.
//
- // TODO(crbug.com/angleproject/4977): make it possible to switch between X11 and Wayland on
+ // TODO(https://anglebug.com/42263550): make it possible to switch between X11 and Wayland on
// Ozone/Linux builds. Probably, it's possible to identify the WAYLAND backend by checking
// the WAYLAND_DISPLAY or XDG_SESSION_TYPE env vars. And also make the IsOzone method less
// vague (read the comment above).
Original Bug Report
Potential GPU OOB Write via ANGLE Vulkan External Memory Format Fallback
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: ANGLE’s Vulkan backend unconditionally selects a renderable fallback format when importing external memory, even if color attachment usage is not requested. This can cause a larger format Vulkan image (e.g., 4bpp) to be bound to a smaller memory allocation (e.g., 2bpp). A compromised renderer can exploit this size mismatch by issuing GL writes, resulting in a potential out-of-bounds write on the GPU heap.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/MemoryObjectVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_format_utils.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_format_table_autogen.cpp
Estimated timestamp from git blame: 2024-01-18
Description
A logic error in ANGLE’s Vulkan backend leads to a potential Out-of-Bounds (OOB) write in the GPU process. When importing external memory to create a texture, ANGLE unconditionally selects a “renderable” fallback format, regardless of whether the requested usage includes COLOR_ATTACHMENT support.
The vulnerability exists in TextureVk::setStorageExternalMemory and MemoryObjectVk::createImage. In both functions, ANGLE calls vkFormat.getActualRenderableImageFormatID() to determine the internal format. If the Vulkan driver does not support a specific format (e.g., R4G4B4A4_UNORM, 2 bytes per pixel) as a color attachment, ANGLE falls back to a wider format (e.g., R8G8B8A8_UNORM, 4 bytes per pixel).
If the external memory was allocated by Chromium using the native 2bpp format (because it only required SAMPLED usage), ANGLE will subsequently create a 4bpp VkImage and bind it to that same 2bpp physical memory. An ASSERT(externalMemoryRequirements.size == mSize) exists in MemoryObjectVk::createImage to catch this, but it compiles out in release builds. The Passthrough Command Buffer does not prevent GL write commands on read-only SharedImages, allowing an attacker to trigger an OOB write.
Potential Attack Path
Note: These are suggested steps; our tooling agent does not currently have the ability to run code to verify a live PoC.
- A compromised renderer process sends a
CreateSharedImageIPC requesting thekRGBA_4444format with strictly read-oriented usages (e.g.,SHARED_IMAGE_USAGE_GLES2_READ), omitting write usages that force color attachment requirements. - Chromium’s
ExternalVkImageBackingFactoryallocates a 2bppVkDeviceMemorybacking because the driver supports sampling fromR4G4B4A4_UNORM. - The renderer triggers GL access via
BeginSharedImageAccessDirectCHROMIUM(in read-only mode). - ANGLE imports the memory via
TextureVk::setStorageExternalMemory. It unconditionally callsgetActualRenderableImageFormatID(). - Lacking color attachment support for
RGBA_4444, ANGLE falls back to the 4bppRGBA8format and creates a 4bppVkImage. - In release builds, the
ASSERTchecking allocation sizes inMemoryObjectVk::createImageis bypassed, and the 4bppVkImageis bound to the 2bppVkDeviceMemory. - The renderer issues a GL write command like
glTexSubImage2D. The passthrough decoder does not block this despite the SharedImage read-only access mode. - The Vulkan driver executes the memory copy assuming a 4bpp destination, writing past the end of the 2bpp allocation, corrupting the Vulkan GPU heap and potentially leading to a sandbox escape.
Suggested Fix
- Respect Usage Flags: Update
TextureVk::setStorageExternalMemoryandMemoryObjectVk::createImageto evaluate the requestedusageFlags. IfVK_IMAGE_USAGE_COLOR_ATTACHMENT_BITis not requested, usevkFormat.getActualImageFormatID(vk::ImageFormatSupport::SampleOnly)instead of unconditionally requiring a renderable format. - Enforce Size Checks: Convert the
ASSERT(externalMemoryRequirements.size == mSize)inMemoryObjectVk::createImageto a hard macro (e.g.,ANGLE_VK_CHECKor returningangle::Result::Stop) so that size mismatches safely fail in release builds.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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. And please feel free to reach out to me directly if you have concerns or feedback on the project.