CVE-2026-9912
Overview
Background
- GLES2 validating command decoder
GLES2DecoderImpl(ingpu/command_buffer/service/gles2_cmd_decoder.cc) is Chrome’s validating GPU command decoder (default on Android); it mirrors GL state to enforce security checks before draws.- Sampler uniforms / texture units
- A sampler uniform names which texture unit a shader samples from; Chrome must know the real unit to run pre-draw safety checks.
- `Program::SetSamplers`
- Records, in the program’s
UniformInfo::texture_units, which units a sampler uniform points at, and range-checks the supplied values againststate_.texture_units.size(). - `ClearUnclearedTextures()`
- Pre-draw routine that lazily zero-initializes textures on the units a program samples, preventing disclosure of uninitialized/cross-origin GPU memory.
- `DoUniform1iv` / `DoUniform1i`
- Handlers for
glUniform1iv/glUniform1i;DoUniform1ialready callsSetSamplersunconditionally, whileDoUniform1ivgated the call on a sampler-type whitelist.
Root Cause Analysis
GLES2DecoderImpl::DoUniform1iv only invoked state_.current_program->SetSamplers(...) when the uniform’s type was in a hardcoded list of ES2 sampler types: GL_SAMPLER_2D, GL_SAMPLER_2D_RECT_ANGLE, GL_SAMPLER_CUBE, GL_SAMPLER_EXTERNAL_OES. For ES3 sampler types (GL_SAMPLER_2D_ARRAY, GL_SAMPLER_3D, etc.) the condition was false, so SetSamplers was skipped entirely — yet the uniform was still forwarded to the driver via api()->glUniform1ivFn(real_location, count, safe_values). The effect is a state desynchronization: the driver binds the sampler to the real unit N, while Chrome’s UniformInfo::texture_units retains its default 0. Pre-draw validation (ClearUnclearedTextures, and the range check inside SetSamplers) therefore inspects the wrong texture unit, so an uncleared texture on unit N is never cleared while a cleared decoy on unit 0 satisfies the check.
The patch deletes the type whitelist and calls SetSamplers unconditionally: if (!state_.current_program->SetSamplers(state_.texture_units.size(), fake_location, count, safe_values)) { LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, "glUniform1iv", "texture unit out of range"); return; } before forwarding to the driver. As the commit message notes, SetSamplers already contains a better internal check (and UniformInfo distinguishes samplers from non-samplers itself), and this aligns DoUniform1iv with DoUniform1i. Now every sampler uniform — ES2 or ES3 — updates Chrome’s tracker and is range-checked, so validation observes the true unit. The advisory labels this a logic error (CWE-200 info exposure); the diff confirms the logic error (incomplete sampler-type list), and the substantiated security consequence is disclosure of uninitialized GPU memory, hence the InfoDisclosure classification here.
DoUniform1iv whitelisted only ES2 sampler types before updating Chrome’s sampler-to-unit tracker, so ES3 samplers were forwarded to the driver without recording their unit — desynchronizing validation state and letting pre-draw clearing/range checks inspect the wrong texture unit. The fix tracks all samplers unconditionally.Attack Path
- Allocate uncleared texture
From WebGL2 JS, create
TasTEXTURE_2D_ARRAYwith storage but no initialization (passnulltotexImage3D). - Cleared decoy
Create and fully zero-initialize decoy
D. - Bind units
Bind
DtoTEXTURE0, uninitializedTtoTEXTURE1. - ES3 sampler program
Use a program with
uniform sampler2DArray u_sampler;. - Trigger desync
gl.uniform1iv(loc, [1])to point the sampler at unit 1; pre-patch,SetSamplersis skipped (ES3 type), so Chrome records unit0while the driver uses unit1. - Draw & exfiltrate
Issue a draw;
ClearUnclearedTexturesinspects unit0(decoy, already clear) and permits it, soTon unit1is sampled uninitialized; read back the output to recover uninitialized GPU memory (potential cross-origin disclosure). A secondary consequence is bypass of the out-of-range unit check (inferred).
Impact Assessment
C:H/I:N/A:N. The diff also restores the per-call range check on the uniform’s unit value, so the “missing bounds check” vector the advisory mentions is closed too; however no direct memory-corruption primitive is shown by the diff, so any escalation beyond info disclosure is (inferred). Requires only standard WebGL2 capabilities; no elevated privilege needed.Files Changed
gpu/command_buffer/service/gles2_cmd_decoder.cc
Audit Directions
- Hardcoded sampler-type listsGrep the decoder for other
GL_SAMPLER_2D || ...enumerations that omit ES3 sampler types (GL_SAMPLER_2D_ARRAY,GL_SAMPLER_3D, shadow samplers); preferUniformInfo::IsSampler(). - State-tracking vs. driver-forwarding divergenceReview other
DoUniform*/state-setting handlers for paths that forward toapi()->gl...Fnwithout updating the mirrored validation state. - Lazy-clear reliance on tracked unitsAudit
ClearUnclearedTexturesand related pre-draw checks for any assumption thatUniformInfo::texture_unitsis always current.
Patch
From 1cd84c139db0d8b7d32dd631f5c431d72fb9f2b0 Mon Sep 17 00:00:00 2001
From: Vasiliy Telezhnikov <vasilyt@chromium.org>
Date: Thu, 23 Apr 2026 14:12:31 -0700
Subject: [PATCH] Remove unnecessary type check in DoUniform1iv
SetSamplers does have better check inside [1] and this is aligned with
what DoUniform1i does.
[1] https://source.chromium.org/chromium/chromium/src/+/main:gpu/command_buffer/service/program_manager.cc;drc=c0f23dc884b194c040601b8e4248919106bcbf20;l=1427
Bug: 499873765
Change-Id: Ifa938bf63f0a1b507e71d153ca3bca4506c9e801
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7789562
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1619760}
---
diff --git a/gpu/command_buffer/service/gles2_cmd_decoder.cc b/gpu/command_buffer/service/gles2_cmd_decoder.cc
index f9c0990c..f43b3cd8 100644
--- a/gpu/command_buffer/service/gles2_cmd_decoder.cc
+++ b/gpu/command_buffer/service/gles2_cmd_decoder.cc
@@ -8516,14 +8516,11 @@
auto values_copy = std::make_unique<GLint[]>(count);
GLint* safe_values = values_copy.get();
std::copy(values, UNSAFE_TODO(values + count), safe_values);
- if (type == GL_SAMPLER_2D || type == GL_SAMPLER_2D_RECT_ANGLE ||
- type == GL_SAMPLER_CUBE || type == GL_SAMPLER_EXTERNAL_OES) {
- if (!state_.current_program->SetSamplers(
- state_.texture_units.size(), fake_location, count, safe_values)) {
- LOCAL_SET_GL_ERROR(
- GL_INVALID_VALUE, "glUniform1iv", "texture unit out of range");
- return;
- }
+ if (!state_.current_program->SetSamplers(state_.texture_units.size(),
+ fake_location, count, safe_values)) {
+ LOCAL_SET_GL_ERROR(GL_INVALID_VALUE, "glUniform1iv",
+ "texture unit out of range");
+ return;
}
api()->glUniform1ivFn(real_location, count, safe_values);
}
Original Bug Report
Potential uninitialized GPU memory leak via ES3 sampler desync in validating command decoder
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 security team.
Overview: A logic error in GLES2DecoderImpl::DoUniform1iv skips updating the internal state tracker for ES3-specific sampler types. This causes Chrome to lose track of which texture units are active, allowing an attacker to bypass pre-draw security checks like ClearUnclearedTextures(). This can lead to a potential leak of uninitialized GPU memory, disclosing sensitive cross-origin data.
Affected files:
gpu/command_buffer/service/gles2_cmd_decoder.ccgpu/command_buffer/service/program_manager.hgpu/command_buffer/service/program_manager.cc
Estimated timestamp from git blame: 2025-05-26
Description
A state desynchronization vulnerability exists in the GLES2 validating command decoder (GLES2DecoderImpl), which is currently the default on Android.
When setting sampler uniforms via glUniform1iv, Chrome needs to track which texture unit the sampler points to in order to perform security validations before a draw call. However, in gpu/command_buffer/service/gles2_cmd_decoder.cc, the DoUniform1iv function explicitly restricts the state update (Program::SetSamplers()) to legacy ES2 sampler types:
if (type == GL_SAMPLER_2D || type == GL_SAMPLER_2D_RECT_ANGLE ||
type == GL_SAMPLER_CUBE || type == GL_SAMPLER_EXTERNAL_OES) {
if (!state_.current_program->SetSamplers(
state_.texture_units.size(), fake_location, count, safe_values)) {
// ... error handling ...
return;
}
}
api()->glUniform1ivFn(real_location, count, safe_values);
If an ES3 sampler type is used (e.g., GL_SAMPLER_2D_ARRAY, GL_SAMPLER_3D), the if condition evaluates to false, and SetSamplers() is skipped. As a result, Chrome’s internal tracker (uniform_info->texture_units) retains its default value of 0. However, the uniform is still forwarded to the underlying GPU driver via api()->glUniform1ivFn.
This creates a critical desynchronization: the GPU driver correctly uses texture unit N for the sampler, while Chrome’s validation logic incorrectly believes the program is sampling from unit 0.
Security Impact
This desynchronization allows an attacker to bypass critical security-enforcement routines:
- Bypass of
ClearUnclearedTextures()(Information Leak): Chrome lazily clears uninitialized textures before they are sampled to prevent leaking sensitive cross-origin GPU memory. Because the validator checks the state of the wrong texture unit (unit 0), an attacker can easily bypass this by placing a cleared decoy texture on unit 0, while placing an uncleared texture on unit N. - Missing Bounds Checks: Because
SetSamplers()is skipped, the check ensuring the uniform value does not exceedstate_.texture_units.size()is bypassed, allowing out-of-bounds uniform indices to be sent directly to the underlying driver.
Suggested Exploitation Steps
These are potential steps an attacker could follow from malicious WebGL2 JavaScript (tooling limitations prevent runtime verification):
- Create a texture
T(e.g.,TEXTURE_2D_ARRAY), allocate its storage, but leave it uninitialized (e.g., passnulltotexImage3D). - Create a decoy texture
Dand fully initialize it with safe zeros. - Bind the decoy texture
DtoTEXTURE0. - Bind the uninitialized texture
TtoTEXTURE1. - Compile a shader program with an ES3 sampler:
uniform sampler2DArray u_sampler;. - Trigger the bug by using
gl.uniform1ivto setu_samplerto point to unit1. Chrome records the sampler as pointing to unit0, but the driver points it to unit1. - Issue a draw call.
ClearUnclearedTexturesinspects unit0, sees the safe decoyD, and allows the draw. The GPU executes the shader, sampling from the uninitialized textureTon unit1. - Read back the rendered pixels to exfiltrate uninitialized GPU memory.
Suggested Fix
Instead of a hardcoded list of ES2 sampler types, DoUniform1iv should rely on a generalized check to determine if the uniform is a sampler. The UniformInfo struct already possesses an IsSampler() method that covers all ES2 and ES3 sampler types. Alternatively, an explicit check matching the one in IsSampler() can be used to ensure SetSamplers() is correctly called for all samplers.
Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad
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.