Firefox · DOM
CVE-2026-92052
Logic Error in DOM
Overview
Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/canvas/WebGLTextureUpload.cpp |
modified | |
UploadInitMemTrackerdom/canvas/WebGLTextureUpload.cpp |
modified | |
fordom/canvas/WebGLTextureUpload.cpp |
modified | |
UnmappableSourceSurfacedom/canvas/gtest/TestWebGLTextureUploadInit.cpp |
modified | |
WebGLTextureUploadInitdom/canvas/gtest/TestWebGLTextureUploadInit.cpp |
modified | |
ifdom/canvas/gtest/TestWebGLTextureUploadInit.cpp |
modified | |
TEST_Fdom/canvas/gtest/TestWebGLTextureUploadInit.cpp |
modified |
Files Changed
dom/canvas/WebGLTextureUpload.cppdom/canvas/gtest/TestWebGLTextureUploadInit.cppdom/canvas/gtest/moz.build
Patch
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…
References
On This Page