CVE-2026-12324
Overview
Background
- Transform Feedback
- a WebGL2 feature that captures the outputs of the vertex/geometry shader stage into GPU buffer objects instead of (or alongside) rasterization.
- `WebGLTransformFeedback`
- the Gecko C++ object that tracks transform-feedback state, its bound buffers, and how many vertices may safely be written.
- `mActive_VertCapacity`
- the cached maximum number of vertices the currently bound feedback buffers can hold, used as the upper bound for capture.
- Pause/Resume
- WebGL2 lets a running transform-feedback operation be suspended with
pauseTransformFeedback()and later continued withresumeTransformFeedback().
Root Cause Analysis
When WebGLTransformFeedback::BeginTransformFeedback started a capture, it validated the bound buffers and computed mActive_VertCapacity from the smallest attached buffer’s vertCapacity, then stored it once. The invariant that mActive_VertCapacity must reflect the current sizes of the bound feedback buffers was violated by WebGLTransformFeedback::ResumeTransformFeedback, which resumed capture without recomputing that capacity. While the transform feedback was paused, the application could rebind, resize, or reallocate the feedback buffers so that the stored capacity no longer matched the real buffer bounds, an incorrect-boundary condition.
The fix extracts all of the validation and the mActive_VertCapacity computation into a new WebGLTransformFeedback::PrepareTransformFeedback helper and calls it from both BeginTransformFeedback and ResumeTransformFeedback. Because resume now re-validates buffer bindings and recomputes the vertex capacity against the buffers’ present state, capture can no longer proceed with a stale, too-large bound. This closes the window in which a paused-then-modified buffer left the capacity out of sync with reality.
mActive_VertCapacity only at begin time and trusting it across a pause during which the buffers could change; the fix re-runs the same validation and capacity computation on resume via PrepareTransformFeedback, so the bound always tracks the buffers’ current size.Attack Path
- Begin capture
Malicious WebGL2 content calls
beginTransformFeedbackwith valid, large feedback buffers so a largemActive_VertCapacityis computed and cached. - Pause
The content calls
pauseTransformFeedback, suspending the operation while the cached capacity remains in effect. - Shrink or rebind buffers While paused, the content reallocates or rebinds the transform-feedback buffers to smaller storage, so their real capacity is now below the cached value.
- Resume with stale bound
The content calls
resumeTransformFeedback, which previously did not recompute capacity and continued using the oversizedmActive_VertCapacity. - Overrun-driving draw A subsequent draw captures more vertices than the now-smaller buffers hold, exercising the incorrect boundary condition.
Impact Assessment
Canvas: WebGL. The precondition is that the page can begin, pause, resize/rebind the feedback buffers, and resume, all reachable from ordinary scripted WebGL2 APIs. Consistent with the metadata this is rated low severity, a logic/boundary error rather than a demonstrated memory-corruption primitive.Changed Functions
| Function | Change | Notes |
|---|---|---|
switchdom/canvas/WebGLTransformFeedback.cpp |
modified |
Files Changed
dom/canvas/WebGLTransformFeedback.cppdom/canvas/WebGLTransformFeedback.h
Audit Directions
- Cached-bound-across-suspendAudit every place that caches a size, capacity, or length at operation-begin time and reuses it after a pause/resume or other suspend point where the underlying resource can change.
- Asymmetric begin/resume validationFlag resume-style entry points that skip the validation their corresponding begin-style function performs; both must re-check invariants against current state.
- GPU buffer rebind/reallocation windowsReview WebGL/WebGL2 paths where buffers can be resized or rebound between related calls, ensuring downstream bounds are recomputed rather than trusted from earlier state.
Patch
diff --git a/dom/canvas/WebGLTransformFeedback.cpp b/dom/canvas/WebGLTransformFeedback.cpp
index 6c4bc590fbd..88bf4274278 100644
--- a/dom/canvas/WebGLTransformFeedback.cpp
+++ b/dom/canvas/WebGLTransformFeedback.cpp
@@ -29,33 +29,24 @@ WebGLTransformFeedback::~WebGLTransformFeedback() {
////////////////////////////////////////
-void WebGLTransformFeedback::BeginTransformFeedback(GLenum primMode) {
- if (mIsActive) return mContext->ErrorInvalidOperation("Already active.");
-
- switch (primMode) {
- case LOCAL_GL_POINTS:
- case LOCAL_GL_LINES:
- case LOCAL_GL_TRIANGLES:
- break;
- default:
- mContext->ErrorInvalidEnum(
- "`primitiveMode` must be one of POINTS, LINES, or"
- " TRIANGLES.");
- return;
- }
-
+/// Do some validation when beginning/resuming the TF, and compute the
+/// vertex capacity.
+bool WebGLTransformFeedback::PrepareTransformFeedback() {
const auto& prog = mContext->mCurrentProgram;
+
if (!prog || !prog->IsLinked() ||
prog->LinkInfo()->componentsPerTFVert.empty()) {
mContext->ErrorInvalidOperation(
"Current program not valid for transform"
" feedback.");
- return;
+ return false;
}
const auto& linkInfo = prog->LinkInfo();
const auto& componentsPerTFVert = linkInfo->componentsPerTFVert;
+ mActive_VertCapacity = 0;
+
size_t minVertCapacity = SIZE_MAX;
for (size_t i = 0; i < componentsPerTFVert.size(); i++) {
const auto& indexedBinding = mIndexedBindings[i];
@@ -67,7 +58,7 @@ void WebGLTransformFeedback::BeginTransformFeedback(GLenum primMode) {
"No buffer attached to required transform"
" feedback index %u.",
(uint32_t)i);
- return;
+ return false;
}
for (const auto iBound : IntegerRange(mIndexedBindings.size())) {
@@ -76,7 +67,7 @@ void WebGLTransformFeedback::BeginTransformFeedback(GLenum primMode) {
mContext->GenErrorIllegalUse(
LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<uint32_t>(i),
LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<uint32_t>(iBound));
- return;
+ return false;
}
}
@@ -84,6 +75,30 @@ void WebGLTransformFeedback::BeginTransformFeedback(GLenum primMode) {
minVertCapacity = std::min(minVertCapacity, vertCapacity);
}
+ mActive_VertCapacity = minVertCapacity;
+
+ return true;
+}
+
+void WebGLTransformFeedback::BeginTransformFeedback(GLenum primMode) {
+ if (mIsActive) return mContext->ErrorInvalidOperation("Already active.");
+
+ switch (primMode) {
+ case LOCAL_GL_POINTS:
+ case LOCAL_GL_LINES:
+ case LOCAL_GL_TRIANGLES:
+ break;
+ default:
+ mContext->ErrorInvalidEnum(
+ "`primitiveMode` must be one of POINTS, LINES, or"
+ " TRIANGLES.");
+ return;
+ }
+
+ if (!PrepareTransformFeedback()) {
+ return;
+ }
+
////
const auto& gl = mContext->gl;
@@ -94,10 +109,9 @@ void WebGLTransformFeedback::BeginTransformFeedback(GLenum primMode) {
mIsActive = true;
MOZ_ASSERT(!mIsPaused);
- mActive_Program = prog;
+ mActive_Program = mContext->mCurrentProgram;
mActive_PrimMode = primMode;
mActive_VertPosition = 0;
- mActive_VertCapacity = minVertCapacity;
////
@@ -158,6 +172,12 @@ void WebGLTransformFeedback::ResumeTransformFeedback() {
return;
}
+ // Re-run prepare in case some of the buffers have been modified
+ // while the TF was paused.
+ if (!PrepareTransformFeedback()) {
+ return;
+ }
+
////
const auto& gl = mContext->gl;
diff --git a/dom/canvas/WebGLTransformFeedback.h b/dom/canvas/WebGLTransformFeedback.h
index 30cc8fa34b9..593513589c5 100644
--- a/dom/canvas/WebGLTransformFeedback.h
+++ b/dom/canvas/WebGLTransformFeedback.h
@@ -46,6 +46,7 @@ class WebGLTransformFeedback final : public WebGLContextBoundObject {
private:
~WebGLTransformFeedback() override;
+ bool PrepareTransformFeedback();
public:
bool IsActiveAndNotPaused() const { return mIsActive && !mIsPaused; }