| // |
| // Copyright 2026 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| |
| // BaseInstanceOverflowTest: Reproduces integer overflow in vertex buffer streaming path. |
| |
| #include "test_utils/ANGLETest.h" |
| #include "test_utils/gl_raii.h" |
| |
| using namespace angle; |
| |
| class BaseInstanceOverflowTest : public ANGLETest<> |
| { |
| protected: |
| BaseInstanceOverflowTest() |
| { |
| setWindowWidth(1); |
| setWindowHeight(1); |
| setConfigRedBits(8); |
| setConfigGreenBits(8); |
| setConfigBlueBits(8); |
| setConfigAlphaBits(8); |
| } |
| }; |
| |
| class RobustBaseInstanceOverflowTest : public ANGLETest<> |
| { |
| protected: |
| RobustBaseInstanceOverflowTest() |
| { |
| if (!IsMac() && !IsIOS()) |
| { |
| setRobustAccess(true); |
| } |
| } |
| }; |
| |
| // Regression Test (crbug.com/489791424) |
| // Reproduces integer overflow in vertex buffer streaming path. |
| // The bug occurs when baseInstance is large, causing a wrap-around in reservation |
| // while the copy path uses 64-bit math. |
| TEST_P(RobustBaseInstanceOverflowTest, BaseInstanceOverflow) |
| { |
| // Note: this test does a large buffer overflow read from heap. |
| ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_KHR_robust_buffer_access_behavior")); |
| ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_base_vertex_base_instance")); |
| |
| // We need a dynamic vertex attribute to trigger the streaming path. |
| // In D3D11, using an unaligned offset forces the streaming path. |
| GLBuffer buffer; |
| glBindBuffer(GL_ARRAY_BUFFER, buffer); |
| std::vector<GLfloat> data(1000, 1.0f); |
| glBufferData(GL_ARRAY_BUFFER, data.size() * sizeof(GLfloat), data.data(), GL_DYNAMIC_DRAW); |
| |
| GLProgram program; |
| program.makeRaster(essl1_shaders::vs::Simple(), essl1_shaders::fs::Red()); |
| glUseProgram(program); |
| |
| GLint posLoc = glGetAttribLocation(program, essl1_shaders::PositionAttrib()); |
| glEnableVertexAttribArray(posLoc); |
| |
| // Unaligned offset (1 byte) forces streaming path in D3D11. |
| glVertexAttribPointer(posLoc, 4, GL_FLOAT, GL_FALSE, 16, reinterpret_cast<void *>(1)); |
| glVertexAttribDivisor(posLoc, 1); |
| |
| // Trigger overflow by using a large baseInstance. |
| GLuint baseInstance = 0xFFFFFF00; |
| GLsizei instanceCount = 300; |
| |
| // The call should fail due to validation of base instance + count overflow. |
| glDrawArraysInstancedBaseInstanceANGLE(GL_TRIANGLES, 0, 3, instanceCount, baseInstance); |
| EXPECT_GL_ERROR(GL_INVALID_OPERATION); |
| } |
| |
| // Test that baseinstance + primcount overflow is properly validated for backends that cannot draw |
| // large baseInstance + primcount. |
| TEST_P(BaseInstanceOverflowTest, BaseInstanceOverflow2) |
| { |
| ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_base_vertex_base_instance")); |
| |
| constexpr char kVS[] = R"( |
| attribute vec4 a_position; |
| attribute float a_instanceData; |
| varying float v_data; |
| void main() { |
| gl_Position = a_position; |
| v_data = a_instanceData; |
| })"; |
| |
| constexpr char kFS[] = R"( |
| precision mediump float; |
| varying float v_data; |
| void main() { |
| gl_FragColor = vec4(0.0, v_data, 0.0, 1.0); |
| })"; |
| |
| GLProgram program; |
| program.makeRaster(kVS, kFS); |
| ASSERT_TRUE(program.valid()); |
| glUseProgram(program); |
| |
| GLint posLoc = glGetAttribLocation(program, "a_position"); |
| GLint dataLoc = glGetAttribLocation(program, "a_instanceData"); |
| ASSERT_NE(posLoc, -1); |
| ASSERT_NE(dataLoc, -1); |
| |
| // Non-instanced position buffer: full-screen triangle. |
| // clang-format off |
| const GLfloat kQuadVerts[] = { |
| -1.0f, -1.0f, |
| 3.0f, -1.0f, |
| -1.0f, 3.0f, |
| }; |
| // clang-format on |
| GLBuffer posBuffer; |
| glBindBuffer(GL_ARRAY_BUFFER, posBuffer); |
| glBufferData(GL_ARRAY_BUFFER, sizeof(kQuadVerts), kQuadVerts, GL_STATIC_DRAW); |
| glEnableVertexAttribArray(posLoc); |
| glVertexAttribPointer(posLoc, 2, GL_FLOAT, GL_FALSE, 0, nullptr); |
| |
| // Instanced attribute setup. |
| // Use a large divisor so that all instances read the same element: |
| // element index = baseInstance + floor(i / divisor) |
| // With divisor >= primcount, floor(i / divisor) == 0 for all i in [0, primcount-1]. |
| // So only element at index baseInstance is accessed. |
| glEnableVertexAttribArray(dataLoc); |
| |
| struct Subcase |
| { |
| GLsizei primcount; |
| GLuint baseInstance; |
| }; |
| |
| // Three subcases: |
| // small: 5 + 100 = 105, well within GLuint range -> should pass |
| // medium: 0x7FFFFFFF + 1000 = 0x800003E7, within GLuint range -> should pass |
| // large: 0xFFFFFF00 + 300 = overflow past GLuint max -> should fail |
| Subcase subcases[] = { |
| {100, 5}, |
| {1000, 0x7FFFFFFFu}, |
| {300, 0xFFFFFF00u}, |
| }; |
| |
| for (const auto &subcase : subcases) |
| { |
| SCOPED_TRACE(testing::Message() << "primcount: " << subcase.primcount |
| << " baseInstance: " << subcase.baseInstance); |
| // Use divisor = primcount so only one element (at baseInstance) is accessed. |
| const GLuint divisor = static_cast<GLuint>(subcase.primcount); |
| glVertexAttribDivisor(dataLoc, divisor); |
| |
| // Allocate attribute buffer large enough for the one element at baseInstance. |
| // Size needed: (baseInstance + 1) * sizeof(GLfloat). |
| GLsizeiptr bufSize = (static_cast<GLsizeiptr>(subcase.baseInstance) + 1) * sizeof(GLfloat); |
| |
| GLBuffer instBuffer; |
| glBindBuffer(GL_ARRAY_BUFFER, instBuffer); |
| glBufferData(GL_ARRAY_BUFFER, bufSize, nullptr, GL_DYNAMIC_DRAW); |
| GLenum error = glGetError(); |
| ANGLE_SKIP_TEST_IF(error == GL_OUT_OF_MEMORY); |
| |
| const bool largeBuffersMayBeInvalid = !isMetalRenderer(); |
| if (largeBuffersMayBeInvalid) |
| { |
| continue; |
| } |
| EXPECT_EQ(error, static_cast<GLenum>(GL_NO_ERROR)); |
| |
| // Write 1.0f at element index baseInstance. |
| GLfloat one = 1.0f; |
| glBufferSubData(GL_ARRAY_BUFFER, subcase.baseInstance * sizeof(GLfloat), sizeof(GLfloat), |
| &one); |
| ASSERT_GL_NO_ERROR(); |
| |
| glVertexAttribPointer(dataLoc, 1, GL_FLOAT, GL_FALSE, sizeof(GLfloat), nullptr); |
| |
| glClearColor(0.0f, 0.0f, 0.0f, 1.0f); |
| glClear(GL_COLOR_BUFFER_BIT); |
| glDrawArraysInstancedBaseInstanceANGLE(GL_TRIANGLES, 0, 3, subcase.primcount, |
| subcase.baseInstance); |
| |
| const bool expectOverflow = |
| static_cast<uint64_t>(subcase.primcount - 1) + subcase.baseInstance > |
| std::numeric_limits<GLuint>::max(); |
| error = glGetError(); |
| // The D3D backend may return OOM in emulation path |
| ANGLE_SKIP_TEST_IF(error == GL_OUT_OF_MEMORY); |
| if (error == GL_INVALID_OPERATION) |
| { |
| EXPECT_TRUE(expectOverflow); |
| } |
| else |
| { |
| EXPECT_EQ(error, static_cast<GLenum>(GL_NO_ERROR)); |
| EXPECT_TRUE(!expectOverflow); |
| |
| // No overflow; the draw should succeed and produce green. |
| ASSERT_GL_NO_ERROR(); |
| EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); |
| } |
| } |
| } |
| |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(BaseInstanceOverflowTest); |
| ANGLE_INSTANTIATE_TEST(BaseInstanceOverflowTest, |
| ES3_D3D11().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions), |
| ES3_OPENGL().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions), |
| ES3_OPENGLES().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions), |
| ES3_VULKAN().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions), |
| ES3_METAL().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions)); |
| |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(RobustBaseInstanceOverflowTest); |
| ANGLE_INSTANTIATE_TEST(RobustBaseInstanceOverflowTest, |
| ES3_D3D11().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions), |
| ES3_OPENGL().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions), |
| ES3_OPENGLES().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions), |
| ES3_VULKAN().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions), |
| ES3_METAL().enable(Feature::AlwaysEnableEmulatedMultidrawExtensions)); |