bufferData(COPY_WRITE_BUFFER, ...) on a Kind::ElementArray buffer drops mIndexCache without rebuilding it, 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: reporter-external, sec-moderate, Whiteboard: [adv-main152+][adv-esr140.12+])
Attachments
(4 files)
|
6.70 KB,
text/html
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
tjr
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
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:
bufferData(COPY_WRITE_BUFFER, ...) on a Kind::ElementArray buffer drops mIndexCache without rebuilding it, allowing index-validation bypass and uncramped gl_VertexID delivery to vertex shaders
Filing fields
| Field | Value |
|---|---|
| Product | Core |
| Component | Graphics: CanvasWebGL |
| Type | defect |
| Group | Core Security (mark as security bug) |
| Version | Firefox 150.0 |
| Platform / OS | All; tested on macOS |
| See Also | bug 2037323, bug 1836705 (CVE-2023-5724), bug 1028891 (CVE-2014-1556) |
Attach: sibling.html (the PoC).
Summary
This is a sibling to bug 2037323 (copyBufferSubData on EAB). Different code path, different fix surface, same impact class (validator bypass, uncramped gl_VertexID, OOB attribute fetches on non-robust drivers).
WebGLBuffer::BufferData allocates the index-validation cache only when its target argument is ELEMENT_ARRAY_BUFFER (WebGLBuffer.cpp line 120). However, a buffer with Kind::ElementArray is bindable to COPY_WRITE_BUFFER per WebGL2 spec section 5.1, and bufferData(COPY_WRITE_BUFFER, ...) is a valid call. When invoked with the COPY_WRITE_BUFFER target, the function:
- Skips the
newIndexCacheallocation (line 120 condition is false). - Updates the GL VBO contents via
gl->fBufferData(target, ...)at line 142 / 156. - At line 164, executes
mIndexCache = std::move(newIndexCache). SincenewIndexCachewas never assigned,mIndexCachebecomes null. - Skips the
mIndexRanges.clear()at line 170 because the guard at line 166 (if (mIndexCache)) is false after the move; stale memoized ranges remain (not directly exploitable since the consumer short-circuits earlier, but worth noting).
The downstream consumer at WebGLContextDraw.cpp line 1006-1008:
const auto globalMaxVertId =
indexBuffer->GetIndexedFetchMaxVert(type, 0, indexCapacity);
if (!globalMaxVertId) return true;
When mIndexCache is null, GetIndexedFetchMaxVert returns Nothing() at line 333. The lambda short-circuits to "fetch valid". drawElements proceeds to the driver with no clamping, and the post-bufferData GPU index value reaches the vertex shader.
Reproduction
Attached PoC firefox-eab-cache-drop-via-copy-write.html. Self-contained, R32UI integer FBO for clean readback (no extension required).
Steps:
- Create vertex attribute buffer with 3 vec2 slots (
maxVerts = 3). - Create EAB X.
bindBuffer(ELEMENT_ARRAY_BUFFER, X).mContent = Kind::ElementArray. bufferData(ELEMENT_ARRAY_BUFFER, [0, 1, 2]).mIndexCache(X) = [0, 1, 2]. GL VBO(X) = [0, 1, 2].- WARM draw:
drawElements(POINTS, 1, UNSIGNED_BYTE, 0). Validation populatesmIndexRanges(X)[(UBYTE, 0, 1)] = Some(0). PoC reads backgl_VertexID = 0from the integer FBO. No GL error. - TRIGGER:
bindBuffer(COPY_WRITE_BUFFER, X). Allowed because COPY_* doesn't restrict kind (ValidateCanBindToTargetlines 408-410). bufferData(COPY_WRITE_BUFFER, [42, 99, 99]). InsideWebGLBuffer::BufferData,target != ELEMENT_ARRAY_BUFFER, so the cache is dropped to null. GL VBO(X) becomes [42, 99, 99]. No GL error.- BAD draw:
bindBuffer(ELEMENT_ARRAY_BUFFER, X)then identicaldrawElements(POINTS, 1, UNSIGNED_BYTE, 0). PoC reads backgl_VertexID = 42. No GL error. The validator did not reject the draw despite the index 42 being out of bounds for the vertex attribute capacity of 3.
Test environment confirmed
- macOS, Apple Silicon (Mac15,12, M3 MacBook Air)
- Firefox 150.0
gl.getSupportedExtensions()does not advertise any robust_* extensionmNeedsIndexValidationengaged (default on macOS native GL path)
PoC output:
robust extensions advertised: (none)
warm draw: gl_VertexID echo = 0 (expect 0), gl error = 0x0
trigger bufferData(COPY_WRITE_BUFFER, [42,99,99]) gl error = 0x0
bad draw: gl_VertexID echo = 42 (42 == bypass, 0 == clamp), gl error = 0x0
CONFIRMED: validator bypass via bufferData(COPY_WRITE_BUFFER, ...).
Impact
Confirmed:
- Index validation bypass:
drawElementswith index 42 against a 3-vertex attribute completes withoutINVALID_OPERATION. The cache state corruption invariantmIndexCache mirrors VBO contents for EABs with mNeedsIndexValidation=trueis violated. - Unclamped
gl_VertexID = 42reaches the vertex shader.
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 M3 returned 0 for OOB attribute fetches in adjacent variants tested for bug 2037323. Linux/Mesa testing for bug 2037323 (Comment 1 and Comment 5 of that bug) did demonstrate non-zero leaked values run-to-run including recognizable float bit patterns. The same disclosure path should apply here since the post-bypass driver behavior is determined by the driver, not the trigger.
This is the same architectural pattern as bug 2037323 and the same impact ceiling. The CVE precedents (CVE-2023-5724, CVE-2014-1556) for index/draw validation gaps in the same area were rated sec-high.
Why this is not a duplicate of bug 2037323
| Aspect | bug 2037323 | this filing |
|---|---|---|
| Trigger | copyBufferSubData(COPY_READ, EAB) |
bufferData(COPY_WRITE_BUFFER, ...) on EAB-kind buffer |
| Cache failure mode | mIndexCache retains pre-copy bytes (stale) |
mIndexCache is dropped to null entirely |
| Cache mirror after trigger | stale (still readable, contents diverge from VBO) | absent (!mIndexCache) |
| Validator path | cache-hit returns memoized stale Some(maxVert) |
GetIndexedFetchMaxVert returns Nothing() |
| Consumer behavior | validation passes against stale value | consumer short-circuits to "fetch valid" |
| Fix location | WebGL2Context::CopyBufferSubData (post-fCopyBufferSubData) |
WebGLBuffer::BufferData (target-check at line 120) |
| Suggested fix shape (in 2037323) | OnBytesReplacedExternally() after the copy |
does NOT cover this path; see below |
The fix suggested in bug 2037323 (if (writeBuffer->Content() == Kind::ElementArray) writeBuffer->OnBytesReplacedExternally() inside CopyBufferSubData) does not address this filing. The trigger here is bufferData, not copyBufferSubData, and the bug is the missing allocation of newIndexCache rather than a missing invalidation of an existing one. Both bugs need to be fixed for the cache invariant to hold.
Suggested fix
The check at WebGLBuffer.cpp line 120 conditions cache allocation on the call's target argument:
if (target == LOCAL_GL_ELEMENT_ARRAY_BUFFER && needsIndexCache) {
newIndexCache = UniqueBuffer::Take(malloc(...));
...
}
The intent is "only build the index cache for EABs". The bug is that the buffer's kind (which determines whether the buffer will be used as an EAB later) is decoupled from the target of the current bufferData call. The buffer can be Kind::ElementArray and the call's target can be COPY_WRITE_BUFFER simultaneously.
The fix is to gate on the buffer's kind, not on the call's target:
const bool needsIndexCache = mContext->mNeedsIndexValidation ||
mContext->mMaybeNeedsLegacyVertexAttrib0Handling;
if (mContent == WebGLBuffer::Kind::ElementArray && needsIndexCache) {
newIndexCache = UniqueBuffer::Take(malloc(AssertedCast<size_t>(size)));
if (!newIndexCache) {
mContext->ErrorOutOfMemory("Failed to alloc index cache.");
return;
}
memcpy(newIndexCache.get(), uploadData, size);
uploadData = newIndexCache.get();
}
Note mContent is the buffer's WebGL kind. By the time BufferData is called, the buffer has been bound through some target via ValidateBufferSelection, and the path that reaches here for a Kind::Undefined buffer first goes through BindBuffer โ SetContentAfterBind, so mContent is set when the call reaches BufferData. (Caveat: see the secondary spec note below.)
Alternative minimal patch: keep the target check but add the kind check disjunctively:
if ((target == LOCAL_GL_ELEMENT_ARRAY_BUFFER ||
mContent == WebGLBuffer::Kind::ElementArray) && needsIndexCache) {
This preserves existing behavior for newly-created Undefined buffers while closing the COPY_WRITE_BUFFER path on already-Kind::ElementArray buffers.
Secondary note (separate, low-priority spec compliance)
While auditing, I also noticed WebGLBuffer::SetContentAfterBind (line 27-48) sets mContent = Kind::OtherData for COPY_READ_BUFFER and COPY_WRITE_BUFFER targets. Per WebGL 2.0 spec section 5.1, calling bindBuffer "with the target argument set to any buffer binding point except COPY_READ_BUFFER or COPY_WRITE_BUFFER" sets the WebGL buffer type. The current implementation deviates from spec by setting Kind for COPY_* targets. This is over-restrictive (closes a path the spec leaves open) rather than permissive, so it's not a security issue, but it's a real spec deviation. Filing separately if desired; mentioning here only because it touches the same kind-classification code path.
Notes for the security team
- The PoC HTML is self-contained, reads back integer pixel data only, and does not attempt any privilege boundary crossing. It demonstrates the desync and the leaked
gl_VertexIDvalue. - I did not test on Linux/Mesa or other non-Apple drivers for this filing. The same driver-side OOB-fetch behavior documented in bug 2037323 Comment 1 and Comment 5 (run-to-run variance, recognizable float bit patterns) should apply here on those drivers; the bypass mechanism differs but the post-bypass shader execution is identical.
- The existing bug 2037323 fix surface (
OnBytesReplacedExternallyafterfCopyBufferSubData) does not cover this path. Both bugs need a fix.
Reporter info
Mihalis Haatainen (Bountyy Oy)
Expected results:
Updated•4 months ago
|
| Reporter | ||
Comment 1•4 months ago
|
||
@gw, you set S2 on the sibling bug 2037323 last week. This bug is the same architectural pattern (host-side cache desync, validator trusts stale state) but a different cache and trigger path. Would you have time to triage severity here as well? Repro and source-level root cause in the description.
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 3•4 months ago
|
||
While auditing, I also noticed WebGLBuffer::SetContentAfterBind (line 27-48) sets mContent = Kind::OtherData for COPY_READ_BUFFER and COPY_WRITE_BUFFER targets. Per WebGL 2.0 spec section 5.1, calling bindBuffer "with the target argument set to any buffer binding point except COPY_READ_BUFFER or COPY_WRITE_BUFFER" sets the WebGL buffer type. The current implementation deviates from spec by setting Kind for COPY_* targets. This is over-restrictive (closes a path the spec leaves open) rather than permissive, so it's not a security issue, but it's a real spec deviation. Filing separately if desired; mentioning here only because it touches the same kind-classification code path.
Looks like chromium is similarly over-restrictive: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/webgl/webgl2_rendering_context_base.cc;l=5128;drc=a0f96f45d931f3fc57126fad5d8050ea63bd2d25
| Assignee | ||
Comment 4•4 months ago
|
||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 6•4 months ago
•
|
||
Comment on attachment 9587208 [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?: Yes
- If not, how different, hard to create, and risky will they be?: I expect that this will apply cleanly to all 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 paragraph about the exploit, I got mixed up with another webgl sec bug and confused OOB reads vs writes.
Updated•4 months ago
|
Updated•4 months ago
|
Comment 8•3 months ago
|
||
Comment 9•3 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.
Comment 10•3 months ago
|
||
Please also request ESR140 uplift.
| Assignee | ||
Comment 11•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D301032
Updated•3 months ago
|
| Assignee | ||
Comment 12•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D301032
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 13•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Comment 14•3 months ago
|
||
| uplift | ||
| Assignee | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
| Reporter | ||
Updated•3 months ago
|
Updated•2 months ago
|
Updated•2 days ago
|
Description
•