Moderate firefox Logic Error 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionPrivilege escalation in the Graphics: CanvasWebGL component
ComponentDOM
Bug ClassLogic Error
Tracker2061503
Fix commit70183d718570 (firefox) +171/-37
CISA KEVNot listed
CreditedMozilla
Disclosed2026-09-15

Changed Functions

FunctionChangeNotes
if
dom/canvas/WebGLProgram.cpp
modified
for
dom/canvas/WebGLProgram.cpp
modified
for
dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html
modified
if
dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html
modified

Files Changed

  • dom/canvas/WebGLProgram.cpp
  • dom/canvas/test/webgl-mochitest/mochitest.toml
  • dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html
diff --git a/dom/canvas/WebGLProgram.cpp b/dom/canvas/WebGLProgram.cpp
index 05f549dc7d5..d80d3cb85e9 100644
--- a/dom/canvas/WebGLProgram.cpp
+++ b/dom/canvas/WebGLProgram.cpp
@@ -609,6 +609,8 @@ const char* webgl::ToString(const webgl::UniformBaseType x) {
   MOZ_CRASH("pacify gcc6 warning");
 }
 
+static uint8_t NumUsedLocationsByElemType(GLenum elemType);
+
 const webgl::CachedDrawFetchLimits*
 webgl::LinkedProgramInfo::GetDrawFetchLimits() const {
   const auto& webgl = prog->mContext;
@@ -643,46 +645,50 @@ webgl::LinkedProgramInfo::GetDrawFetchLimits() const {
     if (loc == -1) continue;
     hasActiveAttrib |= true;
 
-    const auto& binding = vao->AttribBinding(loc);
-    const auto& buffer = binding.buffer;
-    const auto& layout = binding.layout;
-    hasActiveDivisor0 |= (layout.divisor == 0);
-
-    webgl::AttribBaseType attribDataBaseType;
-    if (layout.isArray) {
-      MOZ_ASSERT(buffer);
-      fetchLimits.usedBuffers.push_back(
-          {buffer.get(), static_cast<uint32_t>(loc)});
-
-      attribDataBaseType = layout.baseType;
-
-      const auto availBytes = buffer->ByteLength();
-      const auto availElems = AvailGroups(availBytes, layout.byteOffset,
-                                          layout.byteSize, layout.byteStride);
-      if (layout.divisor) {
-        const auto availInstances =
-            CheckedInt<uint64_t>(availElems) * layout.divisor;
-        if (availInstances.isValid()) {
-          fetchLimits.maxInstances =
-              std::min(fetchLimits.maxInstances, availInstances.value());
-        }  // If not valid, it overflowed too large, so we're super safe.
+    const auto numUsedLocs = NumUsedLocationsByElemType(progAttrib.elemType);
+    for (uint8_t locOffset = 0; locOffset < numUsedLocs; locOffset++) {
+      const auto usedLoc = static_cast<uint32_t>(loc) + locOffset;
+
+      const auto& binding = vao->AttribBinding(usedLoc);
+      const auto& buffer = binding.buffer;
+      const auto& layout = binding.layout;
+      hasActiveDivisor0 |= (layout.divisor == 0);
+
+      webgl::AttribBaseType attribDataBaseType;
+      if (layout.isArray) {
+        MOZ_ASSERT(buffer);
+        fetchLimits.usedBuffers.push_back({buffer.get(), usedLoc});
+
+        attribDataBaseType = layout.baseType;
+
+        const auto availBytes = buffer->ByteLength();
+        const auto availElems = AvailGroups(availBytes, layout.byteOffset,
+                                            layout.byteSize, layout.byteStride);
+        if (layout.divisor) {
+          const auto availInstances =
+              CheckedInt<uint64_t>(availElems) * layout.divisor;
+          if (availInstances.isValid()) {
+            fetchLimits.maxInstances =
+                std::min(fetchLimits.maxInstances, availInstances.value());
+          }  // If not valid, it overflowed too large, so we're super safe.
+        } else {
+          fetchLimits.maxVerts = std::min(fetchLimits.maxVerts, availElems);
+        }
       } else {
-        fetchLimits.maxVerts = std::min(fetchLimits.maxVerts, availElems);
+        attribDataBaseType = webgl->mGenericVertexAttribTypes[usedLoc];
       }
-    } else {
-      attribDataBaseType = webgl->mGenericVertexAttribTypes[loc];
-    }
 
-    const auto& progBaseType = progAttrib.baseType;
-    if ((attribDataBaseType != progBaseType) &&
-        (progBaseType != webgl::AttribBaseType::Boolean)) {
-      const auto& dataType = ToString(attribDataBaseType);
-      const auto& progType = ToString(progBaseType);
-      webgl->ErrorInvalidOperation(
-          "Vertex attrib %u requires data of type %s,"
-          " but is being supplied with type %s.",
-          loc, progType, dataType);
-      return nullptr;
+      const auto& progBaseType = progAttrib.baseType;
+      if ((attribDataBaseType != progBaseType) &&
+          (progBaseType != webgl::AttribBaseType::Boolean)) {
+        const auto& dataType = ToString(attribDataBaseType);
+        const auto& progType = ToString(progBaseType);
+        webgl->ErrorInvalidOperation(
+            "Vertex attrib %u requires data of type %s,"
+            " but is being supplied with type %s.",
+            usedLoc, progType, dataType);
+        return nullptr;
+      }
     }
   }
 
diff --git a/dom/canvas/test/webgl-mochitest/mochitest.toml b/dom/canvas/test/webgl-mochitest/mochitest.toml
index 3f775a34f0b..9a6d1f6fb5c 100644
--- a/dom/canvas/test/webgl-mochitest/mochitest.toml
+++ b/dom/canvas/test/webgl-mochitest/mochitest.toml
@@ -170,6 +170,8 @@ support-files = ["../captureStream_common.js"]
 
 ["test_draw_fakevert_large_offset.html"]
 
+["test_draw_matrix_attrib_fetch_limits.html"]
+
 ["test_fb_param.html"]
 
 ["test_fb_param_crash.html"]
diff --git a/dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html b/dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html
new file mode 100644
index 00000000000..db55898ce04
--- /dev/null
+++ b/dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<script src='/tests/SimpleTest/SimpleTest.js'></script>
+</head>
+<body>
+<pre id=e_out></pre>
+<script>
+ok = window.ok || function(e, s) {
+  e_out.textContent += `\n${!e ? 'FAIL' : 'pass'}: ${s}`;
+};
+
+// Bug 2061503
+function run(version, vsSrc, attribName, numLocs, comps, opts = {}) {
+  const gl = document.createElement("canvas").getContext(version);
+  const fsSrc = version == "webgl2"
+    ? `#version 300 es
+       precision mediump float;
+       out vec4 o;
+       void main() { o = vec4(0.0); }`
+    : `void main() { gl_FragColor = vec4(0.0); }`;
+
+  const vs = gl.createShader(gl.VERTEX_SHADER);
+  gl.shaderSource(vs, vsSrc);
+  gl.compileShader(vs);
+  const fs = gl.createShader(gl.FRAGMENT_SHADER);
+  gl.shaderSource(fs, fsSrc);
+  gl.compileShader(fs);
+
+  const prog = gl.createProgram();
+  gl.attachShader(prog, vs);
+  gl.attachShader(prog, fs);
+  gl.bindAttribLocation(prog, 0, attribName);
+  gl.linkProgram(prog);
+  gl.useProgram(prog);
+
+  const N = 4096;
+  const vertBytes = comps * 4;
+
+  const big = gl.createBuffer();
+  gl.bindBuffer(gl.ARRAY_BUFFER, big);
+  gl.bufferData(gl.ARRAY_BUFFER, N * vertBytes, gl.STATIC_DRAW);
+
+  const tiny = gl.createBuffer();
+  gl.bindBuffer(gl.ARRAY_BUFFER, tiny);
+  gl.bufferData(gl.ARRAY_BUFFER, vertBytes, gl.STATIC_DRAW);
+
+  for (let loc = 0; loc < numLocs; loc++) {
+    const colBig = opts.allBig ||
+                   (loc == 0 ? !opts.firstColSmall : false);
+    gl.bindBuffer(gl.ARRAY_BUFFER, colBig ? big : tiny);
+    gl.enableVertexAttribArray(loc);
+    gl.vertexAttribPointer(loc, comps, gl.FLOAT, false, vertBytes, 0);
+    if (opts.instanced) {
+      gl.vertexAttribDivisor(loc, 1);
+    }
+  }
+
+  // Ensure no pending errors.
+  ok(gl.getError() == gl.NO_ERROR, "no error during setup");
+
+  if (opts.instanced) {
+    gl.drawArraysInstanced(gl.POINTS, 0, 1, N);
+  } else {
+    gl.drawArrays(gl.POINTS, 0, N);
+  }
+  return gl.getError();
+}
+
+const INVALID_OPERATION =
+    document.createElement("canvas").getContext("webgl").INVALID_OPERATION;
+
+function check(desc, expectError, version, ...args) {
+  const err = run(version, ...args);
+  const errored = err == INVALID_OPERATION;
+  ok(errored == expectError,
+     `${desc}: err:0x${err.toString(16)} ` +
+     `(expected ${expectError ? "INVALID_OPERATION" : "no error"})`);
+}
+
+const WEBGL1 = {
+  vec4: `attribute vec4 a; void main() { gl_Position = a; gl_PointSize = 1.0; }`,
+  mat2: `attribute mat2 m;
+         void main() { gl_Position = vec4(m[0], m[1]); gl_PointSize = 1.0; }`,
+  mat3: `attribute mat3 m;
+         void main() { gl_Position = vec4(m[0] + m[1] + m[2], 1.0);
+                       gl_PointSize = 1.0; }`,
+  mat4: `attribute mat4 m;
+         void main() { gl_Position = m[0] + m[1] + m[2] + m[3];
+                       gl_PointSize = 1.0; }`,
+};
+
+check("vec4 over-fetch", true, "webgl", WEBGL1.vec4, "a", 1, 4, {firstColSmall: true});
+check("mat2 later-column over-fetch", true, "webgl", WEBGL1.mat2, "m", 2, 4);
+check("mat3 later-column over-fetch", true, "webgl", WEBGL1.mat3, "m", 3, 4);
+check("mat4 later-column over-fetch", true, "webgl", WEBGL1.mat4, "m", 4, 4);
+
+check("mat4 fully backed", false, "webgl", WEBGL1.mat4, "m", 4, 4,
+      {allBig: true});
+
+if (document.createElement("canvas").getContext("webgl2")) {
+  const WEBGL2 = {
+    mat2x4: `#version 300 es
+            in mat2x4 m;
+            void main() { gl_Position = m[0] + m[1]; gl_PointSize = 1.0; }`,
+    mat4x2: `#version 300 es
+            in mat4x2 m;
+            void main() { gl_Position = vec4(m[0], m[1]) + vec4(m[2], m[3]);
+                          gl_PointSize = 1.0; }`,
+  };
+
+  check("mat2x4 later-column over-fetch", true, "webgl2",
+        WEBGL2.mat2x4, "m", 2, 4);
+  check("mat4x2 later-column over-fetch", true, "webgl2",
+        WEBGL2.mat4x2, "m", 4, 2);
+  check("mat4x2 instanced later-column over-fetch", true, "webgl2",
+        WEBGL2.mat4x2, "m", 4, 2, {instanced: true});
+} else {
+  ok(true, "webgl2 unavailable, skipping non-square/instanced cases");
+}
+
+ok(true, 'done');
+</script>
+</body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/dom/canvas/test/webgl-mochitest/mochitest.toml b/dom/canvas/test/webgl-mochitest/mochitest.toml
index 3f775a34f0b..9a6d1f6fb5c 100644
--- a/dom/canvas/test/webgl-mochitest/mochitest.toml
+++ b/dom/canvas/test/webgl-mochitest/mochitest.toml
@@ -170,6 +170,8 @@ support-files = ["../captureStream_common.js"]
 
 ["test_draw_fakevert_large_offset.html"]
 
+["test_draw_matrix_attrib_fetch_limits.html"]
+
 ["test_fb_param.html"]
 
 ["test_fb_param_crash.html"]
diff --git a/dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html b/dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html
new file mode 100644
index 00000000000..db55898ce04
--- /dev/null
+++ b/dom/canvas/test/webgl-mochitest/test_draw_matrix_attrib_fetch_limits.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<script src='/tests/SimpleTest/SimpleTest.js'></script>
+</head>
+<body>
+<pre id=e_out></pre>
+<script>
+ok = window.ok || function(e, s) {
+  e_out.textContent += `\n${!e ? 'FAIL' : 'pass'}: ${s}`;
+};
+
+// Bug 2061503
+function run(version, vsSrc, attribName, numLocs, comps, opts = {}) {
+  const gl = document.createElement("canvas").getContext(version);
+  const fsSrc = version == "webgl2"
+    ? `#version 300 es
+       precision mediump float;
+       out vec4 o;
+       void main() { o = vec4(0.0); }`
+    : `void main() { gl_FragColor = vec4(0.0); }`;
+
+  const vs = gl.createShader(gl.VERTEX_SHADER);
+  gl.shaderSource(vs, vsSrc);
+  gl.compileShader(vs);
+  const fs = gl.createShader(gl.FRAGMENT_SHADER);
+  gl.shaderSource(fs, fsSrc);
+  gl.compileShader(fs);
+
+  const prog = gl.createProgram();
+  gl.attachShader(prog, vs);
+  gl.attachShader(prog, fs);
+  gl.bindAttribLocation(prog, 0, attribName);
+  gl.linkProgram(prog);
+  gl.useProgram(prog);
+
+  const N = 4096;
+  const vertBytes = comps * 4;
+
+  const big = gl.createBuffer();
+  gl.bindBuffer(gl.ARRAY_BUFFER, big);
+  gl.bufferData(gl.ARRAY_BUFFER, N * vertBytes, gl.STATIC_DRAW);
+
+  const tiny = gl.createBuffer();
+  gl.bindBuffer(gl.ARRAY_BUFFER, tiny);
+  gl.bufferData(gl.ARRAY_BUFFER, vertBytes, gl.STATIC_DRAW);
+
+  for (let loc = 0; loc < numLocs; loc++) {
+    const colBig = opts.allBig ||
+                   (loc == 0 ? !opts.firstColSmall : false);
+    gl.bindBuffer(gl.ARRAY_BUFFER, colBig ? big : tiny);
+    gl.enableVertexAttribArray(loc);
+    gl.vertexAttribPointer(loc, comps, gl.FLOAT, false, vertBytes, 0);
+    if (opts.instanced) {
+      gl.vertexAttribDivisor(loc, 1);
+    }
+  }
+
+  // Ensure no pending errors.
+  ok(gl.getError() == gl.NO_ERROR, "no error during setup");
+
+  if (opts.instanced) {
+    gl.drawArraysInstanced(gl.POINTS, 0, 1, N);
+  } else {
+    gl.drawArrays(gl.POINTS, 0, N);
+  }
+  return gl.getError();
+}
+
+const INVALID_OPERATION =
+    document.createElement("canvas").getContext("webgl").INVALID_OPERATION;
+
+function check(desc, expectError, version, ...args) {
+  const err = run(version, ...args);
+  const errored = err == INVALID_OPERATION;
+  ok(errored == expectError,
+     `${desc}: err:0x${err.toString(16)} ` +
+     `(expected ${expectError ? "INVALID_OPERATION" : "no error"})`);
+}
+
+const WEBGL1 = {
+  vec4: `attribute vec4 a; void main() { gl_Position = a; gl_PointSize = 1.0; }`,
+  mat2: `attribute mat2 m;
+         void main() { gl_Position = vec4(m[0], m[1]); gl_PointSize = 1.0; }`,
+  mat3: `attribute mat3 m;
+         void main() { gl_Position = vec4(m[0] + m[1] + m[2], 1.0);
+                       gl_PointSize = 1.0; }`,
+  mat4: `attribute mat4 m;
+         void main() { gl_Position = m[0] + m[1] + m[2] + m[3];
+                       gl_PointSize = 1.0; }`,
+};
+
+check("vec4 over-fetch", true, "webgl", WEBGL1.vec4, "a", 1, 4, {firstColSmall: true});
+check("mat2 later-column over-fetch", true, "webgl", WEBGL1.mat2, "m", 2, 4);
+check("mat3 later-column over-fetch", true, "webgl", WEBGL1.mat3, "m", 3, 4);
+check("mat4 later-column over-fetch", true, "webgl", WEBGL1.mat4, "m", 4, 4);
+
+check("mat4 fully backed", false, "webgl", WEBGL1.mat4, "m", 4, 4,
+      {allBig: true});
+
+if (document.createElement("canvas").getContext("webgl2")) {
+  const WEBGL2 = {
+    mat2x4: `#version 300 es
+            in mat2x4 m;
+            void main() { gl_Position = m[0] + m[1]; gl_PointSize = 1.0; }`,
+    mat4x2: `#version 300 es
+            in mat4x2 m;
+            void main() { gl_Position = vec4(m[0], m[1]) + vec4(m[2], m[3]);
+                          gl_PointSize = 1.0; }`,
+  };
+
+  check("mat2x4 later-column over-fetch", true, "webgl2",
+        WEBGL2.mat2x4, "m", 2, 4);
+  check("mat4x2 later-column over-fetch", true, "webgl2",
+        WEBGL2.mat4x2, "m", 4, 2);
+  check("mat4x2 instanced later-column over-fetch", true, "webgl2",
+        WEBGL2.mat4x2, "m", 4, 2, {instanced: true});
+} else {
+  ok(true, "webgl2 unavailable, skipping non-square/instanced cases");
+}
+
+ok(true, 'done');
+</script>
+</body>
+</html>
Loading diff…