CVE-2026-87647
Overview
Background
- SharedImage
- a GPU-process abstraction for a cross-API GPU resource (backed here by a GL texture) that can be produced by one client and consumed by another.
- Compressed texture format
- a block-compressed GL format such as ETC1 (
viz::SinglePlaneFormat::kETC1) whose contents must be supplied viaglCompressedTexImage2Dand which is not color-renderable. - Lazy clearing
- the SharedImage subsystem normally leaves a texture “uncleared” and initializes (clears) it on first read, but clearing relies on the format being renderable, which compressed formats are not.
- `ValidateCompressedTexDimensions` / `GetCompressedTexSizeInBytes`
- GLES2 helpers that validate compressed-texture dimensions and compute the exact byte count a compressed image of a given size requires.
Root Cause Analysis
In the compressed-image validation code (identified by the commit message as GLCommonImageBackingFactory::CanCreateTexture; the hunk’s @@ header does not name the enclosing function), all size and format validation was gated behind if (!pixel_data.empty()), so when a caller supplied no initial data the compressed-format branch was skipped entirely and creation was allowed to proceed. Because compressed formats are not color-renderable, the backing cannot be cleared after creation, and glCompressedTexImage2D rejects null data for non-zero dimensions, so the resulting texture’s storage was left in an uninitialized state. The invariant violated is that every SharedImage must either carry correctly sized initial data or be clearable to a known value before it can be read; a compressed image with empty pixel_data satisfied neither.
The fix runs the compressed-format branch unconditionally: it now validates dimensions, computes bytes_required, and returns false unless pixel_data.size() exactly equals that non-zero size, which rejects the empty-data case. Only after that does the else if (!pixel_data.empty()) branch handle the uncompressed path, preserving prior behavior for renderable formats.
pixel_data being non-empty, which let a compressed SharedImage be created with no initial data and no way to be cleared; the fix makes compressed-format checking unconditional and mandates that pixel_data.size() match the exact required compressed byte count.Attack Path
- Request a compressed SharedImage with no data
A renderer/client asks the GPU process to create a SharedImage using a compressed format (e.g. ETC1) at non-zero dimensions while supplying empty
pixel_data. - Validation is skipped
Pre-patch,
if (!pixel_data.empty())is false, soValidateCompressedTexDimensions,GetCompressedTexSizeInBytes, and the size-match check never run, and the request is accepted. - Uninitialized backing is created
The compressed GL texture is allocated but, being non-color-renderable, is never validly initialized (no
glCompressedTexImage2Dupload, and clearing does not apply), leaving its storage holding whatever GPU memory was previously resident. - Uninitialized contents are consumed A subsequent read/sample/copy of the SharedImage returns the stale, uninitialized GPU memory to the client (inferred: the diff shows the creation flaw but not the specific downstream read sink).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc |
modified | |
TEST_Fgpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc |
modified | |
ifgpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.ccgpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc
Audit Directions
- Validation gated on optional initial dataLook for other resource-creation paths that only validate size/format
if (!pixel_data.empty())(or similar), since a non-clearable resource created without data may be left uninitialized. - Non-renderable / non-clearable formatsAudit backings for formats that cannot be lazily cleared (compressed, external, certain YUV) to ensure they mandate correctly sized initial data rather than relying on post-creation clearing.
Patch
From 5696f96dbbe9994e7025880bd78057444557ccdf Mon Sep 17 00:00:00 2001
From: vikas soni <vikassoni@chromium.org>
Date: Tue, 01 Sep 2026 13:58:04 -0700
Subject: [PATCH] [GPU Security]: Require initial data for compressed GL SharedImages.
GLCommonImageBackingFactory::CanCreateTexture only ran compressed-format
validation when pixel_data was non-empty, so a compressed SharedImage
could be created with no initial data. Compressed formats are not
color-renderable and therefore cannot be cleared after creation, and
glCompressedTexImage2D does not accept null data for non-zero
dimensions.
Run compressed-format validation unconditionally and require that
pixel_data matches the computed compressed size, which rejects requests
with empty pixel data.
Bug: 536673946
Change-Id: I6c25102d8d10685aadbd993efac0bb6cf5296cd6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8299334
Commit-Queue: vikas soni <vikassoni@chromium.org>
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1690102}
---
diff --git a/gpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc b/gpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc
index 48fbdfd..f9153cc 100644
--- a/gpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc
+++ b/gpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc
@@ -219,62 +219,63 @@
return false;
}
- // If we have initial data to upload, ensure it is sized appropriately.
- if (!pixel_data.empty()) {
- DCHECK_EQ(format_infos.size(), 1u);
+ if (format_infos[0].is_compressed) {
+ CHECK_EQ(format_infos.size(), 1u);
const FormatInfo& format_info = format_infos[0];
+ const char* error_message = "unspecified";
+ if (!gles2::ValidateCompressedTexDimensions(
+ target, /*level=*/0, size.width(), size.height(), /*depth=*/1,
+ format_info.image_internal_format, &error_message)) {
+ DVLOG(2) << "CreateSharedImage: "
+ "ValidateCompressedTexDimensionsFailed with error: "
+ << error_message;
+ return false;
+ }
- if (format_info.is_compressed) {
- const char* error_message = "unspecified";
- if (!gles2::ValidateCompressedTexDimensions(
- target, /*level=*/0, size.width(), size.height(), /*depth=*/1,
- format_info.image_internal_format, &error_message)) {
- DVLOG(2) << "CreateSharedImage: "
- "ValidateCompressedTexDimensionsFailed with error: "
- << error_message;
- return false;
- }
+ GLsizei bytes_required = 0;
+ if (!gles2::GetCompressedTexSizeInBytes(
+ /*function_name=*/nullptr, size.width(), size.height(),
+ /*depth=*/1, format_info.image_internal_format, &bytes_required,
+ /*error_state=*/nullptr)) {
+ DVLOG(2) << "CreateSharedImage: Unable to compute required size for "
+ "initial texture upload.";
+ return false;
+ }
- GLsizei bytes_required = 0;
- if (!gles2::GetCompressedTexSizeInBytes(
- /*function_name=*/nullptr, size.width(), size.height(),
- /*depth=*/1, format_info.image_internal_format, &bytes_required,
- /*error_state=*/nullptr)) {
- DVLOG(2) << "CreateSharedImage: Unable to compute required size for "
+ // Compressed textures cannot be cleared so they must be created with
+ // initial pixel data of the correct size.
+ if (bytes_required < 0 ||
+ pixel_data.size() != static_cast<size_t>(bytes_required)) {
+ DVLOG(2) << "CreateSharedImage: Initial data does not have expected "
+ "size.";
+ return false;
+ }
+ } else if (!pixel_data.empty()) {
+ // If we have initial data to upload, ensure it is sized appropriately.
+ CHECK_EQ(format_infos.size(), 1u);
+ const FormatInfo& format_info = format_infos[0];
+ uint32_t bytes_required;
+ uint32_t unpadded_row_size = 0u;
+ uint32_t padded_row_size = 0u;
+ if (!gles2::GLES2Util::ComputeImageDataSizes(
+ size.width(), size.height(), /*depth=*/1, format_info.gl_format,
+ format_info.gl_type, /*alignment=*/4, &bytes_required,
+ &unpadded_row_size, &padded_row_size)) {
+ LOG(ERROR) << "CreateSharedImage: Unable to compute required size for "
"initial texture upload.";
- return false;
- }
+ return false;
+ }
- if (bytes_required < 0 ||
- pixel_data.size() != static_cast<size_t>(bytes_required)) {
- DVLOG(2) << "CreateSharedImage: Initial data does not have expected "
+ // The GL spec, used in the computation for required bytes in the function
+ // above, assumes no padding is required for the last row in the image.
+ // But the client data does include this padding, so we add it for the
+ // data validation check here.
+ uint32_t padding = padded_row_size - unpadded_row_size;
+ bytes_required += padding;
+ if (pixel_data.size() != bytes_required) {
+ LOG(ERROR) << "CreateSharedImage: Initial data does not have expected "
"size.";
- return false;
- }
- } else {
- uint32_t bytes_required;
- uint32_t unpadded_row_size = 0u;
- uint32_t padded_row_size = 0u;
- if (!gles2::GLES2Util::ComputeImageDataSizes(
- size.width(), size.height(), /*depth=*/1, format_info.gl_format,
- format_info.gl_type, /*alignment=*/4, &bytes_required,
- &unpadded_row_size, &padded_row_size)) {
- LOG(ERROR) << "CreateSharedImage: Unable to compute required size for "
- "initial texture upload.";
- return false;
- }
-
- // The GL spec, used in the computation for required bytes in the function
- // above, assumes no padding is required for the last row in the image.
- // But the client data does include this padding, so we add it for the
- // data validation check here.
- uint32_t padding = padded_row_size - unpadded_row_size;
- bytes_required += padding;
- if (pixel_data.size() != bytes_required) {
- LOG(ERROR) << "CreateSharedImage: Initial data does not have expected "
- "size.";
- return false;
- }
+ return false;
}
}
diff --git a/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc
index 94139b6c..13f1050 100644
--- a/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc
@@ -167,6 +167,33 @@
using GLTextureImageBackingFactoryWithReadbackTest =
GLTextureImageBackingFactoryWithUploadTest;
+TEST_F(GLTextureImageBackingFactoryTest, CompressedFormatRequiresInitialData) {
+ if (!supports_etc1_) {
+ GTEST_SKIP();
+ }
+
+ auto format = viz::SinglePlaneFormat::kETC1;
+ gfx::Size size(64, 64);
+ // Note: The specific usage doesn't matter here as long as it's supported by
+ // GLTextureImageBacking.
+ gpu::SharedImageUsageSet usage = SHARED_IMAGE_USAGE_GLES2_READ;
+
+ // Compressed textures cannot be cleared so they must be created with initial
+ // pixel data.
+ bool supported = backing_factory_->CanCreateSharedImage(
+ usage, format, size, /*thread_safe=*/false, gfx::EMPTY_BUFFER,
+ GrContextType::kGL, {});
+ EXPECT_FALSE(supported);
+
+ // With correctly sized initial data the format is supported.
+ size_t required_size = format.MaybeEstimatedSizeInBytes(size).value();
+ std::vector<uint8_t> initial_data(required_size);
+ supported = backing_factory_->CanCreateSharedImage(
+ usage, format, size, /*thread_safe=*/false, gfx::EMPTY_BUFFER,
+ GrContextType::kGL, initial_data);
+ EXPECT_TRUE(supported);
+}
+
TEST_F(GLTextureImageBackingFactoryTest, InvalidFormat) {
auto format = viz::SinglePlaneFormat::kBGR_565;
gfx::Size size(256, 256);
Regression Test / PoC
diff --git a/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc
index 94139b6c..13f1050 100644
--- a/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/gl_texture_image_backing_factory_unittest.cc
@@ -167,6 +167,33 @@
using GLTextureImageBackingFactoryWithReadbackTest =
GLTextureImageBackingFactoryWithUploadTest;
+TEST_F(GLTextureImageBackingFactoryTest, CompressedFormatRequiresInitialData) {
+ if (!supports_etc1_) {
+ GTEST_SKIP();
+ }
+
+ auto format = viz::SinglePlaneFormat::kETC1;
+ gfx::Size size(64, 64);
+ // Note: The specific usage doesn't matter here as long as it's supported by
+ // GLTextureImageBacking.
+ gpu::SharedImageUsageSet usage = SHARED_IMAGE_USAGE_GLES2_READ;
+
+ // Compressed textures cannot be cleared so they must be created with initial
+ // pixel data.
+ bool supported = backing_factory_->CanCreateSharedImage(
+ usage, format, size, /*thread_safe=*/false, gfx::EMPTY_BUFFER,
+ GrContextType::kGL, {});
+ EXPECT_FALSE(supported);
+
+ // With correctly sized initial data the format is supported.
+ size_t required_size = format.MaybeEstimatedSizeInBytes(size).value();
+ std::vector<uint8_t> initial_data(required_size);
+ supported = backing_factory_->CanCreateSharedImage(
+ usage, format, size, /*thread_safe=*/false, gfx::EMPTY_BUFFER,
+ GrContextType::kGL, initial_data);
+ EXPECT_TRUE(supported);
+}
+
TEST_F(GLTextureImageBackingFactoryTest, InvalidFormat) {
auto format = viz::SinglePlaneFormat::kBGR_565;
gfx::Size size(256, 256);
Original Bug Report
Validation Bypass in GLCommonImageBackingFactory Leads to GPU Memory Disclosure via ETC1
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: A validation gap in GLCommonImageBackingFactory::CanCreateTexture allows the creation of compressed-format SharedImages with empty pixel data. This bypasses ANGLE’s initialization and Chromium’s automated clearing, allowing a compromised renderer to sample uninitialized Vulkan memory and leak cross-origin GPU data.
Affected files:
gpu/command_buffer/service/shared_image/gl_common_image_backing_factory.ccgpu/command_buffer/service/shared_image/gl_texture_holder.ccgpu/command_buffer/service/gles2_cmd_decoder_passthrough.ccgpu/command_buffer/service/shared_image/egl_image_backing.cc
Estimated timestamp from git blame: 2018-12-11
1. Summary of the Issue (Meant for Human Triage)
A potential high-severity security vulnerability exists in Chromium’s GPU command buffer service due to a validation gap when creating compressed-format SharedImage textures. When a compromised renderer requests a new SharedImage with a compressed format (e.g., ETC1) but provides no initial pixel data, the validation logic inside GLCommonImageBackingFactory::CanCreateTexture is entirely bypassed. This bypass allows the creation of a compressed texture without performing the necessary dimension and bounds checks.
Because the release-build SharedContextState operates with validation disabled (KHR_no_error) and robust_resource_initialization=false, ANGLE marks the texture state as fully Initialized upon creation without staging any clear or memory upload. Furthermore, because ETC1 is not a color-renderable format, Chromium’s automated clearing mechanism (EnsureClear) fails because the framebuffer remains incomplete, preventing the texture from actually being cleared.
Since BeginSharedImageAccess is not strictly enforced on the service side by the passthrough command decoder, a compromised renderer can directly bind and draw/sample from this texture. When sampled, ANGLE’s Vulkan backend lazily allocates a VkImage from the process-wide Vulkan Memory Allocator (VMA) pool. Because ANGLE incorrectly believes the texture is already initialized, no robust clear is performed, allowing the renderer to read uninitialized GPU memory. As VMA is a process-wide suballocator, this memory can contain recycled contents from other origins, the compositor, or the OS, leading to cross-origin GPU memory disclosure.
Note: The following exploitation steps are suggested/potential, as our tooling agent does not currently have the ability to run a live proof-of-concept.
2. Proof-of-Concept & Detailed Execution Flow
An attacker from a compromised renderer process (A-RENDERER) on an Android device (where the GPU process is unsandboxed and ANGLE-Vulkan is the default backend) could trigger this vulnerability through the following potential execution sequence:
Step 1: Create a SharedImage with a Compressed Format and Empty Pixel Data
A-RENDERERsends agpu.mojom.GpuChannel::FlushDeferredRequestsIPC message containing aDeferredSharedImageRequest::kCreateSharedImagerequest.- The request specifies a mailbox
M, formatviz::mojom::SingleplanarFormat::ETC1, size (e.g., 64x64), usageSHARED_IMAGE_USAGE_GLES2_READ, and an emptypixel_dataspan (pixel_data.empty() == true). - The request reaches
SharedImageStub::OnCreateSharedImage(gpu/ipc/service/shared_image_stub.cc), which callsSharedImageFactory::CreateSharedImage.
Step 2: SharedImage Factory Selection and Validation Bypass
SharedImageFactoryiterates through available backing factories. On Android,WrappedSkImageBackingFactoryrejectsSHARED_IMAGE_USAGE_GLES2_READ(wrapped_sk_image_backing_factory.cc:145-147).GLTextureImageBackingFactoryevaluates the request viaGLCommonImageBackingFactory::CanCreateTexture(gpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc:201).- During validation, the checks for compressed formats are incorrectly nested inside an
if (!pixel_data.empty())block:
// gpu/command_buffer/service/shared_image/gl_common_image_backing_factory.cc:220
if (!pixel_data.empty()) { // ← Gated on non-empty data
DCHECK_EQ(format_infos.size(), 1u);
const FormatInfo& format_info = format_infos[0];
if (format_info.is_compressed) {
if (!gles2::ValidateCompressedTexDimensions(...)) return false; // SKIPPED
if (!gles2::GetCompressedTexSizeInBytes(...)) return false; // SKIPPED
if (... pixel_data.size() != bytes_required) return false; // SKIPPED
}
...
}
return true; // ← Returns true without validation!
- Because
pixel_datais empty, the compressed dimensions validation is completely bypassed, andCanCreateTexturereturnstrue.
Step 3: Allocation of Texture with Spec-Invalid Arguments
- During initialization,
GLTextureHolder::Initializeis called (gpu/command_buffer/service/shared_image/gl_texture_holder.cc:145). - Because
GL_ETC1_RGB8_OESis compressed,format_info.is_compressedis true andsupports_storageis false, forcing theglCompressedTexImage2DFnbranch:
// gpu/command_buffer/service/shared_image/gl_texture_holder.cc:212
} else if (format_info.is_compressed) {
ScopedUnpackState scoped_unpack_state(!pixel_data.empty());
gl::ScopedProgressReporter scoped_progress_reporter(progress_reporter_);
api->glCompressedTexImage2DFn(format_desc_.target, 0,
format_desc_.image_internal_format,
size_.width(), size_.height(), /*border=*/0,
pixel_data.size(), pixel_data.data()); // 0, nullptr
}
- The driver is invoked with
imageSize = 0anddata = nullptr, despite non-zero dimensions, violating OpenGL ES /OES_compressed_ETC1_RGB8_texturespecifications.
Step 4: ANGLE Bypasses Validation and Marks Texture as Initialized
- This initialization occurs on the process-wide
SharedContextState. In release builds,gpu_channel_manager.cc:917-920configures this context withattribs.robust_resource_initialization = falseandattribs.can_skip_validation = true. ui/gl/gl_context_egl.cc:145-150appendsEGL_CONTEXT_OPENGL_NO_ERROR_KHR=EGL_TRUE.- ANGLE’s autogenerated entry point checks
context->skipValidation()(which is true) and skipsValidateCompressedTexImage2D. - Inside
Texture::setCompressedImage, ANGLE determines the initial state:
// third_party/angle/src/libANGLE/Texture.cpp:47
InitState DetermineInitState(const Context *context, Buffer *unpackBuffer, const uint8_t *pixels) {
if (!context || !context->isRobustResourceInitEnabled())
return InitState::Initialized; // ← Since robust init is off, returns Initialized
return (!pixels && !unpackBuffer) ? InitState::MayNeedInit : InitState::Initialized;
}
- The texture’s
mInitStateis set toInitState::Initialized. TextureVk::setSubImageImplis invoked, but sinceunpackBuffer == nullptrandpixels == nullptr, no subresource update or upload is staged. TheVkImagememory is left uninitialized.
Step 5: Chromium Clear-Tracking Remains Uncleared & EnsureClear Fails
- Back in
GLTextureImageBacking::InitializeGLTexture,SetCleared()is only called if!pixel_data.empty()(gl_texture_image_backing.cc:528). Sincepixel_datais empty, the backing remains marked as uncleared in Chromium’s tracking. A-RENDERERissuesgles2::cmds::CreateAndTexStorage2DSharedImageINTERNALto bind the mailbox to aclient_id.- The passthrough decoder unconditionally maps
client_idto the ANGLEservice_idviatexture_id_map.SetIDMapping(gles2_cmd_decoder_passthrough_doers.cc:4679). - It then attempts to clear the texture via
EnsureClear(gles2_cmd_decoder_passthrough.cc:613). EnsureClearattaches the uncleared texture to a framebuffer and callsglClearFn.EnsureClearthen checksglCheckFramebufferStatusEXTFn(GL_FRAMEBUFFER). However,GL_ETC1_RGB8_OEShastextureAttachmentSupport = NeverSupported(third_party/angle/src/libANGLE/formatutils.cpp:1222).- The framebuffer is incomplete.
glCheckFramebufferStatusEXTFndoes not returnGL_FRAMEBUFFER_COMPLETE, meaningrepresentation_->SetCleared()is never called.
Step 6: Renderer Bypasses BeginAccess and Samples Uninitialized Memory
A-RENDERERdeliberately omitsgles2::cmds::BeginSharedImageAccessDirectCHROMIUM. The passthrough decoder does not enforce callingBeginAccessfor bound textures (gles2_cmd_decoder_passthrough.cc:596-597).A-RENDERERsendsgles2::cmds::BindTexture, which resolves to the ANGLEservice_idusing the already-populatedtexture_id_map.A-RENDERERsets up a fragment shader that executestexture2D()to sample the bound texture, binds an RGBA8 framebuffer, and issuesgles2::cmds::DrawArrays.- When ANGLE evaluates the bound texture during the draw call, it calls
ensureInitializedon theTextureobject. Because the level was already marked asInitState::Initialized(Step 14),ensureInitializedshort-circuits. - ANGLE’s Vulkan backend lazily allocates the underlying
VkImage(VK_FORMAT_ETC2_R8G8B8_UNORM_BLOCK)from the process-wide Vulkan Memory Allocator (vk::Renderer/ VMA pool). - Because the state is
Initialized, no robust clear is performed. The shader executes, directly sampling the uninitializedVkDeviceMemorybacking theVkImage. A-RENDERERissuesgles2::cmds::ReadPixelsto exfiltrate the sampled values. Because VMA operates as a single, process-wide suballocator, this memory can contain recycled data from other origins, the compositor, or OS surfaces.
Proposed Fix
Move the compressed format validation in GLCommonImageBackingFactory::CanCreateTexture outside of the if (!pixel_data.empty()) check. The dimensions, limits, and supported formats must be checked regardless of whether initial data is provided. The if (!pixel_data.empty()) gate should only wrap the specific check that validates pixel_data.size() == bytes_required.
3. Technical Verification Details (Automated Audit Logs - Reviewers may skip this section)
> The vulnerability is valid and highly exploitable. By exploiting a validation gap in GLCommonImageBackingFactory::CanCreateTexture where compressed-format dimension checks are skipped for empty pixel_data, a compromised renderer can create an ETC1 SharedImage with no initial data.
>
> Since the SharedContextState is created with robust_resource_initialization=false and validation disabled, ANGLE marks the texture’s InitState as Initialized and stages no upload. Chromium’s EnsureClear attempts to clear the texture by attaching it to an FBO, but fails because ETC1 is not color-renderable (framebuffer incomplete), resulting in SetCleared() being skipped.
>
> Crucially, the passthrough command decoder does not enforce BeginSharedImageAccess on the service side. The renderer can directly bind the texture and sample it. When sampled in a WebGL context, ANGLE’s robust-init validation is bypassed because the texture is already marked Initialized. The Vulkan backend lazily allocates a VkImage from the process-wide Vulkan Memory Allocator (VMA) without clearing it, returning uninitialized memory to the renderer.
>
> I am rating this as High (S1) rather than the report’s Medium (S2). The report author conservatively rated it S2 because they didn’t trace cross-origin VMA recycling end-to-end. However, because VMA operates as a single process-wide suballocator for the entire GPU process (vk::Renderer owns a single vk::Allocator), any recycled memory can inherently belong to other origins, the compositor, or the OS. This constitutes a cross-origin GPU-memory disclosure, which is S1 (High) per the severity guidelines.
Environmental Assumptions & Risk Factors Confirmed:
- VMA Pooling: Confirmed via
third_party/angle/src/libANGLE/renderer/vulkan/vk_renderer.cpp:2847thatmAllocator.init()builds a singlevk::Allocatorfor the GPU process. Any recycled memory blocks are shared across different contexts and origins, making cross-origin data disclosure deterministic. - Android Backend: ANGLE-Vulkan is the default backend on Android devices, where the GPU process is unsandboxed on many configurations.
- SharedContextState Attributes: Verified via
gpu/ipc/service/gpu_channel_manager.cc:909-923andui/gl/gl_features.cc:230-241. In Android release builds (!DCHECK_IS_ON()),kDefaultEnableANGLEValidationis disabled. This results incan_skip_validation=trueandrobust_resource_initialization=falsebeing passed, which causesui/gl/gl_context_egl.cc:145-150to initialize the EGL context withEGL_CONTEXT_OPENGL_NO_ERROR_KHR=EGL_TRUE. - ETC1 Renderability: Verified via
third_party/angle/src/libANGLE/formatutils.cpp:1222thatGL_ETC1_RGB8_OESusesNeverSupportedfortextureAttachmentSupport. - Passthrough Enforcements: Verified via
gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc:493-536(DoBindTexture) andgpu/command_buffer/service/gles2_cmd_decoder_passthrough.cc:596-599that there is no automatic enforcement ofBeginSharedImageAccessDirectCHROMIUMfor bound textures.
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.