CVE-2026-106202
Overview
Files Changed
src/tests/gl_tests/TextureTest.cppsrc/libANGLE/renderer/d3d/d3d11/Image11.cppsrc/libANGLE/renderer/d3d/TextureD3D.cppsrc/libANGLE/renderer/d3d/TextureD3D.h
Patch
From 17c57c6066ac4cd7f5b09a7e1a0fccf6be3dec97 Mon Sep 17 00:00:00 2001 From: wangra <[email protected]> Date: Wed, 09 Sep 2026 20:20:22 -0400 Subject: [PATCH] D3D11: Backup texture storage contents on base level changes Preserve subresource data when recreating texture storage on base level change or release, preventing fast-path texture contents from being dropped and avoiding uninitialized memory disclosure under robust initialization. Test: angle_end2end_tests --gtest_filter="SetBaseLevelPreservesFastPathSubImageData" Bug: b/553117809, b/561613771 Change-Id: I4d688fa799e9ca48d1c4bd90216dd4b301726dc4 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8377969 Commit-Queue: Ran Wang <[email protected]> Reviewed-by: Shahbaz Youssefi <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp index 3dbff7c..9d47c3d 100644 --- a/src/libANGLE/renderer/d3d/TextureD3D.cpp +++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp @@ -327,6 +327,39 @@ return (mTexStorage && !internalFormat.compressed); } +bool TextureD3D::isImageSubresourceMatchingStorage(const gl::ImageIndex &index, + const ImageD3D *image, + int storageWidth0, + int storageHeight0, + int storageDepth0, + GLenum storageFormat, + size_t storageLevels) const +{ + if (!image) + { + return false; + } + + if (index.getLevelIndex() < 0 || index.getLevelIndex() >= static_cast<int>(storageLevels)) + { + return false; + } + + if (mState.getType() == gl::TextureType::_2DArray && index.getLayerIndex() >= storageDepth0) + { + return false; + } + + const int storageWidth = std::max(1, storageWidth0 >> index.getLevelIndex()); + const int storageHeight = std::max(1, storageHeight0 >> index.getLevelIndex()); + const int storageDepth = (mState.getType() == gl::TextureType::_3D) + ? std::max(1, storageDepth0 >> index.getLevelIndex()) + : 1; + + return image->getWidth() == storageWidth && image->getHeight() == storageHeight && + image->getDepth() == storageDepth && image->getInternalFormat() == storageFormat; +} + angle::Result TextureD3D::setImageImpl(const gl::Context *context, const gl::ImageIndex &index, GLenum type, @@ -823,27 +856,43 @@ (newStorageWidth != oldStorageWidth || newStorageHeight != oldStorageHeight || newStorageDepth != oldStorageDepth || newStorageFormat != oldStorageFormat)) { - markAllImagesDirty(); + // Iterate over all images and backup contents from texture storage before it is released. + // We need to copy for both: + // 1. Render targets: GPU rendering modified the storage directly. + // 2. Fast-path uploads: TextureStorage11::setData wrote pixel data directly to the GPU + // texture without creating an Image11 staging buffer (leaving the image clean and + // unassociated). If storage is destroyed without backing this up, subsequent storage + // recreation will not re-upload the data, leading to uninitialized memory reads. + const size_t storageLevels = mTexStorage->getLevelCount(); - // Iterate over all images, and backup the content if it's been used as a render target. The - // D3D11 backend can automatically restore images on storage destroy, but it only works for - // images that have been associated with the texture storage before, which is insufficient - // here. - if (mTexStorage->isRenderTarget()) + gl::ImageIndexIterator iterator = imageIterator(); + while (iterator.hasNext()) { - gl::ImageIndexIterator iterator = imageIterator(); - while (iterator.hasNext()) + const gl::ImageIndex index = iterator.next(); + ImageD3D *image = getImage(index); + + RenderTargetD3D *renderTarget = nullptr; + if (mTexStorage->isRenderTarget()) { - const gl::ImageIndex index = iterator.next(); - RenderTargetD3D *renderTarget = nullptr; ANGLE_TRY(mTexStorage->findRenderTarget(context, index, &renderTarget)); - if (renderTarget) - { - ANGLE_TRY(getImage(index)->copyFromTexStorage(context, index, mTexStorage)); - } + } + + const bool dimensionsMatch = isImageSubresourceMatchingStorage( + index, image, oldStorageWidth, oldStorageHeight, oldStorageDepth, + static_cast<GLenum>(oldStorageFormat), storageLevels); + + const bool hasNoStagingCopy = + image && !image->isDirty() && + mState.getImageDesc(index).initState == gl::InitState::Initialized; + + if (dimensionsMatch && (renderTarget || hasNoStagingCopy)) + { + ANGLE_TRY(image->copyFromTexStorage(context, index, mTexStorage)); } } + markAllImagesDirty(); + ANGLE_TRY(releaseTexStorage(context, gl::TexLevelMask())); } @@ -884,7 +933,17 @@ return angle::Result::Continue; } - if (mTexStorage->isRenderTarget()) + bool anyCopyMask = false; + for (const gl::TexLevelMask &mask : copyStorageToImagesMask) + { + if (mask.any()) + { + anyCopyMask = true; + break; + } + } + + if (anyCopyMask) { const GLenum storageFormat = getBaseLevelInternalFormat(); const size_t storageLevels = mTexStorage->getLevelCount(); @@ -894,9 +953,6 @@ { const gl::ImageIndex index = iterator.next(); ImageD3D *image = getImage(index); - const int storageWidth = std::max(1, getLevelZeroWidth() >> index.getLevelIndex()); - const int storageHeight = std::max(1, getLevelZeroHeight() >> index.getLevelIndex()); - bool copyImage = false; if (mState.getType() == gl::TextureType::CubeMap) { @@ -908,10 +964,11 @@ copyImage = copyStorageToImagesMask[0][index.getLevelIndex()]; } - if (image && isImageComplete(index) && image->getWidth() == storageWidth && - image->getHeight() == storageHeight && - image->getInternalFormat() == storageFormat && - index.getLevelIndex() < static_cast<int>(storageLevels) && copyImage) + const bool dimensionsMatch = isImageSubresourceMatchingStorage( + index, image, getLevelZeroWidth(), getLevelZeroHeight(), getBaseLevelStorageDepth(), + storageFormat, storageLevels); + + if (dimensionsMatch && copyImage) { ANGLE_TRY(image->copyFromTexStorage(context, index, mTexStorage)); } diff --git a/src/libANGLE/renderer/d3d/TextureD3D.h b/src/libANGLE/renderer/d3d/TextureD3D.h index 3e3e02e..d44c64e 100644 --- a/src/libANGLE/renderer/d3d/TextureD3D.h +++ b/src/libANGLE/renderer/d3d/TextureD3D.h @@ -232,6 +232,13 @@ virtual angle::Result updateStorage(const gl::Context *context) = 0; bool shouldUseSetData(const gl::ImageIndex &index, const ImageD3D *image) const; + bool isImageSubresourceMatchingStorage(const gl::ImageIndex &index, + const ImageD3D *image, + int storageWidth0, + int storageHeight0, + int storageDepth0, + GLenum storageFormat, + size_t storageLevels) const; angle::Result generateMipmapUsingImages(const gl::Context *context, const GLuint maxLevel); diff --git a/src/libANGLE/renderer/d3d/d3d11/Image11.cpp b/src/libANGLE/renderer/d3d/d3d11/Image11.cpp index c9b1c48..58e0e68 100644 --- a/src/libANGLE/renderer/d3d/d3d11/Image11.cpp +++ b/src/libANGLE/renderer/d3d/d3d11/Image11.cpp @@ -420,8 +420,12 @@ ANGLE_TRY(storage11->getSubresourceIndex(context, imageIndex, &subresourceIndex)); gl::Box sourceBox(0, 0, 0, mWidth, mHeight, mDepth); - return copyWithoutConversion(context, gl::Offset(), sourceBox, *textureHelper, - subresourceIndex); + ANGLE_TRY( + copyWithoutConversion(context, gl::Offset(), sourceBox, *textureHelper, subresourceIndex)); + + disassociateStorage(); + + return angle::Result::Continue; } angle::Result Image11::copyFromFramebuffer(const gl::Context *context, diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp index 5e7b3cd..9a1c19b 100644 --- a/src/tests/gl_tests/TextureTest.cpp +++ b/src/tests/gl_tests/TextureTest.cpp @@ -23416,6 +23416,108 @@ EXPECT_PIXEL_RECT_EQ(0, 0, 128, 1, GLColor::blue); } +// Test that changing base level preserves texture content uploaded via texSubImage2D +// fast path (TextureStorage11::setData) when storage is recreated. +TEST_P(Texture2DTestES3RobustInit, SetBaseLevelPreservesFastPathSubImageData) +{ + constexpr GLsizei kLevel0Size = 8; + constexpr GLsizei kLevel1Size = 4; + + GLTexture tex; + glBindTexture(GL_TEXTURE_2D, tex); + + // Define levels 0 and 1 with null data. + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevel0Size, kLevel0Size, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Size, kLevel1Size, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + EXPECT_GL_NO_ERROR(); + + // Attach level 0 to an FBO and clear to instantiate storage. + GLFramebuffer fbo0; + glBindFramebuffer(GL_FRAMEBUFFER, fbo0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + glClearColor(1.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + EXPECT_GL_NO_ERROR(); + + // Full-coverage texSubImage2D of level 1 (triggers setData fast path). + const std::vector<GLColor> level1Data(kLevel1Size * kLevel1Size, GLColor::blue); + glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, kLevel1Size, kLevel1Size, GL_RGBA, GL_UNSIGNED_BYTE, + level1Data.data()); + EXPECT_GL_NO_ERROR(); + + // Change base level to 5 (causing dimension mismatch with storage, triggering release). + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 5); + EXPECT_GL_NO_ERROR(); + + // Change base level back to 1. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1); + EXPECT_GL_NO_ERROR(); + + // Attach level 1 to an FBO and read pixels. + GLFramebuffer fbo1; + glBindFramebuffer(GL_FRAMEBUFFER, fbo1); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1); + EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + + EXPECT_PIXEL_RECT_EQ(0, 0, kLevel1Size, kLevel1Size, GLColor::blue); +} + +// Test that changing base level preserves texture content when storage is created for sampling +// (not as a render target). +TEST_P(Texture2DTestES3RobustInit, SetBaseLevelPreservesFastPathSubImageDataSampledStorage) +{ + constexpr GLsizei kLevel0Size = 8; + constexpr GLsizei kLevel1Size = 4; + + setUpProgram(); + + GLTexture tex; + glBindTexture(GL_TEXTURE_2D, tex); + + // Define levels 0 and 1 with null data. + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevel0Size, kLevel0Size, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Size, kLevel1Size, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + EXPECT_GL_NO_ERROR(); + + // Sample from the texture in a draw call to instantiate storage without RenderTarget + // flags. + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex); + glUseProgram(mProgram); + glUniform1i(mTexture2DUniformLocation, 0); + drawQuad(mProgram, "position", 0.5f); + EXPECT_GL_NO_ERROR(); + + // Full-coverage texSubImage2D of level 1 (triggers setData fast path). + const std::vector<GLColor> level1Data(kLevel1Size * kLevel1Size, GLColor::blue); + glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, kLevel1Size, kLevel1Size, GL_RGBA, GL_UNSIGNED_BYTE, + level1Data.data()); + EXPECT_GL_NO_ERROR(); + + // Change base level to 5 (causes dimension mismatch, triggers release). + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 5); + EXPECT_GL_NO_ERROR(); + + // Change base level back to 1. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1); + EXPECT_GL_NO_ERROR(); + + // Attach level 1 to an FBO and read pixels. + GLFramebuffer fbo1; + glBindFramebuffer(GL_FRAMEBUFFER, fbo1); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1); + EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER); + + EXPECT_PIXEL_RECT_EQ(0, 0, kLevel1Size, kLevel1Size, GLColor::blue); +} + // Test that robust initialization works when glCopyTexImage2D is outside the bounds of the // framebuffer. TEST_P(Texture2DTestES3RobustInit, CopyTexImageOutOfBounds)
Regression Test / PoC
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 5e7b3cd..9a1c19b 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -23416,6 +23416,108 @@
EXPECT_PIXEL_RECT_EQ(0, 0, 128, 1, GLColor::blue);
}
+// Test that changing base level preserves texture content uploaded via texSubImage2D
+// fast path (TextureStorage11::setData) when storage is recreated.
+TEST_P(Texture2DTestES3RobustInit, SetBaseLevelPreservesFastPathSubImageData)
+{
+ constexpr GLsizei kLevel0Size = 8;
+ constexpr GLsizei kLevel1Size = 4;
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+
+ // Define levels 0 and 1 with null data.
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevel0Size, kLevel0Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Size, kLevel1Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ // Attach level 0 to an FBO and clear to instantiate storage.
+ GLFramebuffer fbo0;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo0);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+ glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ EXPECT_GL_NO_ERROR();
+
+ // Full-coverage texSubImage2D of level 1 (triggers setData fast path).
+ const std::vector<GLColor> level1Data(kLevel1Size * kLevel1Size, GLColor::blue);
+ glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, kLevel1Size, kLevel1Size, GL_RGBA, GL_UNSIGNED_BYTE,
+ level1Data.data());
+ EXPECT_GL_NO_ERROR();
+
+ // Change base level to 5 (causing dimension mismatch with storage, triggering release).
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 5);
+ EXPECT_GL_NO_ERROR();
+
+ // Change base level back to 1.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ EXPECT_GL_NO_ERROR();
+
+ // Attach level 1 to an FBO and read pixels.
+ GLFramebuffer fbo1;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo1);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ EXPECT_PIXEL_RECT_EQ(0, 0, kLevel1Size, kLevel1Size, GLColor::blue);
+}
+
+// Test that changing base level preserves texture content when storage is created for sampling
+// (not as a render target).
+TEST_P(Texture2DTestES3RobustInit, SetBaseLevelPreservesFastPathSubImageDataSampledStorage)
+{
+ constexpr GLsizei kLevel0Size = 8;
+ constexpr GLsizei kLevel1Size = 4;
+
+ setUpProgram();
+
+ GLTexture tex;
+ glBindTexture(GL_TEXTURE_2D, tex);
+
+ // Define levels 0 and 1 with null data.
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kLevel0Size, kLevel0Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+ glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kLevel1Size, kLevel1Size, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+ nullptr);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ EXPECT_GL_NO_ERROR();
+
+ // Sample from the texture in a draw call to instantiate storage without RenderTarget
+ // flags.
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, tex);
+ glUseProgram(mProgram);
+ glUniform1i(mTexture2DUniformLocation, 0);
+ drawQuad(mProgram, "position", 0.5f);
+ EXPECT_GL_NO_ERROR();
+
+ // Full-coverage texSubImage2D of level 1 (triggers setData fast path).
+ const std::vector<GLColor> level1Data(kLevel1Size * kLevel1Size, GLColor::blue);
+ glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, kLevel1Size, kLevel1Size, GL_RGBA, GL_UNSIGNED_BYTE,
+ level1Data.data());
+ EXPECT_GL_NO_ERROR();
+
+ // Change base level to 5 (causes dimension mismatch, triggers release).
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 5);
+ EXPECT_GL_NO_ERROR();
+
+ // Change base level back to 1.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+ EXPECT_GL_NO_ERROR();
+
+ // Attach level 1 to an FBO and read pixels.
+ GLFramebuffer fbo1;
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo1);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 1);
+ EXPECT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+ EXPECT_PIXEL_RECT_EQ(0, 0, kLevel1Size, kLevel1Size, GLColor::blue);
+}
+
// Test that robust initialization works when glCopyTexImage2D is outside the bounds of the
// framebuffer.
TEST_P(Texture2DTestES3RobustInit, CopyTexImageOutOfBounds)
Original Bug Report
Uninitialized VRAM disclosure in ANGLE D3D11 via TextureD3D::setBaseLevel
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: In ANGLE’s Direct3D 11 backend, changing a texture’s base level can release texture storage using an empty copy mask, destroying texture data written via the setData fast path. When storage is subsequently recreated, Image11::isDirty() returns false for formats without custom initializers, preventing re-upload while front-end state remains marked as initialized. This can bypass robust resource initialization and potentially allow WebGL content to read uninitialized GPU memory across origins via glReadPixels.
Affected files:
third_party/angle/src/libANGLE/renderer/d3d/TextureD3D.cppthird_party/angle/src/libANGLE/renderer/d3d/d3d11/TextureStorage11.cppthird_party/angle/src/libANGLE/renderer/d3d/d3d11/Image11.cppthird_party/angle/src/libANGLE/Texture.cppthird_party/angle/src/libANGLE/Framebuffer.cpp
Estimated timestamp from git blame: 2020-04-30
Summary
A potential information disclosure vulnerability exists in ANGLE’s Direct3D 11 backend. When TextureD3D::setBaseLevel encounters a dimension mismatch, it destroys the underlying texture storage with an empty preservation mask and only copies back subresources that have an existing, cached RenderTargetD3D. If a subresource was previously updated via the setDataFasterThanImageUpload fast path, its data only exists in the texture storage and is permanently lost.
When new storage is later allocated, Image11::isDirty() fails to report the image as dirty for standard formats (such as GL_RGBA8), preventing re-upload. Concurrently, the front-end descriptor remains marked as InitState::Initialized, causing robust resource initialization checks to be bypassed during subsequent reads (e.g., via glReadPixels), potentially exposing uninitialized Direct3D 11 VRAM to web content.
Note: These findings are based on static source code analysis; our tooling does not currently have the capability to execute live exploit code.
Root Cause Analysis
-
Destruction of
setDataFast-Path Data (TextureD3D.cpp:794-842): WhenTextureD3D::subImageuses the fast path (TextureD3D.cpp:384-387),TextureStorage11::setData(TextureStorage11.cpp:779-894) performs anUpdateSubresourcedirectly into the D3D11 texture. It does not create a staging texture and does not associate theImage11instance with the storage.When
TextureD3D::setBaseLevel(TextureD3D.cpp:794-842) is called with a base level that changes computed zero-level dimensions, it releasesmTexStorage:if (mTexStorage->isRenderTarget()) { // Only subresources with an already-cached RenderTarget are backed up: mTexStorage->findRenderTarget(context, index, &renderTarget); if (renderTarget) { getImage(index)->copyFromTexStorage(context, index, mTexStorage); } } ANGLE_TRY(releaseTexStorage(context, gl::TexLevelMask())); // Empty mask passedBecause subresources that were only written via
setData(and never bound to an FBO) have no cached render target and no image association (mAssociatedImages[]), their contents are destroyed. -
Image11::isDirty()False Negative (Image11.cpp:127-144): AftermarkAllImagesDirty()setsmDirty = true,Image11::isDirty()executes:if (mDirty && !mStagingTexture.valid() && !mRecoverFromStorage) { const auto &formatInfo = d3d11::Format::Get(mInternalFormat, deviceCaps); if (formatInfo.dataInitializerFunction == nullptr) { return false; // Returns false despite mDirty being true } } return mDirty;For formats like
GL_RGBA8(dataInitializerFunction == nullptr),isDirty()returnsfalse. When storage is recreated,updateStorageLevel(TextureD3D.cpp:1731) skips re-uploading the level. -
Bypass of Robust Resource Initialization (
Texture.cpp:1498,Framebuffer.cpp:2733-2786): The front-endTexturesetsdesc.initState = InitState::Initializedupon sub-image upload and never resets it on storage release. When reading viaglReadPixels,Framebuffer::ensureReadAttachmentsInitializedobserves that the attachment is markedInitializedand skips clearing the attachment. Consequently, the newly allocated Direct3D 11 texture (allocated viaCreateTexture2D(&desc, nullptr)with null initial data) is read directly.
Potential Trigger Steps
On Windows using Google Chrome (default D3D11 backend), an attacker could potentially execute the following sequence via WebGL2:
- Allocate levels 0 and 1 without initial data:
const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 8, 8, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.texImage2D(gl.TEXTURE_2D, 1, gl.RGBA, 4, 4, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); - Bind level 0 to an FBO and clear it, instantiating a 4-level
TextureStorage11_2Dand cachingmRenderTarget[0]. - Call
gl.texSubImage2Don level 1 with pixel data. This takes thesetDatafast path directly to GPU storage and marks level 1 asInitState::Initializedin the front end. - Call
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, 5). This induces a storage dimension mismatch, triggering storage release. Level 0 is backed up (cached RT exists), but level 1 is dropped and destroyed. - Call
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_BASE_LEVEL, 1)to reset the base level without recreating storage. - Attach level 1 to a new FBO and call
gl.readPixels. Robust initialization is skipped due to the staleInitializedstate, storage is recreated withCreateTexture2D(pInitialData=nullptr), re-upload is skipped becauseImage11::isDirty()returnsfalse, and uninitialized VRAM is read back into JavaScript.
Suggested Fix
- Ensure Comprehensive Data Preservation on Storage Release:
In
TextureD3D::setBaseLevel(TextureD3D.cpp:817-838), do not rely solely on cachedRenderTargetD3Dpointers to back up images before dropping storage. Alternatively, pass a full copy mask toreleaseTexStoragecovering all defined mip levels that require preservation. - Correct
Image11::isDirty()Logic: InImage11::isDirty()(Image11.cpp:127-144), ensure that when robust resource initialization is enabled, uninitialized or lost levels correctly report as dirty or trigger initialization. - Synchronize Front-End InitState on Storage Drops:
When texture storage is released without preserving subresource content, notify the front-end
Textureobject to reset affectedImageDesc::initStatevalues toInitState::MayNeedInit.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.