CVE-2026-117988
Overview
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 returnsSome(ErrorInfo)to raise a GL error and reject the call, or{}(emptyMaybe) to accept it. - `GLintptr` / `GLsizeiptr`
- Signed pointer-width integer types the GL driver uses for buffer
offsetandsize. 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 ifoffsetround-trips throughGLintptrwithout 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.)
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
- Renderer script
Attacker-controlled JS in a WebGL context calls
gl.bindBufferRange(target, index, buffer, offset, size)with anoffsetorsizethat is representable as a WebGL 64-bit value but does not fitGLintptr/GLsizeiptr. - Client accepts
The pre-patch
CheckBindBufferRangereturns{}(no error), so the client records the out-of-range range as the bound range. - Host rejects The driver rejects the range and keeps its previous binding, creating a client/host mismatch about the current buffer range.
- State desync Subsequent draws/transform-feedback use the binding the client believes is active while the host operates on a different one. (inferred)
- 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
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
| Function | Change | Notes |
|---|---|---|
ifdom/canvas/ClientWebGLContext.cpp |
modified |
Files Changed
dom/canvas/ClientWebGLContext.cpp
Audit Directions
- 64-bit API value vs. GL type fitAudit other
ClientWebGLContextvalidators that forward useroffset/size/lengthvalues to the host for missingCheckedInt<GLintptr>/<GLsizeiptr>(orGLint) range checks. - Client-recorded state on rejected callsLook 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 rangesReview 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.
Patch
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 {};
}