Moderate firefox Logic Error 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionPrivilege escalation due to uninitialized memory in the Graphics: CanvasWebGL component
ComponentDOM
Bug ClassLogic Error
Tracker2061499
Fix commite965d74906f3 (firefox) +473/-40
CISA KEVNot listed
CreditedMozilla
Disclosed2026-09-15

Changed Functions

FunctionChangeNotes
if
dom/canvas/WebGLTextureUpload.cpp
modified
UploadInitMemTracker
dom/canvas/WebGLTextureUpload.cpp
modified
for
dom/canvas/WebGLTextureUpload.cpp
modified
UnmappableSourceSurface
dom/canvas/gtest/TestWebGLTextureUploadInit.cpp
modified
WebGLTextureUploadInit
dom/canvas/gtest/TestWebGLTextureUploadInit.cpp
modified
if
dom/canvas/gtest/TestWebGLTextureUploadInit.cpp
modified
TEST_F
dom/canvas/gtest/TestWebGLTextureUploadInit.cpp
modified

Files Changed

  • dom/canvas/WebGLTextureUpload.cpp
  • dom/canvas/gtest/TestWebGLTextureUploadInit.cpp
  • dom/canvas/gtest/moz.build
diff --git a/dom/canvas/WebGLTextureUpload.cpp b/dom/canvas/WebGLTextureUpload.cpp
index a9d2d4e8321..ca607fb60f1 100644
--- a/dom/canvas/WebGLTextureUpload.cpp
+++ b/dom/canvas/WebGLTextureUpload.cpp
@@ -526,51 +526,108 @@ static bool DoChannelsMatchForCopyTexImage(const webgl::FormatInfo* srcFormat,
   }
 }
 
-static bool EnsureImageDataInitializedForUpload(
-    WebGLTexture* tex, TexImageTarget target, uint32_t level,
-    const uvec3& offset, const uvec3& size, webgl::ImageInfo* imageInfo,
-    bool* const out_expectsInit = nullptr) {
-  if (out_expectsInit) {
-    *out_expectsInit = false;
+// Tracker for uninitialized image memory uploads. For partial image uploads
+// this is used to zero slice memory and mark it as initialized. For full image
+// uploads this is used to mark slice memory as initialized after a successful
+// upload.
+class UploadInitMemTracker {
+ private:
+  const detail::IntegerRange<uint32_t> sliceRange;
+  const webgl::ImageInfo* imageInfo;
+
+  enum class UploadMem {
+    // Memory is initialized, no action needed.
+    Initialized,
+    // Uninitialized and writing a partial image, needs to be zeroed to prevent
+    // reading uninitialized memory.
+    NeedsPartial,
+    // Uninitialized and writing a full image, all memory should be overwritten
+    // therefore zeroing unnecessary if successful.
+    NeedsFull,
+  } uploadInitState;
+
+  UploadMem UploadInitState(const uvec3& size) {
+    if (!imageInfo->mUninitializedSlices) {
+      return UploadMem::Initialized;
+    } else if (size.x == imageInfo->mWidth && size.y == imageInfo->mHeight) {
+      auto& isSliceUninit = *imageInfo->mUninitializedSlices;
+      for (const auto i : sliceRange) {
+        if (isSliceUninit[i]) {
+          return UploadMem::NeedsFull;
+        }
+      }
+      return UploadMem::Initialized;
+    } else {
+      return UploadMem::NeedsPartial;
+    }
   }
-  if (!imageInfo->mUninitializedSlices) return true;
 
-  if (size.x == imageInfo->mWidth && size.y == imageInfo->mHeight) {
-    bool expectsInit = false;
-    auto& isSliceUninit = *imageInfo->mUninitializedSlices;
-    for (const auto z : IntegerRange(offset.z, offset.z + size.z)) {
-      if (!isSliceUninit[z]) continue;
-      expectsInit = true;
-      isSliceUninit[z] = false;
+ public:
+  UploadInitMemTracker(const uvec3& offset, const uvec3& size,
+                       webgl::ImageInfo* imageInfo)
+      : sliceRange(IntegerRange(offset.z, offset.z + size.z)),
+        imageInfo(imageInfo),
+        uploadInitState(UploadInitState(size)) {}
+
+  // Zero-initializes the target image and clears the uninitialized slices.
+  bool EnsureImageDataZeroedBeforePartialUpload(WebGLTexture* tex,
+                                                TexImageTarget target,
+                                                uint32_t level) {
+    if (uploadInitState != UploadMem::NeedsPartial) {
+      return true;
     }
-    if (out_expectsInit) {
-      *out_expectsInit = expectsInit;
+
+    WebGLContext* webgl = tex->mContext;
+    webgl->GenerateWarning(
+        "Texture has not been initialized prior to a"
+        " partial upload, forcing the browser to clear it."
+        " This may be slow.");
+
+    if (!tex->EnsureImageDataInitialized(target, level)) {
+      MOZ_ASSERT(false, "Unexpected failure to init image data.");
+      return false;
     }
 
-    if (!expectsInit) return true;
+    uploadInitState = UploadMem::Initialized;
+
+    return true;
+  }
+
+  // True when awaiting confirmation that a full upload was successful to clear
+  // the associated uninitialized slice tracking.
+  bool ExpectsFullUpload() { return uploadInitState == UploadMem::NeedsFull; }
+
+  // Marks slices associated to a full upload as initialized, clearing
+  // uninitialized slices if all slices are initialized.
+  void ClearSliceUninitAfterFullUpload() {
+    if (uploadInitState != UploadMem::NeedsFull) {
+      return;
+    }
+
+    uploadInitState = UploadMem::Initialized;
+
+    if (!imageInfo->mUninitializedSlices) return;
 
+    // Mark upload associated slices as initialized.
+    bool sliceWasInitialized = false;
+    auto& isSliceUninit = *imageInfo->mUninitializedSlices;
+    for (const auto i : sliceRange) {
+      sliceWasInitialized |= isSliceUninit[i];
+      isSliceUninit[i] = false;
+    }
+
+    if (!sliceWasInitialized) return;
+
+    // Clear uninitialized slices if they're all initialized.
     bool hasUninitialized = false;
-    for (const auto z : IntegerRange(imageInfo->mDepth)) {
-      hasUninitialized |= isSliceUninit[z];
+    for (const auto isUninit : isSliceUninit) {
+      hasUninitialized |= isUninit;
     }
     if (!hasUninitialized) {
       imageInfo->mUninitializedSlices.reset();
     }
-    return true;
   }
-
-  WebGLContext* webgl = tex->mContext;
-  webgl->GenerateWarning(
-      "Texture has not been initialized prior to a"
-      " partial upload, forcing the browser to clear it."
-      " This may be slow.");
-  if (!tex->EnsureImageDataInitialized(target, level)) {
-    MOZ_ASSERT(false, "Unexpected failure to init image data.");
-    return false;
-  }
-
-  return true;
-}
+};
 
 //////////////////////////////////////////////////////////////////////////////////////////
 //////////////////////////////////////////////////////////////////////////////////////////
@@ -1072,6 +1129,7 @@ void WebGLTexture::TexImage(uint32_t level, GLenum respecFormat,
 
   Maybe<webgl::ImageInfo> newImageInfo;
   bool isRespec = false;
+  Maybe<UploadInitMemTracker> uploadInitTracker;
   if (respecFormat) {
     // It's tempting to do allocation first, and TexSubImage second, but this is
     // generally slower.
@@ -1089,8 +1147,10 @@ void WebGLTexture::TexImage(uint32_t level, GLenum respecFormat,
       mContext->ErrorInvalidValue("`source` cannot be null.");
       return;
     }
-    if (!EnsureImageDataInitializedForUpload(this, imageTarget, level, offset,
-                                             size, imageInfo)) {
+
+    uploadInitTracker.emplace(offset, size, imageInfo);
+    if (!uploadInitTracker->EnsureImageDataZeroedBeforePartialUpload(
+            this, imageTarget, level)) {
       return;
     }
   }
@@ -1129,6 +1189,9 @@ void WebGLTexture::TexImage(uint32_t level, GLenum respecFormat,
     return;
   }
 
+  uploadInitTracker.apply(
+      [](auto& uit) { uit.ClearSliceUninitAfterFullUpload(); });
+
   ////////////////////////////////////
   // Update our specification data?
 
@@ -1295,9 +1358,11 @@ void WebGLTexture::CompressedTexImage(bool sub, GLenum imageTarget,
   ////////////////////////////////////
   // Do the thing!
 
+  Maybe<UploadInitMemTracker> uploadInitTracker;
   if (sub) {
-    if (!EnsureImageDataInitializedForUpload(this, imageTarget, level, offset,
-                                             size, imageInfo)) {
+    uploadInitTracker.emplace(UploadInitMemTracker(offset, size, imageInfo));
+    if (!uploadInitTracker->EnsureImageDataZeroedBeforePartialUpload(
+            this, imageTarget, level)) {
       return;
     }
   }
@@ -1345,6 +1410,9 @@ void WebGLTexture::CompressedTexImage(bool sub, GLenum imageTarget,
     return;
   }
 
+  uploadInitTracker.apply(
+      [](auto& uit) { uit.ClearSliceUninitAfterFullUpload(); });
+
   ////////////////////////////////////
   // Update our specification data?
 
@@ -1881,12 +1949,16 @@ void WebGLTexture::CopyTexImage(GLenum imageTarget, uint32_t level,
 
   const bool isSubImage = !respecFormat;
   bool expectsInit = true;
+  Maybe<UploadInitMemTracker> uploadInitTracker;
+
   if (isSubImage) {
-    if (!EnsureImageDataInitializedForUpload(this, imageTarget, level,
-                                             dstOffset, size, imageInfo,
-                                             &expectsInit)) {
+    uploadInitTracker.emplace(UploadInitMemTracker(dstOffset, size, imageInfo));
+    if (!uploadInitTracker->EnsureImageDataZeroedBeforePartialUpload(
+            this, imageTarget, level)) {
       return;
     }
+
+    expectsInit = uploadInitTracker->ExpectsFullUpload();
   }
 
   if (!DoCopyTexOrSubImage(mContext, isSubImage, expectsInit, this, imageTarget,
@@ -1902,6 +1974,9 @@ void WebGLTexture::CopyTexImage(GLenum imageTarget, uint32_t level,
   ////////////////////////////////////
   // Update our specification data?
 
+  uploadInitTracker.apply(
+      [](auto& uit) { uit.ClearSliceUninitAfterFullUpload(); });
+
   if (respecFormat) {
     constexpr auto uninitializedSlices = std::nullopt;
     const webgl::ImageInfo newImageInfo{dstUsage, size.x, size.y, size.z,
diff --git a/dom/canvas/gtest/TestWebGLTextureUploadInit.cpp b/dom/canvas/gtest/TestWebGLTextureUploadInit.cpp
new file mode 100644
index 00000000000..8de2fcee7ff
--- /dev/null
+++ b/dom/canvas/gtest/TestWebGLTextureUploadInit.cpp
@@ -0,0 +1,357 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <vector>
+
+#include "WebGLContext.h"
+#include "WebGLTexture.h"
+#include "WebGLTypes.h"
+#include "gfxPlatform.h"
+#include "gtest/gtest.h"
+#include "mozilla/gfx/2D.h"
+
+namespace mozilla {
+
+namespace {
+
+constexpr uint32_t kTexSize = 16;
+constexpr uint32_t kSubSize = kTexSize / 2;
+
+constexpr webgl::PackingInfo kPI = {LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE};
+
+constexpr GLenum kDXT5 = LOCAL_GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
+constexpr uint32_t kDXT5BlockSize = 4;
+constexpr uint32_t kDXT5BytesPerBlock = 16;
+
+// CompressedTexImage requires the source buffer size to match exactly.
+uint32_t DXT5ByteCount(const uvec3& aSize) {
+  const auto fnBlocks = [](uint32_t aPixels) {
+    return (aPixels + kDXT5BlockSize - 1) / kDXT5BlockSize;
+  };
+  return fnBlocks(aSize.x) * fnBlocks(aSize.y) * kDXT5BytesPerBlock * aSize.z;
+}
+
+// Mock source surface which passes upload validation but fails when mapped for
+// read, so the upload fails only after the point at which slices used to be
+// marked initialized. Returning real data here would defeat the tests below.
+class UnmappableSourceSurface final : public gfx::DataSourceSurface {
+ public:
+  MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(UnmappableSourceSurface, override)
+
+  explicit UnmappableSourceSurface(uint32_t aSize) : mSize(aSize, aSize) {}
+
+  gfx::IntSize GetSize() const override { return mSize; }
+  gfx::SurfaceFormat GetFormat() const override {
+    return gfx::SurfaceFormat::B8G8R8A8;
+  }
+  uint8_t* GetData() override { return nullptr; }
+  int32_t Stride() override { return 0; }
+
+ private:
+  const gfx::IntSize mSize;
+};
+
+}  // namespace
+
+// Note: these tests only cover upload failure for TexImage, because it is the
+// only one of the three UploadInitMemTracker call sites with an injectable
+// failure seam (the caller-supplied source surface above). CompressedTexImage
+// and CopyTexImage take only PODs and fail solely on driver errors, and both
+// Truncate() the level on failure, which resets mUninitializedSlices anyway.
+class WebGLTextureUploadInit : public ::testing::Test {
+ protected:
+  RefPtr<WebGLContext> mWebgl;
+  RefPtr<WebGLTexture> mTex;
+
+  void SetUp() override {
+    gfxPlatform::GetPlatform();
+
+    webgl::InitContextDesc desc;
+    desc.isWebgl2 = true;
+    desc.size = {kTexSize, kTexSize};
+    webgl::InitContextResult initResult;
+
+    mWebgl = WebGLContext::Create(nullptr, desc, &initResult);
+    if (!mWebgl) {
+      GTEST_SKIP() << "No usable WebGL context in this environment: "
+                   << initResult.error->c_str();
+    }
+
+    mTex = mWebgl->CreateTexture();
+    ASSERT_TRUE(mTex);
+  }
+
+  void ExpectNoError(const char* aWhen) {
+    EXPECT_EQ(mWebgl->GetError(), 0u) << aWhen;
+  }
+
+  void ExpectError(GLenum aExpected, const char* aWhen) {
+    EXPECT_EQ(mWebgl->GetError(), aExpected) << aWhen;
+  }
+
+  bool EnableS3TC() {
+    constexpr auto id = WebGLExtensionID::WEBGL_compressed_texture_s3tc;
+    if (!mWebgl->IsExtensionSupported(id)) {
+      return false;
+    }
+    mWebgl->RequestExtension(id);
+    return true;
+  }
+
+  void BindAndAllocUninitialized(GLenum aTexTarget, uint32_t aDepth = 1) {
+    mWebgl->BindTexture(aTexTarget, mTex);
+
+    webgl::TexUnpackBlobDesc alloc = {aTexTarget, {kTexSize, kTexSize, aDepth}};
+    alloc.unpacking.rowLength = kTexSize;
+    alloc.unpacking.imageHeight = kTexSize;
+    mWebgl->TexImage(0, LOCAL_GL_RGBA, {0, 0, 0}, kPI, alloc);
+  }
+
+  // TexStorage is the only way to get an uninitialized compressed image; a
+  // non-sub CompressedTexImage always ends up with no uninitialized slices.
+  void BindAndAllocUninitializedCompressed() {
+    mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, mTex);
+    mWebgl->TexStorage(LOCAL_GL_TEXTURE_2D, 1, kDXT5, {kTexSize, kTexSize, 1});
+  }
+
+  void TexSubImageFromBytes(GLenum aImageTarget, const uvec3& aOffset,
+                            const uvec3& aSize) {
+    const std::vector<uint8_t> bytes(
+        size_t(aSize.x) * aSize.y * aSize.z * sizeof(uint32_t), 0x7f);
+
+    webgl::TexUnpackBlobDesc sub = {aImageTarget, aSize};
+    sub.cpuData = Some(Span<const uint8_t>(bytes));
+    sub.unpacking.alignmentInTypeElems = 1;
+    sub.unpacking.rowLength = aSize.x;
+    sub.unpacking.imageHeight = aSize.y;
+    mWebgl->TexImage(0, 0, aOffset, kPI, sub);
+  }
+
+  void TexSubImageFromUnmappableSurface(GLenum aImageTarget,
+                                        const uvec3& aOffset, uint32_t aSize) {
+    webgl::TexUnpackBlobDesc sub = {aImageTarget, {aSize, aSize, 1}};
+    sub.sourceSurf = new UnmappableSourceSurface(aSize);
+    sub.structuredSrcSize = Some(uvec2{aSize, aSize});
+    sub.unpacking.alignmentInTypeElems = 1;
+    sub.unpacking.rowLength = aSize;
+    sub.unpacking.imageHeight = aSize;
+    mWebgl->TexImage(0, 0, aOffset, kPI, sub);
+  }
+
+  void CompressedSubImageFromBytes(const uvec3& aOffset, const uvec3& aSize) {
+    const std::vector<uint8_t> bytes(DXT5ByteCount(aSize), 0x7f);
+    mWebgl->CompressedTexImage(
+        true, LOCAL_GL_TEXTURE_2D, 0, kDXT5, aOffset, aSize,
+        Range<const uint8_t>(bytes.data(), bytes.size()), 0, Nothing());
+  }
+};
+
+TEST_F(WebGLTextureUploadInit, FullUploadFailLeavesSliceMarkedUninitialized) {
+  BindAndAllocUninitialized(LOCAL_GL_TEXTURE_2D);
+
+  const auto& imageInfo = mTex->ImageInfoAt(LOCAL_GL_TEXTURE_2D, 0);
+  ASSERT_TRUE(imageInfo.IsDefined());
+  ASSERT_TRUE(imageInfo.mUninitializedSlices);
+  ASSERT_TRUE((*imageInfo.mUninitializedSlices)[0]);
+  ExpectNoError("Texture allocation should not error.");
+
+  TexSubImageFromUnmappableSurface(LOCAL_GL_TEXTURE_2D, {0, 0, 0}, kTexSize);
+
+  ExpectError(LOCAL_GL_OUT_OF_MEMORY,
+              "Upload should have failed while mapping the source surface, "
+              "rather than during validation.");
... (file diff truncated)
Loading diff…