CVE-2026-17675
Overview
Files Changed
extensions/ANGLE_shader_pixel_local_storage.txtsrc/libANGLE/validationES.cppsrc/libANGLE/validationES.h
Patch
From 1184faa057e3b606b072733a12f528fcd02d3c75 Mon Sep 17 00:00:00 2001 From: Ken Russell <[email protected]> Date: Fri, 22 May 2026 18:10:16 -0700 Subject: [PATCH] PLS: Reject redefinition of attachments while active. "glRenderbufferStorage*", "glTexStorage*", "glTexImage*", and "glGenerateMipmap" can change the size of the current framebuffer's attachments while PLS is active, even if they aren't bound as PLS planes, which can cause problems with the active PLS planes since they won't be resized. Generate GL_INVALID_OPERATION in these cases. Update the PLS spec defining this behavior. Add a unit test verifying the behavior of all of these API calls while PLS is active, including when the objects aren't bound to the current framebuffer. Co-authored with jetski-cli. Fixed: chromium:513920258 Change-Id: I8c52c3983a2514e333740a463a865c73c766ab8d Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7871931 Auto-Submit: Kenneth Russell <[email protected]> Reviewed-by: Alexey Knyazev <[email protected]> Commit-Queue: Kenneth Russell <[email protected]> Reviewed-by: Kai Ninomiya <[email protected]> --- diff --git a/extensions/ANGLE_shader_pixel_local_storage.txt b/extensions/ANGLE_shader_pixel_local_storage.txt index de15d33..14ef4ee 100644 --- a/extensions/ANGLE_shader_pixel_local_storage.txt +++ b/extensions/ANGLE_shader_pixel_local_storage.txt @@ -484,6 +484,12 @@ e.g., TexSubImage*, TexParameter*, GenerateMipmap, etc. + * INVALID_OPERATION is generated by calls that would redefine a texture + or renderbuffer that is bound to the current framebuffer (either as an + attachment or as an active pixel local storage plane). + + e.g., RenderbufferStorage*, TexStorage*, TexImage*, GenerateMipmap + * INVALID_OPERATION is generated by Enable(), Disable() if <cap> is not one of: BLEND, CULL_FACE, DEBUG_OUTPUT, DEBUG_OUTPUT_SYNCHRONOUS, DEPTH_CLAMP_EXT, DEPTH_TEST, POLYGON_OFFSET_POINT_NV, diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp index 5d49ec7..c551299 100644 --- a/src/libANGLE/validationES.cpp +++ b/src/libANGLE/validationES.cpp @@ -1617,6 +1617,11 @@ return false; } + if (!ValidateNoActivePLSConflict(context, entryPoint, id)) + { + return false; + } + return true; } @@ -2177,9 +2182,8 @@ return false; } - if (context->getState().isTextureBoundToActivePLS(texture->id())) + if (!ValidateNoActivePLSConflict(context, entryPoint, texture->id())) { - ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kActivePLSBackingTexture); return false; } @@ -8329,4 +8333,112 @@ return true; } + +static bool IsTextureBoundToFramebuffer(const Context *context, + const Framebuffer *framebuffer, + TextureID textureId) +{ + if (framebuffer == nullptr) + { + return false; + } + + for (const FramebufferAttachment &colorAttachment : framebuffer->getColorAttachments()) + { + if (colorAttachment.isTextureWithId(textureId)) + { + return true; + } + } + + const FramebufferAttachment *depth = framebuffer->getDepthAttachment(); + if ((depth != nullptr) && depth->isTextureWithId(textureId)) + { + return true; + } + + const FramebufferAttachment *stencil = framebuffer->getStencilAttachment(); + if ((stencil != nullptr) && stencil->isTextureWithId(textureId)) + { + return true; + } + + return false; +} + +static bool IsRenderbufferBoundToFramebuffer(const Context *context, + const Framebuffer *framebuffer, + RenderbufferID renderbufferId) +{ + if (framebuffer == nullptr) + { + return false; + } + + for (const FramebufferAttachment &colorAttachment : framebuffer->getColorAttachments()) + { + if (colorAttachment.isRenderbufferWithId(renderbufferId.value)) + { + return true; + } + } + + const FramebufferAttachment *depth = framebuffer->getDepthAttachment(); + if ((depth != nullptr) && depth->isRenderbufferWithId(renderbufferId.value)) + { + return true; + } + + const FramebufferAttachment *stencil = framebuffer->getStencilAttachment(); + if ((stencil != nullptr) && stencil->isRenderbufferWithId(renderbufferId.value)) + { + return true; + } + + return false; +} + +bool ValidateNoActivePLSConflict(const Context *context, + angle::EntryPoint entryPoint, + TextureID textureId) +{ + if (context->getState().getPixelLocalStorageActivePlanes() == 0) + { + return true; + } + + if (context->getState().isTextureBoundToActivePLS(textureId)) + { + ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kPLSActive); + return false; + } + + const Framebuffer *framebuffer = context->getState().getDrawFramebuffer(); + if (IsTextureBoundToFramebuffer(context, framebuffer, textureId)) + { + ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kPLSActive); + return false; + } + + return true; +} + +bool ValidateNoActivePLSConflict(const Context *context, + angle::EntryPoint entryPoint, + RenderbufferID renderbufferId) +{ + if (context->getState().getPixelLocalStorageActivePlanes() == 0) + { + return true; + } + + const Framebuffer *framebuffer = context->getState().getDrawFramebuffer(); + if (IsRenderbufferBoundToFramebuffer(context, framebuffer, renderbufferId)) + { + ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kPLSActive); + return false; + } + + return true; +} } // namespace gl diff --git a/src/libANGLE/validationES.h b/src/libANGLE/validationES.h index e8b7a83..4d9f19f 100644 --- a/src/libANGLE/validationES.h +++ b/src/libANGLE/validationES.h @@ -1216,6 +1216,13 @@ ErrorSet *errors, angle::EntryPoint entryPoint, LogicalOperation opcodePacked); + +bool ValidateNoActivePLSConflict(const Context *context, + angle::EntryPoint entryPoint, + TextureID textureId); +bool ValidateNoActivePLSConflict(const Context *context, + angle::EntryPoint entryPoint, + RenderbufferID renderbufferId); } // namespace gl
Regression Test / PoC
diff --git a/src/tests/gl_tests/PixelLocalStorageTest.cpp b/src/tests/gl_tests/PixelLocalStorageTest.cpp
index da90bff..c085f2f 100644
--- a/src/tests/gl_tests/PixelLocalStorageTest.cpp
+++ b/src/tests/gl_tests/PixelLocalStorageTest.cpp
@@ -4858,6 +4858,131 @@
ASSERT_GL_NO_ERROR();
}
+// Check that redefining textures or renderbuffers bound to the current framebuffer
+// while pixel local storage is active generates GL_INVALID_OPERATION.
+TEST_P(PixelLocalStorageTest, RedefineBoundAttachmentsConflict)
+{
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_shader_pixel_local_storage"));
+
+ // Case 1: Texture attachment
+ {
+ GLTexture texFB;
+ glBindTexture(GL_TEXTURE_2D, texFB);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, W, H, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ ASSERT_GL_NO_ERROR();
+
+ GLTexture texNonFB;
+ glBindTexture(GL_TEXTURE_2D, texNonFB);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, W, H, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ ASSERT_GL_NO_ERROR();
+
+ GLTexture texNonFBStorage;
+ // Bind it to initialize it as a 2D texture, but don't define images yet.
+ glBindTexture(GL_TEXTURE_2D, texNonFBStorage);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texFB, 0);
+ ASSERT_GL_NO_ERROR();
+
+ // Set up PLS on plane 0 (using a different texture)
+ GLTexture texPLS;
+ glBindTexture(GL_TEXTURE_2D, texPLS);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
+ ASSERT_GL_NO_ERROR();
+ glFramebufferTexturePixelLocalStorageANGLE(0, texPLS, 0, 0, GL_NONE);
+ ASSERT_GL_NO_ERROR();
+
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Begin PLS
+ glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_LOAD_OP_ZERO_ANGLE}));
+ ASSERT_GL_NO_ERROR();
+
+ // Attempt to redefine texFB (bound to FB)
+ glBindTexture(GL_TEXTURE_2D, texFB);
+
+ // 1. glTexImage2D
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, W, H, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ // 2. glTexStorage2D
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ // 3. glGenerateMipmap
+ glGenerateMipmap(GL_TEXTURE_2D);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ // Attempt to redefine texNonFB (NOT bound to FB) - should succeed
+ glBindTexture(GL_TEXTURE_2D, texNonFB);
+
+ // 1. glTexImage2D
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, W, H, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ // 2. glGenerateMipmap
+ glGenerateMipmap(GL_TEXTURE_2D);
+ EXPECT_GL_NO_ERROR();
+
+ // 3. glTexStorage2D on texNonFBStorage (NOT bound to FB) - should succeed
+ glBindTexture(GL_TEXTURE_2D, texNonFBStorage);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
+ EXPECT_GL_NO_ERROR();
+
+ // End PLS
+ glEndPixelLocalStorageANGLE(1, GLenumArray({GL_STORE_OP_STORE_ANGLE}));
+ ASSERT_GL_NO_ERROR();
+ }
+
+ // Case 2: Renderbuffer attachment
+ {
+ GLRenderbuffer rboFB;
+ glBindRenderbuffer(GL_RENDERBUFFER, rboFB);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, W, H);
+ ASSERT_GL_NO_ERROR();
+
+ GLRenderbuffer rboNonFB;
+ glBindRenderbuffer(GL_RENDERBUFFER, rboNonFB);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, W, H);
+ ASSERT_GL_NO_ERROR();
+
+ GLFramebuffer fbo;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rboFB);
+ ASSERT_GL_NO_ERROR();
+
+ // Set up PLS on plane 0
+ GLTexture texPLS;
+ glBindTexture(GL_TEXTURE_2D, texPLS);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
+ ASSERT_GL_NO_ERROR();
+ glFramebufferTexturePixelLocalStorageANGLE(0, texPLS, 0, 0, GL_NONE);
+ ASSERT_GL_NO_ERROR();
+
+ ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Begin PLS
+ glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_LOAD_OP_ZERO_ANGLE}));
+ ASSERT_GL_NO_ERROR();
+
+ // Attempt to redefine rboFB (bound to FB)
+ glBindRenderbuffer(GL_RENDERBUFFER, rboFB);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, W, H);
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ // Attempt to redefine rboNonFB (NOT bound to FB) - should succeed
+ glBindRenderbuffer(GL_RENDERBUFFER, rboNonFB);
+ glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, W, H);
+ EXPECT_GL_NO_ERROR();
+
+ // End PLS
+ glEndPixelLocalStorageANGLE(1, GLenumArray({GL_STORE_OP_STORE_ANGLE}));
+ ASSERT_GL_NO_ERROR();
+ }
+}
+
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(PixelLocalStorageTest);
#define PLATFORM(API, BACKEND) API##_##BACKEND()
#define PLS_INSTANTIATE_RENDERING_TEST_AND(TEST, API, ...) \
@@ -8221,26 +8346,32 @@
glBeginPixelLocalStorageANGLE(2, GLenumArray({GL_DONT_CARE, GL_DONT_CARE}));
ASSERT_GL_NO_ERROR();
-#define CHECK_TEXTURE_2D_MODIFICATION(FN) \
- glBindTexture(GL_TEXTURE_2D, pls2d); \
- FN; \
- EXPECT_GL_SINGLE_ERROR(GL_INVALID_OPERATION); \
- EXPECT_GL_SINGLE_ERROR_MSG( \
- "Operation not permitted on an active pixel local storage backing texture.") \
- glBindTexture(GL_TEXTURE_2D, nonpls2d); \
- FN; \
+#define CHECK_TEXTURE_2D_MODIFICATION_MSG(FN, MSG) \
+ glBindTexture(GL_TEXTURE_2D, pls2d); \
+ FN; \
+ EXPECT_GL_SINGLE_ERROR(GL_INVALID_OPERATION); \
+ EXPECT_GL_SINGLE_ERROR_MSG(MSG) \
+ glBindTexture(GL_TEXTURE_2D, nonpls2d); \
+ FN; \
EXPECT_GL_NO_ERROR();
-#define CHECK_TEXTURE_2D_ARRAY_MODIFICATION(FN) \
- glBindTexture(GL_TEXTURE_2D_ARRAY, pls2darray); \
- FN; \
- EXPECT_GL_SINGLE_ERROR(GL_INVALID_OPERATION); \
- EXPECT_GL_SINGLE_ERROR_MSG( \
- "Operation not permitted on an active pixel local storage backing texture.") \
- glBindTexture(GL_TEXTURE_2D_ARRAY, nonpls2darray); \
- FN; \
+#define CHECK_TEXTURE_2D_MODIFICATION(FN) \
+ CHECK_TEXTURE_2D_MODIFICATION_MSG( \
+ FN, "Operation not permitted on an active pixel local storage backing texture.")
+
+#define CHECK_TEXTURE_2D_ARRAY_MODIFICATION_MSG(FN, MSG) \
+ glBindTexture(GL_TEXTURE_2D_ARRAY, pls2darray); \
+ FN; \
+ EXPECT_GL_SINGLE_ERROR(GL_INVALID_OPERATION); \
+ EXPECT_GL_SINGLE_ERROR_MSG(MSG) \
+ glBindTexture(GL_TEXTURE_2D_ARRAY, nonpls2darray); \
+ FN; \
EXPECT_GL_NO_ERROR();
+#define CHECK_TEXTURE_2D_ARRAY_MODIFICATION(FN) \
+ CHECK_TEXTURE_2D_ARRAY_MODIFICATION_MSG( \
+ FN, "Operation not permitted on an active pixel local storage backing texture.")
+
std::vector<uint8_t> imageData(H * W * 4);
CHECK_TEXTURE_2D_MODIFICATION(
@@ -8249,9 +8380,13 @@
CHECK_TEXTURE_2D_ARRAY_MODIFICATION(glTexSubImage3D(
GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, W, H, 1, GL_RGBA, GL_UNSIGNED_BYTE, imageData.data()));
- CHECK_TEXTURE_2D_MODIFICATION(glGenerateMipmap(GL_TEXTURE_2D));
+ CHECK_TEXTURE_2D_MODIFICATION_MSG(
+ glGenerateMipmap(GL_TEXTURE_2D),
+ "Operation not permitted while pixel local storage is active.");
- CHECK_TEXTURE_2D_ARRAY_MODIFICATION(glGenerateMipmap(GL_TEXTURE_2D_ARRAY));
+ CHECK_TEXTURE_2D_ARRAY_MODIFICATION_MSG(
+ glGenerateMipmap(GL_TEXTURE_2D_ARRAY),
+ "Operation not permitted while pixel local storage is active.");
if (EnsureGLExtensionEnabled("GL_ANGLE_robust_client_memory"))
{
Original Bug Report
Out-of-Bounds write in ANGLE PLS via resized attachments during active session
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 vulnerability in ANGLE’s Pixel Local Storage (PLS) implementation potentially allows out-of-bounds image writes in the GPU process. By resizing an attached renderbuffer or texture during an active PLS session, the rendering area can exceed PLS dimensions, leading to OOB writes. This could result in memory corruption and a potential sandbox escape on platforms like Android where the GPU process is privileged.
Affected files:
third_party/angle/scripts/generate_entry_points.pythird_party/angle/src/libANGLE/validationES.cppthird_party/angle/src/libANGLE/Framebuffer.cppthird_party/angle/src/libANGLE/Renderbuffer.cppthird_party/angle/src/compiler/translator/tree_ops/RewritePixelLocalStorage.cpp
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A potential out-of-bounds (OOB) write vulnerability exists in ANGLE’s implementation of the GL_ANGLE_shader_pixel_local_storage extension. When a Pixel Local Storage (PLS) session is active, resizing an attached renderbuffer or texture can enlarge the rendering area beyond the dimensions of the PLS backing textures. This leads to out-of-bounds writes on backends that use the ImageLoadStore implementation (such as Vulkan and D3D11).
Root Cause
The vulnerability stems from missing guards in the autogenerated GLES entry points for resource resizing functions. Specifically, glRenderbufferStorage*, glTexImage*, and glTexStorage* are missing from the PLS_DISABLE_LIST and PLS_DISABLE_WILDCARDS in third_party/angle/scripts/generate_entry_points.py. This list determines which entry points receive an implicit guard to end an active PLS session via context->endPixelLocalStorageImplicit().
Because these functions lack the guard, a user can modify the storage of a bound and attached resource while PLS is active. Furthermore, the validation logic in third_party/angle/src/libANGLE/validationES2.cpp and validationES3.cpp does not check for active PLS sessions during these operations.
Potential Attack Scenario
A compromised renderer could potentially trigger this vulnerability using the following steps:
- Request and enable the
GL_ANGLE_shader_pixel_local_storageextension. - Create a framebuffer and attach a 64x64 depth renderbuffer (RB).
- Bind a 64x64 texture (T) to PLS plane 0 using
glFramebufferTexturePixelLocalStorageANGLE. - Start a PLS session:
glBeginPixelLocalStorageANGLE(1, {GL_LOAD_OP_ZERO_ANGLE}). This passes validation as the RB and T dimensions match. - Resize the depth attachment while the session is active:
glBindRenderbuffer(GL_RENDERBUFFER, RB); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, 4096, 4096). Due to the missing guard, the session is not terminated, and the rendering area expands to 4096x4096. - Execute a draw call with a PLS-enabled fragment shader. The ANGLE translator (
RewritePixelLocalStorage.cpp) emitsimageStorecalls usinggl_FragCoord.xyas coordinates without clamping. Fragments at the edge of the 4096x4096 area will write far beyond the 64x64 bounds of the PLS backing texture.
In the Vulkan backend, ANGLE typically does not enable robustImageAccess (it only enables robustBufferAccess), meaning OOB storage-image writes are undefined and can lead to memory corruption in the GPU process. On Android, where the GPU process is often unsandboxed, this represents a significant sandbox escape vector.
Suggested Fix
- Add
glRenderbufferStorage*,glTexImage*, andglTexStorage*(including multisample and 3D variants) to thePLS_DISABLE_LISTorPLS_DISABLE_WILDCARDSinthird_party/angle/scripts/generate_entry_points.pyto ensure PLS sessions are implicitly ended before resizing. - Add explicit validation checks in
validationES2.cppandvalidationES3.cppfor these entry points to returnGL_INVALID_OPERATIONif a PLS session is active. - Consider adding coordinate clamping in
RewritePixelLocalStorage.cppto provide a defense-in-depth measure against OOB accesses.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
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.