High watch CVSS 9.6 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Sub-componentPLS
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513920258
Fix commit1184faa057e3 (angle/angle) +298/-23
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-06-05 (public git)
Disclosed2026-07-29
Patch gap54 days — fix public in source before the advisory

Background

Pixel Local Storage (PLS)
the GL_ANGLE_shader_pixel_local_storage extension, which binds textures as “planes” tied to the current framebuffer’s dimensions and keeps them active across draws between glBeginPixelLocalStorageANGLE and glEndPixelLocalStorageANGLE.
Attachment redefinition
calls such as glRenderbufferStorage*, glTexStorage*, glTexImage*, and glGenerateMipmap that reallocate or resize a texture’s or renderbuffer’s backing store.
`isTextureBoundToActivePLS`
an existing state query that reports whether a texture is currently bound as an active PLS plane.
`getPixelLocalStorageActivePlanes`
returns the number of PLS planes currently active, i.e. whether a PLS session is in progress.

Root Cause Analysis

While a PLS session is active, the size of the PLS planes and of the current framebuffer’s attachments must stay fixed, because the active PLS planes are configured once at glBeginPixelLocalStorageANGLE and are not re-sized mid-session.

Pre-patch, ANGLE only blocked redefinition of a texture that was itself an active PLS plane (via a bare isTextureBoundToActivePLS check inside the texture-storage validator), and did not block resizing a texture or renderbuffer that was merely attached to the current draw framebuffer, nor did the renderbuffer-storage path check PLS at all. A caller could therefore resize a bound attachment while PLS was live, breaking the framebuffer-vs-plane size invariant and, per the advisory, causing an out-of-bounds write when subsequent PLS rendering used the now-stale plane dimensions against the resized backing store.

The fix introduces ValidateNoActivePLSConflict (overloaded for TextureID and RenderbufferID), which returns success immediately when no PLS is active, otherwise emits GL_INVALID_OPERATION (kPLSActive) if the object is an active PLS plane or is bound to the current draw framebuffer as an attachment. Wiring this validator into the renderbuffer-storage path (line ~1617) and the texture-storage path (replacing the old plane-only check at line ~2182) enforces the invariant for both object kinds.

Key insight
The core mistake was guarding only against redefining the PLS plane texture and ignoring other textures/renderbuffers attached to the same framebuffer (and ignoring renderbuffers entirely), so a resize of a bound attachment during an active PLS session left plane and attachment sizes inconsistent; ValidateNoActivePLSConflict closes this by rejecting redefinition of any object bound to the active framebuffer.

Attack Path

  1. Begin a PLS session Create a framebuffer with a color/PLS attachment, configure a PLS plane with glFramebufferTexturePixelLocalStorageANGLE, and call glBeginPixelLocalStorageANGLE so getPixelLocalStorageActivePlanes() is non-zero.
  2. Redefine a bound attachment With PLS still active, bind and call glTexImage2D/glTexStorage2D/glGenerateMipmap on a texture attached to the framebuffer, or glRenderbufferStorage on a bound renderbuffer, at a different size.
  3. Break the size invariant Pre-patch these calls succeed, resizing the attachment’s backing store while the active PLS planes keep their original dimensions.
  4. Out-of-bounds write (per advisory) Subsequent PLS rendering operates with mismatched plane/attachment sizes, producing an out-of-bounds write; the diff itself shows only the added validation, not the concrete corruption sink.

Impact Assessment

An attacker running attacker-controlled GLES/WebGL commands in the GPU process could resize a framebuffer attachment mid-PLS to desynchronize plane and attachment dimensions, yielding an out-of-bounds write as reported in the advisory (bug class OOB, “Out of bounds write in ANGLE”). Preconditions are support for GL_ANGLE_shader_pixel_local_storage and an active PLS session; no exploited-in-the-wild status is stated, and any escalation to code execution beyond the memory write is (inferred).

Files Changed

  • extensions/ANGLE_shader_pixel_local_storage.txt
  • src/libANGLE/validationES.cpp
  • src/libANGLE/validationES.h

Audit Directions

  • Stateful-session size invariants
    Audit extensions that pin resource dimensions at a Begin* call for missing rejection of resize/redefine operations on any resource reachable during the session, not just the primary resource.
  • Attachment vs. plane coverage gaps
    Where a check targets one object kind (texture) or one binding role (PLS plane), verify the sibling kinds (renderbuffers) and roles (plain framebuffer attachments) are equally covered.
  • Redefinition entry points
    Enumerate all APIs that reallocate backing store (glTexImage*, glTexStorage*, glRenderbufferStorage*, glGenerateMipmap) and confirm each routes through the same conflict validator when a size-sensitive session is active.
From 1184faa057e3b606b072733a12f528fcd02d3c75 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
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 <kbr@chromium.org>
Reviewed-by: Alexey Knyazev <lexa.knyazev@gmail.com>
Commit-Queue: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
---

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 vm...@google.com

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