High CVSS 8.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Sub-componentPLS
Bug ClassOOB
Tracker500080303
Fix commitd4364d6ab8ad (angle/angle) +371/-218
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-06 (per vendor advisory)
Fix landed2026-05-12 (public git)
Disclosed2026-05-27
Patch gap15 days — fix public in source before the advisory

Background

Pixel Local Storage (PLS)
An ANGLE extension (ANGLE_shader_pixel_local_storage) that lets a fragment shader read/write per-pixel “planes” backed by textures or memoryless storage. Planes are bound to a draw framebuffer and activated between glBeginPixelLocalStorageANGLE and glEndPixelLocalStorageANGLE.
PLS interrupt/restore
glFramebufferPixelLocalStorageInterruptANGLE suspends an active PLS session (saving how many planes were active, mActivePlanesAtInterrupt); glFramebufferPixelLocalStorageRestoreANGLE resumes it, implicitly re-running the begin sequence for the saved plane count.
Rendering-area/plane dimension invariant
Begin validation requires the PLS backing-texture dimensions to match the framebuffer’s rendering area; a mismatch is kPLSDimensionsDontMatchRenderingArea. The GPU-side imageStore emitted by the translator uses gl_FragCoord without clamping, so this check is the only bound guarantee.

Root Cause Analysis

The restore path resumed a suspended PLS session by implicitly re-invoking the begin sequence, but it never re-ran the global-state validation that glBeginPixelLocalStorageANGLE performs. While a session was interrupted the framebuffer could legally be mutated (for example, a large color attachment added, changing the rendering area), and on restore ANGLE would reactivate the saved planes against the now-incompatible framebuffer. The backing texture (e.g. 16x16) no longer matched the new rendering area (e.g. 4096x4096), so fragments at coordinates well outside the plane texture would be generated and the unclamped imageStore would write out of bounds of the plane’s backing storage in the GPU process.

The patch (“Validate global state when resuming pixel local storage”) closes this by re-validating on resume. ValidatePLSCommon gains a new PLSInterruptedStatus parameter (Interrupted/Resumed/Any) so callers can require a specific interrupt state and emit kPLSNotInterrupted/kPLSInterrupted accordingly, and a new ValidateBeginAndRestorePixelLocalStorageGlobalState(context, entryPoint, n) factors out the global-state checks so both begin and restore run them. A activePlanesAtInterrupt() accessor is added to PixelLocalStorage.h so restore knows how many planes it is about to reactivate. In effect, restore now performs the same dimension/global-state validation as begin, and a restore against an incompatible framebuffer fails with a GL error instead of proceeding to an out-of-bounds draw. Note the supplied diff is truncated and shows the validation-helper refactor; the out-of-bounds imageStore sink itself is described by the advisory and is not contained in this diff (inferred).

Key insight
Resuming a suspended PLS session re-entered the begin sequence but skipped begin-time validation, so framebuffer state that changed during the interrupt (a larger rendering area than the PLS backing texture) was never re-checked — letting a shader store past the end of the plane texture.

Attack Path

  1. Compromised renderer Attacker runs raw GLES command-buffer operations against the GPU process (inferred: requires an already-compromised renderer).
  2. Small-plane begin Create an FBO, attach a small (16x16) immutable texture as a PLS plane with no standard color attachment, and glBeginPixelLocalStorageANGLE; with no standard attachment the rendering-area dimension check is skipped at begin.
  3. Interrupt Call glFramebufferPixelLocalStorageInterruptANGLE, which suspends PLS and records one active plane while zeroing the live active-plane count.
  4. Enlarge rendering area Attach a large (4096x4096) texture to GL_COLOR_ATTACHMENT0 while interrupted; the implicit end-guard is bypassed because the live active-plane count was zeroed.
  5. Restore without re-validation Call glFramebufferPixelLocalStorageRestoreANGLE; pre-patch, restore only checked that an interrupt was pending and reactivated the 16x16 plane against the 4096x4096 rendering area.
  6. OOB write Issue a draw with a PLS-storing shader at the 4096x4096 viewport; unclamped imageStore at coordinates up to (4095,4095) writes out of bounds of the 16x16 plane backing storage in the GPU process (inferred sink; not in this diff).

Impact Assessment

Runs in the GPU process, which is privileged and (depending on platform) unsandboxed. Precondition is a compromised/cooperating renderer issuing command-buffer calls; the advisory’s CVSS (AV:N/AC:H/UI:R, Scope:Changed) reflects remote reachability with user interaction and a high-complexity setup. The achieved primitive is an out-of-bounds imageStore write into GPU memory with partial attacker control over offset (via framebuffer size) and data, which the advisory escalates to code execution/sandbox escape. The OOB write is consistent with the advisory and the validation-gap the diff fixes, but the memory-corruption sink is not shown in this (truncated) diff and is therefore inferred.

Files Changed

  • extensions/ANGLE_shader_pixel_local_storage.txt
  • src/libANGLE/PixelLocalStorage.h
  • src/libANGLE/validationESEXT.cpp

Audit Directions

  • Suspend/resume re-validation
    Audit every “resume/restore” or “implicit begin” path in ANGLE (and command decoders) that re-enters a validated operation without re-running the entry point’s validation; look for state (sizes, formats, attachments) mutable during the suspended window.
  • Unclamped image access
    Review RewritePixelLocalStorage.cpp and other translator paths emitting imageStore/imageLoad from gl_FragCoord for cases that assume a validated size invariant rather than clamping at the sink.
  • Interrupt-count guards
    Check other PLS entry points that key behavior off interruptCount()/active-plane counts for the same “count zeroed while interrupted” bypass used to sneak in the oversized attachment.
  • Robust image access
    Confirm whether the Vulkan backend enables robustImageAccess/VK_EXT_image_robustness for storage images on paths that rely on validation for bounds.
From d4364d6ab8ad0aaed843e4aee307cc7056f8f2b8 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Thu, 07 May 2026 15:01:15 -0400
Subject: [PATCH] PLS: Validate global state when resuming pixel local storage.

This protects against getting into state that is invalid for PLS while
interrupted (such as modifying the framebuffer size) and then resuming
PLS and using the invalid state.

Fixed: chromium:500080303
Change-Id: I609b05ffc483b9929958e685423b064e1106da10
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7828809
Reviewed-by: Alexey Knyazev <lexa.knyazev@gmail.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---

diff --git a/extensions/ANGLE_shader_pixel_local_storage.txt b/extensions/ANGLE_shader_pixel_local_storage.txt
index 2d44f24..7c582c0 100644
--- a/extensions/ANGLE_shader_pixel_local_storage.txt
+++ b/extensions/ANGLE_shader_pixel_local_storage.txt
@@ -623,11 +623,12 @@
     Decrements the pixel local storage interrupt counter on the current draw
     framebuffer. When the counter decrements from 1 to 0, and
     PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_ANGLE was nonzero at the time this
-    framebuffer entered an interrupted state, the command also begins pixel
-    local storage using LOAD_OP_LOAD_ANGLE for texture backed planes and
+    framebuffer entered an interrupted state, BeginPixelLocalStorageANGLE is
+    implicitly called using LOAD_OP_LOAD_ANGLE for texture backed planes and
     DONT_CARE for memoryless planes. The number of planes activated is equal to
     the value of PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_ANGLE at the time this
-    framebuffer entered an interrupted state. This command is ignored when the
+    framebuffer entered an interrupted state. Validation associated with
+    BeingPixelLocalStorageANGLE is also run. This command is ignored when the
     default framebuffer object name 0 is bound.
 
   Modify Section 3.8.10.3 "Rendering Feedback Loops"
diff --git a/src/libANGLE/PixelLocalStorage.h b/src/libANGLE/PixelLocalStorage.h
index 10a71fb..ec4827a 100644
--- a/src/libANGLE/PixelLocalStorage.h
+++ b/src/libANGLE/PixelLocalStorage.h
@@ -159,6 +159,7 @@
     const PixelLocalStoragePlaneVector &getPlanes() { return mPlanes; }
 
     size_t interruptCount() const { return mInterruptCount; }
+    GLsizei activePlanesAtInterrupt() const { return mActivePlanesAtInterrupt; }
 
     // ANGLE_shader_pixel_local_storage API.
     void deinitialize(Context *context, GLint plane) { mPlanes[plane].deinitialize(context); }
diff --git a/src/libANGLE/validationESEXT.cpp b/src/libANGLE/validationESEXT.cpp
index db50a36..33b02a2 100644
--- a/src/libANGLE/validationESEXT.cpp
+++ b/src/libANGLE/validationESEXT.cpp
@@ -1722,9 +1722,17 @@
     Any
 };
 
+enum class PLSInterruptedStatus
+{
+    Interrupted,
+    Resumed,
+    Any,
+};
+
 bool ValidatePLSCommon(const Context *context,
                        angle::EntryPoint entryPoint,
-                       PLSExpectedStatus expectedStatus)
+                       PLSExpectedStatus expectedStatus,
+                       PLSInterruptedStatus expectedInterruptedStatus)
 {
     Framebuffer *framebuffer = context->getState().getDrawFramebuffer();
     if (expectedStatus != PLSExpectedStatus::Active)
@@ -1738,11 +1746,19 @@
         }
     }
 
-    // INVALID_FRAMEBUFFER_OPERATION is generated if pixel local storage on the draw framebuffer is
-    // in an interrupted state.
     const PixelLocalStorage *pls = framebuffer->peekPixelLocalStorage();
-    if (pls != nullptr && pls->interruptCount() != 0)
+    const bool plsInterrupted    = pls != nullptr && pls->interruptCount() != 0;
+    if (expectedInterruptedStatus == PLSInterruptedStatus::Interrupted && !plsInterrupted)
     {
+        // INVALID_FRAMEBUFFER_OPERATION is generated if pixel local storage on the draw framebuffer
+        // is not in an interrupted state.
+        ANGLE_VALIDATION_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION, kPLSNotInterrupted);
+        return false;
+    }
+    else if (expectedInterruptedStatus == PLSInterruptedStatus::Resumed && plsInterrupted)
+    {
+        // INVALID_FRAMEBUFFER_OPERATION is generated if pixel local storage on the draw framebuffer
+        // is in an interrupted state.
         ANGLE_VALIDATION_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION, kPLSInterrupted);
         return false;
     }
@@ -1772,9 +1788,10 @@
 bool ValidatePLSCommon(const Context *context,
                        angle::EntryPoint entryPoint,
                        GLint plane,
-                       PLSExpectedStatus expectedStatus)
+                       PLSExpectedStatus expectedStatus,
+                       PLSInterruptedStatus expectedInterruptedStatus)
 {
-    if (!ValidatePLSCommon(context, entryPoint, expectedStatus))
+    if (!ValidatePLSCommon(context, entryPoint, expectedStatus, expectedInterruptedStatus))
     {
         return false;
     }
@@ -2005,178 +2022,26 @@
             return false;
     }
 }
-}  // namespace
 
-bool ValidateFramebufferMemorylessPixelLocalStorageANGLE(const Context *context,
+bool ValidateBeginAndRestorePixelLocalStorageGlobalState(const Context *context,
                                                          angle::EntryPoint entryPoint,
-                                                         GLint plane,
-                                                         GLenum internalformat,
-                                                         GLbitfield usage)
+                                                         GLsizei n)
 {
-    if (!ValidatePLSCommon(context, entryPoint, plane, PLSExpectedStatus::Any))
-    {
-        return false;
-    }
-
-    // INVALID_ENUM is generated if <internalformat> is not one of the acceptable values in Table
-    // X.2, or NONE.
-    if (internalformat != GL_NONE)
-    {
-        if (!ValidatePLSInternalformat(context, entryPoint, internalformat))
-        {
-            return false;
-        }
-
-        // INVALID_OPERATION is generated if <internalformat> is not NONE and not color-renderable.
-        if (!context->getTextureCaps().get(internalformat).textureAttachment)
-        {
-            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kFormatNotRenderable);
-            return false;
-        }
-    }
-
-    if ((usage & kValidPLSUsages) != usage)
-    {
-        ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kPLSInvalidUsage);
-        return false;
-    }
-
-    return true;
-}
-
-bool ValidateFramebufferTexturePixelLocalStorageANGLE(const Context *context,
-                                                      angle::EntryPoint entryPoint,
-                                                      GLint plane,
-                                                      TextureID backingtexture,
-                                                      GLint level,
-                                                      GLint layer,
-                                                      GLbitfield usage)
-{
-    if (!ValidatePLSCommon(context, entryPoint, plane, PLSExpectedStatus::Any))
-    {
-        return false;
-    }
-
-    if (backingtexture.value != 0)
-    {
-        Texture *tex = context->getTexture(backingtexture);
-
-        // INVALID_OPERATION is generated if <backingtexture> is not the name of an existing
-        // immutable texture object, or zero.
-        if (!tex)
-        {
-            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kInvalidTextureName);
-            return false;
-        }
-        if (!tex->getImmutableFormat())
-        {
-            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, kTextureIsNotImmutable);
-            return false;
-        }
-
-        // INVALID_OPERATION is generated if <backingtexture> is nonzero
-        // and not of type GL_TEXTURE_2D, GL_TEXTURE_2D_ARRAY, GL_TEXTURE_CUBE_MAP,
-        // or GL_TEXTURE_CUBE_MAP_ARRAY.
-        size_t textureDepth;
-        if (!ValidatePLSTextureType(context, entryPoint, tex, &textureDepth))
-        {
-            return false;
-        }
-
-        // INVALID_VALUE is generated if <backingtexture> is nonzero and <level> < 0.
-        if (level < 0)
-        {
-            ANGLE_VALIDATION_ERROR(GL_INVALID_VALUE, kNegativeLevel);
-            return false;
-        }
-
-        // INVALID_VALUE is generated if <backingtexture> is nonzero and <level> >= the
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 a4da689..44fd114 100644
--- a/src/tests/gl_tests/PixelLocalStorageTest.cpp
+++ b/src/tests/gl_tests/PixelLocalStorageTest.cpp
@@ -6256,6 +6256,11 @@
         ASSERT_GL_INTEGER(GL_PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_ANGLE, 0);
     }
 
+    GLTexture tex;
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 100, 100);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+
     // INVALID_OPERATION is generated if <loadops>[0..<n>-1] is LOAD_OP_LOAD_ANGLE and
     // the pixel local storage plane at that same index is memoryless.
     glFramebufferMemorylessPixelLocalStorageANGLE(0, GL_RGBA8, GL_NONE);
@@ -6463,6 +6468,98 @@
     ASSERT_GL_INTEGER(GL_PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_ANGLE, 0);
 }
 
+// Test that operations performed during interrupted PLS which make PLS invalid are validated on
+// resume.
+TEST_P(PixelLocalStorageValidationTest, ResumeValidatesFramebufferState)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_shader_pixel_local_storage"));
+
+    // Small PLS backing texture (4x4) and large color attachment (WxH = 128x128).
+    PLSTestTexture smallPLS(GL_RGBA8, 4, 4);
+    PLSTestTexture largeAttachment(GL_RGBA8, W, H);
+
+    std::vector<GLubyte> zeros(W * H * 4, 0);
+    glBindTexture(GL_TEXTURE_2D, largeAttachment);
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, zeros.data());
+    ASSERT_GL_NO_ERROR();
+
+    // FBO with NO attachments, only a PLS plane.
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glFramebufferTexturePixelLocalStorageANGLE(0, smallPLS, 0, 0, GL_NONE);
+    ASSERT_GL_NO_ERROR();
+
+    // Shader that writes to both color attachment 0 and PLS plane 0.
+    PLSProgram p;
+    p.compile(R"(
+    layout(binding=0, rgba8) uniform highp pixelLocalANGLE pls;
+    layout(location=0) out vec4 fragColor;
+    void main()
+    {
+        pixelLocalStoreANGLE(pls, vec4(0, 1, 0, 1));
+        fragColor = vec4(1, 0, 0, 1);
+    })");
+
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    glViewport(0, 0, W, H);
+
+    glBeginPixelLocalStorageANGLE(1, GLenumArray({GL_LOAD_OP_ZERO_ANGLE}));
+    ASSERT_GL_NO_ERROR();
+    ASSERT_GL_INTEGER(GL_PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_ANGLE, 1);
+
+    glFramebufferPixelLocalStorageInterruptANGLE();
+    ASSERT_GL_NO_ERROR();
+    ASSERT_GL_INTEGER(GL_PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_ANGLE, 0);
+
+    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                           largeAttachment, 0);
+    ASSERT_GL_NO_ERROR();
+
+    // Invalid because fbo attachment and PLS plane now have different sizes.
+    glFramebufferPixelLocalStorageRestoreANGLE();
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+}
+
+// Test that state changes which are invalid for PLS are validated on restore
+TEST_P(PixelLocalStorageValidationTest, RestoreValidatesSameStatesAsBegin)
+{
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+    ASSERT_GL_NO_ERROR();
+
+    PLSTestTexture pls0(GL_RGBA8, 256, 256, 9);
+    glFramebufferTexturePixelLocalStorageANGLE(0, pls0, 2, 0, GL_NONE);
+
+    GLenum dontCare[1] = {GL_DONT_CARE};
+    glBeginPixelLocalStorageANGLE(1, dontCare);
+    ASSERT_GL_NO_ERROR();
+
+    glFramebufferPixelLocalStorageInterruptANGLE();
+    EXPECT_GL_NO_ERROR();
+
+    glEnable(GL_DITHER);
+    EXPECT_GL_NO_ERROR();
+
+    glFramebufferPixelLocalStorageRestoreANGLE();
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+    glDisable(GL_DITHER);
+    glFramebufferPixelLocalStorageRestoreANGLE();
+    glEndPixelLocalStorageANGLE(1, dontCare);
+    EXPECT_GL_NO_ERROR();
+}
+
+// Restoring PLS is always valid if begin has not been called.
+TEST_P(PixelLocalStorageValidationTest, RestoreAlwaysValidIfNotBegun)
+{
+    glFramebufferPixelLocalStorageInterruptANGLE();
+    EXPECT_GL_NO_ERROR();
+
+    glEnable(GL_DITHER);
+    glFramebufferPixelLocalStorageRestoreANGLE();
+    EXPECT_GL_NO_ERROR();
+}
+
 // Check that glBeginPixelLocalStorageANGLE validates feedback loops with GL_TEXTURE_2D as
 // specified:
 //
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU Process OOB Write via Pixel Local Storage Dimension Validation Bypass

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 without the security team.

Overview: A logic flaw in ANGLE’s Pixel Local Storage (PLS) implementation allows a compromised renderer to bypass dimension validation during PLS restoration. By injecting an oversized framebuffer attachment while PLS is interrupted, an attacker can cause out-of-bounds imageStore operations in the GPU process. This can lead to memory corruption and potential arbitrary code execution within the privileged GPU process.

Affected files:

  • third_party/angle/src/libANGLE/PixelLocalStorage.cpp
  • third_party/angle/src/compiler/translator/tree_ops/RewritePixelLocalStorage.cpp
  • third_party/angle/src/libANGLE/validationESEXT.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/vk_caps_utils.cpp

Estimated timestamp from git blame: 2023-07-13

Vulnerability Mechanism

A vulnerability exists in ANGLE’s implementation of the ANGLE_shader_pixel_local_storage extension where a logic flaw allows an attacker to bypass critical dimension validation during a Pixel Local Storage (PLS) restore operation.

The core issue is that restoring a suspended PLS session via glFramebufferPixelLocalStorageRestoreANGLE invokes PixelLocalStorage::restore(), which directly calls the internal Context::beginPixelLocalStorage() method. This completely bypasses the standard validation path (ValidateBeginPixelLocalStorageANGLE), which is responsible for ensuring that the PLS backing texture dimensions exactly match the dimensions of the framebuffer’s rendering area.

Potential Exploit Steps

Note: These are potential steps as our tooling agent has not executed a live proof of concept.

An attacker with a compromised renderer process could trigger this out-of-bounds write via raw GLES command buffer operations by following these steps:

  1. Initialize PLS with a small texture: Create a Framebuffer Object (FBO) and attach a small texture (e.g., 16x16) to a PLS plane using glFramebufferTexturePixelLocalStorageANGLE. Ensure there are no standard color/depth attachments.
  2. Begin PLS: Call glBeginPixelLocalStorageANGLE. Because there are no standard attachments, ValidateBeginPixelLocalStorageANGLE skips the kPLSDimensionsDontMatchRenderingArea dimension check. The PLS session starts with an active plane dimension of 16x16.
  3. Interrupt PLS: Call glFramebufferPixelLocalStorageInterruptANGLE. This safely suspends the PLS session and zeros out the active plane count in the context state, while saving the fact that 1 plane was active.
  4. Inject an oversized attachment: Call glFramebufferTexture2D to attach a large texture (e.g., 4096x4096) to GL_COLOR_ATTACHMENT0. Because the active PLS plane count was zeroed during the interrupt, the implicit PLS-end guard in the entry point is bypassed, and the standard attachment succeeds.
  5. Restore PLS: Call glFramebufferPixelLocalStorageRestoreANGLE. The validation for this command only verifies that an interrupt is pending. The restoration process directly invokes the internal PLS start sequence, bypassing ValidateBeginPixelLocalStorageANGLE. The system fails to realize the 16x16 PLS texture no longer matches the new 4096x4096 rendering area.
  6. Draw with PLS: Execute a draw call (e.g., glDrawArrays) using a shader that utilizes pixelLocalStoreANGLE and a viewport of 4096x4096.

Impact

During shader compilation, the ANGLE shader translator (RewritePixelLocalStorage.cpp) rewrites PLS store operations to native imageStore instructions using gl_FragCoord.xy as the coordinate, without performing any bounds clamping.

Because the rendering area is 4096x4096, fragments are generated with coordinates up to (4095, 4095). The shader executes imageStore using these coordinates into the PLS image unit, which is backed by the 16x16 texture.

ANGLE’s Vulkan renderer does not enable VK_EXT_image_robustness or the robustImageAccess feature for storage images. Consequently, these operations result in an unchecked out-of-bounds write to GPU memory. This memory corruption can be leveraged by the attacker to overwrite critical data structures, hijack control flow, and achieve Remote Code Execution (RCE) and Sandbox Escape within the highly privileged GPU process.

Suggested Fix

The PLS implementation must ensure that the framebuffer attachments are still compatible with the PLS planes upon restoration.

One approach is to have Context::framebufferPixelLocalStorageRestore (or ValidateFramebufferPixelLocalStorageRestoreANGLE) perform the same dimension checks found in ValidateBeginPixelLocalStorageANGLE. If the dimensions of the texture-backed PLS planes no longer match the rendering area (the intersection of all attachment extents), the restore operation should fail and return a GL_INVALID_OPERATION error.

Alternatively, modifying the framebuffer attachments (e.g., via glFramebufferTexture2D) while PLS is interrupted could be explicitly forbidden, or forced to clear the pending PLS interrupt state.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker