CVE-2026-12306
Overview
Background
- `WebGL2Context::CopyBufferSubData`
- the WebGL 2 entry point that copies a byte range from one GPU buffer object into another via
gl->fCopyBufferSubData. - `mIndexCache`
- a CPU-side mirror of an element-array buffer’s contents that WebGL keeps so it can validate draw-call indices without reading back from the GPU.
- Index validation
- the check WebGL performs before an indexed draw to ensure every element index stays within the bounds of the bound vertex attribute arrays.
- `InvalidateCacheRange`
- a
WebGLBuffermethod that marks a region of the cached index data as needing revalidation on the next indexed draw.
Root Cause Analysis
WebGL2Context::CopyBufferSubData copied a byte range directly on the GPU through gl->fCopyBufferSubData but never updated the destination buffer’s CPU-side mIndexCache, so if the write buffer was an element-array (index) buffer with an allocated cache, that cache retained its pre-copy contents. This violated the invariant that mIndexCache always mirrors the buffer’s real bytes, which WebGL’s index-validation path relies on to bound-check indexed draws. A page could seed the write buffer’s cache with small, in-range indices, then overwrite the GPU bytes with larger out-of-range indices via CopyBufferSubData while the stale cache still reported the old, safe values.
The fix copies the matching range from readBuffer->mIndexCache into writeBuffer->mIndexCache (both being mByteLength-sized when allocated) with memcpy and then calls InvalidateCacheRange(writeOffset, size) so the affected range is revalidated. This keeps the cache consistent with the GPU buffer and forces the bounds check to run against the real, updated index values.
mIndexCache stale so draw-call bounds checks trusted outdated indices; the fix propagates the copied bytes into the destination cache and invalidates the written range so validation reflects the buffer’s true contents.Attack Path
- Prepare a validated index buffer
The attacker’s WebGL 2 page creates an element-array buffer and populates it with small, in-bounds indices, causing WebGL to build and validate its
mIndexCache. - Stage malicious indices in a source buffer A second buffer is filled with larger indices that point beyond the bound vertex attribute arrays.
- Overwrite via `CopyBufferSubData`
copyBufferSubDatacopies the malicious range from the source buffer into the validated index buffer, changing the GPU bytes whilemIndexCachestill holds the old safe values. - Issue an indexed draw An indexed draw call passes the stale-cache bounds check but executes against the out-of-range indices now resident in the GPU buffer.
- Trigger out-of-bounds access Vertex fetch reads past the attribute arrays, producing the memory-safety violation.
Impact Assessment
copyBufferSubData into an index buffer that owns an allocated mIndexCache. Rated moderate/medium; the diff supports a stale-cache validation bypass but does not itself establish write primitives or specific disclosure beyond the out-of-bounds access.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/canvas/WebGL2ContextBuffers.cpp |
modified |
Files Changed
dom/canvas/WebGL2ContextBuffers.cpp
Audit Directions
- Cache-mirror desyncAudit every GPU-side buffer mutation path (
fCopyBufferSubData,fBufferSubData,getBufferSubData, mapped-buffer writes) to confirm each one updates or invalidates any CPU-sidemIndexCachemirror it can affect. - Validation on stale stateFlag security checks that read from a cached copy (
mIndexCacheand similar) without ensuring the cache was refreshed after the underlying data could change. - Invalidate-after-writeVerify that byte-range writes into cached buffers pair a data update with an
InvalidateCacheRange-style call so no partially-updated range escapes revalidation.
Patch
diff --git a/dom/canvas/WebGL2ContextBuffers.cpp b/dom/canvas/WebGL2ContextBuffers.cpp
index 5012c70ff3e..761a3ee3d0d 100644
--- a/dom/canvas/WebGL2ContextBuffers.cpp
+++ b/dom/canvas/WebGL2ContextBuffers.cpp
@@ -73,6 +73,22 @@ void WebGL2Context::CopyBufferSubData(GLenum readTarget, GLenum writeTarget,
gl->fCopyBufferSubData(readTarget, writeTarget, readOffset, writeOffset,
size);
+ // Update the destination index cache if needed.
+ if (writeBuffer->mIndexCache) {
+ MOZ_ASSERT(readBuffer->mIndexCache);
+ if (readBuffer->mIndexCache) {
+ // The read and write ranges have been validated above by fnValidateOffsetSize.
+ // If allocated, the size of mIndexCache is always mByteLength.
+ const auto* src =
+ static_cast<const uint8_t*>(readBuffer->mIndexCache.get()) +
+ readOffset;
+ auto* dst =
+ static_cast<uint8_t*>(writeBuffer->mIndexCache.get()) + writeOffset;
+ memcpy(dst, src, size);
+ }
+ writeBuffer->InvalidateCacheRange(writeOffset, size);
+ }
+
writeBuffer->ResetLastUpdateFenceId();
}