Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in ANGLE
DescriptionOut of bounds read in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker503985322
Fix commit66be111b5507 (angle/angle) +355/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
LineLoopTestES3
src/tests/gl_tests/LineLoopTest.cpp
modified

Files Changed

  • src/libANGLE/renderer/vulkan/ContextVk.cpp
  • src/libANGLE/renderer/vulkan/ContextVk.h
  • src/tests/angle_end2end_tests_expectations.txt
  • src/tests/gl_tests/LineLoopTest.cpp
From 66be111b55079129f7d2f1dc3f8e99499c558c25 Mon Sep 17 00:00:00 2001
From: Shahbaz Youssefi <syoussefi@chromium.org>
Date: Wed, 13 May 2026 00:01:47 -0400
Subject: [PATCH] Vulkan: Add baseInstance/instanceCount to setupLineLoopDraw

Fix credit boliu@chromium.org

Bug: chromium:503985322
Change-Id: I80ecfbd87169e8dd22e609e85e2a5454f7e5d304
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7842767
Reviewed-by: Charlie Lao <cclao@google.com>
Reviewed-by: Amirali Abdolrashidi <abdolrashidi@google.com>
Commit-Queue: Shahbaz Youssefi <syoussefi@chromium.org>
---

diff --git a/src/libANGLE/renderer/vulkan/ContextVk.cpp b/src/libANGLE/renderer/vulkan/ContextVk.cpp
index 7fab06e..188f3c7 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.cpp
+++ b/src/libANGLE/renderer/vulkan/ContextVk.cpp
@@ -1861,6 +1861,8 @@
                                            gl::PrimitiveMode mode,
                                            GLint firstVertex,
                                            GLsizei vertexOrIndexCount,
+                                           GLsizei baseInstance,
+                                           GLsizei instanceCount,
                                            gl::DrawElementsType indexTypeOrInvalid,
                                            const void *indices,
                                            uint32_t *numIndicesOut)
@@ -1878,8 +1880,8 @@
     mCurrentDrawElementsType = indexTypeOrInvalid != gl::DrawElementsType::InvalidEnum
                                    ? indexTypeOrInvalid
                                    : gl::DrawElementsType::UnsignedInt;
-    return setupDraw(context, mode, firstVertex, vertexOrIndexCount, 0, 1, indexTypeOrInvalid,
-                     indices, mIndexedDirtyBitsMask);
+    return setupDraw(context, mode, firstVertex, vertexOrIndexCount, baseInstance, instanceCount,
+                     indexTypeOrInvalid, indices, mIndexedDirtyBitsMask);
 }
 
 angle::Result ContextVk::setupDispatch(const gl::Context *context)
@@ -3935,8 +3937,8 @@
     if (mode == gl::PrimitiveMode::LineLoop)
     {
         uint32_t numIndices;
-        ANGLE_TRY(setupLineLoopDraw(context, mode, first, count, gl::DrawElementsType::InvalidEnum,
-                                    nullptr, &numIndices));
+        ANGLE_TRY(setupLineLoopDraw(context, mode, first, count, 0, 1,
+                                    gl::DrawElementsType::InvalidEnum, nullptr, &numIndices));
         LineLoopHelper::Draw(numIndices, 0, mRenderPassCommandBuffer);
     }
     else
@@ -3959,7 +3961,7 @@
     {
         uint32_t clampedVertexCount = gl::GetClampedVertexCount<uint32_t>(count);
         uint32_t numIndices;
-        ANGLE_TRY(setupLineLoopDraw(context, mode, first, clampedVertexCount,
+        ANGLE_TRY(setupLineLoopDraw(context, mode, first, clampedVertexCount, 0, instances,
                                     gl::DrawElementsType::InvalidEnum, nullptr, &numIndices));
         mRenderPassCommandBuffer->drawIndexedInstanced(numIndices, instances);
         return angle::Result::Continue;
@@ -3983,8 +3985,9 @@
     {
         uint32_t clampedVertexCount = gl::GetClampedVertexCount<uint32_t>(count);
         uint32_t numIndices;
-        ANGLE_TRY(setupLineLoopDraw(context, mode, first, clampedVertexCount,
-                                    gl::DrawElementsType::InvalidEnum, nullptr, &numIndices));
+        ANGLE_TRY(setupLineLoopDraw(context, mode, first, clampedVertexCount, baseInstance,
+                                    instances, gl::DrawElementsType::InvalidEnum, nullptr,
+                                    &numIndices));
         mRenderPassCommandBuffer->drawIndexedInstancedBaseVertexBaseInstance(numIndices, instances,
                                                                              0, 0, baseInstance);
         return angle::Result::Continue;
@@ -4006,7 +4009,7 @@
     if (mode == gl::PrimitiveMode::LineLoop)
     {
         uint32_t indexCount;
-        ANGLE_TRY(setupLineLoopDraw(context, mode, 0, count, type, indices, &indexCount));
+        ANGLE_TRY(setupLineLoopDraw(context, mode, 0, count, 0, 1, type, indices, &indexCount));
         LineLoopHelper::Draw(indexCount, 0, mRenderPassCommandBuffer);
     }
     else
@@ -4028,7 +4031,7 @@
     if (mode == gl::PrimitiveMode::LineLoop)
     {
         uint32_t indexCount;
-        ANGLE_TRY(setupLineLoopDraw(context, mode, 0, count, type, indices, &indexCount));
+        ANGLE_TRY(setupLineLoopDraw(context, mode, 0, count, 0, 1, type, indices, &indexCount));
         LineLoopHelper::Draw(indexCount, baseVertex, mRenderPassCommandBuffer);
     }
     else
@@ -4050,7 +4053,8 @@
     if (mode == gl::PrimitiveMode::LineLoop)
     {
         uint32_t indexCount;
-        ANGLE_TRY(setupLineLoopDraw(context, mode, 0, count, type, indices, &indexCount));
+        ANGLE_TRY(
+            setupLineLoopDraw(context, mode, 0, count, 0, instances, type, indices, &indexCount));
         count = indexCount;
     }
     else
@@ -4073,7 +4077,8 @@
     if (mode == gl::PrimitiveMode::LineLoop)
     {
         uint32_t indexCount;
-        ANGLE_TRY(setupLineLoopDraw(context, mode, 0, count, type, indices, &indexCount));
+        ANGLE_TRY(
+            setupLineLoopDraw(context, mode, 0, count, 0, instances, type, indices, &indexCount));
         count = indexCount;
     }
     else
@@ -4097,7 +4102,8 @@
     if (mode == gl::PrimitiveMode::LineLoop)
     {
         uint32_t indexCount;
-        ANGLE_TRY(setupLineLoopDraw(context, mode, 0, count, type, indices, &indexCount));
+        ANGLE_TRY(setupLineLoopDraw(context, mode, 0, count, baseInstance, instances, type, indices,
+                                    &indexCount));
         count = indexCount;
     }
     else
diff --git a/src/libANGLE/renderer/vulkan/ContextVk.h b/src/libANGLE/renderer/vulkan/ContextVk.h
index 7af6811..63a682b 100644
--- a/src/libANGLE/renderer/vulkan/ContextVk.h
+++ b/src/libANGLE/renderer/vulkan/ContextVk.h
@@ -1153,6 +1153,8 @@
                                     gl::PrimitiveMode mode,
                                     GLint firstVertex,
                                     GLsizei vertexOrIndexCount,
+                                    GLsizei baseInstance,
+                                    GLsizei instanceCount,
                                     gl::DrawElementsType indexTypeOrInvalid,
                                     const void *indices,
                                     uint32_t *numIndicesOut);
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 318d08c..fb47211 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -571,6 +571,8 @@
 496259841 WIN D3D11 : RobustResourceInitTestES3.InvalidateThenReadBack/* = SKIP
 496259841 WIN D3D11 : RobustResourceInitTestES3.DrawThenInvalidateThenReadBack/* = SKIP
 496259841 WIN D3D11 : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP
+512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawArraysInstancedBaseInstance/* = SKIP
+512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawElementsInstancedBaseVertexBaseInstance/* = SKIP
 
 // Android
 42264624 ANDROID GLES : GLSLTest_ES3.InitGlobalComplexConstant/* = SKIP
diff --git a/src/tests/gl_tests/LineLoopTest.cpp b/src/tests/gl_tests/LineLoopTest.cpp
index 2dc6551..d0ff047 100644
--- a/src/tests/gl_tests/LineLoopTest.cpp
+++ b/src/tests/gl_tests/LineLoopTest.cpp
@@ -461,7 +461,166 @@
 }
 
 class LineLoopTestES3 : public LineLoopTest
-{};
+{
+  protected:
+    void runTestInstanced(std::function<void(void)> definePositionBuffer,
+                          std::function<void(void)> defineColorBuffer,
+                          std::function<void(void)> draw)
+    {
+        // http://anglebug.com/42265165: Disable D3D11 SDK Layers warnings checks.
+        ignoreD3D11SDKLayersWarnings();
+
+        constexpr char kVS[] = R"(#version 300 es
+    precision mediump float;
+    in vec2 position;
+    in vec2 colorIn;
+    out vec2 color;
+    void main()
+    {
+        gl_Position = vec4(position, 0, 1);
+        if ((gl_InstanceID / 2) % 2 == 1)
+        {
+            gl_Position.y += 1.0;
+        }
+        if (gl_InstanceID % 2 == 1)
+        {
+            gl_Position.x += 1.0;
+        }
+        color = colorIn;
+    })";
+
+        constexpr char kFS[] = R"(#version 300 es
+    precision mediump float;
+    in vec2 color;
+    out vec4 colorOut;
+    void main()
+    {
+        colorOut = vec4(color, 0, 1);
+    })";
+
+        ANGLE_GL_PROGRAM(program, kVS, kFS);
+        glUseProgram(program);
+
+        const GLint posLoc = glGetAttribLocation(program, "position");
+        const GLint colLoc = glGetAttribLocation(program, "colorIn");
+
+        // Draw 4 squares.  With a divisor of 2 on color, 2 of them will be one color, two the
+        // other.
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/angle_end2end_tests_expectations.txt b/src/tests/angle_end2end_tests_expectations.txt
index 318d08c..fb47211 100644
--- a/src/tests/angle_end2end_tests_expectations.txt
+++ b/src/tests/angle_end2end_tests_expectations.txt
@@ -571,6 +571,8 @@
 496259841 WIN D3D11 : RobustResourceInitTestES3.InvalidateThenReadBack/* = SKIP
 496259841 WIN D3D11 : RobustResourceInitTestES3.DrawThenInvalidateThenReadBack/* = SKIP
 496259841 WIN D3D11 : RobustResourceInitTestES3.DrawThenInvalidateThenVerifyDepthStencil/* = SKIP
+512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawArraysInstancedBaseInstance/* = SKIP
+512896813 WIN D3D11 : LineLoopTestES3.LineLoopDrawElementsInstancedBaseVertexBaseInstance/* = SKIP
 
 // Android
 42264624 ANDROID GLES : GLSLTest_ES3.InitGlobalComplexConstant/* = SKIP
diff --git a/src/tests/gl_tests/LineLoopTest.cpp b/src/tests/gl_tests/LineLoopTest.cpp
index 2dc6551..d0ff047 100644
--- a/src/tests/gl_tests/LineLoopTest.cpp
+++ b/src/tests/gl_tests/LineLoopTest.cpp
@@ -461,7 +461,166 @@
 }
 
 class LineLoopTestES3 : public LineLoopTest
-{};
+{
+  protected:
+    void runTestInstanced(std::function<void(void)> definePositionBuffer,
+                          std::function<void(void)> defineColorBuffer,
+                          std::function<void(void)> draw)
+    {
+        // http://anglebug.com/42265165: Disable D3D11 SDK Layers warnings checks.
+        ignoreD3D11SDKLayersWarnings();
+
+        constexpr char kVS[] = R"(#version 300 es
+    precision mediump float;
+    in vec2 position;
+    in vec2 colorIn;
+    out vec2 color;
+    void main()
+    {
+        gl_Position = vec4(position, 0, 1);
+        if ((gl_InstanceID / 2) % 2 == 1)
+        {
+            gl_Position.y += 1.0;
+        }
+        if (gl_InstanceID % 2 == 1)
+        {
+            gl_Position.x += 1.0;
+        }
+        color = colorIn;
+    })";
+
+        constexpr char kFS[] = R"(#version 300 es
+    precision mediump float;
+    in vec2 color;
+    out vec4 colorOut;
+    void main()
+    {
+        colorOut = vec4(color, 0, 1);
+    })";
+
+        ANGLE_GL_PROGRAM(program, kVS, kFS);
+        glUseProgram(program);
+
+        const GLint posLoc = glGetAttribLocation(program, "position");
+        const GLint colLoc = glGetAttribLocation(program, "colorIn");
+
+        // Draw 4 squares.  With a divisor of 2 on color, 2 of them will be one color, two the
+        // other.
+
+        GLBuffer position;
+        glBindBuffer(GL_ARRAY_BUFFER, position);
+        definePositionBuffer();
+        glEnableVertexAttribArray(posLoc);
+        glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+
+        GLBuffer color;
+        glBindBuffer(GL_ARRAY_BUFFER, color);
+        defineColorBuffer();
+        glEnableVertexAttribArray(colLoc);
+        glVertexAttribPointer(colLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
+        glVertexAttribDivisor(colLoc, 2);
+
+        glClear(GL_COLOR_BUFFER_BIT);
+        draw();
+
+        // Verify pixels.  The result looks like the following:
+        //
+        //      +-----------------------+
+        //      |                       |
+        //      |   +----+     +----+   |
+        //      |   |    |     |    |   |  <-- two red squares
+        //      |   |    |     |    |   |
+        //      |   +----+     +----+   |
+        //      |                       |
+        //      |                       |
+        //      |   +----+     +----+   |
+        //      |   |    |     |    |   |  <-- two green squares
+        //      |   |    |     |    |   |
+        //      |   +----+     +----+   |
+        //      |                       |
+        //      +-----------------------+
+        //            |
+        //            V
+        //  Black inside and outside the squares
+        //
+        const int w = getWindowWidth();
+        const int h = getWindowHeight();
+        // Don't check too close to the lines to account for precision issues.  This is the margin
+        // from the middle of the lines.
+        const int m = 2;
+        const GLColor green(51, 153, 0, 255);
+
+        // Left of squares
+        EXPECT_PIXEL_RECT_EQ(0, 0, w / 8 - m, h, GLColor::black);
+        // Between squares
+        EXPECT_PIXEL_RECT_EQ(w / 2 - w / 8 + m, 0, w / 4 - m * 2, h, GLColor::black);
+        // Right of squares
+        EXPECT_PIXEL_RECT_EQ(w - w / 8 + m, 0, w / 8 - m, h, GLColor::black);
+
+        // Above squares
+        EXPECT_PIXEL_RECT_EQ(0, 0, w, h / 8 - m, GLColor::black);
+        // Between squares
+        EXPECT_PIXEL_RECT_EQ(0, h / 2 - h / 8 + m, w, h / 4 - m * 2, GLColor::black);
+        // Below squares
+        EXPECT_PIXEL_RECT_EQ(0, h - h / 8 + m, w, h / 8 - m, GLColor::black);
+
+        // Check the lines of the squares, leaving a margin for precision errors.
+        std::vector<GLColor> result(w * h);
+        glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, result.data());
+
+        auto checkHorizontalLine = [&result, w](int x, int y, int width, GLColor expect) {
+            for (int c = 0; c < width; ++c)
+            {
+                if (result[y * w + x + c] != expect)
+                {
+                    return false;
+                }
+            }
+            return true;
+        };
+        auto checkVerticalLine = [&result, w](int x, int y, int height, GLColor expect) {
+            for (int r = 0; r < height; ++r)
+            {
+                if (result[(y + r) * w + x] != expect)
+                {
+                    return false;
+                }
+            }
+            return true;
+        };
+
+        for (int x : {w / 8, w / 2 - w / 8, w / 2 + w / 8, w - w / 8})
+        {
+            const int y      = h / 8 + m;
+            const int length = h / 4 - m * 2;
+            // The line may not exactly land on |x|, check around it too, one of them should be
+            // non-black.
+            EXPECT_TRUE(checkVerticalLine(x - 1, y, length, GLColor::red) ^
+                        checkVerticalLine(x, y, length, GLColor::red) ^
+                        checkVerticalLine(x + 1, y, length, GLColor::red));
+            EXPECT_TRUE(checkVerticalLine(x - 1, h / 2 + y, length, green) ^
+                        checkVerticalLine(x, h / 2 + y, length, green) ^
+                        checkVerticalLine(x + 1, h / 2 + y, length, green));
+        }
+        for (int y : {h / 8, h / 2 - h / 8, h / 2 + h / 8, h - h / 8})
+        {
+            const int x          = w / 8 + m;
+            const int length     = w / 4 - m * 2;
+            const GLColor expect = y < h / 2 ? GLColor::red : green;
+            EXPECT_PIXEL_RECT_EQ(x, y, length, 1, expect);
+            EXPECT_PIXEL_RECT_EQ(w / 2 + x, y, length, 1, expect);
+
+            EXPECT_TRUE(checkHorizontalLine(x, y - 1, length, expect) ^
+                        checkHorizontalLine(x, y, length, expect) ^
+                        checkHorizontalLine(x, y + 1, length, expect));
+            EXPECT_TRUE(checkHorizontalLine(w / 2 + x, y - 1, length, expect) ^
+                        checkHorizontalLine(w / 2 + x, y, length, expect) ^
+                        checkHorizontalLine(w / 2 + x, y + 1, length, expect));
+        }
+
+        ASSERT_GL_NO_ERROR();
+    }
+};
 
 // Test that uploading data to buffer that's in use then using it for line loop elements works.
 TEST_P(LineLoopTestES3, UseAsUBOThenUpdateThenLineLoopUByteIndexBuffer)
@@ -487,6 +646,179 @@
     EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
 }
 
+// Test glDrawArraysInstanced with a line loop and a non-zero divisor.
+TEST_P(LineLoopTestES3, LineLoopDrawArraysInstanced)
+{
+    const int w        = getWindowWidth();
+    const int h        = getWindowHeight();
+    const float left   = static_cast<float>(w / 8) / (w - 1) * 2.0 - 1.0;
+    const float right  = static_cast<float>(w / 2 - w / 8) / (w - 1) * 2.0 - 1.0;
+    const float top    = static_cast<float>(h / 8) / (h - 1) * 2.0 - 1.0;
+    const float bottom = static_cast<float>(h / 2 - h / 8) / (h - 1) * 2.0 - 1.0;
+
+    const GLfloat kPositions[] = {
+        // Top left.  Instances 1 and 3 shift this square to the right.  Instances 2 and 3 shift it
+        // down.
+        left, top, left, bottom, right, bottom, right, top,
+    };
+    constexpr GLfloat kColors[] = {
+        // Top
+        1.0f,
+        0.0f,
+        // Bottom
+        0.2f,
+        0.6f,
+    };
+    runTestInstanced(
+        [&kPositions]() {
+            glBufferData(GL_ARRAY_BUFFER, sizeof(kPositions), kPositions, GL_STATIC_DRAW);
+        },
+        [&kColors]() { glBufferData(GL_ARRAY_BUFFER, sizeof(kColors), kColors, GL_STATIC_DRAW); },
+        []() { glDrawArraysInstanced(GL_LINE_LOOP, 0, 4, 4); });
+}
+
+// Test glDrawArraysInstancedBaseInstance with a line loop and a non-zero divisor.
+TEST_P(LineLoopTestES3, LineLoopDrawArraysInstancedBaseInstance)
+{
+    const bool hasEXT   = IsGLExtensionEnabled("GL_EXT_base_instance");
+    const bool hasANGLE = IsGLExtensionEnabled("GL_ANGLE_base_vertex_base_instance");
+    ANGLE_SKIP_TEST_IF(!hasEXT && !hasANGLE);
+
+    const int w        = getWindowWidth();
+    const int h        = getWindowHeight();
+    const float left   = static_cast<float>(w / 8) / (w - 1) * 2.0 - 1.0;
+    const float right  = static_cast<float>(w / 2 - w / 8) / (w - 1) * 2.0 - 1.0;
+    const float top    = static_cast<float>(h / 8) / (h - 1) * 2.0 - 1.0;
+    const float bottom = static_cast<float>(h / 2 - h / 8) / (h - 1) * 2.0 - 1.0;
+
+    const GLfloat kPositions[] = {
+        // Top left.  Instances 1 and 3 shift this square to the right.  Instances 2 and 3 shift it
+        // down.
+        left, top, left, bottom, right, bottom, right, top,
+    };
+    constexpr GLfloat kColors[] = {
+        // 12 unused attributes, skipped by base instance
+        // NOTE: a multiple of 4 is used due to this bug in the Vulkan backend:
+        // http://anglebug.com/512514748.  Once fixed, this could be any number of unused
+        // attributes.
+        // clang-format off
+        0.1f, 0.2f,
+        0.3f, 0.4f,
+        0.5f, 0.6f,
+        0.7f, 0.8f,
+        0.9f, 0.95f,
+        0.85f, 0.75f,
+        0.65f, 0.55f,
+        0.45f, 0.35f,
+        0.25f, 0.15f,
+        0.05f, 0.15f,
+        0.25f, 0.35f,
+        0.45f, 0.55f,
+        // Top
+        1.0f, 0.0f,
+        // Bottom
+        0.2f, 0.6f,
+        // clang-format on
+    };
+    runTestInstanced(
+        [&kPositions]() {
+            glBufferData(GL_ARRAY_BUFFER, sizeof(kPositions), kPositions, GL_STATIC_DRAW);
+        },
+        [&kColors]() { glBufferData(GL_ARRAY_BUFFER, sizeof(kColors), kColors, GL_STATIC_DRAW); },
+        [hasEXT]() {
+            if (hasEXT)
+            {
+                glDrawArraysInstancedBaseInstanceEXT(GL_LINE_LOOP, 0, 4, 4, 12);
+            }
+            else
+            {
+                glDrawArraysInstancedBaseInstanceANGLE(GL_LINE_LOOP, 0, 4, 4, 12);
+            }
+        });
+}
+
+// Test glDrawElementsInstancedBaseVertexBaseInstance with a line loop and a non-zero divisor.
+TEST_P(LineLoopTestES3, LineLoopDrawElementsInstancedBaseVertexBaseInstance)
+{
+    const bool hasEXT   = IsGLExtensionEnabled("GL_EXT_base_instance");
+    const bool hasANGLE = IsGLExtensionEnabled("GL_ANGLE_base_vertex_base_instance");
+    ANGLE_SKIP_TEST_IF(!hasEXT && !hasANGLE);
+
+    const int w        = getWindowWidth();
+    const int h        = getWindowHeight();
+    const float left   = static_cast<float>(w / 8) / (w - 1) * 2.0 - 1.0;
+    const float right  = static_cast<float>(w / 2 - w / 8) / (w - 1) * 2.0 - 1.0;
+    const float top    = static_cast<float>(h / 8) / (h - 1) * 2.0 - 1.0;
+    const float bottom = static_cast<float>(h / 2 - h / 8) / (h - 1) * 2.0 - 1.0;
+
+    const GLfloat kPositions[] = {
+        // 5 unused vertices, skipped by base vertex
+        // clang-format off
+        0.1f, 0.4f,
+        0.3f, 0.6f,
+        0.5f, 0.8f,
... (truncated)
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential OOB read and OOB write in ANGLE Vulkan buffer management

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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: ANGLE’s Vulkan backend contains two buffer management logic flaws. First, setupLineLoopDraw hardcodes an instance count of 1 during vertex stream allocation, potentially leading to an out-of-bounds read. Second, convertBufferToRGBA fails to resize cached conversion buffers when the source buffer grows, potentially leading to an out-of-bounds write via a compute shader.

Affected files:

  • third_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cpp
  • third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp

Estimated timestamp from git blame: 2024-08-30

Overview

There are two distinct potential vulnerabilities in ANGLE’s Vulkan backend related to buffer size calculations and allocations during emulation paths.

Issue 1: Potential Out-of-bounds Read in setupLineLoopDraw

In third_party/angle/src/libANGLE/renderer/vulkan/ContextVk.cpp, ContextVk::setupLineLoopDraw is responsible for setting up the state for LINE_LOOP primitives. When setting up the vertex attributes, it calls setupDraw with a hardcoded instanceCount of 1:

    return setupDraw(context, mode, firstVertex, vertexOrIndexCount, 1, indexTypeOrInvalid, indices,
                     mIndexedDirtyBitsMask);

If the draw call is an instanced draw (e.g., drawArraysInstanced), and one of the vertex attributes requires software emulation (for instance, because its divisor exceeds the maximum supported hardware divisor, typically 255), VertexArrayVk::updateStreamedAttribs will allocate a streaming vertex buffer. Because instanceCount is passed as 1, it will allocate a buffer sized for only a single instance.

The subsequent Vulkan draw call will be dispatched with the full, attacker-controlled instance count. The vertex shader will fetch data for all instances, reading past the end of the undersized streaming buffer. Because the streaming buffer is sub-allocated from a larger dynamic buffer pool, this may not cause an immediate hard crash but can leak uninitialized GPU memory or data belonging to other contexts.

Potential Steps to Trigger (WebGL):

  1. Create a WebGL context and enable instancing.
  2. Create a vertex attribute and set its divisor to 256 (exceeding mMaxVertexAttribDivisor).
  3. Issue a gl.drawArraysInstanced(gl.LINE_LOOP, 0, count, 1000) call.

Issue 2: Potential Out-of-bounds Write in convertBufferToRGBA

In third_party/angle/src/libANGLE/renderer/vulkan/TextureVk.cpp, TextureVk::convertBufferToRGBA handles the conversion of RGB buffer data to RGBA for TEXTURE_BUFFER targets. The conversion results are cached in ConversionBuffer objects attached to the BufferVk.

The cache key used to retrieve an existing ConversionBuffer (VertexConversionBuffer::CacheKey) includes the format and offset, but does not include the buffer size.

If an attacker creates a texture buffer with a small size, a small ConversionBuffer is allocated. If the attacker subsequently resizes the underlying buffer using glBufferData to a much larger size, the existing ConversionBuffer is merely marked dirty but is not freed or resized. When convertBufferToRGBA is called again, it reuses the small destination buffer but dispatches a compute shader (UtilsVk::copyRgbToRgba) to process the new, larger pixel count.

The compute shader will write the converted RGBA values past the end of the undersized destination buffer. In native GLES contexts (e.g., Android apps on Chrome OS ARCVM), robust buffer access is often disabled for performance, meaning the GPU hardware will not clamp the out-of-bounds writes. This could allow an attacker to overwrite adjacent Vulkan objects in GPU memory, potentially leading to a sandbox escape.

Potential Steps to Trigger (Native GLES):

  1. Create a buffer with a small size (e.g., 48 bytes) and attach it to a texture buffer with an RGB format.
  2. Issue a draw/compute call that samples the texture, triggering the allocation of a 64-byte conversion buffer.
  3. Call glBufferData to resize the source buffer to a large size (e.g., 10 MB).
  4. Issue another draw/compute call sampling the texture buffer. The compute shader will write 10 MB of RGBA data into the 64-byte destination buffer.

Suggested Fixes

  1. For Issue 1: In ContextVk::setupLineLoopDraw (and similar functions if applicable), pass the actual instanceCount from the parent draw call to setupDraw instead of hardcoding 1.
  2. For Issue 2: In TextureVk::convertBufferToRGBA, before reusing an existing ConversionBuffer, ensure that bufferHelper->getSize() is greater than or equal to the required conversionBufferSize. Alternatively, include the required size or a buffer generation ID in the CacheKey so that a new buffer is allocated when the size increases.

Evaluated with Chrome root at commit: 2b349e31cb87959d6a548625986c65e0a2d2e380


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