CVE-2026-74941
Overview
Background
- Transform Feedback
- a WebGL feature that captures the outputs of a vertex/geometry shader into GPU buffer objects during a draw call.
- `WebGLTransformFeedback`
- the Gecko object that tracks the state of a transform feedback session, including which buffers are bound and how much room remains for captured vertices.
- `mActive_VertCapacity`
- the maximum number of vertices that the currently bound feedback buffers can hold, computed from the smallest bound buffer range.
- `mActive_VertPosition`
- a running counter of how many vertices have already been written into the feedback buffers during the active session.
Root Cause Analysis
When a transform feedback session is (re)prepared in WebGLTransformFeedback::PrepareTransformFeedback, the code recomputes mActive_VertCapacity from the currently bound buffers (minVertCapacity) but did not correspondingly re-clamp the stale mActive_VertPosition carried over from a prior state. This violates the invariant that mActive_VertPosition <= mActive_VertCapacity, which downstream draw/capacity accounting relies on to bound writes into the feedback buffers. If a newly computed capacity was smaller than the leftover position value, later captured vertices would be written starting past the intended buffer boundary, producing an out-of-bounds condition.
The fix adds mActive_VertPosition = std::min(mActive_VertPosition, mActive_VertCapacity);, forcing the position back within the freshly computed capacity so the invariant holds before any vertices are captured. Because the clamp runs on every preparation of the session, any subsequent capacity accounting starts from a valid, in-bounds position.
position <= capacity invariant by clamping the position to the new capacity.Attack Path
- Set up feedback buffers
Script binds transform feedback buffer ranges and begins a session so that
mActive_VertPositionadvances during captures. - Force a capacity change
The attacker re-prepares the transform feedback with bound buffers whose smallest range yields a
minVertCapacitylower than the currentmActive_VertPosition. - Skip the clamp
Without the fix,
mActive_VertCapacityshrinks while the stale position remains larger, breaking the capacity invariant. - Trigger captures A draw call captures vertices using the now-inconsistent position/capacity accounting, writing past the intended buffer bounds.
Impact Assessment
Graphics: CanvasWebGL component.Files Changed
dom/canvas/WebGLTransformFeedback.cpp
Audit Directions
- Recompute-without-reclampWhen a capacity, length, or bound field is recomputed, check that every dependent cursor or position field (like
mActive_VertPosition) is re-validated against the new value in the same code path. - State carried across re-preparationAudit WebGL
Prepare*/Begin*paths for stale per-session counters that survive a reconfiguration and could exceed freshly derived limits. - Min-derived boundsScrutinize capacities derived from a
std::minover multiple buffer ranges, since binding a smaller buffer can silently reduce a limit below previously advanced offsets.
Patch
diff --git a/dom/canvas/WebGLTransformFeedback.cpp b/dom/canvas/WebGLTransformFeedback.cpp
index 88bf4274278..2f98988d605 100644
--- a/dom/canvas/WebGLTransformFeedback.cpp
+++ b/dom/canvas/WebGLTransformFeedback.cpp
@@ -76,6 +76,7 @@ bool WebGLTransformFeedback::PrepareTransformFeedback() {
}
mActive_VertCapacity = minVertCapacity;
+ mActive_VertPosition = std::min(mActive_VertPosition, mActive_VertCapacity);
return true;
}