CVE-2026-0908
Overview
Files Changed
scripts/code_generation_hashes/GL_EGL_entry_points.jsonscripts/generate_entry_points.pysrc/libANGLE/Constants.hsrc/libANGLE/Context.cpp
Patch
From e156412823246a4d68a7e39f65995f9a5ce6c2e4 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Thu, 06 Nov 2025 15:45:43 -0500
Subject: [PATCH] Set an upper limit on simultaneous GL object handles.
Generate GL_OUT_OF_MEMORY when attempting to allocate more than 2^24
simultaneous object handles of the same type. Update the HandleAllocator
to communicate allocation failures.
This protects us from theoretically exhausting the GLuint ID range and
crashing as well as limits the ability for WebGL to easily generate many
allocations.
Bug: angleproject:457772564, chromium:452209503
Change-Id: I9cee19115f4c50ebe53497706155fe6fa422d654
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7127342
Reviewed-by: Liza Burakova <liza@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
---
diff --git a/scripts/code_generation_hashes/GL_EGL_entry_points.json b/scripts/code_generation_hashes/GL_EGL_entry_points.json
index 35ff7f4..56e77d6 100644
--- a/scripts/code_generation_hashes/GL_EGL_entry_points.json
+++ b/scripts/code_generation_hashes/GL_EGL_entry_points.json
@@ -6,7 +6,7 @@
"scripts/entry_point_packed_gl_enums.json":
"cc42c7d7f13b0ba7966c84629f735299",
"scripts/generate_entry_points.py":
- "606e148f063cbc5db2e41c49bd72b2de",
+ "39414cc2beac01698144c9c04576e055",
"scripts/gl_angle_ext.xml":
"da4ecccdd77635f1b0e9d4664f856706",
"scripts/registry_xml.py":
@@ -24,7 +24,7 @@
"src/libANGLE/Context_gles_2_0_autogen.h":
"63f297a21ff3d1ef014595d70e7f94ae",
"src/libANGLE/Context_gles_3_0_autogen.h":
- "32a1ba3b4db9db343fe9b43a0d859076",
+ "6f7e3f65e013135786455ea33b197e07",
"src/libANGLE/Context_gles_3_1_autogen.h":
"2b81a1dd0d3109090afc073417e0667c",
"src/libANGLE/Context_gles_3_2_autogen.h":
@@ -64,7 +64,7 @@
"src/libANGLE/capture/capture_gles_ext_autogen.h":
"f13556b6b8aa39a0e0ce1e34ca2aa8a6",
"src/libANGLE/context_private_call_autogen.h":
- "3bed70b648c7cb8b0fd9e6923629897c",
+ "82434f431a5851ec2168f8517329de82",
"src/libANGLE/validationCL_autogen.h":
"034b45381592c163c4f7aaa9325223b3",
"src/libANGLE/validationEGL_autogen.h":
@@ -78,9 +78,9 @@
"src/libANGLE/validationES32_autogen.h":
"67429f628946feab206d1ce9bdcf6fe9",
"src/libANGLE/validationES3_autogen.h":
- "c6150e9073885a2a8344c40ed0be0fb0",
+ "127ed661a38f20de9ba13fa989ce05d8",
"src/libANGLE/validationESEXT_autogen.h":
- "32c253b727cecf45b3100bd89c85a778",
+ "ed490c09b8abef586b011972d534edf2",
"src/libEGL/libEGL_autogen.cpp":
"77d82db4a45c2f08108929ef7fe8b698",
"src/libEGL/libEGL_autogen.def":
@@ -118,7 +118,7 @@
"src/libGLESv2/entry_points_gles_2_0_autogen.h":
"691c60c2dfed9beca68aa1f32aa2c71b",
"src/libGLESv2/entry_points_gles_3_0_autogen.cpp":
- "53198b435a1983717987cc292fb89f93",
+ "02476248c56dcf3e6487883c1ccefd93",
"src/libGLESv2/entry_points_gles_3_0_autogen.h":
"4ac2582759cdc6a30f78f83ab684d555",
"src/libGLESv2/entry_points_gles_3_1_autogen.cpp":
@@ -130,7 +130,7 @@
"src/libGLESv2/entry_points_gles_3_2_autogen.h":
"647f932a299cdb4726b60bbba059f0d2",
"src/libGLESv2/entry_points_gles_ext_autogen.cpp":
- "0c46de9633a4aa0e13f63198d77cb211",
+ "44c2341f03e8d6315c28b3de19b9d579",
"src/libGLESv2/entry_points_gles_ext_autogen.h":
"9edbc6d2857cbe0349a5b7c70bc22ed8",
"src/libGLESv2/libGLESv2_autogen.cpp":
diff --git a/scripts/generate_entry_points.py b/scripts/generate_entry_points.py
index ca35374..f40273d 100755
--- a/scripts/generate_entry_points.py
+++ b/scripts/generate_entry_points.py
@@ -106,8 +106,6 @@
'glEnablei',
'glEnableVertexAttribArray',
'glFrontFace',
- 'glGenVertexArrays',
- 'glGenVertexArraysOES',
'glHint',
'glIsEnabled',
'glIsEnabledi',
diff --git a/src/libANGLE/Constants.h b/src/libANGLE/Constants.h
index 36a37dd..7b49653 100644
--- a/src/libANGLE/Constants.h
+++ b/src/libANGLE/Constants.h
@@ -105,6 +105,9 @@
// TODO (anglebug.com/42266906): Implement support for multiple layers
IMPLEMENTATION_MAX_NUM_LAYERS = 1,
IMPLEMENTATION_MAX_FOCAL_POINTS = 2,
+
+ // Maximum simultaneous handles of an object type.
+ IMPLEMENTATION_MAX_OBJECT_HANDLES = 1 << 24,
};
namespace limits
diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp
index e6a1d4f..b1b5310 100644
--- a/src/libANGLE/Context.cpp
+++ b/src/libANGLE/Context.cpp
@@ -7,6 +7,7 @@
// Context.cpp: Implements the gl::Context class, managing all GL state and performing
// rendering operations. It is the GLES2 specific implementation of EGLContext.
+#include "common/entry_points_enum_autogen.h"
#ifdef UNSAFE_BUFFERS_BUILD
# pragma allow_unsafe_buffers
#endif
@@ -700,6 +701,9 @@
mLabel(nullptr),
mCompiler(),
mConfig(config),
+ mFenceNVHandleAllocator(IMPLEMENTATION_MAX_OBJECT_HANDLES),
+ mQueryHandleAllocator(IMPLEMENTATION_MAX_OBJECT_HANDLES),
+ mTransformFeedbackHandleAllocator(IMPLEMENTATION_MAX_OBJECT_HANDLES),
mHasBeenCurrent(false),
mSurfacelessSupported(displayExtensions.surfacelessContext),
mCurrentDrawSurface(static_cast<egl::Surface *>(EGL_NO_SURFACE)),
@@ -1124,52 +1128,71 @@
mErrors.validationError(entryPoint, GL_CONTEXT_LOST, err::kContextLost);
}
-BufferID Context::createBuffer()
+void Context::handleExhaustionError(angle::EntryPoint entryPoint)
{
- return mState.mBufferManager->createBuffer();
+ mErrors.validationError(entryPoint, GL_OUT_OF_MEMORY, err::kHandleExhaustion);
+}
+
+bool Context::createBuffer(BufferID *outBuffer)
+{
+ return mState.mBufferManager->createBuffer(outBuffer);
}
GLuint Context::createProgram()
{
- return mState.mShaderProgramManager->createProgram(mImplementation.get()).value;
+ ShaderProgramID id;
+ if (!mState.mShaderProgramManager->createProgram(mImplementation.get(), &id))
+ {
+ handleExhaustionError(angle::EntryPoint::GLCreateProgram);
+ return 0;
+ }
+ return id.value;
}
GLuint Context::createShader(ShaderType type)
{
- return mState.mShaderProgramManager
- ->createShader(mImplementation.get(), mState.getLimitations(), type)
- .value;
+ ShaderProgramID id;
+ if (!mState.mShaderProgramManager->createShader(mImplementation.get(), mState.getLimitations(),
+ type, &id))
+ {
+ handleExhaustionError(angle::EntryPoint::GLCreateShader);
+ return 0;
+ }
+ return id.value;
}
-TextureID Context::createTexture()
+bool Context::createTexture(TextureID *outTexture)
{
- return mState.mTextureManager->createTexture();
+ return mState.mTextureManager->createTexture(outTexture);
}
-RenderbufferID Context::createRenderbuffer()
+bool Context::createRenderbuffer(RenderbufferID *outRenderbuffer)
{
- return mState.mRenderbufferManager->createRenderbuffer();
+ return mState.mRenderbufferManager->createRenderbuffer(outRenderbuffer);
}
// Returns an unused framebuffer name
-FramebufferID Context::createFramebuffer()
+bool Context::createFramebuffer(FramebufferID *outFramebuffer)
{
- return mState.mFramebufferManager->createFramebuffer();
+ return mState.mFramebufferManager->createFramebuffer(outFramebuffer);
}
void Context::genFencesNV(GLsizei n, FenceNVID *fences)
{
for (int i = 0; i < n; i++)
{
- GLuint handle = mFenceNVHandleAllocator.allocate();
Regression Test / PoC
diff --git a/src/libANGLE/HandleAllocator_unittest.cpp b/src/libANGLE/HandleAllocator_unittest.cpp
index 43e7fcc..5376603 100644
--- a/src/libANGLE/HandleAllocator_unittest.cpp
+++ b/src/libANGLE/HandleAllocator_unittest.cpp
@@ -20,9 +20,11 @@
namespace
{
+constexpr GLuint kMaxHandleForTesting = std::numeric_limits<GLuint>::max();
+
TEST(HandleAllocatorTest, ReservationsWithGaps)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
std::set<GLuint> allocationList;
for (GLuint id = 2; id < 50; id += 2)
@@ -38,7 +40,8 @@
std::set<GLuint> allocatedList;
for (size_t allocationNum = 0; allocationNum < allocationList.size() * 2; ++allocationNum)
{
- GLuint handle = allocator.allocate();
+ GLuint handle = 0;
+ EXPECT_TRUE(allocator.allocate(&handle));
EXPECT_EQ(0u, allocationList.count(handle));
EXPECT_EQ(0u, allocatedList.count(handle));
allocatedList.insert(handle);
@@ -47,7 +50,7 @@
TEST(HandleAllocatorTest, Random)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
std::set<GLuint> allocationList;
for (size_t iterationCount = 0; iterationCount < 40; ++iterationCount)
@@ -64,7 +67,8 @@
for (size_t normalCount = 0; normalCount < 40; ++normalCount)
{
- GLuint normalHandle = allocator.allocate();
+ GLuint normalHandle = 0;
+ EXPECT_TRUE(allocator.allocate(&normalHandle));
EXPECT_EQ(0u, allocationList.count(normalHandle));
allocationList.insert(normalHandle);
}
@@ -78,7 +82,8 @@
for (GLuint count = 1; count < 10; count++)
{
- GLuint result = limitedAllocator.allocate();
+ GLuint result = 0;
+ EXPECT_TRUE(limitedAllocator.allocate(&result));
EXPECT_EQ(count, result);
}
@@ -92,47 +97,50 @@
limitedAllocator.reserve(count);
}
- GLint finalResult = limitedAllocator.allocate();
- EXPECT_EQ(finalResult, 1);
+ GLuint finalResult = 0;
+ EXPECT_TRUE(limitedAllocator.allocate(&finalResult));
+ EXPECT_EQ(finalResult, 1u);
}
// The following test covers reserving a handle with max uint value. See
// http://anglebug.com/42260058
TEST(HandleAllocatorTest, ReserveMaxUintHandle)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
GLuint maxUintHandle = std::numeric_limits<GLuint>::max();
allocator.reserve(maxUintHandle);
- GLuint normalHandle = allocator.allocate();
+ GLuint normalHandle = 0;
+ EXPECT_TRUE(allocator.allocate(&normalHandle));
EXPECT_EQ(1u, normalHandle);
}
// The following test covers reserving a handle with max uint value minus one then max uint value.
TEST(HandleAllocatorTest, ReserveMaxUintHandle2)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
GLuint maxUintHandle = std::numeric_limits<GLuint>::max();
allocator.reserve(maxUintHandle - 1);
allocator.reserve(maxUintHandle);
- GLuint normalHandle = allocator.allocate();
+ GLuint normalHandle = 0;
+ EXPECT_TRUE(allocator.allocate(&normalHandle));
EXPECT_EQ(1u, normalHandle);
}
// To test if the allocator keep the handle in a sorted order.
TEST(HandleAllocatorTest, SortedOrderHandle)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
allocator.reserve(3);
GLuint allocatedList[5];
for (GLuint count = 0; count < 5; count++)
{
- allocatedList[count] = allocator.allocate();
+ EXPECT_TRUE(allocator.allocate(&allocatedList[count]));
}
EXPECT_EQ(1u, allocatedList[0]);
@@ -145,14 +153,18 @@
// Tests the reset method.
TEST(HandleAllocatorTest, Reset)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
for (int iteration = 0; iteration < 1; ++iteration)
{
allocator.reserve(3);
- EXPECT_EQ(1u, allocator.allocate());
- EXPECT_EQ(2u, allocator.allocate());
- EXPECT_EQ(4u, allocator.allocate());
+ GLuint handle = 0;
+ EXPECT_TRUE(allocator.allocate(&handle));
+ EXPECT_EQ(1u, handle);
+ EXPECT_TRUE(allocator.allocate(&handle));
+ EXPECT_EQ(2u, handle);
+ EXPECT_TRUE(allocator.allocate(&handle));
+ EXPECT_EQ(4u, handle);
allocator.reset();
}
}
@@ -165,10 +177,15 @@
const std::unordered_set<GLuint> expectedHandles = {1, 2, 3};
std::unordered_set<GLuint> handles;
+ auto allocateHandle = [&]() {
+ GLuint handle = 0;
+ EXPECT_TRUE(allocator.allocate(&handle));
+ handles.insert(handle);
+ };
EXPECT_EQ(allocator.anyHandleAvailableForAllocation(), true);
- handles.insert(allocator.allocate());
- handles.insert(allocator.allocate());
- handles.insert(allocator.allocate());
+ allocateHandle();
+ allocateHandle();
+ allocateHandle();
EXPECT_EQ(expectedHandles, handles);
EXPECT_EQ(allocator.anyHandleAvailableForAllocation(), false);
@@ -176,9 +193,9 @@
allocator.reset();
EXPECT_EQ(allocator.anyHandleAvailableForAllocation(), true);
- handles.insert(allocator.allocate());
- handles.insert(allocator.allocate());
- handles.insert(allocator.allocate());
+ allocateHandle();
+ allocateHandle();
+ allocateHandle();
EXPECT_EQ(expectedHandles, handles);
EXPECT_EQ(allocator.anyHandleAvailableForAllocation(), false);
}
@@ -186,15 +203,16 @@
// Covers a particular bug with reserving and allocating sub ranges.
TEST(HandleAllocatorTest, ReserveAndAllocateIterated)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
for (int iteration = 0; iteration < 3; ++iteration)
{
allocator.reserve(5);
allocator.reserve(6);
- GLuint a = allocator.allocate();
- GLuint b = allocator.allocate();
- GLuint c = allocator.allocate();
+ GLuint a = 0, b = 0, c = 0;
+ EXPECT_TRUE(allocator.allocate(&a));
+ EXPECT_TRUE(allocator.allocate(&b));
+ EXPECT_TRUE(allocator.allocate(&c));
allocator.release(c);
allocator.release(a);
allocator.release(b);
@@ -206,11 +224,11 @@
// This test reproduces invalid heap bug when reserve resources after release.
TEST(HandleAllocatorTest, ReserveAfterReleaseBug)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
for (int iteration = 1; iteration <= 16; ++iteration)
{
- allocator.allocate();
+ EXPECT_TRUE(allocator.allocate(nullptr));
}
allocator.release(15);
@@ -223,19 +241,19 @@
allocator.reserve(1);
- allocator.allocate();
+ EXPECT_TRUE(allocator.allocate(nullptr));
}
// This test is to verify that we consolidate handle ranges when releasing a handle.
TEST(HandleAllocatorTest, ConsolidateRangeDuringRelease)
{
- gl::HandleAllocator allocator;
+ gl::HandleAllocator allocator(kMaxHandleForTesting);
// Reserve GLuint(-1)
allocator.reserve(static_cast<GLuint>(-1));
// Allocate a few others
- allocator.allocate();
- allocator.allocate();
+ EXPECT_TRUE(allocator.allocate(nullptr));
+ EXPECT_TRUE(allocator.allocate(nullptr));
// Release GLuint(-1)
allocator.release(static_cast<GLuint>(-1));
@@ -243,8 +261,38 @@
// Allocate one more handle.
// Since we consolidate handle ranges during a release we do not expect to get back a
// handle value of GLuint(-1).
- GLuint handle = allocator.allocate();
+ GLuint handle = 0;
+ EXPECT_TRUE(allocator.allocate(&handle));
EXPECT_NE(handle, static_cast<GLuint>(-1));
}
+// Test that HandleAllocator::allocate returns false when there are no more available handles.
+TEST(HandleAllocatorTest, HandleExhaustion)
+{
+ constexpr size_t kCount = 16;
+ gl::HandleAllocator allocator(kCount);
+
+ // Use all available handles
+ std::vector<GLuint> handles;
+ for (size_t iteration = 0; iteration < kCount; ++iteration)
+ {
+ GLuint handle;
+ EXPECT_TRUE(allocator.allocate(&handle));
+ handles.push_back(handle);
+ }
+
+ // allocations should fail
+ EXPECT_FALSE(allocator.allocate(nullptr));
+ EXPECT_FALSE(allocator.anyHandleAvailableForAllocation());
+
+ // Release one handle, the next allocation should succeed
+ allocator.release(handles[0]);
+ EXPECT_TRUE(allocator.anyHandleAvailableForAllocation());
+ EXPECT_TRUE(allocator.allocate(nullptr));
+
+ // The allocator is full again, allocations should fail
+ EXPECT_FALSE(allocator.allocate(nullptr));
+ EXPECT_FALSE(allocator.anyHandleAvailableForAllocation());
+}
+
} // anonymous namespace
diff --git a/src/libANGLE/ResourceManager_unittest.cpp b/src/libANGLE/ResourceManager_unittest.cpp
index c15a8d8..b84b393 100644
--- a/src/libANGLE/ResourceManager_unittest.cpp
+++ b/src/libANGLE/ResourceManager_unittest.cpp
@@ -48,7 +48,8 @@
EXPECT_CALL(mMockFactory, createTexture(_)).Times(1).RetiresOnSaturation();
mTextureManager->checkTextureAllocation(&mMockFactory, {1}, TextureType::_2D);
- TextureID newTexture = mTextureManager->createTexture();
+ TextureID newTexture;
+ EXPECT_TRUE(mTextureManager->createTexture(&newTexture));
EXPECT_NE(1u, newTexture.value);
}
@@ -57,7 +58,8 @@
EXPECT_CALL(mMockFactory, createBuffer(_)).Times(1).RetiresOnSaturation();
mBufferManager->checkBufferAllocation(&mMockFactory, {1});
- BufferID newBuffer = mBufferManager->createBuffer();
+ BufferID newBuffer;
+ EXPECT_TRUE(mBufferManager->createBuffer(&newBuffer));
EXPECT_NE(1u, newBuffer.value);
}
@@ -66,7 +68,8 @@
EXPECT_CALL(mMockFactory, createRenderbuffer(_)).Times(1).RetiresOnSaturation();
mRenderbuffermanager->checkRenderbufferAllocation(&mMockFactory, {1});
- RenderbufferID newRenderbuffer = mRenderbuffermanager->createRenderbuffer();
+ RenderbufferID newRenderbuffer;
+ EXPECT_TRUE(mRenderbuffermanager->createRenderbuffer(&newRenderbuffer));
... (truncated)
Original Bug Report
ANGLE HandleAllocator: Invalid iterator access on empty vector → UAF + OOB (double-free plausible)
Reporter statement
I reported the same issue to Firefox(status: confirmed) because ANGLE’s HandleAllocator does not handle ID-exhaustion correctly. Since Chromium ships the same ANGLE code, I believe Chromium is affected as well. ()
Why I believe Chromium/ANGLE is affected
Firefox and Chromium use the same ANGLE upstream sources for this component (the HandleAllocator implementation is identical or functionally equivalent at the affected site).
I couldn’t find handling for ID-exhaustion on the Chromium side either (same behavior at the allocator level)
VULNERABILITY DETAILS
third_party/angle/src/libANGLE/HandleAllocator.cpp (gl::HandleAllocator::allocate()) mishandles handle-exhaustion. After erasing the last HandleRange from mUnallocatedList, the very next allocation does:
First, when the ID is exhausted,
if (listIt->begin == listIt->end)
{
mUnallocatedList.erase(listIt);
}
due to this code, mUnallocatedList becomes an invalid iterator, and the HandleRange object that was inside calls its destructor.
The problem occurs when, after all IDs are exhausted, you create an object again to receive an ID. When you attempt another allocation after all IDs are exhausted,
auto listIt = mUnallocatedList.begin();
this again accesses an invalid iterator, and due to begin(), you once again obtain the HandleRange object whose destructor should already have been called. Since this HandleRange object did not initialize its begin and end values again,
if (listIt->begin == listIt->end)
{
mUnallocatedList.erase(listIt);
}
this code causes one more erase. At this time, since the size of mUnallocatedList is 0, a problem also occurs in that erase. (In Firefox, when performing that erase, a 4-byte container overflow read occurred in the ASan log, and while attempting to read an inaccessible region, an unhandled exception occurred during debugging. In Firefox, I was able to check the ASan log and follow the debugging to the end, but I could not do so in Chromium. Please refer to the debugging video that will be attached later, and if you can let me know why I could not debug to the end and why the ASan log did not occur, it would be very helpful for submitting the ASan log in the future. I will continue to update the report and attach the ASan log later.)
Environment
Debugger: Visual Studio 2022 Browser: Google Chrome 143.0.7473.0 (Developer Build) (64-bit) OS Name: Microsoft Windows 11 Pro OS Version: 10.0.26100 N/A build 26100 Precondition: Sufficient computer memory resources are required to create a UINTMAX number of shader objects
Reproduction:
Since I do not have sufficient computer memory resources, I have manipulated the begin value as follows: The original HandleAllocator constructor:
HandleAllocator::HandleAllocator() : mBaseValue(1), mNextValue(1), mLoggingEnabled(false) { mUnallocatedList.push_back(HandleRange(1, std::numeric_limits<GLuint>::max())); } was modified to:
HandleAllocator::HandleAllocator() : mBaseValue(1), mNextValue(1), mLoggingEnabled(false) { // EXPERIMENT: narrow free range to (2^32-1000 .. 2^32-1) for fast exhaustion
const GLuint kStart = std::numeric_limits<GLuint>::max() - 1000u; const GLuint kEnd = std::numeric_limits<GLuint>::max(); mUnallocatedList.push_back(HandleRange(kStart, kEnd)); } Then, I entered the following code into the web browser’s console:
PoC
(() => {
let c = document.querySelector('#dbg-webgl-canvas');
if (!c) {
c = document.createElement('canvas');
c.id = 'dbg-webgl-canvas';
c.width = 4; c.height = 4;
document.body.appendChild(c);
}
window.gl = c.getContext('webgl2') || c.getContext('webgl');
console.log('gl ready:', gl ? (gl instanceof WebGL2RenderingContext ? 'WebGL2' : 'WebGL1') : 'FAILED');
})();
(() => {
if (!gl) { console.warn('run setup first'); return; }
const N = 2000; // Increase if necessary
window.__shaders = window.__shaders || [];
for (let i = 0; i < N; ++i) {
const type = (i & 1) ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER;
__shaders.push(gl.createShader(type));
}
console.log('created shaders:', __shaders.length);
})();