Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in Dawn
DescriptionUninitialized resource in Dawn
ComponentDawn
Sub-componentGL
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker532923954
Fix commit840ec28a3255 (dawn) +142/-1
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-07-20 (public git)
Disclosed2026-08-25
Patch gap36 days — fix public in source before the advisory

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_COMPLETE before BlitFramebuffer, 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.

Key insight
A debug-only 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

  1. Provoke incompleteness Craft a WebGPU workload whose GL-backed temporary framebuffer becomes incomplete (attachment/format mismatch, unsupported combination).
  2. Trigger the GL op Drive an MSAA resolve, a texture-to-buffer copy (ReadPixels), or a render pass that operates on that incomplete framebuffer.
  3. Undefined result In a release build the elided DAWN_ASSERT is skipped, so BlitFramebuffer/ReadPixels/render executes against undefined GL state.
  4. Capture leaked memory The destination buffer or texture receives uninitialized GPU memory contents.
  5. Read back The page reads the destination via WebGPU, disclosing residual GPU memory.

Impact Assessment

An attacker-controlled page could read uninitialized GPU memory, which may contain residual pixel data, prior texture contents, or other process-adjacent state, constituting a cross-origin/cross-context information disclosure. Exploitation requires only WebGPU access on the OpenGL backend and a framebuffer configuration that fails completeness, with no memory corruption needed. The leak is probabilistic in content but repeatable, making it usable for reconnaissance or defeating memory-layout mitigations.

Changed Functions

FunctionChangeNotes
switch
src/dawn/native/opengl/UtilsGL.cpp
modified
GLFramebufferCompletenessTests
src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp
modified
TEST_P
src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp
modified

Files Changed

  • src/dawn/tests/white_box/GLFramebufferCompletenessTests.cpp
  • src/dawn/native/opengl/UtilsGL.cpp
  • src/dawn/tests/CMakeLists.txt
  • src/dawn/tests/BUILD.gn
  • src/dawn/native/opengl/CommandBufferGL.cpp
  • src/dawn/native/opengl/UtilsGL.h

Audit Directions

  • Debug-only guards on security invariants
    Search Dawn and other backends for DAWN_ASSERT/assert gating GL/driver state that determines whether output memory is initialized, and promote them to DAWN_TRY error returns.
  • Undefined-result GL/driver calls
    Audit 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 destinations
    Trace 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.
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" ]
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_party/dawn/src/dawn/native/CommandBuffer.cpp
  • third_party/dawn/src/dawn/native/opengl/PhysicalDeviceGL.cpp
  • third_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:

  1. Create a texture with wgpu::TextureFormat::RGBA16Float and binding usages (RENDER_ATTACHMENT | TEXTURE_BINDING).
  2. Begin a render pass that clears this texture and stores it.
  3. The backend fails to clear the texture due to the incomplete framebuffer.
  4. Perform a subsequent compute pass to sample the texture and write its contents to a map-readable GPUBuffer.
  5. 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.

View on issue tracker