Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker513920258
Fix commit1184faa057e3 (angle/angle) +298/-23
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • extensions/ANGLE_shader_pixel_local_storage.txt
  • src/libANGLE/validationES.cpp
  • src/libANGLE/validationES.h
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
 
Loading diff…

Regression Test / PoC

shipped with the fix
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"))
     {
Loading diff…

Original Bug Report

reported by [email protected]

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.py
  • third_party/angle/src/libANGLE/validationES.cpp
  • third_party/angle/src/libANGLE/Framebuffer.cpp
  • third_party/angle/src/libANGLE/Renderbuffer.cpp
  • third_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:

  1. Request and enable the GL_ANGLE_shader_pixel_local_storage extension.
  2. Create a framebuffer and attach a 64x64 depth renderbuffer (RB).
  3. Bind a 64x64 texture (T) to PLS plane 0 using glFramebufferTexturePixelLocalStorageANGLE.
  4. Start a PLS session: glBeginPixelLocalStorageANGLE(1, {GL_LOAD_OP_ZERO_ANGLE}). This passes validation as the RB and T dimensions match.
  5. 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.
  6. Execute a draw call with a PLS-enabled fragment shader. The ANGLE translator (RewritePixelLocalStorage.cpp) emits imageStore calls using gl_FragCoord.xy as 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

  1. Add glRenderbufferStorage*, glTexImage*, and glTexStorage* (including multisample and 3D variants) to the PLS_DISABLE_LIST or PLS_DISABLE_WILDCARDS in third_party/angle/scripts/generate_entry_points.py to ensure PLS sessions are implicitly ended before resizing.
  2. Add explicit validation checks in validationES2.cpp and validationES3.cpp for these entry points to return GL_INVALID_OPERATION if a PLS session is active.
  3. Consider adding coordinate clamping in RewritePixelLocalStorage.cpp to 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.

View on issue tracker