CVE-2026-7359
Overview
Files Changed
src/libANGLE/Buffer.cppsrc/libANGLE/Context.cppsrc/libANGLE/Context.hsrc/libANGLE/Fence.cpp
Patch
From 3ba420fa0598eac541a516a7661482eb96e5e7ad Mon Sep 17 00:00:00 2001 From: dan sinclair <[email protected]> Date: Mon, 20 Apr 2026 13:31:54 -0400 Subject: [PATCH] Defer the recycling of handles This CL changes the resource manager to defer the recycling of IDs if bindGeneratesResources is not enabled. This will hold onto a resource ID until the resource has its `onDestroy` method called. Bug: chromium:496284494 Change-Id: Id45d09738d906d66ca545861a31e445fc36a1267 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7757179 Commit-Queue: Shahbaz Youssefi <[email protected]> Auto-Submit: dan sinclair <[email protected]> Reviewed-by: Geoff Lang <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Commit-Queue: dan sinclair <[email protected]> --- diff --git a/src/libANGLE/Buffer.cpp b/src/libANGLE/Buffer.cpp index 525ae59..82eed7d 100644 --- a/src/libANGLE/Buffer.cpp +++ b/src/libANGLE/Buffer.cpp @@ -18,7 +18,7 @@ { namespace { -constexpr size_t kInvalidContentsObserverIndex = std::numeric_limits<size_t>::max(); +constexpr size_t kInvalidContentsObserverIndex = std::numeric_limits<size_t>::max(); } // anonymous namespace // VertexArrayBufferBindingMaskAndContext implementation @@ -95,8 +95,7 @@ Buffer::Buffer(rx::GLImplFactory *factory, BufferID id) : RefCountObject(factory->generateSerial(), id), mImpl(factory->createBuffer(mState)) -{ -} +{} Buffer::~Buffer() { @@ -107,9 +106,16 @@ { mContentsObservers.clear(); + if (context && context->retainIdUntilObjectDestroyed()) + { + context->onBufferDestroy(this); + } + // In tests, mImpl might be null. if (mImpl) + { mImpl->destroy(context); + } } void Buffer::onBind(const Context *context, BufferBinding target) diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp index 9614024..36cd567 100644 --- a/src/libANGLE/Context.cpp +++ b/src/libANGLE/Context.cpp @@ -732,7 +732,8 @@ mFrameCapture(new angle::FrameCapture), mRefCount(0), mOverlay(mImplementation.get()), - mIsDestroyed(false) + mIsDestroyed(false), + mDestroyedManagers(false) { for (angle::SubjectIndex uboIndex = kUniformBuffer0SubjectIndex; uboIndex < kUniformBufferMaxSubjectIndex; ++uboIndex) @@ -1031,19 +1032,40 @@ void Context::releaseSharedObjects() { + mDestroyedManagers = true; + mState.mBufferManager->release(this); + mState.mBufferManager = nullptr; + // mProgramPipelineManager must be before mShaderProgramManager to give each // PPO the chance to release any references they have to the Programs that // are bound to them before the Programs are released()'ed. mState.mProgramPipelineManager->release(this); + mState.mProgramPipelineManager = nullptr; + mState.mShaderProgramManager->release(this); + mState.mShaderProgramManager = nullptr; + mState.mTextureManager->release(this); + mState.mTextureManager = nullptr; + mState.mRenderbufferManager->release(this); + mState.mRenderbufferManager = nullptr; + mState.mSamplerManager->release(this); + mState.mSamplerManager = nullptr; + mState.mSyncManager->release(this); + mState.mSyncManager = nullptr; + mState.mFramebufferManager->release(this); + mState.mFramebufferManager = nullptr; + mState.mMemoryObjectManager->release(this); + mState.mMemoryObjectManager = nullptr; + mState.mSemaphoreManager->release(this); + mState.mSemaphoreManager = nullptr; } Context::~Context() {} @@ -10115,6 +10137,51 @@ mPrivateStateCache.invalidateCachedBasicDrawElementsError(); } +bool Context::retainIdUntilObjectDestroyed() const +{ + // If BindGeneratesResource is disabled, then we can defer recycling the handle ID until the + // object has had the `onDestroy` method called, preventing ID reuse bugs. + // + // If the context is being destroyed however, we don't want to try to recycle as the handle + // manager may be gone. + return !mDestroyedManagers && !mState.isBindGeneratesResourceEnabled(); +} + +void Context::onBufferDestroy(const Buffer *buffer) const +{ + mState.mBufferManager->recycleHandle(buffer->id()); +} + +void Context::onTextureDestroy(const Texture *texture) const +{ + mState.mTextureManager->recycleHandle(texture->id()); +} + +void Context::onRenderbufferDestroy(const Renderbuffer *renderBuffer) const +{ + mState.mRenderbufferManager->recycleHandle(renderBuffer->id()); +} + +void Context::onSamplerDestroy(const Sampler *sampler) const +{ + mState.mSamplerManager->recycleHandle(sampler->id()); +} + +void Context::onSyncDestroy(const Sync *sync) const +{ + mState.mSyncManager->recycleHandle(sync->id()); +} + +void Context::onFramebufferDestroy(const Framebuffer *framebuffer) const +{ + mState.mFramebufferManager->recycleHandle(framebuffer->id()); +} + +void Context::onProgramPipelineDestroy(const ProgramPipeline *programPipeline) const +{ + mState.mProgramPipelineManager->recycleHandle(programPipeline->id()); +} + // ErrorSet implementation. ErrorSet::ErrorSet(Debug *debug, const angle::FrontendFeatures &frontendFeatures, diff --git a/src/libANGLE/Context.h b/src/libANGLE/Context.h index 9bd6b60..0d0b473 100644 --- a/src/libANGLE/Context.h +++ b/src/libANGLE/Context.h @@ -979,6 +979,16 @@ GLint64 getInstancedVertexElementLimit() const; void onActiveTransformFeedbackChange(); + bool retainIdUntilObjectDestroyed() const; + + void onBufferDestroy(const Buffer *buffer) const; + void onTextureDestroy(const Texture *texture) const; + void onRenderbufferDestroy(const Renderbuffer *renderBuffer) const; + void onSamplerDestroy(const Sampler *sampler) const; + void onSyncDestroy(const Sync *sync) const; + void onFramebufferDestroy(const Framebuffer *framebuffer) const; + void onProgramPipelineDestroy(const ProgramPipeline *programPipeline) const; + private: void initializeDefaultResources(); void releaseSharedObjects(); @@ -1146,6 +1156,7 @@ OverlayType mOverlay; bool mIsDestroyed; + bool mDestroyedManagers; std::unique_ptr<Framebuffer> mDefaultFramebuffer; }; diff --git a/src/libANGLE/Fence.cpp b/src/libANGLE/Fence.cpp index adf0999..07115c9 100644 --- a/src/libANGLE/Fence.cpp +++ b/src/libANGLE/Fence.cpp @@ -11,6 +11,7 @@
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests.gni b/src/tests/angle_end2end_tests.gni
index a551075..43ed001 100644
--- a/src/tests/angle_end2end_tests.gni
+++ b/src/tests/angle_end2end_tests.gni
@@ -41,6 +41,7 @@
"gl_tests/BPTCCompressedTextureTest.cpp",
"gl_tests/BaseInstanceOverflowTest.cpp",
"gl_tests/BindGeneratesResourceTest.cpp",
+ "gl_tests/BindRecyclesResourceTest.cpp",
"gl_tests/BindUniformLocationTest.cpp",
"gl_tests/BlendFuncExtendedTest.cpp",
"gl_tests/BlendIntegerTest.cpp",
diff --git a/src/tests/gl_tests/BindRecyclesResourceTest.cpp b/src/tests/gl_tests/BindRecyclesResourceTest.cpp
new file mode 100644
index 0000000..d259a56
--- /dev/null
+++ b/src/tests/gl_tests/BindRecyclesResourceTest.cpp
@@ -0,0 +1,48 @@
+//
+// Copyright 2015 The ANGLE Project Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+
+// BindRecyclesResourceTest.cpp : Tests of the GL_CHROMIUM_bind_generates_resource extension.
+
+#include "test_utils/ANGLETest.h"
+
+namespace angle
+{
+
+class BindRecyclesResourceTest : public ANGLETest<>
+{
+ protected:
+ BindRecyclesResourceTest() { setBindGeneratesResource(false); }
+};
+
+// crbug.com/496284494
+TEST_P(BindRecyclesResourceTest, BufferRecycling)
+{
+ GLuint vao;
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+
+ GLuint idA;
+ glGenBuffers(1, &idA);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idA);
+
+ // Unbind the VAO
+ glBindVertexArray(0);
+
+ glDeleteBuffers(1, &idA);
+
+ GLuint idB;
+ glGenBuffers(1, &idB);
+ EXPECT_NE(idA, idB);
+
+ glDeleteVertexArrays(1, &vao);
+ glDeleteBuffers(1, &idB);
+}
+
+// Use this to select which configurations (e.g. which renderer, which GLES major version) these
+// tests should be run against.
+ANGLE_INSTANTIATE_TEST_ES3_AND_ES31_AND_ES32(BindRecyclesResourceTest);
+
+} // namespace angle
Original Bug Report
GPU Process UAF Write via Service ID Collision in VAO-Orphaned Buffers
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A Use-After-Free (UAF) Write vulnerability potentially exists in the GPU process passthrough command decoder due to service ID recycling in ANGLE and incorrect client ID resolution for orphaned buffers. A compromised renderer can force a service ID collision between an orphaned buffer and a new buffer, poisoning the passthrough decoder’s mapping state to write arbitrary data into freed GPU memory.
Affected files:
gpu/command_buffer/service/gles2_cmd_decoder_passthrough.ccgpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.ccgpu/command_buffer/service/gles2_cmd_decoder_passthrough_handlers.cc
Estimated timestamp from git blame: 2025-11-28
Summary
A potential Use-After-Free (UAF) Write vulnerability exists in GLES2DecoderPassthroughImpl. The issue occurs because of a service ID collision when a buffer is orphaned by a Vertex Array Object (VAO). When a buffer is deleted while bound to a non-current VAO, ANGLE immediately recycles its service ID handle even though the buffer object itself remains alive (orphaned). This allows a newly generated buffer to inherit the same service ID. If the orphaned VAO is subsequently used, the passthrough decoder incorrectly attributes the orphan’s mapped memory to the new buffer’s client ID. This results in a poisoned entry in mapped_buffer_map containing a dangling pointer that can be used to perform arbitrary writes to freed GPU memory when the new buffer is flushed.
Theoretical Exploit Steps
(Note: This is a potential vulnerability identified via static analysis by an AI agent; a working proof-of-concept exploit has not been executed).
A compromised renderer process can potentially trigger this vulnerability using the following sequence of command buffer operations:
- Establish the Orphan: The renderer creates VAO 1 (
client_vao_1) and Buffer A (client_buf_A). The decoder queries ANGLE for a service ID, receivingservice_A. The decoder mapsclient_buf_A<->service_Ainresources_->buffer_id_map. - The renderer binds VAO 1 and then binds Buffer A to the
GL_ELEMENT_ARRAY_BUFFERtarget. VAO 1 now holds a strong reference to Buffer A in ANGLE. - The renderer unbinds VAO 1 (it is no longer the active VAO) and deletes Buffer A (
glDeleteBuffers(1, &client_buf_A)). - In
DoDeleteBuffers, the decoder removes the mapping forclient_buf_Aand calls ANGLE’sglDeleteBuffers(1, &service_A). Because Buffer A is not bound to the current context, ANGLE’sContext::deleteBufferimmediately releases the IDservice_Aback to the allocator. However, because VAO 1 still references Buffer A, its reference count is > 0, so the underlying driver memory is not destroyed. Buffer A is now an “orphan.” - Force Collision: The renderer immediately creates a new Buffer B (
glGenBuffers(1, &client_buf_B)). ANGLE recycles and returns the IDservice_A. The decoder maps the newclient_buf_B<->service_A. - Poison Mapping State: The renderer re-binds VAO 1, setting
bound_element_array_buffer_dirty_ = truein the decoder. - The renderer sends a
glMapBufferRange(GL_ELEMENT_ARRAY_BUFFER, ...)command.DoMapBufferRangeexecutes and successfully maps the actual underlying buffer (the orphaned Buffer A) via ANGLE, returningmapped_ptr. DoMapBufferRangethen callsLazilyUpdateCurrentlyBoundElementArrayBuffer(). This queries ANGLE forGL_ELEMENT_ARRAY_BUFFER_BINDING. ANGLE inspects the bound VAO 1 and returns the original service ID of its attached buffer:service_A.- The decoder performs a reverse-lookup:
GetClientID(&resources_->buffer_id_map, service_A, ...). Becauseservice_Awas recycled, this incorrectly resolves to the client ID of the new buffer:client_buf_B. The decoder updates its tracking:bound_buffers_[GL_ELEMENT_ARRAY_BUFFER] = client_buf_B. DoMapBufferRangeinsertsmapped_ptrintoresources_->mapped_buffer_mapunder the keyclient_buf_B. The decoder now believes Buffer B is mapped, but the pointer actually points to the driver memory of the orphan Buffer A.- Trigger UAF Write: The renderer deletes VAO 1 (
glDeleteVertexArraysOES). VAO 1 drops its reference to the orphan Buffer A. Buffer A is destroyed by ANGLE, and the graphics driver frees the backing memory. Themapped_ptrstored underclient_buf_Bis now a dangling pointer. - The renderer binds Buffer B to
GL_ARRAY_BUFFERand invokesglFlushMappedBufferRange(GL_ARRAY_BUFFER, offset, size). DoFlushMappedBufferRangeretrieves the poisonedmap_infoforclient_buf_Band executes an unconstrainedmemcpyfrom an attacker-controlled shared memory segment into the danglingmap_ptr.
Impact
This vulnerability provides a robust write-what-where primitive in the GPU process. Because the memory is allocated and managed by the underlying graphics driver (e.g., Vulkan/D3D11) rather than Chromium’s PartitionAlloc, MiraclePtr (BackupRefPtr) does not protect this pointer. An attacker can leverage this primitive to overwrite reused GPU structures (such as command buffers, shaders, or descriptors) to achieve Code Execution in the GPU process, resulting in a renderer-to-GPU sandbox escape.
Suggested Fix
The core issue is that the passthrough decoder relies on a reverse-lookup (GetClientID) using a service ID that may have been recycled by ANGLE while the underlying buffer object remains alive as an orphan.
Potential fixes include:
- Preventing ID Recycling in ANGLE: Modify ANGLE’s
HandleAllocatorto prevent the immediate reuse of a service ID if the underlyingRefCountObject(like aBuffer) still has a non-zero reference count (e.g., from an unbound VAO). - Tracking Orphans in the Decoder: The passthrough decoder could track orphaned buffers (buffers that have been deleted but might still be referenced by VAOs). If
glGetIntegervFn(GL_ELEMENT_ARRAY_BUFFER_BINDING)returns an ID that belongs to an orphaned buffer, the decoder should handle it safely (e.g., by returning a pseudo-client ID or rejecting the mapping operation). - Validating Client IDs: In
LazilyUpdateCurrentlyBoundElementArrayBuffer(), add a verification step to ensure that the resolvedclient_element_array_bufferactually corresponds to the expected state of the currently bound VAO, rather than blindly trusting the reverse lookup of a potentially recycled service ID.
Evaluated with Chrome root at commit: a3f5fcb392f2902650ca2b71820e7e418787e18b
Results 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. Please feel free to reach out to me if you have concerns or feedback.