High CVSS 5.9 firefox OOB 🔧 Commit mapped

Overview

High
Severity
5.9
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionModification of specific WebGL shader attributes could trigger an out-of-bounds read, which, when chained with other vulnerabilities, could be used to escalate privileges.<br>*This bug only affects Firefox for macOS. Other versions of Firefox are unaffected.*
ComponentDOM
Bug ClassOOB
Tracker1937097
Fix commitfef871d1edf0 (firefox) +110/-108
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N
CISA KEVNot listed
Creditedun3xploitable & GF
Disclosed2025-04-29

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 in WebGLContext::DoFakeVertexAttrib0 (dom/canvas/WebGLContextDraw.cpp).
`WebGLVertexAttrib0Status`
Enum describing what attrib 0 needs: Default (real buffer), EmulatedInitializedArray (buffer must contain the generic value), or EmulatedUninitializedArray (buffer need only exist/be large enough, contents unused).
Buffer-size bookkeeping
Old code tracked a single mFakeVertexAttrib0BufferObjectSize and a bool mFakeVertexAttrib0DataDefined. The patch splits this into mFakeVertexAttrib0BufferAllocSize (bytes allocated) and mFakeVertexAttrib0BufferInitializedSize (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 the first offset.

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.

Key insight
The emulated attrib-0 buffer’s “allocated” and “initialized” sizes were conflated, so a draw could bind a buffer that was large enough but not filled (or sized without the instanced 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

  1. WebGL context Attacker JS obtains a WebGL context (inferred standard web reachability; the diff is renderer draw code).
  2. 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.
  3. Prime under-initialized state Via a sequence of draws (including EmulatedUninitializedArray then EmulatedInitializedArray, or an instanced draw with first != 0), the attacker causes the fake buffer to be reused while under-initialized or under-sized for the fetched vertex range.
  4. Issue draw A drawArraysInstanced/drawElementsInstanced fetches beyond the initialized/allocated range of the bound fake buffer.
  5. 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

The code executes in the renderer/GPU-serving WebGL path and the bug is macOS-only (other platforms unaffected per the advisory). No elevated privilege is needed — ordinary WebGL from web content reaches it. The substantiated primitive is an out-of-bounds read (CWE-125; CVSS confidentiality-only 5.9) whose value leaks into rendered/readback data; code execution or sandbox escape is only reached by chaining with other vulnerabilities and is (inferred). Gating: the shader/VAO configuration must force fake-attrib-0 emulation with a non-trivial first/vertex range.

Changed Functions

FunctionChangeNotes
if
dom/canvas/WebGLContextDraw.cpp
modified
for
dom/canvas/WebGLContextDraw.cpp
modified
switch
dom/canvas/WebGLContextDraw.cpp
modified
while
dom/canvas/WebGLContextDraw.cpp
modified

Files Changed

  • dom/canvas/WebGLContext.h
  • dom/canvas/WebGLContextDraw.cpp

Audit Directions

  • Alloc-vs-initialized conflation
    Audit other WebGL emulation buffers (client-side arrays, element-index emulation) for a single size field that conflates allocation with written contents.
  • Offset-aware sizing
    Review all draw paths that use first/driverFirst and instanced divisors to ensure emulated buffers are sized for the full fetched vertex range, not just the vertex count.
  • Scope-exit cleanup ordering
    Examine other MakeScopeExit/undo patterns in WebGLContextDraw.cpp for release/ordering mistakes that could leave stale bindings.
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
Loading diff…