CVE-2026-12290
Overview
Background
- WebGL2
- The browser API exposing OpenGL ES 3.0 rendering to untrusted web content through the
WebGLContextclass. - Shader validator
- The
webgl::ShaderValidator(ANGLE-derived) that translates and validates GLSL before it is handed to the real GL driver. - Uniform block
- A GLSL construct grouping uniforms into a buffer-backed block, limited per-stage by
GL_MAX_VERTEX_UNIFORM_BLOCKSandGL_MAX_FRAGMENT_UNIFORM_BLOCKS. - `ShBuiltInResources`
- The
resourcesstruct that tells the validator the driver’s real capability limits so it can reject over-limit shaders.
Root Cause Analysis
The vulnerable path was WebGLContext::CreateShaderValidator, which built the validator’s resources limits for WebGL2 but never populated the per-stage uniform-block maxima (MaxVertexUniformBlocks / MaxFragmentUniformBlocks), and never enabled the validator option that enforces them. As a result the compile options left validatePerStageMaxUniformBlocks unset, so a shader declaring more uniform blocks than a single stage actually supports could pass validation and reach the underlying GL driver. The violated invariant is that every resource limit the driver enforces must also be enforced by the validator, so that content accepted by the browser is guaranteed to be within driver bounds.
The fix queries the real limits during WebGLContext::InitWebGL2, stores them in the new mGLMaxVertexUniformBlocks / mGLMaxFragmentUniformBlocks fields, copies them into resources, and sets compileOptions.validatePerStageMaxUniformBlocks = true for WebGL2. This makes the validator reject over-limit shaders in the content process before they can drive the GL backend past its bounds.
validatePerStageMaxUniformBlocks so the validator enforces them.Attack Path
- Obtain a WebGL2 context
A malicious page creates a
WebGL2RenderingContext, entering theIsWebGL2()code path that skipped per-stage block validation. - Craft an over-limit shader
The attacker writes GLSL declaring more uniform blocks in a single stage than
GL_MAX_VERTEX_UNIFORM_BLOCKSorGL_MAX_FRAGMENT_UNIFORM_BLOCKSpermits. - Pass validation
Because
MaxVertexUniformBlocks/MaxFragmentUniformBlockswere never set and enforcement was off, the shader validates successfully. - Reach the driver The validated but out-of-bounds shader is compiled and linked by the real GL driver, exceeding its per-stage capacity.
- Trigger memory unsafety The driver mishandles the over-limit block count, producing the memory-safety condition described in the advisory.
Impact Assessment
Files Changed
dom/canvas/WebGL2Context.cppdom/canvas/WebGLContext.hdom/canvas/WebGLShaderValidator.cpp
Audit Directions
- Validator-vs-driver limit parityFor every driver limit the GL backend enforces, verify the shader validator’s
resourcesstruct is populated with the same value and that the corresponding enforcement flag is enabled. - Feature-gated resource fieldsAudit
IsWebGL2()-conditional blocks for capability fields that are declared but left at their0defaults, since an unset limit silently disables its check. - New validator optionsWhen ANGLE adds a
validate*compile option, confirm Firefox opts in wherever the corresponding limit is meaningful rather than relying on defaults.
Patch
diff --git a/dom/canvas/WebGL2Context.cpp b/dom/canvas/WebGL2Context.cpp
index bb4abd4abff..ee7c2845c1c 100644
--- a/dom/canvas/WebGL2Context.cpp
+++ b/dom/canvas/WebGL2Context.cpp
@@ -116,6 +116,10 @@ bool WebGLContext::InitWebGL2(FailureReason* const out_failReason) {
gl->GetIntAs<uint32_t>(LOCAL_GL_MIN_PROGRAM_TEXEL_OFFSET);
mGLMaxProgramTexelOffset =
gl->GetIntAs<uint32_t>(LOCAL_GL_MAX_PROGRAM_TEXEL_OFFSET);
+ mGLMaxVertexUniformBlocks =
+ gl->GetIntAs<uint32_t>(LOCAL_GL_MAX_VERTEX_UNIFORM_BLOCKS);
+ mGLMaxFragmentUniformBlocks =
+ gl->GetIntAs<uint32_t>(LOCAL_GL_MAX_FRAGMENT_UNIFORM_BLOCKS);
mIndexedUniformBufferBindings.resize(mLimits->maxUniformBufferBindings);
diff --git a/dom/canvas/WebGLContext.h b/dom/canvas/WebGLContext.h
index 2116e3834e0..8327f63d89a 100644
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -970,6 +970,8 @@ class WebGLContext : public VRefCounted, public SupportsWeakPtr {
// ES3:
uint32_t mGLMinProgramTexelOffset = 0;
uint32_t mGLMaxProgramTexelOffset = 0;
+ uint32_t mGLMaxVertexUniformBlocks = 0;
+ uint32_t mGLMaxFragmentUniformBlocks = 0;
public:
auto GLMaxDrawBuffers() const { return mLimits->maxColorDrawBuffers; }
diff --git a/dom/canvas/WebGLShaderValidator.cpp b/dom/canvas/WebGLShaderValidator.cpp
index 562a172c135..016f86e7e7e 100644
--- a/dom/canvas/WebGLShaderValidator.cpp
+++ b/dom/canvas/WebGLShaderValidator.cpp
@@ -150,6 +150,8 @@ std::unique_ptr<webgl::ShaderValidator> WebGLContext::CreateShaderValidator(
if (IsWebGL2()) {
resources.MinProgramTexelOffset = mGLMinProgramTexelOffset;
resources.MaxProgramTexelOffset = mGLMaxProgramTexelOffset;
+ resources.MaxVertexUniformBlocks = mGLMaxVertexUniformBlocks;
+ resources.MaxFragmentUniformBlocks = mGLMaxFragmentUniformBlocks;
}
resources.MaxDrawBuffers = MaxValidDrawBuffers();
@@ -212,8 +214,12 @@ std::unique_ptr<webgl::ShaderValidator> WebGLContext::CreateShaderValidator(
// -
- const auto compileOptions =
- webgl::ChooseValidatorCompileOptions(resources, gl);
+ auto compileOptions = webgl::ChooseValidatorCompileOptions(resources, gl);
+
+ if (IsWebGL2()) {
+ compileOptions.validatePerStageMaxUniformBlocks = true;
+ }
+
auto ret = webgl::ShaderValidator::Create(shaderType, spec, outputLanguage,
resources, compileOptions);
if (!ret) return ret;