CVE-2025-4082
Overview
Background
- Fake vertex attrib 0
- WebGL requires attribute 0 to be enabled for a draw; when the shader uses a generic (constant)
gl_Vertex/attrib 0 value, the implementation emulates it by binding a temporary buffer (mFakeVertexAttrib0BufferObject) filled with the generic value, sized to cover the draw’s vertex range. Logic lives inWebGLContext::DoFakeVertexAttrib0(dom/canvas/WebGLContextDraw.cpp). - `WebGLVertexAttrib0Status`
- Enum describing what attrib 0 needs:
Default(real buffer),EmulatedInitializedArray(buffer must contain the generic value), orEmulatedUninitializedArray(buffer need only exist/be large enough, contents unused). - Buffer-size bookkeeping
- Old code tracked a single
mFakeVertexAttrib0BufferObjectSizeand abool mFakeVertexAttrib0DataDefined. The patch splits this intomFakeVertexAttrib0BufferAllocSize(bytes allocated) andmFakeVertexAttrib0BufferInitializedSize(bytes actually written with generic data). - `DrawArraysInstanced` with `first != 0`
- When
driverFirst != first, WebGL applies an instanced-attrib-fetch workaround; the fake attrib 0 buffer must still cover the fetched vertex range for the draw, including thefirstoffset.
Root Cause Analysis
The emulated attrib-0 buffer could be bound to a draw while being smaller than, or less initialized than, the range the GPU would fetch — producing an out-of-bounds read by the driver. Two defects combined. First, the buffer was allocated (fBufferData sized to dataSize) but for EmulatedUninitializedArray the function returned early (return true) before uploading any contents, and the single mFakeVertexAttrib0BufferObjectSize/mFakeVertexAttrib0DataDefined state conflated “allocated” with “initialized.” A later draw requiring an initialized array could reuse a buffer that was only allocated, so the memcmp(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data, bytesPerVert) fast-path would wrongly conclude the buffer already held the right data and skip the upload, leaving the GPU to read uninitialized/under-filled memory. Second, DoFakeVertexAttrib0 was called with a fakeVertCount computed without consistently accounting for the first offset used in the instanced-fetch workaround, and the old UndoFakeVertexAttrib0/undoAttrib0 scope-exit bookkeeping (with MOZ_RELEASE_ASSERT(whatDoesAttrib0Need != Default)) tangled the cleanup ordering, so the bound fake buffer could be too small for the fetched vertex range, leading to an OOB read in the driver on macOS.
The patch rewrites the sizing/initialization logic: allocation and initialization are tracked separately (AllocSize vs InitializedSize); fBufferData is reallocated only when AllocSize < dataSize (now STATIC_DRAW), and a new FillWithGenericVertexAttrib0Data lambda builds a full dataSize-byte upload buffer (one DataPerVertT per vertex, static_assert(sizeof==4*float), MOZ_RELEASE_ASSERT(dataSize % sizeof(DataPerVertT)==0)) and uploads it with fBufferSubData, setting InitializedSize = dataSize; the fast-path now requires dataSize <= InitializedSize before skipping. The instanced-fetch workaround is refactored into explicit needsFix_InstancedUserAttribFetch/needsFix_FakeVertexAttrib0 scope-exit handlers in DrawArraysInstanced/DrawElementsInstanced, and UndoFakeVertexAttrib0 is removed in favor of mBoundVertexArray->DoVertexAttrib(0). This guarantees the bound fake buffer is both large enough for the fetched range and fully initialized before the draw.
first offset), causing the GPU to read out of bounds; the fix tracks alloc vs. initialized size separately and always fills the full fetched range.Attack Path
- WebGL context Attacker JS obtains a WebGL context (inferred standard web reachability; the diff is renderer draw code).
- Shape attrib 0
The page sets a program/VAO that needs a generic (constant) attrib 0, driving
WhatDoesVertexAttrib0Need()to an emulated status and modifying attrib base type/divisor so the emulated buffer is engaged. - Prime under-initialized state
Via a sequence of draws (including
EmulatedUninitializedArraythenEmulatedInitializedArray, or an instanced draw withfirst != 0), the attacker causes the fake buffer to be reused while under-initialized or under-sized for the fetched vertex range. - Issue draw
A
drawArraysInstanced/drawElementsInstancedfetches beyond the initialized/allocated range of the bound fake buffer. - Primitive Out-of-bounds read of GPU/driver memory on macOS, which the advisory notes can be chained with other bugs toward privilege escalation (escalation inferred; the diff substantiates only the OOB read).
Impact Assessment
first/vertex range.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/canvas/WebGLContextDraw.cpp |
modified | |
fordom/canvas/WebGLContextDraw.cpp |
modified | |
switchdom/canvas/WebGLContextDraw.cpp |
modified | |
whiledom/canvas/WebGLContextDraw.cpp |
modified |
Files Changed
dom/canvas/WebGLContext.hdom/canvas/WebGLContextDraw.cpp
Audit Directions
- Alloc-vs-initialized conflationAudit other WebGL emulation buffers (client-side arrays, element-index emulation) for a single size field that conflates allocation with written contents.
- Offset-aware sizingReview all draw paths that use
first/driverFirstand instanced divisors to ensure emulated buffers are sized for the full fetched vertex range, not just the vertex count. - Scope-exit cleanup orderingExamine other
MakeScopeExit/undo patterns inWebGLContextDraw.cppfor release/ordering mistakes that could leave stale bindings.
Patch
diff --git a/dom/canvas/WebGLContext.h b/dom/canvas/WebGLContext.h
index 334e92b5981..8b786ebf61c 100644
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -929,7 +929,6 @@ class WebGLContext : public VRefCounted, public SupportsWeakPtr {
WebGLVertexAttrib0Status WhatDoesVertexAttrib0Need() const;
bool DoFakeVertexAttrib0(uint64_t fakeVertexCount,
WebGLVertexAttrib0Status whatDoesAttrib0Need);
- void UndoFakeVertexAttrib0();
bool mResetLayer = true;
bool mOptionsFrozen = false;
@@ -1220,8 +1219,8 @@ class WebGLContext : public VRefCounted, public SupportsWeakPtr {
CacheInvalidator mGenericVertexAttribTypeInvalidator;
GLuint mFakeVertexAttrib0BufferObject = 0;
- intptr_t mFakeVertexAttrib0BufferObjectSize = 0;
- bool mFakeVertexAttrib0DataDefined = false;
+ intptr_t mFakeVertexAttrib0BufferAllocSize = 0;
+ intptr_t mFakeVertexAttrib0BufferInitializedSize = 0;
alignas(alignof(float)) uint8_t
mGenericVertexAttrib0Data[sizeof(float) * 4] = {};
alignas(alignof(float)) uint8_t
diff --git a/dom/canvas/WebGLContextDraw.cpp b/dom/canvas/WebGLContextDraw.cpp
index 72ef2d47ae4..076d904b0c6 100644
--- a/dom/canvas/WebGLContextDraw.cpp
+++ b/dom/canvas/WebGLContextDraw.cpp
@@ -781,7 +781,9 @@ void WebGLContext::DrawArraysInstanced(const GLenum mode, const GLint first,
driverFirst = 0;
}
}
- if (driverFirst != first) {
+ const bool needsFix_InstancedUserAttribFetch = (driverFirst != first);
+
+ if (needsFix_InstancedUserAttribFetch) {
for (const auto& a : activeAttribs) {
if (a.location == -1) continue;
const auto& binding = mBoundVertexArray->AttribBinding(a.location);
@@ -792,6 +794,21 @@ void WebGLContext::DrawArraysInstanced(const GLenum mode, const GLint first,
gl->fUniform1i(mActiveProgramLinkInfo->webgl_gl_VertexID_Offset, first);
}
+ const auto undoFix_InstancedUserAttribFetch = MakeScopeExit([&]() {
+ if (needsFix_InstancedUserAttribFetch) {
+ gl->fUniform1i(mActiveProgramLinkInfo->webgl_gl_VertexID_Offset, 0);
+
+ for (const auto& a : activeAttribs) {
+ if (a.location == -1) continue;
+ const auto& binding = mBoundVertexArray->AttribBinding(a.location);
+ if (binding.layout.divisor) continue;
+
+ mBoundVertexArray->DoVertexAttrib(a.location, 0);
+ }
+ }
+ });
+
+ // -
{
const auto whatDoesAttrib0Need = WhatDoesVertexAttrib0Need();
@@ -803,22 +820,20 @@ void WebGLContext::DrawArraysInstanced(const GLenum mode, const GLint first,
fakeVertCount = 0;
}
- auto undoAttrib0 = MakeScopeExit([&]() {
- MOZ_RELEASE_ASSERT(whatDoesAttrib0Need !=
- WebGLVertexAttrib0Status::Default);
- UndoFakeVertexAttrib0();
- });
- if (fakeVertCount) {
- if (!DoFakeVertexAttrib0(fakeVertCount, whatDoesAttrib0Need)) {
- error = true;
- undoAttrib0.release();
+ const bool needsFix_FakeVertexAttrib0 = bool(fakeVertCount);
+ const auto undoFix_FakeVertexAttrib0 = MakeScopeExit([&]() {
+ if (needsFix_FakeVertexAttrib0) {
+ mBoundVertexArray->DoVertexAttrib(0);
}
- } else {
- // No fake-verts needed.
- undoAttrib0.release();
+ });
+ if (needsFix_FakeVertexAttrib0) {
+ // fmt::println(FMT_STRING("DoFakeVertexAttrib0(fakeVertCount: {},
+ // whatDoesAttrib0Need: {})"), fakeVertCount, (int)whatDoesAttrib0Need);
+ if (!DoFakeVertexAttrib0(fakeVertCount, whatDoesAttrib0Need)) return;
}
ScopedDrawCallWrapper wrapper(*this);
+
if (vertCount && instanceCount) {
if (HasInstancedDrawing(*this)) {
gl->fDrawArraysInstanced(mode, driverFirst, vertCount, instanceCount);
@@ -829,18 +844,6 @@ void WebGLContext::DrawArraysInstanced(const GLenum mode, const GLint first,
}
}
- if (driverFirst != first) {
- gl->fUniform1i(mActiveProgramLinkInfo->webgl_gl_VertexID_Offset, 0);
-
- for (const auto& a : activeAttribs) {
- if (a.location == -1) continue;
- const auto& binding = mBoundVertexArray->AttribBinding(a.location);
- if (binding.layout.divisor) continue;
-
- mBoundVertexArray->DoVertexAttrib(a.location, 0);
- }
- }
-
Draw_cleanup();
scopedTF.Advance();
}
@@ -1031,27 +1034,19 @@ void WebGLContext::DrawElementsInstanced(const GLenum mode,
// -
- bool error = false;
-
- // -
-
- auto undoAttrib0 = MakeScopeExit([&]() {
- MOZ_RELEASE_ASSERT(whatDoesAttrib0Need !=
- WebGLVertexAttrib0Status::Default);
- UndoFakeVertexAttrib0();
- });
- if (fakeVertCount) {
- if (!DoFakeVertexAttrib0(fakeVertCount, whatDoesAttrib0Need)) {
- error = true;
- undoAttrib0.release();
+ const bool needsFix_FakeVertexAttrib0 = bool(fakeVertCount);
+ const auto undoFix_FakeVertexAttrib0 = MakeScopeExit([&]() {
+ if (needsFix_FakeVertexAttrib0) {
+ mBoundVertexArray->DoVertexAttrib(0);
}
- } else {
- // No fake-verts needed.
- undoAttrib0.release();
+ });
+ if (needsFix_FakeVertexAttrib0) {
+ if (!DoFakeVertexAttrib0(fakeVertCount, whatDoesAttrib0Need)) return;
}
// -
+ bool error = false;
const ScopedResolveTexturesForDraw scopedResolve(this, &error);
if (error) return;
@@ -1189,35 +1184,10 @@ bool WebGLContext::DoFakeVertexAttrib0(
mAlreadyWarnedAboutFakeVertexAttrib0 = true;
}
- gl->fEnableVertexAttribArray(0);
- {
- const auto& attrib0 = mBoundVertexArray->AttribBinding(0);
- if (attrib0.layout.divisor) {
- gl->fVertexAttribDivisor(0, 0);
- }
- }
-
if (!mFakeVertexAttrib0BufferObject) {
gl->fGenBuffers(1, &mFakeVertexAttrib0BufferObject);
- mFakeVertexAttrib0BufferObjectSize = 0;
- }
- gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mFakeVertexAttrib0BufferObject);
-
- ////
-
- switch (mGenericVertexAttribTypes[0]) {
- case webgl::AttribBaseType::Boolean:
- case webgl::AttribBaseType::Float:
- gl->fVertexAttribPointer(0, 4, LOCAL_GL_FLOAT, false, 0, 0);
- break;
-
- case webgl::AttribBaseType::Int:
- gl->fVertexAttribIPointer(0, 4, LOCAL_GL_INT, 0, 0);
- break;
-
- case webgl::AttribBaseType::Uint:
- gl->fVertexAttribIPointer(0, 4, LOCAL_GL_UNSIGNED_INT, 0, 0);
- break;
+ mFakeVertexAttrib0BufferAllocSize = 0;
+ mFakeVertexAttrib0BufferInitializedSize = 0;
}
////
@@ -1245,71 +1215,104 @@ bool WebGLContext::DoFakeVertexAttrib0(
}
const auto dataSize = checked_dataSize.value();
- if (mFakeVertexAttrib0BufferObjectSize < dataSize) {
+ if (mFakeVertexAttrib0BufferAllocSize < dataSize) {
+ gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mFakeVertexAttrib0BufferObject);
gl::GLContext::LocalErrorScope errorScope(*gl);
gl->fBufferData(LOCAL_GL_ARRAY_BUFFER, dataSize, nullptr,
- LOCAL_GL_DYNAMIC_DRAW);
+ LOCAL_GL_STATIC_DRAW);
const auto err = errorScope.GetError();
if (err) {
ErrorOutOfMemory(
- "Failed to allocate fake vertex attrib 0 data: %zi bytes", dataSize);
+ "Failed to allocate fake vertex attrib 0 buffer: %zi bytes",
+ dataSize);
return false;
}
- mFakeVertexAttrib0BufferObjectSize = dataSize;
- mFakeVertexAttrib0DataDefined = false;
+ mFakeVertexAttrib0BufferAllocSize = dataSize;
+ mFakeVertexAttrib0BufferInitializedSize = 0;
}
- if (whatDoesAttrib0Need ==
- WebGLVertexAttrib0Status::EmulatedUninitializedArray)
- return true;
-
////
- if (mFakeVertexAttrib0DataDefined &&
- memcmp(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data, bytesPerVert) ==
- 0) {
+ const auto FillWithGenericVertexAttrib0Data = [&]() {
+ if (dataSize <= mFakeVertexAttrib0BufferInitializedSize &&
+ memcmp(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data,
+ bytesPerVert) == 0) {
+ return true;
+ }
+ memcpy(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data, bytesPerVert);
+ mFakeVertexAttrib0BufferInitializedSize = 0;
+
+ using DataPerVertT = decltype(mFakeVertexAttrib0Data);
+ static_assert(sizeof(DataPerVertT) == 4 * sizeof(float));
+ MOZ_RELEASE_ASSERT(dataSize % sizeof(DataPerVertT) == 0);
+ const size_t vertCount = dataSize / sizeof(DataPerVertT);
+ const auto uploadData = std::unique_ptr<DataPerVertT[]>{
+ new (std::nothrow) DataPerVertT[vertCount]};
+ if (!uploadData) {
+ ErrorOutOfMemory("Failed to allocate fake vertex attrib 0 upload data.");
+ return false;
+ }
+ const auto uploadMutSpan = Span{uploadData.get(), vertCount};
+ for (auto& vert : uploadMutSpan) {
+ memcpy(&vert, mFakeVertexAttrib0Data, bytesPerVert);
+ }
+
+ {
+ gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mFakeVertexAttrib0BufferObject);
+ gl::GLContext::LocalErrorScope errorScope(*gl);
+
+ const auto uploadBytes = AsBytes(uploadMutSpan);
+ MOZ_ASSERT(uploadBytes.size() == (size_t)dataSize);
+ gl->fBufferSubData(LOCAL_GL_ARRAY_BUFFER, 0, uploadBytes.size(),
+ uploadBytes.data());
+
+ const auto err = errorScope.GetError();
+ if (err) {
+ ErrorOutOfMemory("Failed to upload fake vertex attrib 0 data.");
+ return false;
+ }
+ }
+
+ mFakeVertexAttrib0BufferInitializedSize = dataSize;
return true;
+ };
+
+ if (whatDoesAttrib0Need ==
+ WebGLVertexAttrib0Status::EmulatedInitializedArray) {
+ if (!FillWithGenericVertexAttrib0Data()) return false;
}
////
- const auto data = UniqueBuffer::Take(malloc(dataSize));
- if (!data) {
- ErrorOutOfMemory("Failed to allocate fake vertex attrib 0 array.");
- return false;
- }
- auto itr = (uint8_t*)data.get();
- const auto itrEnd = itr + dataSize;
- while (itr != itrEnd) {
- memcpy(itr, mGenericVertexAttrib0Data, bytesPerVert);
- itr += bytesPerVert;
+ const auto& attrib0 = mBoundVertexArray->AttribBinding(0);
+ if (attrib0.layout.divisor) {
+ gl->fVertexAttribDivisor(0, 0);
}
- {
- gl::GLContext::LocalErrorScope errorScope(*gl);
+ gl->fEnableVertexAttribArray(0);
- gl->fBufferSubData(LOCAL_GL_ARRAY_BUFFER, 0, dataSize, data.get());
+ gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mFakeVertexAttrib0BufferObject);
+ switch (mGenericVertexAttribTypes[0]) {
+ case webgl::AttribBaseType::Boolean:
+ case webgl::AttribBaseType::Float:
+ gl->fVertexAttribPointer(0, 4, LOCAL_GL_FLOAT, false, 0, 0);
+ break;
- const auto err = errorScope.GetError();
- if (err) {
- ErrorOutOfMemory("Failed to upload fake vertex attrib 0 data.");
- return false;
- }
+ case webgl::AttribBaseType::Int:
+ gl->fVertexAttribIPointer(0, 4, LOCAL_GL_INT, 0, 0);
+ break;
+
+ case webgl::AttribBaseType::Uint:
+ gl->fVertexAttribIPointer(0, 4, LOCAL_GL_UNSIGNED_INT, 0, 0);
+ break;
}
////
- memcpy(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data, bytesPerVert);
- mFakeVertexAttrib0DataDefined = true;
return true;
}
-void WebGLContext::UndoFakeVertexAttrib0() {
- static_assert(IsBufferTargetLazilyBound(LOCAL_GL_ARRAY_BUFFER));
- mBoundVertexArray->DoVertexAttrib(0);
-}
-
} // namespace mozilla