CVE-2026-9899
Overview
Files Changed
src/libANGLE/Context.cppsrc/libANGLE/ErrorStrings.hsrc/libANGLE/TransformFeedback.cpp
Patch
From 835b70c0c9130e261c31e81252e0a30c51c43846 Mon Sep 17 00:00:00 2001 From: Amirali Abdolrashidi <[email protected]> Date: Fri, 24 Apr 2026 18:37:23 -0700 Subject: [PATCH] Add xfb validation for resuming w/ another program * Added the following to TransformFeedbackState for validation checks: * mProgramPipeline * mPPOPrograms * Updated TransformFeedback::begin() to take a program pipeline pointer as well. * Also updated its Context function so it would pass nullptr for the PPO if a program is bound, since programs should override PPOs. * Updated ValidateResumeTransformFeedback() for scenarios involving changing programs or PPOsso the call to resume transform feedback fails with GL_INVALID_OPERATION in the following cases: * if the same program from the beginning of XFB is not in use * if the same PPO from the beginning of XFB is not in use * if a shader stage in the PPO changes * if a shader program is used when a PPO is already active * Updated TransformFeedbackVk::end() to clear the cached data to prevent from using outdated info when binding transform feedback buffers. * Added unit tests to TransformFeedbackTest for program testing. * Added unit tests to ProgramPipelineXFBTest31 for the PPO testing. Bug: chromium:497533569 Change-Id: Id1891e4a5b0be31f14a6ec1f7f8c73e9a22b5b3c Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7793853 Reviewed-by: Charlie Lao <[email protected]> Commit-Queue: Amirali Abdolrashidi <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> --- diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp index 9a8d68c..742a100 100644 --- a/src/libANGLE/Context.cpp +++ b/src/libANGLE/Context.cpp @@ -3869,7 +3869,11 @@ ASSERT(!transformFeedback->isPaused()); // TODO: http://anglebug.com/42265705: Handle PPOs - ANGLE_CONTEXT_TRY(transformFeedback->begin(this, primitiveMode, mState.getProgram())); + // Since programs should override PPOs, no PPO is passed to the transform feedback if a program + // is active. + Program *program = mState.getProgram(); + ProgramPipeline *programPipeline = program == nullptr ? mState.getProgramPipeline() : nullptr; + ANGLE_CONTEXT_TRY(transformFeedback->begin(this, primitiveMode, program, programPipeline)); onActiveTransformFeedbackChange(); } diff --git a/src/libANGLE/ErrorStrings.h b/src/libANGLE/ErrorStrings.h index 589a3da..a6ecd74 100644 --- a/src/libANGLE/ErrorStrings.h +++ b/src/libANGLE/ErrorStrings.h @@ -648,6 +648,10 @@ inline constexpr const char *kTransformFeedbackNotPaused = "The active Transform Feedback object is not paused."; inline constexpr const char *kTransformFeedbackPaused = "The active Transform Feedback object is paused."; inline constexpr const char *kTransformFeedbackProgramBinary = "Cannot change program binary while program is associated with an active transform feedback object."; +inline constexpr const char *kTransformFeedbackProgramNotSameAtResume = "The program when transform feedback began is no longer bound."; +inline constexpr const char *kTransformFeedbackProgramOverridingPipelineAtResume = "A bound program is overriding the bound pipeline from when transform feedback began."; +inline constexpr const char *kTransformFeedbackPipelineNotSameAtResume = "The program pipeline when transform feedback began is no longer bound."; +inline constexpr const char *kTransformFeedbackPipelineChangedStagesAtResume = "The shader stages of the bound program pipeline have changed since transform feedback began."; inline constexpr const char *kTransformFeedbackTargetActive = "Target is TRANSFORM_FEEDBACK_BUFFER and transform feedback is currently active."; inline constexpr const char *kTransformFeedbackUseProgram = "Cannot change active program while transform feedback is unpaused."; inline constexpr const char *kTransformFeedbackVaryingIndexOutOfRange = "Index must be less than the transform feedback varying count in the program."; diff --git a/src/libANGLE/TransformFeedback.cpp b/src/libANGLE/TransformFeedback.cpp index 05608a2..6f31b93 100644 --- a/src/libANGLE/TransformFeedback.cpp +++ b/src/libANGLE/TransformFeedback.cpp @@ -54,6 +54,7 @@ mVerticesDrawn(0), mVertexCapacity(0), mProgram(nullptr), + mProgramPipeline(nullptr), mIndexedBuffers(maxIndexedBuffers) {} @@ -138,7 +139,8 @@ angle::Result TransformFeedback::begin(const Context *context, PrimitiveMode primitiveMode, - Program *program) + Program *program, + ProgramPipeline *programPipeline) { // TODO: http://anglebug.com/42264023: This method should take in as parameter a // ProgramExecutable instead of a Program. @@ -148,7 +150,14 @@ mState.mPrimitiveMode = primitiveMode; mState.mPaused = false; mState.mVerticesDrawn = 0; + + // Program and PPO are not both passed to the transform feedback object. In the event both are + // bound, program takes precedence. + ASSERT(program == nullptr || programPipeline == nullptr); bindProgram(context, program); + bindProgramPipeline(context, programPipeline); + bindPPOPrograms(programPipeline); + recomputeVertexCapacity(context); return angle::Result::Continue; } @@ -161,11 +170,20 @@ mState.mPaused = false; mState.mVerticesDrawn = 0; mState.mVertexCapacity = 0; - if (mState.mProgram) + if (mState.mProgram != nullptr) { mState.mProgram->release(context); mState.mProgram = nullptr; } + if (mState.mProgramPipeline != nullptr) + { + mState.mProgramPipeline->release(context); + mState.mProgramPipeline = nullptr; + } + for (const ShaderType shaderType : gl::AllShaderTypes()) + { + mState.mPPOPrograms[shaderType].value = 0; + } return angle::Result::Continue; } @@ -241,6 +259,41 @@ } } +void TransformFeedback::bindProgramPipeline(const Context *context, + ProgramPipeline *programPipeline) +{ + if (mState.mProgramPipeline != programPipeline) + { + if (mState.mProgramPipeline != nullptr) + { + mState.mProgramPipeline->release(context); + } + mState.mProgramPipeline = programPipeline; + if (mState.mProgramPipeline != nullptr) + { + mState.mProgramPipeline->addRef(); + } + } +} + +void TransformFeedback::bindPPOPrograms(ProgramPipeline *programPipeline) +{ + if (programPipeline == nullptr) + { + for (const ShaderType shaderType : gl::AllShaderTypes()) + { + mState.mPPOPrograms[shaderType].value = 0; + } + return; + } + + for (const ShaderType shaderType : gl::AllShaderTypes()) + { + const Program *program = programPipeline->getShaderProgram(shaderType); + mState.mPPOPrograms[shaderType].value = program != nullptr ? program->id().value : 0; + } +} + void TransformFeedback::recomputeVertexCapacity(const Context *context) { // In one of the angle_unittests - "TransformFeedbackTest.SideEffectsOfStartAndStop" @@ -272,6 +325,29 @@ return mState.mProgram != nullptr && mState.mProgram->id().value == program.value; } +bool TransformFeedback::hasBoundProgramPipeline(ProgramPipelineID programPipeline) const +{ + return mState.mProgramPipeline != nullptr && + mState.mProgramPipeline->id().value == programPipeline.value; +} + +bool TransformFeedback::hasSamePPOPrograms(ProgramPipeline *programPipeline) const +{ + for (const ShaderType shaderType : gl::AllShaderTypes()) + { + GLuint shaderProgramIDValue = + (programPipeline != nullptr && programPipeline->getShaderProgram(shaderType) != nullptr) + ? programPipeline->getShaderProgram(shaderType)->id().value + : 0; + if (mState.mPPOPrograms[shaderType].value != shaderProgramIDValue) + { + return false; + } + } + + return true; +} + angle::Result TransformFeedback::detachBuffer(const Context *context, BufferID bufferID) { bool isBound = context->isCurrentTransformFeedback(this);
Regression Test / PoC
diff --git a/src/libANGLE/TransformFeedback_unittest.cpp b/src/libANGLE/TransformFeedback_unittest.cpp
index c922f11..9706d73 100644
--- a/src/libANGLE/TransformFeedback_unittest.cpp
+++ b/src/libANGLE/TransformFeedback_unittest.cpp
@@ -74,7 +74,7 @@
EXPECT_FALSE(mFeedback->isActive());
EXPECT_CALL(*mImpl, begin(nullptr, gl::PrimitiveMode::Triangles));
EXPECT_EQ(angle::Result::Continue,
- mFeedback->begin(nullptr, gl::PrimitiveMode::Triangles, nullptr));
+ mFeedback->begin(nullptr, gl::PrimitiveMode::Triangles, nullptr, nullptr));
EXPECT_TRUE(mFeedback->isActive());
EXPECT_EQ(gl::PrimitiveMode::Triangles, mFeedback->getPrimitiveMode());
EXPECT_CALL(*mImpl, end(nullptr));
@@ -89,7 +89,7 @@
EXPECT_FALSE(mFeedback->isActive());
EXPECT_CALL(*mImpl, begin(nullptr, gl::PrimitiveMode::Triangles));
EXPECT_EQ(angle::Result::Continue,
- mFeedback->begin(nullptr, gl::PrimitiveMode::Triangles, nullptr));
+ mFeedback->begin(nullptr, gl::PrimitiveMode::Triangles, nullptr, nullptr));
EXPECT_FALSE(mFeedback->isPaused());
EXPECT_CALL(*mImpl, pause(nullptr));
EXPECT_EQ(angle::Result::Continue, mFeedback->pause(nullptr));
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 28972c0..399b0fd 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -420,6 +420,8 @@
494270619 MAC OPENGL : BaseInstanceOverflowTest.BaseInstanceOverflow/* = SKIP
504886981 MAC OPENGL : RobustResourceInitTestES3.PartiallyInitializedTextureWithNonZeroBase/* = SKIP
504886981 MAC OPENGL : RobustResourceInitTestES3.ReadbackWithMippedTexture/* = SKIP
+497533569 MAC INTEL OPENGL : TransformFeedbackTest.ProgramSwitchDuringPauseAndResume/* = SKIP
+497533569 MAC INTEL OPENGL : TransformFeedbackTest.ProgramSwitchDuringPauseAndResumeWithBufferChange/* = SKIP
// BlitFramebufferTest.ScissoredMultisampleStencil failures
42262159 MAC INTEL OPENGL : BlitFramebufferTest.ScissoredMultisampleStencil/* = SKIP
@@ -2065,6 +2067,11 @@
410630704 SAMSUNG GALAXYS24EXYNOS GLES : ProgramPipelineTest31.VaryingLocationMismatch/* = SKIP
410630704 SAMSUNG GALAXYS24EXYNOS GLES : ProgramPipelineXFBTest31.SeparableProgramWithXFBSeparateMode/* = SKIP
410630704 SAMSUNG GALAXYS24EXYNOS GLES : ProgramPipelineXFBTest31.VaryingIOBlockSeparableProgramWithXFB/* = SKIP
+410630704 SAMSUNG GALAXYS24EXYNOS GLES : ProgramPipelineXFBTest31.ChangeShaderStageDuringPauseAndResume/* = SKIP
+410630704 SAMSUNG GALAXYS24EXYNOS GLES : ProgramPipelineXFBTest31.ChangeShaderStageDuringPauseAndResumeWithBufferChange/* = SKIP
+410630704 SAMSUNG GALAXYS24EXYNOS GLES : ProgramPipelineXFBTest31.PipelineSwitchDuringPauseAndResume/* = SKIP
+410630704 SAMSUNG GALAXYS24EXYNOS GLES : ProgramPipelineXFBTest31.BindProgramDuringPauseAndResume/* = SKIP
+410630704 SAMSUNG GALAXYS24EXYNOS GLES : ProgramPipelineXFBTest31.UseProgramAndPPOThenUnbindProgramAndResume/* = SKIP
410630704 SAMSUNG GALAXYS24EXYNOS GLES : SRGBFramebufferTestES3.BlitFramebuffer/* = SKIP
410630704 SAMSUNG GALAXYS24EXYNOS GLES : SRGBFramebufferTest.MultipleFramebuffers/* = SKIP
410630704 SAMSUNG GALAXYS24EXYNOS GLES : SRGBFramebufferTest.NegativeLifetimeTracking/* = SKIP
diff --git a/src/tests/gl_tests/ProgramPipelineTest.cpp b/src/tests/gl_tests/ProgramPipelineTest.cpp
index 014e44e..23cfe01 100644
--- a/src/tests/gl_tests/ProgramPipelineTest.cpp
+++ b/src/tests/gl_tests/ProgramPipelineTest.cpp
@@ -1680,6 +1680,660 @@
EXPECT_GL_NO_ERROR();
}
+// Test that resuming transform feedback after changing a pipeline shader stage results in
+// validation error.
+TEST_P(ProgramPipelineXFBTest31, ChangeShaderStageDuringPauseAndResume)
+{
+ // Only the Vulkan backend supports PPOs
+ ANGLE_SKIP_TEST_IF(!IsVulkan());
+
+ const char *kVS1 = R"(#version 310 es
+out float tfVarying10;
+out float tfVarying11;
+out float tfVarying12;
+out float tfVarying13;
+void main() {
+ tfVarying10 = 1.0;
+ tfVarying11 = 2.0;
+ tfVarying12 = 3.0;
+ tfVarying13 = 4.0;
+ gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
+})";
+ const char *kVS2 = R"(#version 310 es
+out float tfVarying20;
+out float tfVarying21;
+void main() {
+ tfVarying20 = -1.0;
+ tfVarying21 = -2.0;
+ gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
+})";
+
+ const char *tfVaryings1[] = {"tfVarying10", "tfVarying11", "tfVarying12", "tfVarying13"};
+ const char *tfVaryings2[] = {"tfVarying20", "tfVarying21"};
+
+ GLShader vs1(GL_VERTEX_SHADER);
+ GLuint vsProgram1 = glCreateProgram();
+ glShaderSource(vs1, 1, &kVS1, nullptr);
+ glCompileShader(vs1);
+ glProgramParameteri(vsProgram1, GL_PROGRAM_SEPARABLE, GL_TRUE);
+ glAttachShader(vsProgram1, vs1);
+ glTransformFeedbackVaryings(vsProgram1, 4, tfVaryings1, GL_SEPARATE_ATTRIBS);
+ glLinkProgram(vsProgram1);
+ EXPECT_GL_NO_ERROR();
+
+ GLShader vs2(GL_VERTEX_SHADER);
+ GLuint vsProgram2 = glCreateProgram();
+ glShaderSource(vs2, 1, &kVS2, nullptr);
+ glCompileShader(vs2);
+ glProgramParameteri(vsProgram2, GL_PROGRAM_SEPARABLE, GL_TRUE);
+ glAttachShader(vsProgram2, vs2);
+ glTransformFeedbackVaryings(vsProgram2, 2, tfVaryings2, GL_SEPARATE_ATTRIBS);
+ glLinkProgram(vsProgram2);
+
+ glEnable(GL_RASTERIZER_DISCARD);
+
+ // XFB buffers
+ GLBuffer xfbBuffers[4];
+ constexpr GLsizei kInitSize = 4 * 1024;
+ for (int i = 0; i < 4; ++i)
+ {
+ glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffers[i]);
+ glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kInitSize, nullptr, GL_DYNAMIC_DRAW);
+ glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, xfbBuffers[i]);
+ ASSERT_GL_NO_ERROR();
+ }
+
+ // Use the first program which uses four buffers.
+ GLProgramPipeline pipeline;
+ glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vsProgram1);
+ glBindProgramPipeline(pipeline);
+ glUseProgram(0);
+
+ glBeginTransformFeedback(GL_POINTS);
+ glDrawArrays(GL_POINTS, 0, 1);
+ glEndTransformFeedback();
+ ASSERT_GL_NO_ERROR();
+
+ // Validate the XFB values for the first program.
+ for (int i = 0; i < 4; ++i)
+ {
+ glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffers[i]);
+ const float *bufferData = reinterpret_cast<float *>(
+ glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(GLfloat), GL_MAP_READ_BIT));
+ ASSERT_NE(nullptr, bufferData);
+ EXPECT_EQ(*bufferData, 1.0f + static_cast<float>(i));
+ glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
+ }
+
+ // Update the pipeline to use the second program which only uses the first two buffers.
+ glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vsProgram2);
+ glBindProgramPipeline(pipeline);
+ glUseProgram(0);
+ glBeginTransformFeedback(GL_POINTS);
+ glPauseTransformFeedback();
+ ASSERT_GL_NO_ERROR();
+
+ // Resuming transform feedback after changing a shader stage should result in validation error.
+ glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vsProgram1);
+ glBindProgramPipeline(pipeline);
+ glUseProgram(0);
+ ASSERT_GL_NO_ERROR();
+ glResumeTransformFeedback();
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ glDrawArrays(GL_POINTS, 0, 1);
+ ASSERT_GL_NO_ERROR();
+
+ // Resuming transform feedback with the same program from the beginning is OK.
+ glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vsProgram2);
+ glBindProgramPipeline(pipeline);
+ glUseProgram(0);
+ ASSERT_GL_NO_ERROR();
+ glResumeTransformFeedback();
+ ASSERT_GL_NO_ERROR();
+
+ glDrawArrays(GL_POINTS, 0, 1);
+ ASSERT_GL_NO_ERROR();
+
+ glEndTransformFeedback();
+ ASSERT_GL_NO_ERROR();
+
+ // Validate the XFB values for the second program.
+ for (int i = 0; i < 2; ++i)
+ {
+ glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffers[i]);
+ const float *bufferData = reinterpret_cast<float *>(
+ glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(GLfloat), GL_MAP_READ_BIT));
+ ASSERT_NE(nullptr, bufferData);
+ EXPECT_EQ(*bufferData, -(1.0f + static_cast<float>(i)));
+ glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
+ }
+
+ glDeleteProgram(vsProgram1);
+ glDeleteProgram(vsProgram2);
+}
+
+// Test that resuming transform feedback after changing a pipeline shader stage results in
+// validation error, and using the original shader after updating the size of one of its buffers
+// works.
+TEST_P(ProgramPipelineXFBTest31, ChangeShaderStageDuringPauseAndResumeWithBufferChange)
+{
+ // Only the Vulkan backend supports PPOs
+ ANGLE_SKIP_TEST_IF(!IsVulkan());
+
+ const char *kVS1 = R"(#version 310 es
+out float tfVarying10;
+out float tfVarying11;
+out float tfVarying12;
+out float tfVarying13;
+void main() {
+ tfVarying10 = 1.0;
+ tfVarying11 = 2.0;
+ tfVarying12 = 3.0;
+ tfVarying13 = 4.0;
+ gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
+})";
+ const char *kVS2 = R"(#version 310 es
+out float tfVarying20;
+out float tfVarying21;
+void main() {
+ tfVarying20 = -1.0;
+ tfVarying21 = -2.0;
+ gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
+})";
+
+ const char *tfVaryings1[] = {"tfVarying10", "tfVarying11", "tfVarying12", "tfVarying13"};
+ const char *tfVaryings2[] = {"tfVarying20", "tfVarying21"};
+
+ GLShader vs1(GL_VERTEX_SHADER);
+ GLuint vsProgram1 = glCreateProgram();
+ glShaderSource(vs1, 1, &kVS1, nullptr);
+ glCompileShader(vs1);
+ glProgramParameteri(vsProgram1, GL_PROGRAM_SEPARABLE, GL_TRUE);
+ glAttachShader(vsProgram1, vs1);
+ glTransformFeedbackVaryings(vsProgram1, 4, tfVaryings1, GL_SEPARATE_ATTRIBS);
+ glLinkProgram(vsProgram1);
+ EXPECT_GL_NO_ERROR();
+
+ GLShader vs2(GL_VERTEX_SHADER);
+ GLuint vsProgram2 = glCreateProgram();
+ glShaderSource(vs2, 1, &kVS2, nullptr);
+ glCompileShader(vs2);
+ glProgramParameteri(vsProgram2, GL_PROGRAM_SEPARABLE, GL_TRUE);
+ glAttachShader(vsProgram2, vs2);
+ glTransformFeedbackVaryings(vsProgram2, 2, tfVaryings2, GL_SEPARATE_ATTRIBS);
+ glLinkProgram(vsProgram2);
+
+ glEnable(GL_RASTERIZER_DISCARD);
+
+ // XFB buffers
+ GLBuffer xfbBuffers[4];
+ constexpr GLsizei kInitSize = 4 * 1024;
+ for (int i = 0; i < 4; ++i)
+ {
+ glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffers[i]);
+ glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kInitSize, nullptr, GL_DYNAMIC_DRAW);
+ glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, xfbBuffers[i]);
+ ASSERT_GL_NO_ERROR();
+ }
+
+ // Use the first program which uses four buffers.
+ GLProgramPipeline pipeline;
+ glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vsProgram1);
+ glBindProgramPipeline(pipeline);
+ glUseProgram(0);
+
+ glBeginTransformFeedback(GL_POINTS);
+ glDrawArrays(GL_POINTS, 0, 1);
+ glEndTransformFeedback();
+ ASSERT_GL_NO_ERROR();
+
+ // Validate the XFB values for the first program.
+ for (int i = 0; i < 4; ++i)
+ {
+ glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffers[i]);
+ const float *bufferData = reinterpret_cast<float *>(
+ glMapBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(GLfloat), GL_MAP_READ_BIT));
+ ASSERT_NE(nullptr, bufferData);
+ EXPECT_EQ(*bufferData, 1.0f + static_cast<float>(i));
+ glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
+ }
+
+ // One of the buffers that will not be used in the second program is expanded.
+ constexpr GLsizei kLargeSize = 8 * 1024 * 1024;
+ glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffers[2]);
+ glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kLargeSize, nullptr, GL_DYNAMIC_DRAW);
+ ASSERT_GL_NO_ERROR();
+
+ // Update the pipeline to use the second program which only uses the first two buffers.
+ glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vsProgram2);
+ glBindProgramPipeline(pipeline);
+ glUseProgram(0);
+ glBeginTransformFeedback(GL_POINTS);
+ glPauseTransformFeedback();
+ ASSERT_GL_NO_ERROR();
+
+ // Resuming transform feedback after changing a shader stage should result in validation error.
+ glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vsProgram1);
+ glBindProgramPipeline(pipeline);
+ glUseProgram(0);
+ ASSERT_GL_NO_ERROR();
+ glResumeTransformFeedback();
+ EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+
+ glDrawArrays(GL_POINTS, 0, 1);
+ ASSERT_GL_NO_ERROR();
+
+ // Resuming transform feedback with the same shaders from the beginning is OK.
... (truncated)
Original Bug Report
Potential GPU memory corruption via Transform Feedback state confusion in ANGLE Vulkan
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A vulnerability in ANGLE’s Vulkan backend allows a compromised renderer to bypass transform feedback validation and cause the GPU to write data into stale or reallocated memory regions. This is caused by a failure to validate program consistency on resume and a failure to clear cached Vulkan buffer handles.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/TransformFeedbackVk.cppthird_party/angle/src/libANGLE/validationES3.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cppthird_party/angle/src/libANGLE/validationES.cppthird_party/angle/src/libANGLE/validationES2.cppgpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.ccgpu/command_buffer/service/gles2_cmd_decoder.cc
Estimated timestamp from git blame: 2025-01-22
Description
An issue exists in ANGLE’s Vulkan backend where stale VkBuffer handles and offsets can be reused during transform feedback (TF) operations after a program switch and a pause/resume cycle. This leads to an out-of-bounds write or use-after-free scenario on the GPU, where vertex shader output is DMA-written into memory regions that may have been reallocated for other purposes (such as indirect-draw buffers or uniform buffers).
The vulnerability stems from three interacting issues in validation logic and the Vulkan backend implementation:
- Validation Omission:
ValidateResumeTransformFeedback(inthird_party/angle/src/libANGLE/validationES3.cpp) does not verify that the currently bound program matches the program that was active whenglBeginTransformFeedbackwas called. This violates the GLES 3.0 specification (§2.15.2) and allows a paused TF object to be resumed with a completely different program. - Incomplete Cleanup:
TransformFeedbackVk::end()(inthird_party/angle/src/libANGLE/renderer/vulkan/TransformFeedbackVk.cpp) correctly detaches observer bindings (mBufferObserverBindings.reset()) but fails to clear the aggressively cached Vulkan handles inmBufferHandles,mBufferOffsets, andmBufferSizes. Consequently, if a buffer is reallocated (e.g., viaglBufferData), the cached handle and offset become stale because the observer that would normally trigger a cache update (onSubjectStateChange) has been removed. - Missing Refresh on Resume:
TransformFeedbackVk::resume()skips the call toinitializeXFBVariables()when the nativeVK_EXT_transform_feedbackextension is used (i.e.,contextVk->getFeatures().emulateTransformFeedback.enabledis false). This prevents the stale cache from being refreshed during the resume operation.
While Blink includes renderer-side validation to prevent resuming with the wrong program, the GPU process’s command decoders (e.g., GLES2DecoderImpl::DoResumeTransformFeedback) forward the ResumeTransformFeedback command to ANGLE without performing equivalent program identity checks. This allows a compromised renderer to bypass the check entirely.
Potential Reproduction Steps
The following sequence outlines how a compromised renderer could theoretically trigger the vulnerability by sending raw GLES3 commands to the GPU process on a platform using ANGLE’s Vulkan backend with native transform feedback support:
- Setup: Bind 4 buffers using
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, i, buffer[i])for indices 0 to 3. - Use Program A:
glUseProgram(progA)whereprogAis compiled to output 4 separate-attribs XFB varyings. - Begin TF:
glBeginTransformFeedback(GL_POINTS).TransformFeedbackVkresolves all 4 buffers, binds observers, and caches their rawVkBufferhandles intomBufferHandles[0..3]. - End TF:
glEndTransformFeedback(). The observers are reset, but the cached handles inmBufferHandlesare not cleared. - Reallocate Buffer: Call
glBufferDataon the buffers bound to indices 2 and 3. This causesBufferVkto release the old Vulkan suballocation and acquire a new one. The stale cache inTransformFeedbackVkis not updated because the observers were removed in step 4. - Use Program B:
glUseProgram(progB)whereprogBis compiled to output only 2 separate-attribs XFB varyings. - Begin TF:
glBeginTransformFeedback(GL_POINTS). BecauseprogBonly has 2 outputs, only slots 0 and 1 of the cache are refreshed. Slots 2 and 3 remain stale from the first session. - Pause TF:
glPauseTransformFeedback(). - Switch Program:
glUseProgram(progA). This is permitted byValidateUseProgrambecause TF is active but paused. - Resume TF:
glResumeTransformFeedback(). Validation passes becauseValidateResumeTransformFeedbackincorrectly omits the check for program identity. The Vulkan backendresume()skips the cache refresh on the native Vulkan path. - Execute:
glDrawArrays(GL_POINTS, 0, N). The dirty bits handler (ContextVk::handleDirtyGraphicsTransformFeedbackBuffersExtension) queriesprogAfor the buffer count (which is 4). It retrieves themBufferHandlesarray and binds all 4 handles to the Vulkan pipeline (vkCmdBindTransformFeedbackBuffersEXT).
The GPU then DMA-writes the vertex shader output for slots 2 and 3 into the physical memory regions pointed to by the stale handles. If the attacker groomed the GPU memory, these freed suballocations could have been reallocated for a victim buffer, leading to arbitrary memory corruption within the highly privileged GPU process context. MiraclePtr offers no protection here, as this is a GPU memory issue.
Suggested Fix
To remediate this issue, the following changes are recommended:
- Enforce Validation: Update
ValidateResumeTransformFeedbackinthird_party/angle/src/libANGLE/validationES3.cppto correctly enforce the GLES 3.0 specification. It must check that the currently bound program matches the program that was active whenglBeginTransformFeedbackwas called (stored in theTransformFeedbackobject). - Clear Stale Caches: Update
TransformFeedbackVk::end()inthird_party/angle/src/libANGLE/renderer/vulkan/TransformFeedbackVk.cppto explicitly clearmBufferHandles,mBufferOffsets, andmBufferSizeswhen it resets the observer bindings. - Ensure Safe Resume: Consider modifying
TransformFeedbackVk::resume()to refresh the cache (by callinginitializeXFBVariables()) even when native transform feedback is used, to ensure the backend state accurately reflects the current bindings.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.