CVE-2026-12308
Overview
Background
- `WebGLBuffer`
- Gecko’s GPU-buffer wrapper backing WebGL
gl.bufferData/gl.bindBuffercalls in the content process. - `WebGLBuffer::Kind::ElementArray`
- the buffer’s persistent kind, fixed when the buffer is first bound, marking it as an index (element-array) buffer.
- Index cache
- a CPU-side copy (
newIndexCache/mIndexCache) of an element-array buffer’s contents used to validate index values before a draw call. - `target`
- the transient binding-point argument passed to
BufferData(e.g.LOCAL_GL_ELEMENT_ARRAY_BUFFER), which does not necessarily match the buffer’s own kind.
Root Cause Analysis
WebGLBuffer::BufferData decided whether to allocate and populate the shadow index cache by testing the caller-supplied target == LOCAL_GL_ELEMENT_ARRAY_BUFFER rather than the buffer’s own mContent kind. Because a WebGLBuffer’s kind is fixed at first bind while target merely names the binding point used for this particular upload, the two can diverge, so an element-array buffer uploaded through a mismatched target would skip building its index cache while mNeedsIndexValidation still expected one to exist. The violated invariant is that every buffer of kind ElementArray must carry an index cache consistent with its device contents whenever index validation is enabled. Later index-validation logic then reads a stale or absent cache that no longer reflects the buffer’s real size and data, producing an out-of-bounds read.
The fix keys the allocation on the buffer’s stable identity, mContent == WebGLBuffer::Kind::ElementArray, so the cache is built exactly when the buffer actually is an index buffer, restoring the invariant regardless of which target the upload used.
target binding point instead of the buffer’s own kind; the fix substitutes the authoritative mContent field so cache creation tracks what the buffer really is.Attack Path
- Create a mismatched index buffer
Script creates a
WebGLBuffer, binds it so itsmContentbecomesKind::ElementArray, in a context wheremNeedsIndexValidationis set. - Upload via a divergent target
Call
bufferDatain a way where thetargetargument does not equalLOCAL_GL_ELEMENT_ARRAY_BUFFER, so the old check skips allocating the index cache. - Leave the cache absent or stale The element-array buffer now has device data but no matching CPU-side index cache, breaking the size/contents invariant.
- Issue an indexed draw A draw call triggers index validation, which reads the missing or outdated cache instead of the buffer’s true bounds.
- Trigger the out-of-bounds access Validation and the subsequent draw operate on unvalidated indices, causing an out-of-bounds memory access in the content process.
Impact Assessment
mNeedsIndexValidation (or legacy vertex-attrib-0 handling) enabled and that the attacker can drive a buffer to kind ElementArray while uploading through a mismatched target. No elevated privileges are required beyond loading attacker-controlled script.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/canvas/WebGLBuffer.cpp |
modified |
Files Changed
dom/canvas/WebGLBuffer.cpp
Audit Directions
- Transient argument vs. persistent stateFlag any code that makes a per-object durable decision from a caller-supplied binding target or parameter instead of the object’s own stored kind/state field.
- Cache-invariant couplingAudit paths where a validation cache is conditionally allocated and later unconditionally consumed; ensure the allocation condition exactly matches the consumption condition.
- WebGL buffer kind divergenceReview other
WebGLBufferoperations that branch ontargetto confirm they usemContentwherever the buffer’s fixed kind, not the current binding point, is what matters.
Patch
diff --git a/dom/canvas/WebGLBuffer.cpp b/dom/canvas/WebGLBuffer.cpp
index e08211851bc..f80490f90a3 100644
--- a/dom/canvas/WebGLBuffer.cpp
+++ b/dom/canvas/WebGLBuffer.cpp
@@ -119,7 +119,7 @@ void WebGLBuffer::BufferData(const GLenum target, const uint64_t size,
UniqueBuffer newIndexCache;
const bool needsIndexCache = mContext->mNeedsIndexValidation ||
mContext->mMaybeNeedsLegacyVertexAttrib0Handling;
- if (target == LOCAL_GL_ELEMENT_ARRAY_BUFFER && needsIndexCache) {
+ if (mContent == WebGLBuffer::Kind::ElementArray && needsIndexCache) {
newIndexCache = UniqueBuffer::Take(malloc(AssertedCast<size_t>(size)));
if (!newIndexCache) {
mContext->ErrorOutOfMemory("Failed to alloc index cache.");