CVE-2026-14411
Overview
Files Changed
src/libANGLE/renderer/gl/StateManagerGL.cppsrc/libANGLE/renderer/gl/StateManagerGL.hsrc/libANGLE/renderer/gl/TransformFeedbackGL.cppsrc/tests/gl_tests/TransformFeedbackTest.cpp
Patch
From 3373eb28a24608f9ae5d211c95c45e868a3d3758 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
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 <syoussefi@chromium.org>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
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)
{
Regression Test / PoC
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)
{
Original Bug Report
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.cppthird_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:
-
Validation Spec Violation: According to the OpenGL ES 3.0 specification (Section 2.15.1), the program object active when
BeginTransformFeedbackis called must remain active untilEndTransformFeedbackis called. However, ANGLE’sValidateUseProgramonly 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. -
State Manager Desynchronization: In the OpenGL backend,
StateManagerGL::syncStateprocesses dirty bits in a specific order. WhenDIRTY_BIT_TRANSFORM_FEEDBACK_BINDING(ordinal 143) is processed, it callssyncActiveState, which may clobber the native program binding by callingmStateManager->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 thesyncStateloop iterates over a snapshot of dirty bits, the subsequent update toDIRTY_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
- Create a WebGL2 context using the ANGLE GL/GLES backend.
- Prepare
progA(configured for Transform Feedback with a shader that reads out-of-bounds from a UBO) andprogB(a dummy shader). - Bind a small buffer to the UBO index required by
progA. - 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); - Observe that validation is performed against
progB, butprogAis natively executed, potentially leading to a GPU-side out-of-bounds access.
Suggested Fix
- Update
ValidateUsePrograminthird_party/angle/src/libANGLE/validationES2.cppto checkisTransformFeedbackActive()instead ofisTransformFeedbackActiveUnpaused(). - Update
TransformFeedbackGL::syncActiveStateinthird_party/angle/src/libANGLE/renderer/gl/TransformFeedbackGL.cppto 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.