CVE-2026-17802
Overview
Background
- Virtual GL context
- Chrome multiplexes many logical GL contexts over one real driver context, so switching from one virtual context to another must save the outgoing context’s GL state and restore the incoming one’s.
- Occlusion query
- a GL query (e.g.
GL_ANY_SAMPLES_PASSED_EXT) that counts/flags samples passing during draw calls issued while the query is active, exposing whether geometry was rendered. - `GLStateRestorer` / `PauseQueries` / `PauseTransformFeedback`
- the state-save machinery;
PauseTransformFeedbackand (after the fix)PauseQueriessuspend in-progress operations on the outgoing context before another context becomes current. - `MakeVirtuallyCurrent`
- the routine (in
ui/gl/gl_context.cc; the enclosing function is not named in the hunk) that makes a virtual context current on the shared real context, saving the previously current context’s state.
Root Cause Analysis
When switching virtual contexts, the code paused transform feedback on the outgoing context (current_state->PauseTransformFeedback()) but never paused its active occlusion queries.
As a result, an occlusion query begun on context A but not yet ended remained active across the switch, so draw calls issued by a different virtual context (B) on the same underlying real context were counted into A’s query. The invariant that query state must be isolated per virtual context was violated, letting one context’s rendering leak into another context’s query result. The regression test VirtualQueriesOcclusionLeak demonstrates this exactly: it begins a query on gl1_, forces a virtual-context switch by creating a new context gl3 (triggering MakeVirtuallyCurrent), has gl2_ render, then ends the query on gl1_ and asserts the result is 0 — a nonzero result means gl2_’s draw leaked into gl1_’s query.
The fix adds current_state->PauseQueries(); alongside PauseTransformFeedback() so active queries are suspended during the switch, preventing cross-context contamination.
PauseQueries() call.Attack Path
- Begin an occlusion query
A GPU client (e.g. a WebGL/GL context) begins an occlusion query such as
GL_ANY_SAMPLES_PASSED_EXTon its virtual context and does not end it. - Force a virtual-context switch
The client causes another virtual context to become current (the test does this by creating a fresh context, invoking
MakeVirtuallyCurrent), which on the old code path skips pausing the first context’s query. - Render from another context A second virtual context issues draw calls on the shared real context while the first context’s query is still active, so those samples are counted into the first context’s query.
- Read the leaked result The first context ends the query and reads a nonzero result that reflects the other context’s rendering, forming a cross-context side channel.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pgpu/command_buffer/tests/gl_virtual_contexts_unittest.cc |
modified | |
ifgpu/command_buffer/tests/gl_virtual_contexts_unittest.cc |
modified |
Files Changed
gpu/command_buffer/tests/gl_virtual_contexts_unittest.ccui/gl/gl_context.cc
Audit Directions
- Incomplete state save on context switchEnumerate every piece of GL state paused/saved/restored on virtual-context switch (queries, transform feedback, bindings) and confirm none is omitted, as queries were here.
- Query lifetime across shared real contextsLook for query begin/end and other stateful GL operations that span a context switch on a multiplexed real context, where state can bleed between clients.
- Asymmetric pause/resume pairsVerify each
Pause*added for one feature has a counterpart for analogous features (queries mirroring transform feedback) and that resume logic restores them symmetrically.
Patch
From 3d5b3cd7445872d40b9b63e31a41b666ac3e7556 Mon Sep 17 00:00:00 2001
From: Ken Russell <kbr@chromium.org>
Date: Thu, 04 Jun 2026 18:19:34 -0700
Subject: [PATCH] Pause queries during virtual context switch.
Prevents query state from leaking across contexts.
Added minimized test case, ported from the bug report, which catches
the bug.
Co-authored with jetski-cli.
Fixed: 514512198
Change-Id: I080df453f235df0785a02d063f50ac1a68327412
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904480
Auto-Submit: Kenneth Russell <kbr@chromium.org>
Reviewed-by: Zhenyao Mo <zmo@chromium.org>
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1642066}
---
diff --git a/gpu/command_buffer/tests/gl_virtual_contexts_unittest.cc b/gpu/command_buffer/tests/gl_virtual_contexts_unittest.cc
index adfbf39..4ac6ea2b 100644
--- a/gpu/command_buffer/tests/gl_virtual_contexts_unittest.cc
+++ b/gpu/command_buffer/tests/gl_virtual_contexts_unittest.cc
@@ -370,6 +370,71 @@
}
}
+TEST_P(GLVirtualContextsTest, VirtualQueriesOcclusionLeak) {
+ // This test requires occlusion query support.
+ // We use GL_ANY_SAMPLES_PASSED_EXT.
+ const GLenum query_target = GL_ANY_SAMPLES_PASSED_EXT;
+
+ gl1_.MakeCurrent();
+ GLuint query1 = 0;
+ glGenQueriesEXT(1, &query1);
+ glBeginQueryEXT(query_target, query1);
+ const GLenum begin_error = glGetError();
+ if (GL_INVALID_OPERATION == begin_error) {
+ // Not supported, simply skip.
+ glDeleteQueriesEXT(1, &query1);
+ return;
+ }
+ ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), begin_error);
+ glFlush();
+
+ // Context B (gl2_) warmup.
+ gl2_.MakeCurrent();
+ SetUpColoredUnitQuad(kFloatGreen);
+ glDrawArrays(GL_TRIANGLES, 0, 6);
+ glFinish();
+
+ // Switch back to Context A (gl1_) to ensure it is the active context
+ // when the new context is created.
+ gl1_.MakeCurrent();
+
+ // Context C (gl3) - brand new context.
+ // Initializing it should trigger MakeVirtuallyCurrent on a new context,
+ // which should trigger the bug (skipping PauseQueries on gl1_).
+ GLManager gl3;
+ GLManager::Options options;
+ options.context_type = gl1_.GetContextType();
+ options.virtual_manager = &gl_real_shared_;
+ options.size = gfx::Size(kSize0, kSize0);
+ gl3.InitializeWithWorkarounds(options, GetParam());
+
+ // Context B (gl2_) renders.
+ // If gl1_'s query was not paused, gl2_'s rendering will leak into it.
+ gl2_.MakeCurrent();
+ glDrawArrays(GL_TRIANGLES, 0, 6);
+ glFlush();
+
+ // Context A (gl1_) ends query and checks result.
+ gl1_.MakeCurrent();
+ glEndQueryEXT(query_target);
+ glFinish();
+
+ GLuint query1_available = 0;
+ glGetQueryObjectuivEXT(query1, GL_QUERY_RESULT_AVAILABLE_EXT,
+ &query1_available);
+ EXPECT_TRUE(query1_available);
+
+ GLuint query_result = 0;
+ glGetQueryObjectuivEXT(query1, GL_QUERY_RESULT_EXT, &query_result);
+ glDeleteQueriesEXT(1, &query1);
+
+ // Since we drew nothing in gl1_ while the query was active,
+ // the result should be 0. If it's > 0, it leaked from gl2_.
+ EXPECT_EQ(0u, query_result);
+
+ gl3.Destroy();
+}
+
// http://crbug.com/930327
TEST_P(GLVirtualContextsTest, Texture2DArrayAnd3DRestore) {
// This test should only be run for ES3 or higher context
diff --git a/ui/gl/gl_context.cc b/ui/gl/gl_context.cc
index e4655ff3e..0e1cba8 100644
--- a/ui/gl/gl_context.cc
+++ b/ui/gl/gl_context.cc
@@ -456,6 +456,7 @@
if (current_state &&
!virtual_context->GetGLStateRestorer()->IsInitialized()) {
current_state->PauseTransformFeedback();
+ current_state->PauseQueries();
}
if (virtual_context->GetGLStateRestorer()->IsInitialized()) {
Regression Test / PoC
diff --git a/gpu/command_buffer/tests/gl_virtual_contexts_unittest.cc b/gpu/command_buffer/tests/gl_virtual_contexts_unittest.cc
index adfbf39..4ac6ea2b 100644
--- a/gpu/command_buffer/tests/gl_virtual_contexts_unittest.cc
+++ b/gpu/command_buffer/tests/gl_virtual_contexts_unittest.cc
@@ -370,6 +370,71 @@
}
}
+TEST_P(GLVirtualContextsTest, VirtualQueriesOcclusionLeak) {
+ // This test requires occlusion query support.
+ // We use GL_ANY_SAMPLES_PASSED_EXT.
+ const GLenum query_target = GL_ANY_SAMPLES_PASSED_EXT;
+
+ gl1_.MakeCurrent();
+ GLuint query1 = 0;
+ glGenQueriesEXT(1, &query1);
+ glBeginQueryEXT(query_target, query1);
+ const GLenum begin_error = glGetError();
+ if (GL_INVALID_OPERATION == begin_error) {
+ // Not supported, simply skip.
+ glDeleteQueriesEXT(1, &query1);
+ return;
+ }
+ ASSERT_EQ(static_cast<GLenum>(GL_NO_ERROR), begin_error);
+ glFlush();
+
+ // Context B (gl2_) warmup.
+ gl2_.MakeCurrent();
+ SetUpColoredUnitQuad(kFloatGreen);
+ glDrawArrays(GL_TRIANGLES, 0, 6);
+ glFinish();
+
+ // Switch back to Context A (gl1_) to ensure it is the active context
+ // when the new context is created.
+ gl1_.MakeCurrent();
+
+ // Context C (gl3) - brand new context.
+ // Initializing it should trigger MakeVirtuallyCurrent on a new context,
+ // which should trigger the bug (skipping PauseQueries on gl1_).
+ GLManager gl3;
+ GLManager::Options options;
+ options.context_type = gl1_.GetContextType();
+ options.virtual_manager = &gl_real_shared_;
+ options.size = gfx::Size(kSize0, kSize0);
+ gl3.InitializeWithWorkarounds(options, GetParam());
+
+ // Context B (gl2_) renders.
+ // If gl1_'s query was not paused, gl2_'s rendering will leak into it.
+ gl2_.MakeCurrent();
+ glDrawArrays(GL_TRIANGLES, 0, 6);
+ glFlush();
+
+ // Context A (gl1_) ends query and checks result.
+ gl1_.MakeCurrent();
+ glEndQueryEXT(query_target);
+ glFinish();
+
+ GLuint query1_available = 0;
+ glGetQueryObjectuivEXT(query1, GL_QUERY_RESULT_AVAILABLE_EXT,
+ &query1_available);
+ EXPECT_TRUE(query1_available);
+
+ GLuint query_result = 0;
+ glGetQueryObjectuivEXT(query1, GL_QUERY_RESULT_EXT, &query_result);
+ glDeleteQueriesEXT(1, &query1);
+
+ // Since we drew nothing in gl1_ while the query was active,
+ // the result should be 0. If it's > 0, it leaked from gl2_.
+ EXPECT_EQ(0u, query_result);
+
+ gl3.Destroy();
+}
+
// http://crbug.com/930327
TEST_P(GLVirtualContextsTest, Texture2DArrayAnd3DRestore) {
// This test should only be run for ES3 or higher context
Original Bug Report
Potential cross-origin info leak via unpaused GL queries during virtual context switch
Flapjack, 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: A logic flaw in GL context virtualization fails to pause active hardware queries when switching to an uninitialized context. An attacker can use this to orphan a WebGL query, keeping it active while cross-origin or browser contexts render on the GPU. This allows precise cross-origin side-channel leaks of pixel counts or render times.
Affected files:
ui/gl/gl_context.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
A potential logic error exists in GLContext::MakeVirtuallyCurrent (ui/gl/gl_context.cc) regarding the handling of virtual GL context switches. When switching from an initialized context to a newly created, uninitialized virtual context, the code explicitly pauses Transform Feedback but neglects to pause active hardware queries.
In GLContext::MakeVirtuallyCurrent, the following block handles uninitialized target contexts:
if (current_state &&
!virtual_context->GetGLStateRestorer()->IsInitialized()) {
current_state->PauseTransformFeedback();
// MISSING: current_state->PauseQueries();
}
Because the subsequent block that performs standard state restoration (which normally pauses and resumes queries) is gated behind if (virtual_context->GetGLStateRestorer()->IsInitialized()), standard query pausing is bypassed entirely.
As a result, a hardware query (such as an occlusion or timer query) started in an attacker’s WebGL context can remain actively running on the GPU while a victim cross-origin context executes. This allows the attacker to accurately measure cross-origin rendering activity.
Potential Attack Scenario
Note: These are suggested steps based on static code analysis; a working proof-of-concept has not yet been executed by the tooling agent.
- An attacker page creates a WebGL context (Context A) and begins a hardware query (e.g.,
glBeginQuery(GL_ANY_SAMPLES_PASSED)). - The attacker triggers the creation of a second, new WebGL context (Context C) on the same GPU channel.
- The GPU process calls
MakeVirtuallyCurrentto switch from Context A to Context C for initialization. Because Context C is uninitialized,PauseQueries()is skipped. Context A’s query remains active on the GPU hardware, and its internal state manager still considers it “Active” (not paused). - The browser schedules a cross-origin iframe or the browser compositor (Context B) to render. The GPU process switches to Context B. It correctly pauses Context C’s queries (which are empty), completely ignoring Context A’s orphaned hardware query.
- Context B renders. Its drawn pixels or execution time are accumulated into Context A’s running hardware query.
- The GPU process eventually switches back to Context A.
ResumeQueries()is called, but it acts as a no-op because Context A’s query was never internally marked as paused in Step 3. - The attacker calls
glEndQueryin Context A, reads the result, and accurately extracts the cross-origin rendering data.
Suggested Fix
Update GLContext::MakeVirtuallyCurrent in ui/gl/gl_context.cc to ensure queries are also paused when switching to an uninitialized context:
if (current_state &&
!virtual_context->GetGLStateRestorer()->IsInitialized()) {
current_state->PauseTransformFeedback();
current_state->PauseQueries(); // Add this line to prevent orphaned queries
}
Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0
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.