Moderate firefox Input Validation 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSandbox escape due to incorrect boundary conditions in the Graphics: CanvasWebGL component
ComponentDOM
Bug ClassInput Validation
Tracker2079072
Fix commit6a19c1ce009f (firefox) +15/-0
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

`ClientWebGLContext`
The renderer-side (content-process) WebGL client object. It validates WebGL calls and records client-side GL state, then forwards commands to the host-side WebGL implementation (in the GPU/compositor process).
`CheckBindBufferRange`
The client-side validator for bindBufferRange (used for UBO/TFO buffer ranges). It returns Some(ErrorInfo) to raise a GL error and reject the call, or {} (empty Maybe) to accept it.
`GLintptr` / `GLsizeiptr`
Signed pointer-width integer types the GL driver uses for buffer offset and size. The WebGL API accepts 64-bit values, which may not fit these types.
`CheckedInt<T>`
Mozilla’s overflow-checked integer wrapper; CheckedInt<GLintptr>(offset).isValid() is true only if offset round-trips through GLintptr without overflow/truncation.
Client/host state coherence
The client records which buffer range is “bound”. If the client accepts a range that the host/driver actually rejects, the two sides disagree about the current binding.

Root Cause Analysis

CheckBindBufferRange in dom/canvas/ClientWebGLContext.cpp validated the semantic properties of a bindBufferRange call but did not verify that the user-supplied offset and size (64-bit API values) fit the driver’s GLintptr/GLsizeiptr types. As the added comment states, “the driver rejects a range that does not fit GLintptr/GLsizeiptr and keeps its previous binding, so we must not record such a range as bound.” The old code would let such a call pass client validation; the client then recorded the out-of-range binding as the current binding while the host/driver silently kept its previous binding. From then on the client and host disagree about which buffer region is bound.

The patch adds, for the buffer case, two CheckedInt guards that raise LOCAL_GL_INVALID_VALUE and reject the call when offset does not fit GLintptr or size does not fit GLsizeiptr, so the client never records a binding the driver refused. Note the advisory labels this a “sandbox escape due to incorrect boundary conditions”; the diff substantiates a boundary/range-validation fix that keeps client and host buffer-binding state coherent. The memory-safety consequence on the host (reading/writing against the stale binding or an unvalidated offset/size) is the inferred escalation path, not something the diff itself shows. (The diff preview also contains an obvious transcription artifact in the first error string — GLintptr." offset) — which does not affect the analysis.)

Key insight
The client validator accepted bindBufferRange offsets/sizes that overflow the driver’s signed GLintptr/GLsizeiptr, so it recorded a binding the driver had actually rejected — desynchronizing client and host state about the current buffer range.

Attack Path

  1. Renderer script Attacker-controlled JS in a WebGL context calls gl.bindBufferRange(target, index, buffer, offset, size) with an offset or size that is representable as a WebGL 64-bit value but does not fit GLintptr/GLsizeiptr.
  2. Client accepts The pre-patch CheckBindBufferRange returns {} (no error), so the client records the out-of-range range as the bound range.
  3. Host rejects The driver rejects the range and keeps its previous binding, creating a client/host mismatch about the current buffer range.
  4. State desync Subsequent draws/transform-feedback use the binding the client believes is active while the host operates on a different one. (inferred)
  5. Primitive Operating against a stale/mismatched binding on the host is the lever toward out-of-range buffer access in the GPU process; the diff proves only the desync, so the sandbox-escape primitive is inferred.

Impact Assessment

The validator runs in the content process, but the bug’s value is the resulting incoherence with the host WebGL implementation in the GPU/compositor process; no special privilege beyond script in a WebGL page is required. Concretely the diff proves a boundary-validation gap that lets the client treat a driver-rejected bindBufferRange as applied. The advisory classifies this as a sandbox escape; the diff substantiates only the incorrect boundary condition / state desync, so the escalation to a memory-corruption primitive in the GPU process is inferred and gated on how the host consumes the mismatched binding.

Changed Functions

FunctionChangeNotes
if
dom/canvas/ClientWebGLContext.cpp
modified

Files Changed

  • dom/canvas/ClientWebGLContext.cpp

Audit Directions

  • 64-bit API value vs. GL type fit
    Audit other ClientWebGLContext validators that forward user offset/size/length values to the host for missing CheckedInt<GLintptr>/<GLsizeiptr> (or GLint) range checks.
  • Client-recorded state on rejected calls
    Look for other WebGL entry points where the client updates its shadow state before (or regardless of) the host accepting the call, which can desync client and host.
  • Host-side trust of client ranges
    Review the host WebGL implementation for where it trusts client-recorded buffer bindings/ranges and whether it independently re-validates offset/size against the real buffer.
diff --git a/dom/canvas/ClientWebGLContext.cpp b/dom/canvas/ClientWebGLContext.cpp
index d5a15f33a72..cdb92ff4eef 100644
--- a/dom/canvas/ClientWebGLContext.cpp
+++ b/dom/canvas/ClientWebGLContext.cpp
@@ -3501,6 +3501,21 @@ Maybe<webgl::ErrorInfo> CheckBindBufferRange(
     }
   }
 
+  // The driver rejects a range that does not fit GLintptr/GLsizeiptr and keeps
+  // its previous binding, so we must not record such a range as bound.
+  if (isBuffer) {
+    if (!CheckedInt<GLintptr>(offset).isValid()) {
+      const auto info = nsPrintfCString("`offset` (%" PRIu64
+                                        ") must fit in GLintptr." offset);
+      return fnSome(LOCAL_GL_INVALID_VALUE, info);
+    }
+    if (!CheckedInt<GLsizeiptr>(size).isValid()) {
+      const auto info =
+          nsPrintfCString("`size` (%" PRIu64 ") must fit in GLsizeiptr.", size);
+      return fnSome(LOCAL_GL_INVALID_VALUE, info);
+    }
+  }
+
   return {};
 }
 
Loading diff…