CVE-2026-17676
Overview
Files Changed
src/libANGLE/renderer/vulkan/FramebufferVk.cppsrc/tests/angle_end2end_tests_expectations.txtsrc/tests/gl_tests/MultisampledRenderToTextureTest.cpp
Patch
From 16e19b23eab212afaba5dcfb680a3592a1c61bc3 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Wed, 10 Jun 2026 22:27:34 -0400
Subject: [PATCH] Vulkan: Fix MSRTT when all attachments are disabled
The render pass was considered single-sampled in that case when the
render pass starts, which is incorrect. This was caught by an ASSERT
when resolve attachments were being added to this
mistakenly-single-sampled attachment.
Bug: chromium:513920298
Change-Id: I0886bf0c5e075729188dc53de678effaf6fa352c
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7921634
Reviewed-by: Charlie Lao <cclao@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---
diff --git a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
index c45ceabe..4ad7175 100644
--- a/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
+++ b/src/libANGLE/renderer/vulkan/FramebufferVk.cpp
@@ -2935,7 +2935,7 @@
{
// Update descriptions regarding multisampled-render-to-texture use.
bool isRenderToTexture = false;
- for (size_t colorIndexGL : mState.getEnabledDrawBuffers())
+ for (size_t colorIndexGL : mState.getColorAttachmentsMask())
{
const gl::FramebufferAttachment *color = mState.getColorAttachment(colorIndexGL);
ASSERT(color);
@@ -4100,7 +4100,7 @@
{
const gl::FramebufferAttachment *lastAttachment = nullptr;
- for (size_t colorIndexGL : mState.getEnabledDrawBuffers() & mState.getColorAttachmentsMask())
+ for (size_t colorIndexGL : mState.getColorAttachmentsMask())
{
const gl::FramebufferAttachment *color = mState.getColorAttachment(colorIndexGL);
ASSERT(color);
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 3cebdb0..ed6bea0 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -728,6 +728,8 @@
458082809 PIXEL4ORXL GLES : GLSLValidationExtensionDirectiveTest_ES3.Sampler2DShadow/* = SKIP
458082810 PIXEL4ORXL GLES : GLSLValidationExtensionDirectiveTest_ES31.TextureCubeMapArray/* = SKIP
519218481 PIXEL4ORXL GLES : MSRTTES3Test.CubeMap/* = SKIP
+522247737 PIXEL4ORXL GLES : MSRTTES3Test.DrawDisabled/* = SKIP
+522247737 PIXEL4ORXL GLES : MSRTTES3Test.DrawThenDrawDisabled/* = SKIP
42267082 PIXEL4ORXL GLES : BlobCacheTest.FragmentOutputLocationKey/* = SKIP
42267082 PIXEL4ORXL GLES : EGLBlobCacheTest.FragmentOutputLocationKey/* = SKIP
diff --git a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
index b05b6a6..b23b7b7 100644
--- a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
+++ b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
@@ -4776,6 +4776,193 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::white);
}
+// Test that drawing with the color attachment disabled works.
+TEST_P(MSRTTES3Test, DrawDisabled)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+ const bool hasAtomicCounters = getClientMajorVersion() == 3 && getClientMinorVersion() >= 1;
+ constexpr char kFS[] = R"(#version 310 es
+precision mediump float;
+uniform vec4 c;
+layout(binding = 0) uniform atomic_uint ac;
+out vec4 color;
+
+void main()
+{
+ atomicCounterIncrement(ac);
+ color = c;
+})";
+
+ ANGLE_GL_PROGRAM(program,
+ hasAtomicCounters ? essl31_shaders::vs::Simple() : essl1_shaders::vs::Simple(),
+ hasAtomicCounters ? kFS : essl1_shaders::fs::UniformColor());
+ glUseProgram(program);
+ GLint colorLocation =
+ glGetUniformLocation(program, hasAtomicCounters ? "c" : essl1_shaders::ColorUniform());
+ ASSERT_NE(-1, colorLocation);
+
+ GLRenderbuffer color;
+ glBindRenderbuffer(GL_RENDERBUFFER, color);
+ glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, mTestSampleCount, GL_RGBA8, 1, 1);
+
+ GLFramebuffer FBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, color);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ glClearColor(0, 0, 1, 1);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ GLBuffer ac;
+ constexpr GLuint kInitialData = 20;
+ if (hasAtomicCounters)
+ {
+ glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, ac);
+ glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(kInitialData), &kInitialData, GL_STATIC_DRAW);
+ glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, ac);
+ }
+
+ // Draw once to create a render pass, then close it.
+ constexpr GLenum kDisable = GL_NONE;
+ glDrawBuffers(1, &kDisable);
+
+ glUniform4fv(colorLocation, 1, GLColor::red.toNormalizedVector().data());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0);
+ // Attachment should remain blue (the clear color)
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+
+ if (hasAtomicCounters)
+ {
+ // Verify that draw actually happened by looking at the atomic counter.
+ glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
+ const GLuint value = *static_cast<const GLuint *>(
+ glMapBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(GLuint), GL_MAP_READ_BIT));
+ glUnmapBuffer(GL_ATOMIC_COUNTER_BUFFER);
+
+ EXPECT_EQ(value, kInitialData + 1);
+ }
+
+ ASSERT_GL_NO_ERROR();
+}
+
+// Test that drawing with the color attachment disabled after drawing with it enabled works.
+TEST_P(MSRTTES3Test, DrawThenDrawDisabled)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+ const bool hasAtomicCounters = getClientMajorVersion() == 3 && getClientMinorVersion() >= 1;
+ constexpr char kFS[] = R"(#version 310 es
+precision mediump float;
+uniform vec4 c;
+layout(binding = 0) uniform atomic_uint ac;
+out vec4 color;
+
+void main()
+{
+ atomicCounterIncrement(ac);
+ color = c;
+})";
+
+ ANGLE_GL_PROGRAM(program,
+ hasAtomicCounters ? essl31_shaders::vs::Simple() : essl1_shaders::vs::Simple(),
+ hasAtomicCounters ? kFS : essl1_shaders::fs::UniformColor());
+ glUseProgram(program);
+ GLint colorLocation =
+ glGetUniformLocation(program, hasAtomicCounters ? "c" : essl1_shaders::ColorUniform());
+ ASSERT_NE(-1, colorLocation);
+
+ GLRenderbuffer color;
+ glBindRenderbuffer(GL_RENDERBUFFER, color);
+ glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, mTestSampleCount, GL_RGBA8, 1, 1);
+
+ GLFramebuffer FBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, color);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ GLBuffer ac;
+ constexpr GLuint kInitialData = 20;
+ if (hasAtomicCounters)
+ {
+ glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, ac);
+ glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(kInitialData), &kInitialData, GL_STATIC_DRAW);
+ glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, ac);
+ }
+
+ // Draw once to create a render pass, then close it.
+ glUniform4fv(colorLocation, 1, GLColor::red.toNormalizedVector().data());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ if (hasAtomicCounters)
+ {
+ glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT);
+ }
+
+ // Disable the draw buffer and draw again.
+ constexpr GLenum kDisable = GL_NONE;
+ glDrawBuffers(1, &kDisable);
+
+ glUniform4fv(colorLocation, 1, GLColor::green.toNormalizedVector().data());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0);
+
+ // Attachment should still be red.
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ if (hasAtomicCounters)
+ {
+ // Verify that both draws actually happened by looking at the atomic counter.
+ glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
+ const GLuint value = *static_cast<const GLuint *>(
+ glMapBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(GLuint), GL_MAP_READ_BIT));
Regression Test / PoC
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 3cebdb0..ed6bea0 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -728,6 +728,8 @@
458082809 PIXEL4ORXL GLES : GLSLValidationExtensionDirectiveTest_ES3.Sampler2DShadow/* = SKIP
458082810 PIXEL4ORXL GLES : GLSLValidationExtensionDirectiveTest_ES31.TextureCubeMapArray/* = SKIP
519218481 PIXEL4ORXL GLES : MSRTTES3Test.CubeMap/* = SKIP
+522247737 PIXEL4ORXL GLES : MSRTTES3Test.DrawDisabled/* = SKIP
+522247737 PIXEL4ORXL GLES : MSRTTES3Test.DrawThenDrawDisabled/* = SKIP
42267082 PIXEL4ORXL GLES : BlobCacheTest.FragmentOutputLocationKey/* = SKIP
42267082 PIXEL4ORXL GLES : EGLBlobCacheTest.FragmentOutputLocationKey/* = SKIP
diff --git a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
index b05b6a6..b23b7b7 100644
--- a/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
+++ b/src/tests/gl_tests/MultisampledRenderToTextureTest.cpp
@@ -4776,6 +4776,193 @@
EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::white);
}
+// Test that drawing with the color attachment disabled works.
+TEST_P(MSRTTES3Test, DrawDisabled)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+ const bool hasAtomicCounters = getClientMajorVersion() == 3 && getClientMinorVersion() >= 1;
+ constexpr char kFS[] = R"(#version 310 es
+precision mediump float;
+uniform vec4 c;
+layout(binding = 0) uniform atomic_uint ac;
+out vec4 color;
+
+void main()
+{
+ atomicCounterIncrement(ac);
+ color = c;
+})";
+
+ ANGLE_GL_PROGRAM(program,
+ hasAtomicCounters ? essl31_shaders::vs::Simple() : essl1_shaders::vs::Simple(),
+ hasAtomicCounters ? kFS : essl1_shaders::fs::UniformColor());
+ glUseProgram(program);
+ GLint colorLocation =
+ glGetUniformLocation(program, hasAtomicCounters ? "c" : essl1_shaders::ColorUniform());
+ ASSERT_NE(-1, colorLocation);
+
+ GLRenderbuffer color;
+ glBindRenderbuffer(GL_RENDERBUFFER, color);
+ glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, mTestSampleCount, GL_RGBA8, 1, 1);
+
+ GLFramebuffer FBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, color);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ glClearColor(0, 0, 1, 1);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ GLBuffer ac;
+ constexpr GLuint kInitialData = 20;
+ if (hasAtomicCounters)
+ {
+ glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, ac);
+ glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(kInitialData), &kInitialData, GL_STATIC_DRAW);
+ glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, ac);
+ }
+
+ // Draw once to create a render pass, then close it.
+ constexpr GLenum kDisable = GL_NONE;
+ glDrawBuffers(1, &kDisable);
+
+ glUniform4fv(colorLocation, 1, GLColor::red.toNormalizedVector().data());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0);
+ // Attachment should remain blue (the clear color)
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::blue);
+
+ if (hasAtomicCounters)
+ {
+ // Verify that draw actually happened by looking at the atomic counter.
+ glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
+ const GLuint value = *static_cast<const GLuint *>(
+ glMapBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(GLuint), GL_MAP_READ_BIT));
+ glUnmapBuffer(GL_ATOMIC_COUNTER_BUFFER);
+
+ EXPECT_EQ(value, kInitialData + 1);
+ }
+
+ ASSERT_GL_NO_ERROR();
+}
+
+// Test that drawing with the color attachment disabled after drawing with it enabled works.
+TEST_P(MSRTTES3Test, DrawThenDrawDisabled)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+ const bool hasAtomicCounters = getClientMajorVersion() == 3 && getClientMinorVersion() >= 1;
+ constexpr char kFS[] = R"(#version 310 es
+precision mediump float;
+uniform vec4 c;
+layout(binding = 0) uniform atomic_uint ac;
+out vec4 color;
+
+void main()
+{
+ atomicCounterIncrement(ac);
+ color = c;
+})";
+
+ ANGLE_GL_PROGRAM(program,
+ hasAtomicCounters ? essl31_shaders::vs::Simple() : essl1_shaders::vs::Simple(),
+ hasAtomicCounters ? kFS : essl1_shaders::fs::UniformColor());
+ glUseProgram(program);
+ GLint colorLocation =
+ glGetUniformLocation(program, hasAtomicCounters ? "c" : essl1_shaders::ColorUniform());
+ ASSERT_NE(-1, colorLocation);
+
+ GLRenderbuffer color;
+ glBindRenderbuffer(GL_RENDERBUFFER, color);
+ glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, mTestSampleCount, GL_RGBA8, 1, 1);
+
+ GLFramebuffer FBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, color);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ GLBuffer ac;
+ constexpr GLuint kInitialData = 20;
+ if (hasAtomicCounters)
+ {
+ glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, ac);
+ glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(kInitialData), &kInitialData, GL_STATIC_DRAW);
+ glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, ac);
+ }
+
+ // Draw once to create a render pass, then close it.
+ glUniform4fv(colorLocation, 1, GLColor::red.toNormalizedVector().data());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0);
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ if (hasAtomicCounters)
+ {
+ glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT);
+ }
+
+ // Disable the draw buffer and draw again.
+ constexpr GLenum kDisable = GL_NONE;
+ glDrawBuffers(1, &kDisable);
+
+ glUniform4fv(colorLocation, 1, GLColor::green.toNormalizedVector().data());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0);
+
+ // Attachment should still be red.
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
+
+ if (hasAtomicCounters)
+ {
+ // Verify that both draws actually happened by looking at the atomic counter.
+ glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
+ const GLuint value = *static_cast<const GLuint *>(
+ glMapBufferRange(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(GLuint), GL_MAP_READ_BIT));
+ glUnmapBuffer(GL_ATOMIC_COUNTER_BUFFER);
+
+ EXPECT_EQ(value, kInitialData + 2);
+ }
+
+ ASSERT_GL_NO_ERROR();
+}
+
+// Test that drawing with the color attachment disabled then drawing with it enabled works.
+TEST_P(MSRTTES3Test, DrawDisabledThenDraw)
+{
+ ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_multisampled_render_to_texture"));
+
+ ANGLE_GL_PROGRAM(program, essl1_shaders::vs::Simple(), essl1_shaders::fs::UniformColor());
+ glUseProgram(program);
+ GLint colorLocation = glGetUniformLocation(program, essl1_shaders::ColorUniform());
+ ASSERT_NE(-1, colorLocation);
+
+ GLRenderbuffer color;
+ glBindRenderbuffer(GL_RENDERBUFFER, color);
+ glRenderbufferStorageMultisampleEXT(GL_RENDERBUFFER, mTestSampleCount, GL_RGBA8, 1, 1);
+
+ GLFramebuffer FBO;
+ glBindFramebuffer(GL_FRAMEBUFFER, FBO);
+ glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, color);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ // Draw once with color attachment 0 disabled.
+ constexpr GLenum kDisable = GL_NONE;
+ glDrawBuffers(1, &kDisable);
+
+ glUniform4fv(colorLocation, 1, GLColor::red.toNormalizedVector().data());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0);
+
+ // Enable the draw buffer and draw again.
+ constexpr GLenum kEnable = GL_COLOR_ATTACHMENT0;
+ glDrawBuffers(1, &kEnable);
+
+ glUniform4fv(colorLocation, 1, GLColor::green.toNormalizedVector().data());
+ drawQuad(program, essl1_shaders::PositionAttrib(), 0);
+
+ // Verify results
+ EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+ ASSERT_GL_NO_ERROR();
+}
+
ANGLE_INSTANTIATE_TEST_COMBINE_1(
MSRTTTest,
PrintToStringParamName,
@@ -4809,7 +4996,8 @@
.disable(Feature::SupportsExtendedDynamicState2),
ES3_VULKAN().disable(Feature::SupportsExtendedDynamicState2),
ES3_VULKAN().disable(Feature::SupportsSPIRV14),
- ES3_VULKAN_SWIFTSHADER().enable(Feature::EnableMultisampledRenderToTexture));
+ ES3_VULKAN_SWIFTSHADER().enable(Feature::EnableMultisampledRenderToTexture),
+ ES31_VULKAN_SWIFTSHADER().enable(Feature::EnableMultisampledRenderToTexture));
GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(MSRTTSRGBES3Test);
ANGLE_INSTANTIATE_TEST_COMBINE_1(
Original Bug Report
Potential Sample Count Mismatch in ANGLE Vulkan MSRTT Emulation via glDrawBuffers(GL_NONE)
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 logic error in ANGLE’s Vulkan backend leads to a sample count mismatch between VkRenderPass and VkFramebuffer when glDrawBuffers(GL_NONE) is used with Multisampled Render-to-Texture (MSRTT) attachments. This issue specifically affects Android devices utilizing MSRTT emulation on tile-based GPUs. The resulting Vulkan VUID violations may cause exploitable memory corruption in the unsandboxed GPU process.
Affected files:
third_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_cache_utils.cppthird_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/RenderTargetVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cppthird_party/angle/src/libANGLE/renderer/vulkan/vk_renderer.cppthird_party/angle/src/libANGLE/Framebuffer.cpp
Estimated timestamp from git blame: 2021-03-09
Summary
A potential vulnerability has been identified in ANGLE’s Vulkan backend where the sample count for a framebuffer is incorrectly calculated when draw buffers are disabled. Specifically, FramebufferVk::getSamplesImpl() derives the framebuffer’s sample count by iterating only over enabled draw buffers. However, subsequent logic that populates the Vulkan RenderPass and Framebuffer attachments iterates over all color attachments in the mask, regardless of their draw buffer state.
When glDrawBuffers(GL_NONE) is called on a framebuffer with a Multisampled Render-to-Texture (MSRTT) attachment, the sample count is incorrectly cached as 1. This leads to a VkRenderPass where the attachment is declared with 1 sample, while a multisampled VkImageView (e.g., 4 samples) is bound to that same attachment in the VkFramebuffer. This mismatch results in undefined behavior in the GPU driver.
Technical Details
In third_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cpp, the getSamplesImpl() function filters by the enabled draw buffer state:
// lines 4103-4114
for (size_t colorIndexGL : mState.getEnabledDrawBuffers() & mState.getColorAttachmentsMask())
{
const gl::FramebufferAttachment *color = mState.getColorAttachment(colorIndexGL);
if (color->isRenderToTexture())
{
return color->getSamples(); // Skipped if draw buffer is GL_NONE
}
lastAttachment = color;
}
When glDrawBuffers(GL_NONE) is set, mRasterizationSamples is cached as 1. However, updateRenderPassDesc still iterates over the full colorAttachmentMask to pack attachments (lines 2880-2911), leading to a VkRenderPass configuration that expects a single-sampled attachment. Simultaneously, updateAttachmentsAndRenderTargets binds the actual multisampled VkImageView to the framebuffer.
This results in several Vulkan Validation Layer (VUID) violations, including:
- VUID-VkFramebufferCreateInfo-pAttachments-00881: Bound
VkImageViewsample count does not matchVkAttachmentDescriptionsample count. - VUID-VkGraphicsPipelineCreateInfo-rasterizationSamples-06853: Pipeline
rasterizationSamplesdoes not match the bound attachment’s sample count.
Impact
This issue is reachable from a compromised renderer process via standard GLES2 command buffer IPC. It affects the GPU process, which is unsandboxed on Android. On tile-based deferred rendering (TBDR) GPUs common in Android devices (Adreno, Mali, PowerVR), these metadata mismatches can lead to driver-level memory corruption during tile-to-memory operations, such as resolves. An attacker could potentially leverage this corruption to achieve code execution in the context of the GPU process.
Potential Reproduction Steps
Note: These are suggested steps; our analysis is based on code review.
- Use an Android device where ANGLE utilizes MSRTT emulation (typically those lacking
VK_EXT_multisampled_render_to_single_sampled). - Create a texture and an FBO.
- Attach the texture to the FBO using
glFramebufferTexture2DMultisampleEXTwith a sample count > 1. - Set draw buffers to
GL_NONEusingglDrawBuffers(1, {GL_NONE}). - Issue a draw call (e.g.,
glDrawArrays). - The resulting Vulkan calls will contain mismatched sample counts between the RenderPass and Framebuffer, triggering the violation.
Suggested Fix
Modify FramebufferVk::getSamplesImpl() in third_party/angle/src/libANGLE/renderer/vulkan/FramebufferVk.cpp to iterate over the entire mState.getColorAttachmentsMask() instead of intersecting it with mState.getEnabledDrawBuffers(). The physical structure of the RenderPass and Framebuffer should reflect the attached resources regardless of the current draw buffer enablement state.
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.