Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in ANGLE
DescriptionInsufficient validation of untrusted input in ANGLE
ComponentANGLE
Bug ClassLogic Error
Tracker513919827
Fix commit3373eb28a246 (angle/angle) +48/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • src/libANGLE/renderer/gl/StateManagerGL.cpp
  • src/libANGLE/renderer/gl/StateManagerGL.h
  • src/libANGLE/renderer/gl/TransformFeedbackGL.cpp
  • src/tests/gl_tests/TransformFeedbackTest.cpp
From 3373eb28a24608f9ae5d211c95c45e868a3d3758 Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
Date: Tue, 19 May 2026 16:08:55 -0400
Subject: [PATCH] GL: Sync program state after transform feedback.

If we are syncing a call to BeginTransformFeedback,
PauseTransformFeedback, UseProgram all at once, the program will be
bound and then changed to an incorrect program when the transform
feedback active state is synced afterwards.

Fixed: chromium:513919827
Change-Id: I66562446d364d03f628f927b4f4e8f5d7113fe4b
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7858018
Reviewed-by: Shahbaz Youssefi <[email protected]>
Commit-Queue: Shahbaz Youssefi <[email protected]>
---

diff --git a/src/libANGLE/renderer/gl/StateManagerGL.cpp b/src/libANGLE/renderer/gl/StateManagerGL.cpp
index 0d2bf2a..c48ce9f 100644
--- a/src/libANGLE/renderer/gl/StateManagerGL.cpp
+++ b/src/libANGLE/renderer/gl/StateManagerGL.cpp
@@ -2323,14 +2323,8 @@
                 updateDispatchIndirectBufferBinding(context);
                 break;
             case gl::state::DIRTY_BIT_PROGRAM_BINDING:
-            {
-                gl::Program *program = state.getProgram();
-                if (program != nullptr)
-                {
-                    useProgram(GetImplAs<ProgramGL>(program)->getProgramID());
-                }
+                syncProgramState(context);
                 break;
-            }
             case gl::state::DIRTY_BIT_PROGRAM_EXECUTABLE:
             {
                 const gl::ProgramExecutable *executable = state.getProgramExecutable();
@@ -2903,6 +2897,18 @@
         bindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0);
         mCurrentTransformFeedback = nullptr;
     }
+
+    syncProgramState(context);
+}
+
+void StateManagerGL::syncProgramState(const gl::Context *context)
+{
+    gl::Program *program = context->getState().getProgram();
+    if (program != nullptr)
+    {
+        ProgramGL *programGL = GetImplAs<ProgramGL>(program);
+        useProgram(programGL->getProgramID());
+    }
 }
 
 GLuint StateManagerGL::getDefaultVAO() const
diff --git a/src/libANGLE/renderer/gl/StateManagerGL.h b/src/libANGLE/renderer/gl/StateManagerGL.h
index 795a690..b81ef6b 100644
--- a/src/libANGLE/renderer/gl/StateManagerGL.h
+++ b/src/libANGLE/renderer/gl/StateManagerGL.h
@@ -375,6 +375,7 @@
 
     void syncSamplersState(const gl::Context *context);
     void syncTransformFeedbackState(const gl::Context *context);
+    void syncProgramState(const gl::Context *context);
 
     void updateEmulatedClipDistanceState(const gl::ProgramExecutable *executable,
                                          const gl::ClipDistanceEnableBits enables) const;
diff --git a/src/libANGLE/renderer/gl/TransformFeedbackGL.cpp b/src/libANGLE/renderer/gl/TransformFeedbackGL.cpp
index 4caf28b..77b028b 100644
--- a/src/libANGLE/renderer/gl/TransformFeedbackGL.cpp
+++ b/src/libANGLE/renderer/gl/TransformFeedbackGL.cpp
@@ -162,10 +162,16 @@
         mStateManager->bindTransformFeedback(GL_TRANSFORM_FEEDBACK, mTransformFeedbackID);
         if (mIsPaused)
         {
+            // If we're transitioning from active to paused, the currently bound program must be the
+            // one associated with this transform feedback.
+            ASSERT(mStateManager->getProgramID() == mActiveProgram);
             mFunctions->pauseTransformFeedback();
         }
         else
         {
+            // When transitioning to resumed, make sure the program associated with this transform
+            // feedback is set
+            mStateManager->useProgram(mActiveProgram);
             mFunctions->resumeTransformFeedback();
         }
     }
diff --git a/src/tests/gl_tests/TransformFeedbackTest.cpp b/src/tests/gl_tests/TransformFeedbackTest.cpp
index 0e368c5..0ab7588 100644
--- a/src/tests/gl_tests/TransformFeedbackTest.cpp
+++ b/src/tests/gl_tests/TransformFeedbackTest.cpp
@@ -251,6 +251,34 @@
     EXPECT_EQ(primitivesWritten, 0u);
 }
 
+// Test that starting transform feedback with one program but pausing and drawing with another
+// program draws the expected results
+TEST_P(TransformFeedbackTest, DrawWithOtherProgramDuringPause)
+{
+    ANGLE_GL_PROGRAM(blueProgram, essl3_shaders::vs::Simple(), essl3_shaders::fs::Blue());
+
+    std::vector<std::string> tfVaryings;
+    tfVaryings.push_back("gl_Position");
+    compileDefaultProgram(tfVaryings, GL_INTERLEAVED_ATTRIBS);
+    glUseProgram(mProgram);
+
+    // Bind the buffer for transform feedback output and start transform feedback
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, mTransformFeedbackBuffer);
+    glBeginTransformFeedback(GL_TRIANGLES);
+    glPauseTransformFeedback();
+    EXPECT_GL_NO_ERROR();
+
+    glUseProgram(blueProgram);
+    drawQuad(blueProgram, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_GL_NO_ERROR();
+
+    glUseProgram(mProgram);
+    glResumeTransformFeedback();
+    glEndTransformFeedback();
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+}
+
 // Test that resuming transform feedback with a different program results in validation error.
 TEST_P(TransformFeedbackTest, ProgramSwitchDuringPauseAndResume)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/TransformFeedbackTest.cpp b/src/tests/gl_tests/TransformFeedbackTest.cpp
index 0e368c5..0ab7588 100644
--- a/src/tests/gl_tests/TransformFeedbackTest.cpp
+++ b/src/tests/gl_tests/TransformFeedbackTest.cpp
@@ -251,6 +251,34 @@
     EXPECT_EQ(primitivesWritten, 0u);
 }
 
+// Test that starting transform feedback with one program but pausing and drawing with another
+// program draws the expected results
+TEST_P(TransformFeedbackTest, DrawWithOtherProgramDuringPause)
+{
+    ANGLE_GL_PROGRAM(blueProgram, essl3_shaders::vs::Simple(), essl3_shaders::fs::Blue());
+
+    std::vector<std::string> tfVaryings;
+    tfVaryings.push_back("gl_Position");
+    compileDefaultProgram(tfVaryings, GL_INTERLEAVED_ATTRIBS);
+    glUseProgram(mProgram);
+
+    // Bind the buffer for transform feedback output and start transform feedback
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, mTransformFeedbackBuffer);
+    glBeginTransformFeedback(GL_TRIANGLES);
+    glPauseTransformFeedback();
+    EXPECT_GL_NO_ERROR();
+
+    glUseProgram(blueProgram);
+    drawQuad(blueProgram, essl3_shaders::PositionAttrib(), 0.5f);
+    EXPECT_GL_NO_ERROR();
+
+    glUseProgram(mProgram);
+    glResumeTransformFeedback();
+    glEndTransformFeedback();
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+}
+
 // Test that resuming transform feedback with a different program results in validation error.
 TEST_P(TransformFeedbackTest, ProgramSwitchDuringPauseAndResume)
 {
Loading diff…

Original Bug Report

reported by [email protected]

State desync and draw validation bypass in ANGLE GL backend via Transform Feedback

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 potential vulnerability in ANGLE’s OpenGL backend allows an attacker to bypass draw-time validation by desynchronizing the native program binding from the frontend state. This is enabled by a validation bug that allows changing the active program while Transform Feedback is paused, combined with a state management error during lazy synchronization.

Affected files:

  • third_party/angle/src/libANGLE/renderer/gl/TransformFeedbackGL.cpp
  • third_party/angle/src/libANGLE/renderer/gl/StateManagerGL.cpp

Estimated timestamp from git blame: 2023-09-07

Potential Root Cause

Two issues in ANGLE combine to create a potential security vulnerability in the OpenGL backend:

  1. Validation Spec Violation: According to the OpenGL ES 3.0 specification (Section 2.15.1), the program object active when BeginTransformFeedback is called must remain active until EndTransformFeedback is called. However, ANGLE’s ValidateUseProgram only checks if Transform Feedback is active and unpaused (libANGLE/validationES2.cpp:5680). This incorrectly allows an attacker to switch the active program while Transform Feedback is paused.

  2. State Manager Desynchronization: In the OpenGL backend, StateManagerGL::syncState processes dirty bits in a specific order. When DIRTY_BIT_TRANSFORM_FEEDBACK_BINDING (ordinal 143) is processed, it calls syncActiveState, which may clobber the native program binding by calling mStateManager->useProgram(mActiveProgram) (libANGLE/renderer/gl/TransformFeedbackGL.cpp:139). Unlike the deactivation branch, this branch does not capture or restore the previous program binding. Furthermore, because the syncState loop iterates over a snapshot of dirty bits, the subsequent update to DIRTY_BIT_PROGRAM_BINDING (ordinal 137) is missed and then incorrectly cleared at the end of the synchronization loop.

Potential Impact

This desynchronization allows an attacker to pass WebGL draw-time validation using a “safe” program (e.g., one with no resource requirements) while the native driver executes a different, “malicious” program. This could lead to out-of-bounds reads or writes in the GPU process, potentially resulting in cross-origin information leaks or remote code execution. In environments where the GPU process is unsandboxed (such as Android), this represents a critical security risk.

Suggested Potential Reproduction Steps

  1. Create a WebGL2 context using the ANGLE GL/GLES backend.
  2. Prepare progA (configured for Transform Feedback with a shader that reads out-of-bounds from a UBO) and progB (a dummy shader).
  3. Bind a small buffer to the UBO index required by progA.
  4. Execute the following sequence:
    gl.useProgram(progA);
    gl.beginTransformFeedback(gl.POINTS);
    gl.pauseTransformFeedback();
    gl.useProgram(progB); // Incorrectly allowed by ANGLE while XFB is paused
    gl.drawArrays(gl.POINTS, 0, 1);
    
  5. Observe that validation is performed against progB, but progA is natively executed, potentially leading to a GPU-side out-of-bounds access.

Suggested Fix

  1. Update ValidateUseProgram in third_party/angle/src/libANGLE/validationES2.cpp to check isTransformFeedbackActive() instead of isTransformFeedbackActiveUnpaused().
  2. Update TransformFeedbackGL::syncActiveState in third_party/angle/src/libANGLE/renderer/gl/TransformFeedbackGL.cpp to capture and restore the previous program ID when activating Transform Feedback, ensuring native state consistency.

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