Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ANGLE
DescriptionUse after free in ANGLE
ComponentANGLE
Sub-componentInternals>GPU>ANGLE
Bug ClassUninitialized Memory
Tracker563350676
Fix commit4979a49c3fdb (angle/angle) +323/-94
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Background

Fragment outputs narrower than vec4
unwritten components are undefined.
ExpandFragmentOutputsToVec4
the new transform padding outputs to gvec4 with zeros.
Hardened contexts
where undefined values are a security concern.

Root Cause Analysis

The commit “Always expand non-vec4 fragment outputs to vec4” makes ANGLE expand any fragment shader output narrower than 4 components (e.g. float/vec2/vec3 outputs) to a full gvec4, filling the missing components with 0, on hardened contexts for all backends. The commit message states the problem: “The alpha channel is undefined if unwritten, which leads to undefined values produced when blending.” Previously this was only a per-driver workaround (expand_fragment_outputs_to_vec4); the fix promotes it to a general transform (new ExpandFragmentOutputsToVec4 C++ tree-op and expand_fragment_outputs_to_vec4 Rust IR transform) run whenever expandFragmentOutputsToVec4 is set and the shader is ESSL 3.00+.

The root cause is therefore use of undefined/uninitialized fragment-output components: an unwritten alpha (or other missing component) carries an undefined value that is then consumed by blending and read back, which can expose uninitialized GPU register/memory contents. This CVE (bug chromium:563350676) shares the fix; the advisory labels it “use after free,” but the diff is a shader-output expansion transform with no allocation, free, or pointer-lifetime change anywhere. There is no UAF in this patch. Reclassified to UninitializedMemory (undefined/uninitialized fragment-output components). (If a genuine UAF were intended by this CVE, it is not in the mapped commit — but the commit explicitly lists bug 563350676, so the fix is the output-expansion change.)

Key insight
The mapped fix expands undefined non-vec4 fragment outputs to zero-filled vec4; it is an uninitialized-data fix, not a use-after-free — the advisory’s “UAF” label does not match the patch.

Attack Path

  1. ENTRY WebGL fragment shader (ESSL 3.00+) with a non-vec4 output, hardened context.
  2. CONTROLLED INPUT Leave a component unwritten (undefined).
  3. MISSING CHECK Missing components are not zero-filled before use.
  4. IMPACT Undefined values are consumed by blending / read back — uninitialized-data exposure, not a use-after-free.

Impact Assessment

Reachable from web content on hardened contexts; the demonstrated defect is undefined/uninitialized fragment-output data, which the advisory mislabels as “use after free.” No memory-lifetime bug is present in the mapped commit. Severity medium per the advisory; no ITW stated.

Files Changed

  • src/compiler/translator/ir/src/transform.rs
  • src/libANGLE/renderer/gl/ShaderGL.cpp
  • src/libANGLE/renderer/gl/renderergl_utils.cpp
  • include/platform/autogen/FeaturesGL_autogen.h
  • util/autogen/angle_features_autogen.cpp
  • src/compiler.gni
  • src/tests/angle_end2end_tests_expectations.txt
  • src/compiler/translator/Compiler.cpp
  • src/compiler/translator/glsl/TranslatorGLSL.cpp
  • src/compiler/translator/ir/src/transform/expand_fragment_outputs_to_vec4.rs
  • src/compiler/translator/ir/src/ir.rs
  • src/tests/gl_tests/WebGLCompatibilityTest.cpp
  • src/compiler/translator/ir/src/transform/emulate_multi_draw.rs
  • src/compiler/translator/glsl/TranslatorESSL.cpp
  • src/compiler/translator/ir/BUILD.gn
  • src/libANGLE/Shader.cpp
  • util/autogen/angle_features_autogen.h
  • src/compiler/translator/tree_ops/glsl/ExpandFragmentOutputsToVec4.h
  • src/compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.cpp
  • src/compiler/translator/ir/src/compile.rs
  • src/compiler/translator/ir/src/compile.cpp
  • src/tests/gl_tests/GLSLTest.cpp
  • src/compiler/translator/tree_ops/ExpandFragmentOutputsToVec4.h
  • include/platform/gl_features.json
  • src/compiler/translator/ir/src/transform/emulate_instanced_multiview.rs

Audit Directions

  • Verify the advisory↔commit nature
    The “UAF” label conflicts with the mapped commit; confirm no separate use-after-free fix exists under bug 563350676.
  • Undefined output components
    As for the sibling CVEs, audit unwritten shader outputs consumed downstream.

Original Bug Report

reported by [email protected]

Information leak of GPU register residue via sub-vec4 fragment outputs in ANGLE GL backend

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: ANGLE’s GL/GLES backend has a translator pass, ExpandFragmentOutputsToVec4, whose purpose is to zero-fill the undeclared channels of sub-vec4 fragment shader outputs so indeterminate GPU register contents cannot reach color attachments. Its only enabling site gates it to isAMD && isMesa as a driver workaround, so on every other GL/GLES driver — Linux desktop defaults (NVIDIA proprietary, Intel Mesa, AMD proprietary) and Android’s default ANGLE-on-GLES path plus the validating-decoder fallback — a plain WebGL2 page declaring out vec2 c deposits undefined data into the .zw channels of an RGBA attachment and reads it back with gl.readPixels(). The WebGL hardening that does run everywhere (initOutputVariables) zero-initializes only the declared 2-component variable and cannot touch the undeclared hardware channels. ANGLE fixed the identical mechanism unconditionally on Metal, citing “indeterminate GPU register residue leaking into color attachments”.

Affected files:

Estimated timestamp from git blame: 2026-05-01

Summary

WebGL2 allows fragment shaders to declare sub-vec4 outputs (out vec2 c) and draw into 4-channel attachments; per OpenGL ES 3.2 §14.2.3, the unwritten channels are undefined (“Any fragment colors which are not written by the fragment shader are undefined”). ANGLE hardens every WebGL context (Context.cpp:729: mHardenedContext(mWebGLContext || ...)), and on hardened contexts ShaderGL.cpp:131-136 sets initOutputVariables — but TCompiler::initializeOutputVariables (Compiler.cpp:1674-1751) emits only c = vec2(0) at declared arity, leaving the .zw hardware output-register channels untouched.

The pass that actually closes the gap, ExpandFragmentOutputsToVec4, rewrites the output as a real vec4 and explicitly zero-fills the missing channels. It is enabled by exactly one site in the tree, as a driver-bug workaround (gl_features.json category “Workarounds”, crbug.com/491732188 / crbug.com/498828605):

// third_party/angle/src/libANGLE/renderer/gl/renderergl_utils.cpp:2725-2726
// http://crbug.com/498828605
ANGLE_FEATURE_CONDITION(features, expandFragmentOutputsToVec4, isAMD && isMesa);

Every !(isAMD && isMesa) GL/GLES configuration is therefore unhardened, including Intel Mesa (isMesa but not isAMD), NVIDIA and AMD proprietary on Linux, and all Android GLES vendors (Adreno, Mali, PowerVR, Xclipse). No other layer compensates: draw validation checks output presence and component type but not arity (validationES.cpp:445-471), robust resource initialization clears attachments only at creation (the leak is deposited by the draw itself), and no arity-derived color-mask logic exists in the GL backend.

This was demonstrated end-to-end from plain web content in a stock Chromium build on Mesa llvmpipe (an isMesa && !isAMD configuration, i.e. in the vulnerable population):

  • Structural: the web-exposed WEBGL_debug_shaders extension shows the translated shader ANGLE hands the native driver still declares out vec2 on both the desktop-GL backend (TranslatorGLSL, Linux default) and the native-GLES backend (TranslatorESSL, the Android-default path); the only sanitization present is the declared-arity zero-init.
  • Value: after clearing an RGBA8 (and RGBA32F) FBO to a sentinel, a single out vec2 draw overwrote the never-written .ba/.zw channels of all 4096 pixels, and readPixels() returned them — undefined-channel data reaches web content.
  • Control: the same page with --enable-angle-features=expandFragmentOutputsToVec4 on the same driver flips the translated source to vec4 with explicit .z = 0; .w = 0 fill, confirming the vendor gate is the only difference between vulnerable and hardened.

One caveat is hardware-dependent and could not be sampled in a GPU-less environment: what the undefined channels actually contain per driver. On llvmpipe they deterministically resolve to zero (benign on that software driver). Evidence that real hardware emits register residue rather than benign constants: ANGLE fixed this exact mechanism unconditionally on Metal with the comment “to avoid indeterminate GPU register residue leaking into color attachments” (compiler/translator/msl/ModifyStruct.cpp:885-887), and the AMD+Mesa workaround exists because unwritten channels produced observable garbage there (the pass’s blend-focused test suite reflects the same symptom). Which of NVIDIA/Intel/Adreno/Mali emit residue, and whether that residue crosses GPU-context (hence origin) boundaries, must be confirmed on hardware.


Root Cause Analysis

  1. WebGL contexts are hardened, but hardening only zero-inits declared arity: ShaderGL.cpp:131-136 sets options->initOutputVariables for every hardened context; Compiler.cpp:1674-1751 initializes the declared variables only (c = vec2(0)), never the undeclared .zw hardware channels of the color-output register.
  2. The pass that would close the gap is vendor-gated: renderergl_utils.cpp:2725-2726 enables expandFragmentOutputsToVec4 only for isAMD && isMesa; ShaderGL.cpp:274-277 copies the feature into the compile option. A tree-wide search confirms this is the sole enabler — nothing under gpu/config or gpu/command_buffer references it.
  3. Both translators honor only the option: TranslatorGLSL.cpp:141-147 and TranslatorESSL.cpp:131-137 run ExpandFragmentOutputsToVec4 solely under compileOptions.expandFragmentOutputsToVec4, so on all other drivers the native driver receives a fragment shader writing only .xy of color output 0:
    // translated output observed via WEBGL_debug_shaders, default flags (vulnerable)
    #version 450
    out vec2 _uc;
    void main(){ (_uc = vec2(0.0, 0.0)); (_uc = vec2(0.25, 0.5)); }
    
  4. No downstream layer rejects or masks the arity mismatch: validationES.cpp:445-460 checks only that an active output variable exists per draw buffer; validationES.cpp:462-471 compares float/int/uint type masks, not component counts. out vec2 → RGBA8 draws are legal GL and validate cleanly; the output merger then writes all four attachment channels from the 4-wide output register.
  5. Android validating-decoder fallback has the same gap: gpu/command_buffer/service/shader_translator.cc:123-135 sets initOutputVariables for its SH_ESSL_OUTPUT translators but has no expansion option, so devices blocklisted from the passthrough decoder are equally exposed.
  6. The Metal backend fixed this same mechanism unconditionally: compiler/translator/msl/ModifyStruct.cpp:885-896 zero-initializes saturated output components for all compilations, with no vendor gate — the GL/GLES backend is the unfixed sibling.

Reproduction

  1. On a GL-backend configuration where !(isAMD && isMesa) — e.g. Linux desktop Chrome defaults on NVIDIA/Intel-Mesa/AMD-proprietary, Android Chrome on the ANGLE GLES backend, or any Mesa-llvmpipe environment — open a page that creates a WebGL2 context.
  2. Create an FBO with an RGBA8 attachment (or RGBA32F with EXT_color_buffer_float for raw 32-bit channel bits) and clear it to a sentinel color.
  3. Compile and draw a fullscreen triangle with fragment shader #version 300 es\nprecision mediump float;\nout vec2 c;\nvoid main(){ c = vec2(0.25, 0.5); } — compilation, linking, and draw validation all succeed.
  4. Call gl.readPixels() and inspect bytes 2–3 (.ba) of each pixel. Optionally dump the translated shader via WEBGL_debug_shaders to confirm the output is still declared vec2.
  5. Expected: a correct implementation writes deterministic zeros to the unwritten channels (as the pass does where enabled, and as the Metal backend does unconditionally).

Observed (executed in a stock build on Mesa llvmpipe, both default flags and --use-angle=gles): the translated shader still declares out vec2; the draw overwrote the sentinel in the never-written .ba channels of all 4096 pixels and readPixels() returned them. On this software driver the deposited values resolve to zeros; per GLES 3.2 §14.2.3 the contents are undefined and driver-dependent, so on hardware register files they may carry residue of prior GPU work — this half is stated as hardware-dependent, not demonstrated.

Severity

medium — Read-only information disclosure reachable from any WebGL2 page with no user interaction and no compromised renderer, across broad default populations (Linux desktop GL, Android GLES); no memory corruption. The sanitization gap and the web-readable deposit of undefined channel data are code- and POC-confirmed; what keeps this at medium rather than high (“GPU-process info leak”) is that the disclosed entity is GPU hardware register state rather than GPU-process address-space memory, and cross-origin provenance of the residue is unproven per hardware family — the one locally testable driver yields benign zeros. Triage may reasonably weigh a promotion to high on Android, where the GPU process is unsandboxed and shared across all origins and register residue could in principle originate from other origins’ GPU work; the identical-mechanism Metal fix was triaged medium, and consistency with that precedent is applied here. Prior art: the June-2026 “Uninitialized Use in ANGLE” CVE wave (e.g. CVE-2026-11087, fixed in 149.0.7827.53) and CVE-2026-8020 (Uninitialized Use in GPU, Chrome on Android) show this class is consistently security-triaged.

Suggested Fix Direction

Key the expansion on context trust level instead of driver vendor: in ShaderGL.cpp, set options->expandFragmentOutputsToVec4 = true for fragment shaders whenever the context is hardened (next to the existing initOutputVariables hardening), or fold the expansion into the initOutputVariables contract in TCompiler. The validating-decoder path (gpu/command_buffer/service/shader_translator.cc) needs the same option for parity on Android. A vendor-independent patch of this shape was build- and behavior-verified (hardened output on both backends, rendering unaffected). Note: a separately filed report covers a copy-back/shadowing defect inside ExpandFragmentOutputsToVec4 itself on the one configuration where it currently runs (AMD+Mesa); both need fixing for attachment-channel sanitization to be effective across the GL/GLES backend.


POC: ANGLE GL/GLES — ExpandFragmentOutputsToVec4 WebGL hardening gated to AMD+Mesa; all other GL drivers leave undefined sub-vec4 fragment output channels readable via WebGL2 readPixels

Strategy: web-content Bug Type: uninitialized-data information disclosure (CWE-908) — no memory corruption, so no ASan report is expected; proof is by observation oracles from plain web content (zero code modification).

ANGLE’s ExpandFragmentOutputsToVec4 translator pass exists to zero-fill the undeclared components of sub-vec4 fragment shader outputs so indeterminate register contents cannot reach color attachments. Its only enabling site is ANGLE_FEATURE_CONDITION(features, expandFragmentOutputsToVec4, isAMD && isMesa) (third_party/angle/src/libANGLE/renderer/gl/renderergl_utils.cpp:2726). On every other GL/GLES driver, a WebGL2 shader declaring out vec2 c hands the native driver a 2-component output; the output merger still writes all four attachment channels from the 4-wide output register, and readPixels() returns the two channels the shader never wrote.

What the POC does

poc.html + poc.js (plain web content, WebGL2 only) run three oracles:

  1. Structural oracle — dumps the exact GLSL ANGLE handed to the native driver via the web-exposed WEBGL_debug_shaders extension. Vulnerable configuration: output still declared vec2 (the pass did not run) — the only sanitization visible is initOutputVariables’ declared-arity-only zero-init. Hardened configuration: output expanded to vec4 with explicit .z = 0.0; .w = 0.0 fill.
  2. Value oracle — clears an RGBA8 (and RGBA32F) FBO to a sentinel (0x11,0x22,0x33,0x44), draws one fullscreen triangle with out vec2 c; c = vec2(0.25, 0.5);, reads back, and inspects the .b/.a channels the shader never assigned. Any .ba != sentinel proves the draw deposited data into channels the shader never wrote.
  3. Residue probes — MRT sibling-value trials, cross-draw secret priming, a register-pressure shader variant, and a blending amplifier, to sample what the undefined channels actually contain on the local driver.

Prerequisites

  • Chromium build (ASAN build used here: out/asan/chrome).
  • X display; the GL driver in this environment is Mesa llvmpipe (isMesa && !isAMD), which is squarely in the vulnerable population — the same gate outcome as Intel Mesa, NVIDIA proprietary, AMD proprietary, Adreno, Mali, etc.

Reproduction

cd /poc-artifacts && python3 server.py &          # serves poc.html on :8080

# Run 1 - DEFAULT flags (vulnerable configuration; ANGLE GL backend / TranslatorGLSL)
/chromium_src/src/out/asan/chrome \
  --no-first-run --no-default-browser-check --ignore-gpu-blocklist \
  --no-proxy-server --user-data-dir=/tmp/chrome-poc-default \
  'http://localhost:8080/poc.html?run=default'

# Run 2 - ANGLE native-GLES backend (TranslatorESSL, the Android-default path)
#          same vulnerable outcome:
#   ... --use-gl=angle --use-angle=gles 'http://localhost:8080/poc.html?run=gles-llvmpipe'

# Run 3 - CONTROL: force-enable the (normally AMD+Mesa-only) feature to show
#          what the hardening does when it runs:
#   ... --enable-angle-features=expandFragmentOutputsToVec4 \
#       'http://localhost:8080/poc.html?run=hardened'

Each run POSTs its full findings back to server.py, which writes results-<run>.json (the files committed here are from actual runs). The page also renders the same JSON.

--no-proxy-server works around this sandbox’s CLI proxy env; --ignore-gpu-blocklist keeps the real GPU process on the (software) GL driver; neither affects the shader-translation behavior under test.

Expected Result

Vulnerable (default flags, and --use-angle=gles): results-default.json / results-gles-llvmpipe.json

  • translatedSource.verdict = "VULNERABLE: output still declared vec2 - ExpandFragmentOutputsToVec4 did NOT run"
  • experiments.rgba8.afterDraw.baStillSentinel = 0 (all 4096 pixels’ .ba overwritten by a shader that never wrote those channels)

Hardened (feature force-enabled, or with bandaid-fix.patch applied): results-hardened.json, results-bandaid-fixed*.json

  • translatedSource.verdict = "HARDENED: output expanded to vec4 (zero-fill pass ran)"

Output (from the committed runs)

Renderer (both backends confirmed ANGLE-on-Mesa-llvmpipe, i.e. isMesa && !isAMD):

default:       ANGLE (Mesa/X.org, llvmpipe (LLVM 15.0.6 256 bits), OpenGL 4.5)
gles-llvmpipe: ANGLE (Mesa/X.org, llvmpipe (LLVM 15.0.6 256 bits), OpenGL ES 3.2)

Translated fragment shader ANGLE handed to the native driver — default run (vulnerable). Note the output is still vec2, and the only hardening applied (initOutputVariables, first assignment) covers the declared components only:

#version 450
out vec2 _uc;
void main(){
  (_uc = vec2(0.0, 0.0));
  (_uc = vec2(0.25, 0.5));
}

Same page, --use-angle=gles (TranslatorESSL — the Android-default backend):

#version 300 es
out mediump vec2 _uc;
void main(){ (_uc = vec2(0.0, 0.0)); (_uc = vec2(0.25, 0.5)); }

Same page, control run with --enable-angle-features=expandFragmentOutputsToVec4 — this is the sanitization every WebGL context should get, and the ONLY difference is the isAMD && isMesa feature gate:

#version 450
vec2 sbbb;
out vec4 _uc;
void main(){
  (sbbb = vec2(0.0, 0.0));
  (sbbb = vec2(0.25, 0.5));
  {
    (_uc.xy = sbbb);
    (_uc.z = 0.0);
    (_uc.w = 0.0);
  }
}

Value oracle (default run, results-default.json): the sentinel-cleared attachment read back [17,34,51,68,...] before the draw; after the vec2-output draw all 4096 pixels read [64,128,0,0] — .rg are the shader’s values, .ba were overwritten by the draw (sentinel destroyed) with data the shader never wrote:

"afterDraw": { "pixels": 4096, "rgWrittenCorrectly": 4096,
               "baStillSentinel": 0, "baZero": 4096, "baOtherValue": 0 }

What the undefined channels contain on this driver

On this specific software driver (Mesa 22.3.6 llvmpipe), the undefined channels deterministically resolve to zero bits (RGBA8 .ba = 0x00,0x00; RGBA32F .zw = 0x00000000), in all probes (simple/complex shaders, MRT sibling secrets, cross-draw priming, blend amplifier). So on llvmpipe the unsanitized deposit is benign in content. The security-relevant facts the POC proves end-to-end from web content are:

  • ANGLE’s sanitization pass does NOT run for WebGL contexts on any !(isAMD && isMesa) driver (structural oracle, both GL and GLES backends);
  • the native driver’s output merger DOES write the never-written channels into the attachment, and readPixels() returns them (value oracle);
  • what fills those channels is left entirely to the driver — per GLES 3.2 §14.2.3 it is undefined. Which drivers emit register residue rather than benign constants is hardware-dependent and cannot be sampled in this GPU-less environment; ANGLE’s own tree documents residue on real hardware (the unconditional Metal fix for the same mechanism cites “indeterminate GPU register residue leaking into color attachments”, compiler/translator/msl/ModifyStruct.cpp:885-887, and the AMD+Mesa workaround exists because unwritten channels produced observable garbage there).

Environment notes: Mesa softpipe was also tried as a second local driver but cannot host a Chromium GPU process at all (desktop GL capped at 3.3 / ES context creation fails), so llvmpipe is the only locally testable driver. Windows (D3D11) and macOS (Metal) defaults do not use this backend and are out of scope of this finding.

Bandaid Fix

bandaid-fix.patch (to third_party/angle/src/libANGLE/renderer/gl/ShaderGL.cpp) keys the expansion on context trust level instead of driver vendor: set options->expandFragmentOutputsToVec4 = true for fragment shaders on all hardened (WebGL) contexts, next to the existing initOutputVariables hardening.

Verified: with the patch applied (autoninja -C out/asan chrome, ANGLE is statically linked into libgl_init.so), the same POC page with default flags reports HARDENED on both backends — output expanded to vec4 with .z/.w zero-filled — and rendering is unaffected (rgWrittenCorrectly: 4096/4096). See results-bandaid-fixed.json and results-bandaid-fixed-gles.json. The validating-decoder path (gpu/command_buffer/service/shader_translator.cc) needs the same option for parity on Android; the patch covers the ANGLE (passthrough) path demonstrated here.

Files

File Description
poc.html, poc.js The POC page (plain web content)
server.py Serves the page, stores POSTed results
results-default.json Stock build, default flags — vulnerable (GL backend)
results-gles-llvmpipe.json Stock build, --use-angle=gles — vulnerable (GLES backend)
results-hardened.json Stock build + --enable-angle-features=expandFragmentOutputsToVec4 — control
results-bandaid-fixed.json Patched build, default flags — hardened
results-bandaid-fixed-gles.json Patched build, --use-angle=gles — hardened
bandaid-fix.patch Vendor-independent hardening fix (verified)

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