CVE-2026-79221
Overview
Background
- Dawn
- Chromium’s WebGPU implementation, whose OpenGL backend translates WebGPU commands into GL calls.
- Framebuffer completeness
- GL requires a framebuffer to pass
glCheckFramebufferStatus == GL_FRAMEBUFFER_COMPLETEbeforeBlitFramebuffer,ReadPixels, or rendering, or results are undefined. - `DAWN_ASSERT`
- a debug-only check that is compiled out in release builds, so an assertion on framebuffer status enforces nothing for shipping users.
- Uninitialized memory disclosure
- reading undefined GL results into a destination buffer or texture that the page can read back leaks whatever GPU memory backed the operation.
Root Cause Analysis
Dawn’s GL backend constructs temporary framebuffers to perform MSAA resolves (BlitFramebuffer), texture-to-buffer readback (ReadPixels across 2D, cubemap, and 3D-layer paths), and render-pass setup. These framebuffers can become incomplete due to attachment mismatches, unsupported formats, or other driver-specific constraints, yet the code only guarded completeness with DAWN_ASSERT(gl.CheckFramebufferStatus(...) == GL_FRAMEBUFFER_COMPLETE). Because DAWN_ASSERT is elided in release builds, an incomplete framebuffer would proceed to the GL operation, which returns undefined results per the GL specification. The BlitFramebuffer, ReadPixels, and render calls then produced or consumed uninitialized GPU memory, writing it into the destination buffer or texture. Since that destination is readable by the WebGPU page, the undefined contents become an information-disclosure primitive.
The fix replaces the elided assert with a real DAWN_TRY(CheckFramebufferComplete(...)) that returns an error and aborts the operation before any GL call touches uninitialized data.
DAWN_ASSERT provides zero protection in shipping builds, so the security-relevant completeness invariant was never actually enforced for real users. Undefined GL behavior on an incomplete framebuffer is not a correctness footnote here — it is a direct read of uninitialized GPU memory into page-visible output.Attack Path
- Provoke incompleteness Craft a WebGPU workload whose GL-backed temporary framebuffer becomes incomplete (attachment/format mismatch, unsupported combination).
- Trigger the GL op
Drive an MSAA resolve, a texture-to-buffer copy (
ReadPixels), or a render pass that operates on that incomplete framebuffer. - Undefined result
In a release build the elided
DAWN_ASSERTis skipped, soBlitFramebuffer/ReadPixels/render executes against undefined GL state. - Capture leaked memory The destination buffer or texture receives uninitialized GPU memory contents.
- Read back The page reads the destination via WebGPU, disclosing residual GPU memory.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchsrc/dawn/native/opengl/UtilsGL.cpp |
modified | |
GLFramebufferCompletenessTestssrc/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp |
modified | |
TEST_Psrc/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp |
modified |
Files Changed
src/dawn/tests/white_box/GLFramebufferCompletenessTests.cppsrc/dawn/native/opengl/UtilsGL.cppsrc/dawn/tests/CMakeLists.txtsrc/dawn/tests/BUILD.gnsrc/dawn/native/opengl/CommandBufferGL.cppsrc/dawn/native/opengl/UtilsGL.h
Audit Directions
- Debug-only guards on security invariantsSearch Dawn and other backends for
DAWN_ASSERT/assertgating GL/driver state that determines whether output memory is initialized, and promote them toDAWN_TRYerror returns. - Undefined-result GL/driver callsAudit every
BlitFramebuffer,ReadPixels,CopyTexImage, and render entry point for a completeness/validity check that runs in release before the call, across the D3D, Metal, and Vulkan backends’ equivalents. - Page-readable destinationsTrace all paths where GL operation output lands in a buffer or texture reachable via WebGPU readback and confirm the source is fully initialized on every control-flow branch.
Patch
From 840ec28a32556e36b5e5a6f95f63185b09132664 Mon Sep 17 00:00:00 2001
From: Stephen White <senorblanco@chromium.org>
Date: Mon, 20 Jul 2026 13:10:30 -0700
Subject: [PATCH] GL: add checks for framebuffer completeness.
Bug: 532923954
Change-Id: I12032568e3433b95778284b560492dc203d14fac
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/325095
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Stephen White <senorblanco@chromium.org>
---
diff --git a/src/dawn/native/opengl/CommandBufferGL.cpp b/src/dawn/native/opengl/CommandBufferGL.cpp
index b3267f9..c1be42d 100644
--- a/src/dawn/native/opengl/CommandBufferGL.cpp
+++ b/src/dawn/native/opengl/CommandBufferGL.cpp
@@ -712,6 +712,8 @@
ToBackend(renderPass->colorAttachments[i].resolveTarget.Get());
DAWN_GL_TRY(gl, BindFramebuffer(GL_DRAW_FRAMEBUFFER, writeFbo));
DAWN_TRY(resolveView->BindToFramebuffer(gl, GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0));
+ DAWN_TRY(CheckFramebufferComplete(gl, GL_READ_FRAMEBUFFER));
+ DAWN_TRY(CheckFramebufferComplete(gl, GL_DRAW_FRAMEBUFFER));
DAWN_GL_TRY(gl, BlitFramebuffer(0, 0, renderPass->width, renderPass->height, 0, 0,
renderPass->width, renderPass->height,
GL_COLOR_BUFFER_BIT, GL_NEAREST));
@@ -1013,6 +1015,7 @@
DAWN_GL_TRY(gl, FramebufferTexture2D(
GL_READ_FRAMEBUFFER, glAttachment, target,
texture->GetTextureHandle(), src.mipLevel));
+ DAWN_TRY(CheckFramebufferComplete(gl, GL_READ_FRAMEBUFFER));
DAWN_GL_TRY(gl, ReadPixels(dchecked_cast<uint32_t>(src.origin.x),
dchecked_cast<uint32_t>(src.origin.y),
dchecked_cast<uint32_t>(copySize.width),
@@ -1030,6 +1033,7 @@
glAttachment, cubeMapTarget,
texture->GetTextureHandle(),
src.mipLevel));
+ DAWN_TRY(CheckFramebufferComplete(gl, GL_READ_FRAMEBUFFER));
DAWN_GL_TRY(gl, ReadPixels(dchecked_cast<uint32_t>(src.origin.x),
dchecked_cast<uint32_t>(src.origin.y),
dchecked_cast<uint32_t>(copySize.width),
@@ -1051,6 +1055,7 @@
GL_READ_FRAMEBUFFER, glAttachment,
texture->GetTextureHandle(), src.mipLevel,
dchecked_cast<uint32_t>(src.origin.z + z)));
+ DAWN_TRY(CheckFramebufferComplete(gl, GL_READ_FRAMEBUFFER));
DAWN_GL_TRY(gl, ReadPixels(dchecked_cast<uint32_t>(src.origin.x),
dchecked_cast<uint32_t>(src.origin.y),
dchecked_cast<uint32_t>(copySize.width),
@@ -1347,7 +1352,7 @@
}
}
- DAWN_ASSERT(gl.CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE);
+ DAWN_TRY(CheckFramebufferComplete(gl, GL_DRAW_FRAMEBUFFER));
// Set defaults for dynamic state before executing clears and commands.
PersistentPipelineState persistentPipelineState;
diff --git a/src/dawn/native/opengl/UtilsGL.cpp b/src/dawn/native/opengl/UtilsGL.cpp
index 70e5bea..df219b6 100644
--- a/src/dawn/native/opengl/UtilsGL.cpp
+++ b/src/dawn/native/opengl/UtilsGL.cpp
@@ -30,6 +30,7 @@
#include <string>
#include "src/dawn/native/EnumMaskIterator.h"
+#include "src/dawn/native/ErrorInjector.h"
#include "src/dawn/native/opengl/OpenGLFunctions.h"
#include "src/utils/assert.h"
#include "src/utils/log.h"
@@ -195,6 +196,39 @@
#undef ERROR_CASE_STRING
}
+const char* GLFramebufferStatusAsString(GLenum status) {
+#define STATUS_CASE_STRING(statusEnum) \
+ case statusEnum: \
+ return #statusEnum
+
+ switch (status) {
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_COMPLETE);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_UNDEFINED);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS);
+ STATUS_CASE_STRING(GL_FRAMEBUFFER_UNSUPPORTED);
+ default:
+ return "<Unknown OpenGL framebuffer status>";
+ }
+
+#undef STATUS_CASE_STRING
+}
+
+MaybeError CheckFramebufferComplete(const OpenGLFunctions& gl, GLenum target) {
+ GLenum status = INJECT_ERROR_OR_RUN(gl.CheckFramebufferStatus(target),
+ static_cast<GLenum>(GL_FRAMEBUFFER_UNSUPPORTED));
+ if (status == GL_FRAMEBUFFER_COMPLETE) [[likely]] {
+ return {};
+ }
+ return DAWN_FORMAT_INTERNAL_ERROR("glCheckFramebufferStatus returned %s (0x%04X).",
+ GLFramebufferStatusAsString(status), status);
+}
+
void ClearErrors(const OpenGLFunctions& gl,
const char* file,
const char* function,
diff --git a/src/dawn/native/opengl/UtilsGL.h b/src/dawn/native/opengl/UtilsGL.h
index 0465e8d..7cab0ba 100644
--- a/src/dawn/native/opengl/UtilsGL.h
+++ b/src/dawn/native/opengl/UtilsGL.h
@@ -51,6 +51,12 @@
bool HasAnisotropicFiltering(const OpenGLFunctions& gl);
const char* GLErrorAsString(GLenum error);
+const char* GLFramebufferStatusAsString(GLenum status);
+
+// Checks that the framebuffer bound to `target` is complete, returning an internal error
+// otherwise. The check is unconditional so that callers relying on subsequent framebuffer
+// operations having taken effect never proceed past an incomplete framebuffer.
+MaybeError CheckFramebufferComplete(const OpenGLFunctions& gl, GLenum target);
// Clear all errors on the context, emits logs only
void ClearErrors(const OpenGLFunctions& gl,
diff --git a/src/dawn/tests/CMakeLists.txt b/src/dawn/tests/CMakeLists.txt
index fcad3c1..fd92b99 100644
--- a/src/dawn/tests/CMakeLists.txt
+++ b/src/dawn/tests/CMakeLists.txt
@@ -371,6 +371,7 @@
list(APPEND whitebox_conditional_private_platform_depends dawn_khronos_platform)
list(APPEND whitebox_sources
"white_box/EGLImageWrappingTests.cpp"
+ "white_box/GLFramebufferCompletenessTests.cpp"
"white_box/GLTextureWrappingTests.cpp"
)
endif()
diff --git a/src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp b/src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp
new file mode 100644
index 0000000..0a9fc63
--- /dev/null
+++ b/src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp
@@ -0,0 +1,94 @@
+// Copyright 2026 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/dawn/native/ErrorData.h"
+#include "src/dawn/native/opengl/DeviceGL.h"
+#include "src/dawn/native/opengl/UtilsGL.h"
+#include "src/dawn/tests/DawnTest.h"
+
+namespace dawn::native::opengl {
+namespace {
+
+class GLFramebufferCompletenessTests : public DawnTest {
+ protected:
+ void SetUp() override {
+ DawnTest::SetUp();
+ DAWN_TEST_UNSUPPORTED_IF(UsesWire());
+ }
+};
+
+// CheckFramebufferComplete must succeed for a valid color attachment and fail for a
+// framebuffer with no attachments.
+TEST_P(GLFramebufferCompletenessTests, DrawFramebuffer) {
+ Device* deviceGL = ToBackend(FromAPI(device.Get()));
+ const OpenGLFunctions& gl = deviceGL->GetGL();
+
+ GLint prevDrawFBO = 0;
+ gl.GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prevDrawFBO);
+
+ GLuint fbo = 0;
+ gl.GenFramebuffers(1, &fbo);
+ gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+
+ // A newly created framebuffer with no attachments and no default width/height is not
+ // complete (GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT).
+ {
+ MaybeError result = CheckFramebufferComplete(gl, GL_DRAW_FRAMEBUFFER);
+ EXPECT_TRUE(result.IsError());
+ result.AcquireError();
+ }
+
+ // Attaching a color-renderable texture makes the framebuffer complete.
+ GLuint tex = 0;
+ gl.GenTextures(1, &tex);
+ gl.BindTexture(GL_TEXTURE_2D, tex);
+ gl.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
+ gl.FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ {
+ MaybeError result = CheckFramebufferComplete(gl, GL_DRAW_FRAMEBUFFER);
+ EXPECT_FALSE(result.IsError());
+ result.AcquireError();
+ }
+
+ gl.DeleteTextures(1, &tex);
+ gl.DeleteFramebuffers(1, &fbo);
+ gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, prevDrawFBO);
+}
+
+// GLFramebufferStatusAsString must return a readable name for known status values.
+TEST_P(GLFramebufferCompletenessTests, StatusAsString) {
+ EXPECT_STREQ("GL_FRAMEBUFFER_COMPLETE", GLFramebufferStatusAsString(GL_FRAMEBUFFER_COMPLETE));
+ EXPECT_STREQ("GL_FRAMEBUFFER_UNSUPPORTED",
+ GLFramebufferStatusAsString(GL_FRAMEBUFFER_UNSUPPORTED));
+ EXPECT_STREQ("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT",
+ GLFramebufferStatusAsString(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT));
+}
+
+DAWN_INSTANTIATE_TEST(GLFramebufferCompletenessTests, OpenGLESBackend());
+
+} // anonymous namespace
+} // namespace dawn::native::opengl
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 13dba61..53ab265 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -887,6 +887,7 @@
defines += [ "DAWN_ANGLE_LIBS_SUFFIX=\"${angle_libs_suffix}\"" ]
sources += [
"white_box/EGLImageWrappingTests.cpp",
+ "white_box/GLFramebufferCompletenessTests.cpp",
"white_box/GLTextureWrappingTests.cpp",
]
include_dirs = [ "${dawn_root}/third_party/EGL-Registry" ]
Regression Test / PoC
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index 13dba61..53ab265 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -887,6 +887,7 @@
defines += [ "DAWN_ANGLE_LIBS_SUFFIX=\"${angle_libs_suffix}\"" ]
sources += [
"white_box/EGLImageWrappingTests.cpp",
+ "white_box/GLFramebufferCompletenessTests.cpp",
"white_box/GLTextureWrappingTests.cpp",
]
include_dirs = [ "${dawn_root}/third_party/EGL-Registry" ]
diff --git a/src/dawn/tests/CMakeLists.txt b/src/dawn/tests/CMakeLists.txt
index fcad3c1..fd92b99 100644
--- a/src/dawn/tests/CMakeLists.txt
+++ b/src/dawn/tests/CMakeLists.txt
@@ -371,6 +371,7 @@
list(APPEND whitebox_conditional_private_platform_depends dawn_khronos_platform)
list(APPEND whitebox_sources
"white_box/EGLImageWrappingTests.cpp"
+ "white_box/GLFramebufferCompletenessTests.cpp"
"white_box/GLTextureWrappingTests.cpp"
)
endif()
diff --git a/src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp b/src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp
new file mode 100644
index 0000000..0a9fc63
--- /dev/null
+++ b/src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp
@@ -0,0 +1,94 @@
+// Copyright 2026 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/dawn/native/ErrorData.h"
+#include "src/dawn/native/opengl/DeviceGL.h"
+#include "src/dawn/native/opengl/UtilsGL.h"
+#include "src/dawn/tests/DawnTest.h"
+
+namespace dawn::native::opengl {
+namespace {
+
+class GLFramebufferCompletenessTests : public DawnTest {
+ protected:
+ void SetUp() override {
+ DawnTest::SetUp();
+ DAWN_TEST_UNSUPPORTED_IF(UsesWire());
+ }
+};
+
+// CheckFramebufferComplete must succeed for a valid color attachment and fail for a
+// framebuffer with no attachments.
+TEST_P(GLFramebufferCompletenessTests, DrawFramebuffer) {
+ Device* deviceGL = ToBackend(FromAPI(device.Get()));
+ const OpenGLFunctions& gl = deviceGL->GetGL();
+
+ GLint prevDrawFBO = 0;
+ gl.GetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prevDrawFBO);
+
+ GLuint fbo = 0;
+ gl.GenFramebuffers(1, &fbo);
+ gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+
+ // A newly created framebuffer with no attachments and no default width/height is not
+ // complete (GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT).
+ {
+ MaybeError result = CheckFramebufferComplete(gl, GL_DRAW_FRAMEBUFFER);
+ EXPECT_TRUE(result.IsError());
+ result.AcquireError();
+ }
+
+ // Attaching a color-renderable texture makes the framebuffer complete.
+ GLuint tex = 0;
+ gl.GenTextures(1, &tex);
+ gl.BindTexture(GL_TEXTURE_2D, tex);
+ gl.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4);
+ gl.FramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
+ {
+ MaybeError result = CheckFramebufferComplete(gl, GL_DRAW_FRAMEBUFFER);
+ EXPECT_FALSE(result.IsError());
+ result.AcquireError();
+ }
+
+ gl.DeleteTextures(1, &tex);
+ gl.DeleteFramebuffers(1, &fbo);
+ gl.BindFramebuffer(GL_DRAW_FRAMEBUFFER, prevDrawFBO);
+}
+
+// GLFramebufferStatusAsString must return a readable name for known status values.
+TEST_P(GLFramebufferCompletenessTests, StatusAsString) {
+ EXPECT_STREQ("GL_FRAMEBUFFER_COMPLETE", GLFramebufferStatusAsString(GL_FRAMEBUFFER_COMPLETE));
+ EXPECT_STREQ("GL_FRAMEBUFFER_UNSUPPORTED",
+ GLFramebufferStatusAsString(GL_FRAMEBUFFER_UNSUPPORTED));
+ EXPECT_STREQ("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT",
+ GLFramebufferStatusAsString(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT));
+}
+
+DAWN_INSTANTIATE_TEST(GLFramebufferCompletenessTests, OpenGLESBackend());
+
+} // anonymous namespace
+} // namespace dawn::native::opengl
Original Bug Report
Potential cross-origin uninitialized GPU heap leak in Dawn GLES due to unchecked FBO completeness
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: Dawn’s OpenGL backend marks render-pass attachments as initialized prior to recording a pass, but only checks for framebuffer completeness inside debug-only assertions (DAWN_ASSERT). In production release builds, an incomplete framebuffer causes subsequent clears, draws, and resolve operations to silently fail and no-op without raising an error. Because the frontend already marked the attachments as initialized, subsequent sampling of these resources bypasses lazy-clearing and can potentially leak uninitialized GPU heap memory to JavaScript.
Affected files:
third_party/dawn/src/dawn/native/opengl/CommandBufferGL.cppthird_party/dawn/src/dawn/native/CommandBuffer.cppthird_party/dawn/src/dawn/native/opengl/PhysicalDeviceGL.cppthird_party/dawn/src/dawn/native/opengl/TextureGL.cpp
Estimated timestamp from git blame: 2020-01-16
Potential Cross-Origin Uninitialized GPU Heap Leak in Dawn GLES Backend
Summary
We have identified a potential vulnerability in Dawn’s OpenGL/GLES backend where an incomplete Framebuffer Object (FBO) configuration can result in silent failures of draw/clear operations. Because the Dawn frontend eagerly flags the render-pass attachments as initialized before executing the pass, a silent backend failure allows subsequent texture sampling operations to bypass lazy-clearing. This can potentially leak raw, uninitialized GPU heap memory directly to the calling origin.
Technical Analysis
Step 1: Eager Initialization Marking
In third_party/dawn/src/dawn/native/opengl/CommandBufferGL.cpp (lines 859-863), the command recording pipeline invokes LazyClearRenderPassAttachments before running ExecuteRenderPass:
DAWN_TRY(LazyClearRenderPassAttachments(
GetDevice(), cmd, [&](TextureBase* texture, const SubresourceRange& range) {
return ToBackend(texture)->EnsureSubresourceContentInitialized(gl, range);
}));
DAWN_TRY(ExecuteRenderPass(cmd, gl, nextRenderPassNumber));
Inside LazyClearRenderPassAttachments (third_party/dawn/src/dawn/native/CommandBuffer.cpp lines 221-228), color attachments with Store store operations and resolve targets are unconditionally marked as initialized in the frontend tracking state:
resolveView->GetTexture()->SetIsSubresourceContentInitialized(
true, resolveView->GetSubresourceRange());
...
case wgpu::StoreOp::Store:
view->GetTexture()->SetIsSubresourceContentInitialized(true, range);
break;
Step 2: Debug-Only Framebuffer Completeness Check
During ExecuteRenderPass, an FBO is generated and bound to represent the render pass attachments. However, the completeness of this framebuffer is only verified inside a DAWN_ASSERT:
DAWN_GL_TRY(gl, GenFramebuffers(1, &fbo));
DAWN_GL_TRY(gl, BindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo));
... // attach color/depth/stencil views
DAWN_ASSERT(gl.CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); // CommandBufferGL.cpp:1350
In production release builds of Chrome, DAWN_ENABLE_ASSERTS is undefined, and DAWN_ASSERT compiles to a no-op static_assert(sizeof(!!(cond)) == sizeof(bool)). Consequently, the FBO status is never verified at runtime in release builds.
Step 3: Silent Clear Failures & Unchecked GL Errors
If the underlying driver rejects the bound framebuffer configuration (e.g., returning GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT or GL_FRAMEBUFFER_UNSUPPORTED), the FBO is marked incomplete.
Per OpenGL ES 3.2 specification §9.4.2, any clear or draw commands on an incomplete bound draw framebuffer generate a GL_INVALID_FRAMEBUFFER_OPERATION error and are ignored.
Because the release build definitions of the DAWN_GL_TRY macro compile to bare GL calls (#define DAWN_GL_TRY(gl, call) (gl.call)), runtime GL error checks are bypassed, and ExecuteRenderPass returns success ({}).
Step 4: Lazy Clear Bypass
When the texture is subsequently bound and sampled (e.g., in a compute pass), the backend calls EnsureSubresourceContentInitialized (TextureGL.cpp lines 606-614):
if (!IsSubresourceContentInitialized(range)) {
DAWN_TRY(ClearTexture(gl, range, TextureBase::ClearValue::Zero));
}
Since IsSubresourceContentInitialized was marked true during Step 1, the lazy clear is skipped entirely. This allows the compute shader to sample the raw, uninitialized driver memory allocated via glTexStorage2D and copy it to a readable buffer, potentially disclosing sensitive GPU memory (including cross-origin data) to the page.
Suggested/Potential Trigger Path (Not verified via executable POC)
An attacker could potentially trigger this behavior via WebGPU compatibility mode on Android devices that lack native float-renderability or return GL_FRAMEBUFFER_UNSUPPORTED for specific format combinations:
- Create a texture with
wgpu::TextureFormat::RGBA16Floatand binding usages (RENDER_ATTACHMENT | TEXTURE_BINDING). - Begin a render pass that clears this texture and stores it.
- The backend fails to clear the texture due to the incomplete framebuffer.
- Perform a subsequent compute pass to sample the texture and write its contents to a map-readable GPUBuffer.
- Read the buffer asynchronously to inspect the leaked GPU heap contents.
Suggested Fix
To address this potential issue, the OpenGL backend should dynamically verify framebuffer completeness and handle failures gracefully in release builds. This can be achieved by utilizing DAWN_TRY or checking the FBO status during pass setup:
GLenum status = gl.CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
DAWN_INVALID_IF(status != GL_FRAMEBUFFER_COMPLETE, "Framebuffer was incomplete: %x", status);
Additionally, if a render pass fails execution, any previously updated subresource initialization tracking flags for that pass’s attachments should be rolled back to false.
Evaluated with Chrome root at commit: 84065d9121f6e48f67755f0ae963cc09617e5c85
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.