`copyBufferSubData on ELEMENT_ARRAY_BUFFER does not invalidate WebGLBuffer::mIndexCache, allowing index-validation bypass and uncramped gl_VertexID delivery to vertex shaders`
Categories
(Core :: Graphics: CanvasWebGL, defect)
Tracking
()
People
(Reporter: mihalis.haatainen, Assigned: nical)
References
Details
(Keywords: csectype-bounds, reporter-external, sec-moderate, Whiteboard: [adv-main152+][adv-esr140.12+])
Attachments
(9 files, 3 external files)
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:150.0) Gecko/20100101 Firefox/150.0
Steps to reproduce:
Actual results:
Filing fields
| Field | Value |
|---|---|
| Product | Core |
| Component | Graphics: CanvasWebGL |
| Type | defect |
| Group | Core Security (yes, mark as security bug) |
| Version | Firefox 150.0 (also reproducible on 150.0.1) |
| Platform / OS | All; tested on macOS |
| See Also | bug 569431, bug 732660, bug 1836705 (CVE-2023-5724) |
Attach: firefox-webgl-desync-variants-v2.html (the multi-variant PoC).
WebGL2RenderingContext.copyBufferSubData writes to a destination element-array-buffer's GPU storage but does not update or invalidate the corresponding WebGLBuffer::mIndexCache or mIndexRanges state. When a subsequent drawElements* call validates indices against the stale cache, the validator passes for indices that, on the GPU side, exceed the bound vertex attribute capacity. The GPU then runs the vertex shader with gl_VertexID values that violate the WebGL safety contract, and on drivers without KHR_robust_buffer_access_behavior the resulting OOB attribute fetches may return GPU memory contents.
The bug is reachable in the default configuration of Firefox on macOS native GL (which does not advertise robust_buffer_access_behavior), and on any other build where webgl.force-index-validation=1 is set or where the GL backend lacks the robust extension.
Affected file
dom/canvas/WebGL2ContextBuffers.cpp, function WebGL2Context::CopyBufferSubData (currently around line 16-77 of the file at tip).
The bug
Firefox keeps a private mirror of element-array-buffer contents in WebGLBuffer::mIndexCache plus a memoization map mIndexRanges that caches max-vertex-index for (type, byteOffset, indexCount) ranges. This cache is populated by WebGLBuffer::BufferData and updated by WebGLBuffer::BufferSubData (which calls InvalidateCacheRange after the memcpy at WebGLBuffer.cpp:199).
WebGL2Context::CopyBufferSubData performs the GL-side copy via gl->fCopyBufferSubData(...) and then calls only writeBuffer->ResetLastUpdateFenceId(). It does NOT:
- update
mIndexCacheto reflect the new bytes that the GL VBO now contains, - clear
mIndexRanges, which still holds stale max-vertex results computed from the pre-copymIndexCache, - call
mFetchInvalidator.InvalidateCaches().
This means the cache and the GL VBO can diverge, and validation will use the stale cache.
The check at WebGL2ContextBuffers.cpp:63-69 (if (writeType != readType)) makes EAB-to-EAB the only allowed copy direction that bypasses the cache update. EAB-to-OtherData and OtherData-to-EAB are rejected with INVALID_OPERATION, so the desync window is specifically EAB-to-EAB.
Reproduction
The attached firefox-webgl-desync-variants-v2.html runs a sweep of variants and prints both a sanity baseline and per-variant results.
Concrete attack chain (variant G in the PoC):
- Create a vertex attribute buffer with 3
floatslots (maxVerts = 3). - Create EAB
AwithbufferData(EAB, [0, 1, 2]).mIndexCache(A) = [0,1,2], GL VBO(A) =[0,1,2]. - Create EAB
BwithbufferData(EAB, [42, 0, 0]).mIndexCache(B) = [42,0,0], GL VBO(B) =[42,0,0]. - Issue a warm draw
drawElements(POINTS, 1, UNSIGNED_BYTE, 0)against A. Validation populatesmIndexRanges(A)[(UBYTE, 0, 1)] = Some(0). copyBufferSubData(COPY_READ_BUFFER=B, COPY_WRITE_BUFFER=A, 0, 0, 3). GL VBO(A) becomes[42, 0, 0].mIndexCache(A)is unchanged.mIndexRanges(A)[(UBYTE, 0, 1)] = Some(0)is unchanged.- Issue the same
drawElements(POINTS, 1, UNSIGNED_BYTE, 0)against A.WebGLContext::DrawElements_checkcallsGetIndexedFetchMaxVert(UBYTE, 0, 1)which returns the cachedSome(0). Validation passes (0 < maxVerts(3)). The driver fetches index42from the post-copy VBO and runs the vertex shader withgl_VertexID = 42.
The PoC vertex shader for variant G writes float(gl_VertexID) to a varying that is read back from a 1x1 RGBA32F framebuffer. Observed result on Apple M1, Firefox 150.0:
G: gl_VertexID echo (EAB[0]=42) warm=0 bad=42 err=0x0000 desync, NONZERO bad=42
bad = 42 confirms the post-copy GPU index value reached the vertex shader despite the WebGL validator approving the draw based on the stale cache.
Test environment confirmed
- macOS, Apple Silicon
- Firefox 150.0 (
rv:150.0) gl.getSupportedExtensions()does not advertise anyrobust_*extensionmNeedsIndexValidationistrueby default (no override needed)
The PoC includes a self-check that reports whether mNeedsIndexValidation is engaged. On builds where the GL backend supports robust_buffer_access_behavior, the PoC instructs the user to set webgl.force-index-validation=1. The bug is then reproducible there as well; the difference is only whether the on-driver robust path mitigates OOB attribute fetches.
Impact
Confirmed:
- Index validation bypass: drawElements calls that should be rejected with
INVALID_OPERATIONare instead issued to the driver. - Unclamped
gl_VertexIDreaches the vertex shader. Variant G demonstrates this end to end on Apple M1. - Cache state corruption: Firefox internal invariant "mIndexCache mirrors VBO contents for EABs with
mNeedsIndexValidation=true" is violated.
Plausible but not yet demonstrated on tested hardware:
- Information disclosure: on drivers without
KHR_robust_buffer_access_behavior, OOB vertex attribute fetches may return adjacent GPU memory rather than zero. Apple's path on M1 returned 0 for fetches in variants A-F. Mesa, older Linux drivers, and some Android GLES drivers are documented as not implementing robust clamping; testing on those is the next step.
The bug parallels CVE-2023-5724 (bug 1836705) and CVE-2014-1556 (bug 1028891), both of which involved index/draw validation gaps in WebGLContextDraw.cpp and were rated sec-high.
Suggested fix
The cleanest fix is to add a method on WebGLBuffer that invalidates the index cache, and call it from CopyBufferSubData when the destination buffer is an EAB.
In WebGLBuffer.h:
public:
void OnBytesReplacedExternally() {
mFetchInvalidator.InvalidateCaches();
mIndexCache.reset();
mIndexRanges.clear();
}
In WebGL2Context::CopyBufferSubData, after the GL call:
gl->fCopyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size);
// The destination buffer's GPU bytes have changed but our private
// mIndexCache and mIndexRanges still reflect the pre-copy state.
// Invalidate so the next index-validating draw recomputes from the
// fresh GL state. This mirrors what BufferSubData does via
// InvalidateCacheRange (called from WebGLBuffer::BufferSubData) and
// what the destructor does on teardown.
if (writeBuffer->Content() == WebGLBuffer::Kind::ElementArray) {
writeBuffer->OnBytesReplacedExternally();
}
writeBuffer->ResetLastUpdateFenceId();
The if (Content() == ElementArray) guard avoids an unnecessary mFetchInvalidator.InvalidateCaches() for non-EAB destinations, where there is no index cache to invalidate. (mFetchInvalidator is also used for vertex attribute fetch caches but those are a different concern.)
Alternative: read back the modified bytes from the GL VBO into mIndexCache instead of dropping it. Adds a GPU round trip and is only worth it if profiling shows cache rebuild on the next draw is a hot path. Recommend the invalidate approach unless that's the case.
Notes for the security team
- The PoC HTML is self-contained, reads back pixel data only, and does not attempt any privilege boundary crossing. It demonstrates the desync and the leaked
gl_VertexIDvalue. - Variant H in the PoC also confirms the cache-miss path in
GetIndexedFetchMaxVertis vulnerable, not just the cache-hit path. - Variant D (TRIANGLES, multi-index draw) shows
warm=1.75, bad=-9999on Apple M1. The warm draw rasterizes a covering triangle; the bad draw does not, but this is not Firefox catching the bug. After the desync, gl_VertexID for the three triangle vertices is 4, 5, 6 (leaked from the post-copy EAB), the variant's VS computesposfromgl_VertexID == 1andgl_VertexID == 2, both of which are false for {4,5,6}, so all three vertices collapse to (-1,-1). The triangle is degenerate and rasterizes nothing. This is a quirk of the variant's VS, not a sign that validation rejected the draw. Variant D therefore confirms the desync (the leaked gl_VertexID values reached the shader) by the same mechanism as variant G. - I have not tested non-Apple drivers. If the security team tests on Linux/Mesa or older Intel/NVIDIA macOS, please share results so the impact section can be updated.
Reporter info
- Mihalis Haatainen (Bountyy Oy)
Expected results:
Updated•6 months ago
|
| Reporter | ||
Comment 1•6 months ago
|
||
Follow-up: confirmed information disclosure component on Linux Firefox 147, Ubuntu 24.04 ARM64 (virtio-gpu/virgl backend, no robust extensions advertised). Across multiple runs of variant C (index 4 with adjacent allocations) and variant E (index 100, UNSIGNED_SHORT), bad values vary run-to-run: C produced 214, 239, 0 across three runs; E produced 1109, 0, 512. Variance across runs (vs constant zero or constant garbage) is consistent with OOB attribute fetches reading uninitialized adjacent GPU memory, which is the disclosure path the original report flagged as "plausible but not yet demonstrated." Variant G remains deterministic (bad=42) as expected, confirming the validator bypass independently. Both halves of the impact section now have positive evidence on a non-Apple driver stack.
webgl.force-index-validation=true. mNeedsIndexValidation engaged confirmed by the PoC pre-check.
adding screenshots as a attachments...
| Reporter | ||
Comment 2•6 months ago
|
||
| Reporter | ||
Comment 3•6 months ago
|
||
| Reporter | ||
Comment 4•6 months ago
|
||
| Reporter | ||
Comment 5•6 months ago
|
||
One additional run showing recognizable float data in the leak. Variant A (index 100, POINTS, UBYTE): bad bits 0xbf51999a (-0.8187500...), variant C (index 4 + adjacent allocs): bad bits 0xbf72d88a (-0.9486166...). Both decode as well-formed negative single-precision floats with unit-range magnitudes, consistent with attribute fetches reading memory that was previously stored as float data (e.g., another GL buffer or a recently-freed TypedArray) rather than uniform random heap entropy. Same environment as comments 1-4 (Linux, Firefox 147, virtio-gpu, no robust extensions advertised, mNeedsIndexValidation engaged).
| Reporter | ||
Comment 6•6 months ago
|
||
Updated•6 months ago
|
| Reporter | ||
Comment 7•6 months ago
|
||
Filed bug 2038302 as a sibling. Different trigger path (bufferData(COPY_WRITE_BUFFER, ...) on an already-Kind::ElementArray buffer) and different cache failure mode (mIndexCache dropped to null vs. left stale here). The OnBytesReplacedExternally fix suggested in this report does not cover that path - the new bug is in WebGLBuffer::BufferData's target check at line 120, not in CopyBufferSubData. Worth coordinating fixes across both so the cache invariant holds end-to-end.
Updated•6 months ago
|
Updated•6 months ago
|
| Reporter | ||
Comment 8•6 months ago
|
||
Two more runs on UTM Ubuntu / Firefox 150 (virtio-gpu, no robust extensions advertised, mNeedsIndexValidation engaged via webgl.force-index-validation=1).
Run A: variant A bad=0.447265625 (bits 0x3ee50000), variant G deterministic at 42.
Run B: variant A bad=0.001953125 (bits 0x3b000000, exact 2^-9), variant G deterministic at 42.
Run C: variant A clamped to 0, variant H bad=-3.4028234663852886e+38 (bits 0xff7fffff, -FLT_MAX) on the cache-miss path with no warm draw.
The variant H result is new evidence for the cache-miss code path I called out as "also vulnerable" in the original report. Previous runs on Apple M1 and the earlier Linux runs in comment 1 had variant H clamping to 0; this run shows the path can deliver a non-zero post-copy value to the shader without any prior warm draw populating mIndexRanges. -FLT_MAX (0xff7fffff) is a well-known sentinel/init value, consistent with leaked previously-stored data rather than random entropy.
Combined with the run-to-run variance on variants A/C/E (different concrete floats each run, all decoding as well-formed values rather than uniform garbage), both the validator-bypass impact (variant G, deterministic across all runs) and the information-disclosure impact (variants A/C/E/H, variable across runs with recognizable float patterns) now have positive evidence on a non-Apple driver stack across multiple runs.
| Reporter | ||
Comment 9•6 months ago
|
||
| Reporter | ||
Comment 10•6 months ago
|
||
| Reporter | ||
Comment 11•6 months ago
|
||
| Reporter | ||
Comment 12•6 months ago
|
||
| Assignee | ||
Comment 13•5 months ago
|
||
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Comment 14•5 months ago
•
|
||
Comment on attachment 9587267 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: It's fairly easy to guess what the issue is from the fix if you know what you are looking for, but it's somewhat hard to exploit.
The bug this fixes allows the attacker to read out of bounds in GPU memory. On a lot of hardware the out of bound reads will produce only zeroes (so not exploitable), but on some hardware it may read the memory directly. This can potentially let them extract sensitive information (the content of a texture that was rendered by the browser for example). - Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: All branches
- If not all supported branches, which bug introduced the flaw?: None
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: This should apply cleanly to older branches.
- How likely is this patch to cause regressions; how much testing does it need?: Low.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
Edit: rewrote the first paragraph about the exploit. I originally got mixed up with another webgl sec fix and confused OOB read vs write.
Updated•5 months ago
|
Updated•5 months ago
|
Comment 15•5 months ago
|
||
Comment 16•5 months ago
•
|
||
https://hg-edge.mozilla.org/mozilla-central/rev/8d79e01d7736
https://hg-edge.mozilla.org/mozilla-central/rev/af4fa233a8e4
Comment 17•5 months ago
|
||
The patch landed in nightly and beta is affected.
:nical, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox152towontfix.
For more information, please visit BugBot documentation.
| Assignee | ||
Comment 19•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D301065
Updated•5 months ago
|
| Assignee | ||
Comment 20•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D301065
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Comment 21•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•5 months ago
|
Comment 22•5 months ago
|
||
| uplift | ||
| Assignee | ||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
| Reporter | ||
Updated•5 months ago
|
Updated•4 months ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Description
•