CVE-2026-17763
Overview
Background
- `DawnEGLImageRepresentation`
- a shared-image representation that lets Dawn (WebGPU) access a GPU shared image through an EGLImage backed by an underlying GL representation.
- `usage` vs `internal_usage`
usageis the publicly requested texture usage (what the client asked for), whileinternal_usageis additional usage Dawn needs internally (e.g. lazy-clear writes, render-attachment/copy targets).- `ToSharedImageAccessGLMode`
- converts a
wgpu::TextureUsageinto the GL access mode (read vs write) used by the shared-image locking layer to serialize access. - Read/write access locking
- the shared-image system permits concurrent readers but must forbid a reader while a writer holds access; the access mode passed to
BeginAccessdrives this.
Root Cause Analysis
DawnEGLImageRepresentation::BeginAccess computed the inner GL access mode from usage alone (ToSharedImageAccessGLMode(usage)), ignoring internal_usage. When a caller requested a read-only public usage but a write-capable internal_usage (for example a lazy clear or render-attachment write on an uncleared image), the underlying gl_representation_->BeginAccess was told the access was a read, even though Dawn would internally write the image. This broke the invariant that the access mode reflects the strongest operation performed, so the locking layer treated a writer as a reader and admitted a concurrent GL reader on the same backing during the Dawn internal write.
The fix ORs the two usages together — ToSharedImageAccessGLMode(usage | internal_usage) — so any write implied by internal_usage is reflected in the GL access mode, causing the lock to correctly reject concurrent readers. The PoC test Dawn_WriteUnderReadLock_POC encodes this: it opens a Dawn access with read-only usage and writable internal_usage, then asserts that a second GL representation’s read access on the same mailbox is rejected.
usage only, dropping the write intent carried by internal_usage, so Dawn’s internal writes were locked as reads; the fix folds internal_usage into the mode via usage | internal_usage.Attack Path
- Create a shared image usable by Dawn and GL A client creates a thread-safe shared image with WebGPU and GLES usages, left uncleared so Dawn will perform an internal lazy-clear (a write) on first use.
- Begin a mislabeled Dawn access
Via
ProduceDawn/BeginScopedAccess, request a read-only publicusagebut a write-capableinternal_usage; the old code computes GL mode fromusageonly, so the innerBeginAccessruns in read mode. - Admit a concurrent reader during a write While the Dawn access performs its internal write, a second GL representation begins read access on the same backing, which the locking layer wrongly permits.
- Concurrent read/write on the backing The reader observes or races the in-flight Dawn write, an inappropriate/unsynchronized access to the shared image. (inferred)
Impact Assessment
DawnInternalUsages and an image whose internal usage writes (e.g. lazy clear) while another representation reads. The advisory reports no exploited-in-the-wild status; any resulting memory-corruption or information-leak primitive is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pgpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/shared_image/dawn_egl_image_representation.ccgpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc
Audit Directions
- Dropped internal_usage in access-mode computationAudit other shared-image representations (Dawn/Skia/GL variants) that convert usage to an access mode, ensuring
internal_usageis combined withusagewherever internal writes can occur. - Read/write classification of lazy operationsCheck that implicit writes such as lazy-clear, mipmap generation, or copy-on-first-use are classified as writes for locking, not reads.
- Cross-backend wrappersReview representations that wrap another representation’s
BeginAccessand confirm the wrapper forwards the full, strongest access intent rather than a subset.
Patch
From 397e5b9cfb29b5e738704c1780144be027a1330a Mon Sep 17 00:00:00 2001 From: Stephen White <[email protected]> Date: Tue, 23 Jun 2026 04:12:12 -0700 Subject: [PATCH] Reland "Fix missing internal_usage in DawnEGLImageRepresentation" This reverts commit 2513b16d406c5adfca74170a384517a1c1355cc4. Reason for revert: test failures on Pixel 2 suppressed Original change's description: > Revert "Fix missing internal_usage in DawnEGLImageRepresentation" > > This reverts commit cff2ee05d52d1e882e6e91bb4e3e76aa4a4663b2. > > Reason for revert: test failure on Pixel 2. > > Failure Link: https://ci.chromium.org/ui/p/chromium/builders/ci/Android%20FYI%20Release%20(Pixel%202)/93457/blamelist > > Original change's description: > > Fix missing internal_usage in DawnEGLImageRepresentation > > > > Bug: 511738693 > > Change-Id: I0bbf545cafcb414dee8058f140b3091d21da6d82 > > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7964199 > > Commit-Queue: Stephen White <[email protected]> > > Reviewed-by: Corentin Wallez <[email protected]> > > Cr-Commit-Position: refs/heads/main@{#1649660} > > Bug: 511738693 > Change-Id: Ia9164d592d57df790d2c601ae71a39c2b0db9581 > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7962098 > Owners-Override: Quyen Le <[email protected]> > Bot-Commit: [email protected] <[email protected]> > Commit-Queue: Quyen Le <[email protected]> > Cr-Commit-Position: refs/heads/main@{#1649952} Bug: 511738693 Change-Id: I468fe7172e8671e280e5c21f3248442f8bc741db Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7970754 Bot-Commit: [email protected] <[email protected]> Reviewed-by: Corentin Wallez <[email protected]> Commit-Queue: Corentin Wallez <[email protected]> Cr-Commit-Position: refs/heads/main@{#1650906} --- diff --git a/gpu/command_buffer/service/shared_image/dawn_egl_image_representation.cc b/gpu/command_buffer/service/shared_image/dawn_egl_image_representation.cc index 7b52a0ea2..298303e 100644 --- a/gpu/command_buffer/service/shared_image/dawn_egl_image_representation.cc +++ b/gpu/command_buffer/service/shared_image/dawn_egl_image_representation.cc @@ -64,7 +64,8 @@ wgpu::Texture DawnEGLImageRepresentation::BeginAccess( wgpu::TextureUsage usage, wgpu::TextureUsage internal_usage) { - if (!gl_representation_->BeginAccess(ToSharedImageAccessGLMode(usage))) { + if (!gl_representation_->BeginAccess( + ToSharedImageAccessGLMode(usage | internal_usage))) { return nullptr; } wgpu::TextureDescriptor texture_descriptor; diff --git a/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc index 3b24e0f..6b174f5 100644 --- a/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc +++ b/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc @@ -49,7 +49,10 @@ #if BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES) #include <dawn/dawn_proc.h> #include <dawn/native/DawnNative.h> +#include <dawn/native/OpenGLBackend.h> #include <dawn/webgpu_cpp.h> + +#include "ui/gl/gl_implementation.h" #endif // BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES) using testing::AtLeast; @@ -391,9 +394,11 @@ EXPECT_EQ(dst_pixels[3], 255); } +#if BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES) + // TODO(crbug.com/332947916): fix these tests to run on Android/GLES -#if BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES) && \ - !BUILDFLAG(IS_ANDROID) +#if !BUILDFLAG(IS_ANDROID) + // Test to check interaction between Dawn and skia GL representations. TEST_P(EGLImageBackingFactoryThreadSafeTest, Dawn_SkiaGL) { // Find a Dawn GLES adapter @@ -641,6 +646,113 @@ dawnProcSetProcs(nullptr); } +#endif // !BUILDFLAG(IS_ANDROID) + +// Verify that DawnEGLImageRepresentation::BeginAccess includes `internal_usage` +// when computing the inner GL access mode. +TEST_P(EGLImageBackingFactoryThreadSafeTest, Dawn_WriteUnderReadLock_POC) { + // 1) Create the backing on the original GL context BEFORE touching Dawn, + // so EGLImage creation uses a known-good current context. + ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true)); + + const auto mailbox = Mailbox::Generate(); + const auto format = viz::SinglePlaneFormat::kRGBA_8888; + const gfx::Size size(4, 4); + const auto color_space = gfx::ColorSpace::CreateSRGB(); + const gpu::SharedImageUsageSet usage = + SHARED_IMAGE_USAGE_WEBGPU_READ | SHARED_IMAGE_USAGE_WEBGPU_WRITE | + SHARED_IMAGE_USAGE_GLES2_READ | SHARED_IMAGE_USAGE_DISPLAY_READ; + + auto backing = backing_factory_->CreateSharedImage( + mailbox, + {format, size, color_space, kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType, + usage, "POC_WriteUnderReadLock"}, + gpu::kNullSurfaceHandle, /*is_thread_safe=*/true); + ASSERT_NE(backing, nullptr); + // Leave it uncleared so Dawn will lazy-clear (an internal write) on first + // sample. IsCleared()==false is what a renderer using WEBGPU_MAILBOX_DISCARD + // would arrange. + ASSERT_FALSE(backing->IsCleared()); + + std::unique_ptr<SharedImageRepresentationFactoryRef> factory_ref = + shared_image_manager_->Register(std::move(backing), + memory_type_tracker_.get()); + + // 2) Create a Dawn GLES device. featureLevel=Compatibility is required for + // GLES adapter enumeration to succeed. + DawnProcTable procs = dawn::native::GetProcs(); + dawnProcSetProcs(&procs); + + dawn::native::Instance instance; + wgpu::RequestAdapterOptions adapter_options; + adapter_options.backendType = wgpu::BackendType::OpenGLES; + adapter_options.featureLevel = wgpu::FeatureLevel::Compatibility; + // Share Chrome's EGL display & GL proc loader with Dawn so the EGLImage + // created on Chrome's ANGLE display is importable by Dawn (this mirrors + // what WebGPUDecoderImpl does in production). + dawn::native::opengl::RequestAdapterOptionsGetGLProc get_gl_proc = {}; + get_gl_proc.getProc = gl::GetGLProcAddress; + gl::GLDisplayEGL* gl_display = gl::GLSurfaceEGL::GetGLDisplayEGL(); + get_gl_proc.display = gl_display ? gl_display->GetDisplay() : EGL_NO_DISPLAY; + adapter_options.nextInChain = &get_gl_proc; + std::vector<dawn::native::Adapter> adapters = + instance.EnumerateAdapters(&adapter_options); + ASSERT_FALSE(adapters.empty()) << "No Dawn GLES adapter"; + + wgpu::FeatureName dawn_internal_usage = wgpu::FeatureName::DawnInternalUsages; + wgpu::DeviceDescriptor device_descriptor; + device_descriptor.requiredFeatureCount = 1; + device_descriptor.requiredFeatures = &dawn_internal_usage; + wgpu::Device device = + wgpu::Device::Acquire(adapters[0].CreateDevice(&device_descriptor)); + ASSERT_NE(device, nullptr); + + // Dawn made its own EGL context current; restore ours so ProduceDawn (which + // creates a GL sibling on the current context) and subsequent GL work use + // the original display/context. + ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true)); + + // 3) Produce the DawnEGLImageRepresentation and begin a scoped access with + // a read-only public `usage` but a write-capable `internal_usage`. + auto dawn_representation = shared_image_representation_factory_->ProduceDawn( + mailbox, device, wgpu::BackendType::OpenGLES, {}, context_state_); + ASSERT_TRUE(dawn_representation); + + const wgpu::TextureUsage kReadOnlyUsage = wgpu::TextureUsage::TextureBinding; + const wgpu::TextureUsage kWritableInternal = + wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst; + + auto dawn_access = dawn_representation->BeginScopedAccess( + kReadOnlyUsage, kWritableInternal, + SharedImageRepresentation::AllowUnclearedAccess::kYes); + ASSERT_TRUE(dawn_access) + << "DawnEGLImageRepresentation::BeginAccess failed (egl wrap)"; + + // 4) While the Dawn access (which authorises internal writes) is open, a + // second GL representation tries to begin READ access on the same + // backing. With correct locking this MUST be rejected (concurrent + // reader during a write). + auto gl_reader = + shared_image_representation_factory_->ProduceGLTexturePassthrough( + mailbox); + ASSERT_TRUE(gl_reader); + + auto reader_access = gl_reader->BeginScopedAccess( + GL_SHARED_IMAGE_ACCESS_MODE_READ_CHROMIUM, + SharedImageRepresentation::AllowUnclearedAccess::kYes); + bool concurrent_read_admitted = (reader_access != nullptr); + + EXPECT_FALSE(concurrent_read_admitted); + reader_access.reset(); + + gl_reader.reset(); + dawn_access.reset(); + dawn_representation.reset(); + + device = wgpu::Device(); + dawnProcSetProcs(nullptr); + factory_ref.reset(); +} #endif // BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES)
Regression Test / PoC
diff --git a/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc b/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc
index 3b24e0f..6b174f5 100644
--- a/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc
+++ b/gpu/command_buffer/service/shared_image/egl_image_backing_factory_unittest.cc
@@ -49,7 +49,10 @@
#if BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES)
#include <dawn/dawn_proc.h>
#include <dawn/native/DawnNative.h>
+#include <dawn/native/OpenGLBackend.h>
#include <dawn/webgpu_cpp.h>
+
+#include "ui/gl/gl_implementation.h"
#endif // BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES)
using testing::AtLeast;
@@ -391,9 +394,11 @@
EXPECT_EQ(dst_pixels[3], 255);
}
+#if BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES)
+
// TODO(crbug.com/332947916): fix these tests to run on Android/GLES
-#if BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES) && \
- !BUILDFLAG(IS_ANDROID)
+#if !BUILDFLAG(IS_ANDROID)
+
// Test to check interaction between Dawn and skia GL representations.
TEST_P(EGLImageBackingFactoryThreadSafeTest, Dawn_SkiaGL) {
// Find a Dawn GLES adapter
@@ -641,6 +646,113 @@
dawnProcSetProcs(nullptr);
}
+#endif // !BUILDFLAG(IS_ANDROID)
+
+// Verify that DawnEGLImageRepresentation::BeginAccess includes `internal_usage`
+// when computing the inner GL access mode.
+TEST_P(EGLImageBackingFactoryThreadSafeTest, Dawn_WriteUnderReadLock_POC) {
+ // 1) Create the backing on the original GL context BEFORE touching Dawn,
+ // so EGLImage creation uses a known-good current context.
+ ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true));
+
+ const auto mailbox = Mailbox::Generate();
+ const auto format = viz::SinglePlaneFormat::kRGBA_8888;
+ const gfx::Size size(4, 4);
+ const auto color_space = gfx::ColorSpace::CreateSRGB();
+ const gpu::SharedImageUsageSet usage =
+ SHARED_IMAGE_USAGE_WEBGPU_READ | SHARED_IMAGE_USAGE_WEBGPU_WRITE |
+ SHARED_IMAGE_USAGE_GLES2_READ | SHARED_IMAGE_USAGE_DISPLAY_READ;
+
+ auto backing = backing_factory_->CreateSharedImage(
+ mailbox,
+ {format, size, color_space, kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType,
+ usage, "POC_WriteUnderReadLock"},
+ gpu::kNullSurfaceHandle, /*is_thread_safe=*/true);
+ ASSERT_NE(backing, nullptr);
+ // Leave it uncleared so Dawn will lazy-clear (an internal write) on first
+ // sample. IsCleared()==false is what a renderer using WEBGPU_MAILBOX_DISCARD
+ // would arrange.
+ ASSERT_FALSE(backing->IsCleared());
+
+ std::unique_ptr<SharedImageRepresentationFactoryRef> factory_ref =
+ shared_image_manager_->Register(std::move(backing),
+ memory_type_tracker_.get());
+
+ // 2) Create a Dawn GLES device. featureLevel=Compatibility is required for
+ // GLES adapter enumeration to succeed.
+ DawnProcTable procs = dawn::native::GetProcs();
+ dawnProcSetProcs(&procs);
+
+ dawn::native::Instance instance;
+ wgpu::RequestAdapterOptions adapter_options;
+ adapter_options.backendType = wgpu::BackendType::OpenGLES;
+ adapter_options.featureLevel = wgpu::FeatureLevel::Compatibility;
+ // Share Chrome's EGL display & GL proc loader with Dawn so the EGLImage
+ // created on Chrome's ANGLE display is importable by Dawn (this mirrors
+ // what WebGPUDecoderImpl does in production).
+ dawn::native::opengl::RequestAdapterOptionsGetGLProc get_gl_proc = {};
+ get_gl_proc.getProc = gl::GetGLProcAddress;
+ gl::GLDisplayEGL* gl_display = gl::GLSurfaceEGL::GetGLDisplayEGL();
+ get_gl_proc.display = gl_display ? gl_display->GetDisplay() : EGL_NO_DISPLAY;
+ adapter_options.nextInChain = &get_gl_proc;
+ std::vector<dawn::native::Adapter> adapters =
+ instance.EnumerateAdapters(&adapter_options);
+ ASSERT_FALSE(adapters.empty()) << "No Dawn GLES adapter";
+
+ wgpu::FeatureName dawn_internal_usage = wgpu::FeatureName::DawnInternalUsages;
+ wgpu::DeviceDescriptor device_descriptor;
+ device_descriptor.requiredFeatureCount = 1;
+ device_descriptor.requiredFeatures = &dawn_internal_usage;
+ wgpu::Device device =
+ wgpu::Device::Acquire(adapters[0].CreateDevice(&device_descriptor));
+ ASSERT_NE(device, nullptr);
+
+ // Dawn made its own EGL context current; restore ours so ProduceDawn (which
+ // creates a GL sibling on the current context) and subsequent GL work use
+ // the original display/context.
+ ASSERT_TRUE(context_state_->MakeCurrent(surface_.get(), /*needs_gl=*/true));
+
+ // 3) Produce the DawnEGLImageRepresentation and begin a scoped access with
+ // a read-only public `usage` but a write-capable `internal_usage`.
+ auto dawn_representation = shared_image_representation_factory_->ProduceDawn(
+ mailbox, device, wgpu::BackendType::OpenGLES, {}, context_state_);
+ ASSERT_TRUE(dawn_representation);
+
+ const wgpu::TextureUsage kReadOnlyUsage = wgpu::TextureUsage::TextureBinding;
+ const wgpu::TextureUsage kWritableInternal =
+ wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopyDst;
+
+ auto dawn_access = dawn_representation->BeginScopedAccess(
+ kReadOnlyUsage, kWritableInternal,
+ SharedImageRepresentation::AllowUnclearedAccess::kYes);
+ ASSERT_TRUE(dawn_access)
+ << "DawnEGLImageRepresentation::BeginAccess failed (egl wrap)";
+
+ // 4) While the Dawn access (which authorises internal writes) is open, a
+ // second GL representation tries to begin READ access on the same
+ // backing. With correct locking this MUST be rejected (concurrent
+ // reader during a write).
+ auto gl_reader =
+ shared_image_representation_factory_->ProduceGLTexturePassthrough(
+ mailbox);
+ ASSERT_TRUE(gl_reader);
+
+ auto reader_access = gl_reader->BeginScopedAccess(
+ GL_SHARED_IMAGE_ACCESS_MODE_READ_CHROMIUM,
+ SharedImageRepresentation::AllowUnclearedAccess::kYes);
+ bool concurrent_read_admitted = (reader_access != nullptr);
+
+ EXPECT_FALSE(concurrent_read_admitted);
+ reader_access.reset();
+
+ gl_reader.reset();
+ dawn_access.reset();
+ dawn_representation.reset();
+
+ device = wgpu::Device();
+ dawnProcSetProcs(nullptr);
+ factory_ref.reset();
+}
#endif // BUILDFLAG(USE_DAWN) && BUILDFLAG(DAWN_ENABLE_BACKEND_OPENGLES)
CreateAndValidateSharedImageRepresentations::
Original Bug Report
Potential cross-origin GPU memory leak via DawnEGLImageRepresentation missing internal_usage
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: DawnEGLImageRepresentation computes its GL access mode using only the usage parameter, ignoring internal_usage. This potential mismatch allows Dawn to perform writes while the backing holds a non-exclusive read-lock. Concurrent readers can then sample uninitialized GPU memory containing cross-origin data.
Affected files:
gpu/command_buffer/service/shared_image/dawn_egl_image_representation.ccgpu/command_buffer/service/shared_image/shared_image_representation.cc
Estimated timestamp from git blame: 2026-04-27
Final Conclusion
The vulnerability is highly exploitable, potentially allowing an attacker to disclose uninitialized, cross-origin GPU memory via a timeline race condition. Note: these are suggested/potential steps, our tooling agent doesn’t yet have the ability to run code.
Technical Details
Initial logic and parameters for SharedImage creation and mailbox association are validated. Standard processing applied for EGLImageBacking allocation. Standard processing applied for WebGPU context binding.
Due to the missing internal_usage bitmask in ToSharedImageAccessGLMode, Dawn receives a writable RenderAttachment texture while the backing records GL_SHARED_IMAGE_ACCESS_MODE_READ_CHROMIUM. This mismatch allows a secondary read operation from a parallel GPU context to instantly sample the texture memory without waiting on a write_fence_. The concurrent read bypasses driver-level synchronization and directly captures the uninitialized, pre-clear VRAM residue before Dawn’s lazy-clear write executes on the GPU timeline.
Potential Reproduction Steps
- Initial logic and IPC parameters for WebGPU mailbox association are validated.
- Standard processing applied for EGLImageBacking read-lock acquisition.
- The concurrent read bypasses driver-level synchronization and directly captures the uninitialized VRAM residue before Dawn’s lazy-clear write executes.
Suggested Fix
Update DawnEGLImageRepresentation::BeginAccess to compute the access mode using usage | internal_usage, identical to DawnGLTextureRepresentation::BeginAccess.
Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.