High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionPrivilege escalation due to incorrect boundary conditions in the Graphics: CanvasWebGL component
ComponentDOM
Bug ClassLogic Error
Tracker2059997
Fix commit17e5d50fcd1f (firefox) +1/-1
CISA KEVNot listed
Creditedlocust1b
Disclosed2026-08-18

Background

`WebGLBuffer`
Gecko’s representation of a WebGL buffer object holding vertex or index (element array) data on behalf of a web page.
`mIndexRanges`
a per-buffer cache mapping a byte range of index data to the maximum index value it contains, so repeated draw calls can skip rescanning the buffer for bounds validation.
`InvalidateCacheRange`
a method that drops cached mIndexRanges entries overlapping a region of the buffer that has just been modified, keeping the cached max-index values consistent with buffer contents.
`IndexByteSizeByType`
a helper returning the size in bytes of a single index for a given element type (e.g. 1, 2, or 4 bytes).

Root Cause Analysis

When buffer contents are modified, WebGLBuffer::InvalidateCacheRange walks every cached entry in mIndexRanges and invalidates those that overlap the modified [updateBegin, updateEnd) byte region. The overlap test computed the cached entry’s start as rangeBegin = range.byteOffset * indexByteSize, but range.byteOffset is already a byte offset, so multiplying it by indexByteSize inflated the start to a far larger, incorrect position. The invariant that a cached range’s [rangeBegin, rangeEnd) must describe the actual bytes it covers was therefore violated, and the guard if (rangeBegin >= updateEnd || rangeEnd <= updateBegin) continue; could conclude a genuinely overlapping update did not overlap and skip invalidation.

The fix sets rangeBegin = range.byteOffset directly, so the computed span matches the real byte extent and overlapping updates once again invalidate the stale cache entry. Because the cached maximum index is used to accept draw calls without rescanning, an un-invalidated stale entry lets the buffer be validated against out-of-date index bounds.

Key insight
The single mistake is a unit error: range.byteOffset was multiplied by indexByteSize as though it were an index count, inflating the range’s start and defeating the overlap check; the fix uses the byte offset as-is so cache invalidation correctly covers modified index data.

Attack Path

  1. Prime the cache The page issues a draw call from an element-array buffer so Gecko records an mIndexRanges entry capturing the range’s maximum index value.
  2. Rewrite the indices Script updates the buffer’s index bytes (e.g. via bufferSubData) to contain larger, out-of-bounds index values covering the same range.
  3. Miss the invalidation The inflated rangeBegin makes InvalidateCacheRange treat the modified region as non-overlapping, so the stale max-index entry is not dropped.
  4. Reuse stale bounds A subsequent draw call is validated against the cached (now too-small) maximum index and is accepted despite referencing indices past the vertex data.
  5. Out-of-bounds access The GPU fetches vertex attributes for out-of-range indices, reading memory outside the intended vertex buffer.

Impact Assessment

An attacker running untrusted JavaScript/WebGL in a web page can cause index-bounds validation to pass against stale cached data, driving out-of-bounds reads of vertex/buffer memory during draw calls. The corruption occurs in the process handling WebGL (the content/GPU path), and the only precondition is the ability to create a WebGL context, populate an index buffer, and rewrite it after a draw — all available to any web content. The metadata labels this a high-severity privilege escalation arising from the incorrect boundary condition.

Changed Functions

FunctionChangeNotes
for
dom/canvas/WebGLBuffer.cpp
modified

Files Changed

  • dom/canvas/WebGLBuffer.cpp

Audit Directions

  • Unit confusion between offsets and counts
    Audit code that mixes byte offsets and element indices; verify a value already in bytes is never re-multiplied by an element size before comparison or arithmetic.
  • Cache invalidation overlap tests
    Review every range-overlap guard used to invalidate caches and confirm both endpoints are computed in the same units as the update region they are compared against.
  • Validation shortcuts using cached bounds
    Examine places where cached maxima or bounds let later operations skip re-validation, ensuring the cache is provably invalidated on every mutation of the underlying data.
diff --git a/dom/canvas/WebGLBuffer.cpp b/dom/canvas/WebGLBuffer.cpp
index 4ac02bd414d..357b9873269 100644
--- a/dom/canvas/WebGLBuffer.cpp
+++ b/dom/canvas/WebGLBuffer.cpp
@@ -264,7 +264,7 @@ void WebGLBuffer::InvalidateCacheRange(uint64_t byteOffset,
   for (const auto& cur : mIndexRanges) {
     const auto& range = cur.first;
     const auto& indexByteSize = IndexByteSizeByType(range.type);
-    const auto rangeBegin = range.byteOffset * indexByteSize;
+    const auto rangeBegin = range.byteOffset;
     const auto rangeEnd =
         rangeBegin + uint64_t(range.indexCount) * indexByteSize;
     if (rangeBegin >= updateEnd || rangeEnd <= updateBegin) continue;
Loading diff…