CVE-2026-79120
Overview
Files Changed
src/libANGLE/Context.cppsrc/libANGLE/Framebuffer.cppsrc/libANGLE/Shader.cppsrc/libANGLE/VertexArray.cppsrc/libANGLE/renderer/d3d/FramebufferD3D.cppsrc/libANGLE/renderer/d3d/ShaderD3D.cppsrc/libANGLE/renderer/gl/ShaderGL.cppsrc/libANGLE/renderer/metal/ShaderMtl.mmsrc/libANGLE/renderer/vulkan/ShaderVk.cppsrc/libANGLE/validationES.cpp
Patch
From c7e5a77b65ae275b50ccd6fbe92ad023ee7aba93 Mon Sep 17 00:00:00 2001 From: Shahbaz Youssefi <[email protected]> Date: Thu, 06 Aug 2026 14:40:21 -0400 Subject: [PATCH] Automatically mark webgl contexts as hardened No need to do `webgl || hardened` everywhere, checking for hardened is sufficient and includes webgl. Bug: chromium:512971896 Change-Id: Ic461f760f21439fd3168b6b48526ec77a36ad9b7 Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/8214571 Commit-Queue: Shahbaz Youssefi <[email protected]> Reviewed-by: Geoff Lang <[email protected]> --- diff --git a/src/libANGLE/Context.cpp b/src/libANGLE/Context.cpp index 5089ace..460e606 100644 --- a/src/libANGLE/Context.cpp +++ b/src/libANGLE/Context.cpp @@ -724,7 +724,7 @@ mCurrentReadSurface(static_cast<egl::Surface *>(EGL_NO_SURFACE)), mDisplay(display), mWebGLContext(GetWebGLContext(attribs)), - mHardenedContext(GetHardenedContext(attribs)), + mHardenedContext(mWebGLContext || GetHardenedContext(attribs)), mBufferAccessValidationEnabled(false), mRequiresRobustBehavior(false), mExtensionsEnabled(GetExtensionsEnabled(attribs, mWebGLContext)), diff --git a/src/libANGLE/Framebuffer.cpp b/src/libANGLE/Framebuffer.cpp index 5261ffd..c87fa59 100644 --- a/src/libANGLE/Framebuffer.cpp +++ b/src/libANGLE/Framebuffer.cpp @@ -1568,10 +1568,10 @@ err::kFramebufferIncompleteMultisampleNonFixedSamplesWithRenderbuffers); } - // The WebGL conformance tests implicitly define that all framebuffer - // attachments must be unique. For example, the same level of a texture can - // not be attached to two different color attachments. - if (context->isWebGL() || context->isHardenedContext()) + // The WebGL conformance tests implicitly define that all framebuffer attachments must be + // unique. For example, the same level of a texture can not be attached to two different color + // attachments. The same restriction is applied to hardened contexts. + if (context->isHardenedContext()) { if (!mState.colorAttachmentsAreUniqueImages()) { diff --git a/src/libANGLE/Shader.cpp b/src/libANGLE/Shader.cpp index 76c5b76..2a64478 100644 --- a/src/libANGLE/Shader.cpp +++ b/src/libANGLE/Shader.cpp @@ -629,9 +629,9 @@ options.objectCode = true; options.emulateGLDrawID = true; - // Add default options to WebGL shaders to prevent unexpected behavior during - // compilation. - if (context->isWebGL() || context->isHardenedContext()) + // Add default options to WebGL shaders to prevent unexpected behavior during compilation. + // Similarly protect hardened contexts. + if (context->isHardenedContext()) { options.initGLPosition = true; options.limitCallStackDepth = true; diff --git a/src/libANGLE/VertexArray.cpp b/src/libANGLE/VertexArray.cpp index 14db914..2c80758 100644 --- a/src/libANGLE/VertexArray.cpp +++ b/src/libANGLE/VertexArray.cpp @@ -523,7 +523,7 @@ boundBuffer->addRef(); boundBuffer->onNonTFBindingChanged(1); boundBuffer->addVertexArrayBinding(context, bindingIndex); - if (context->isWebGL() || context->isHardenedContext()) + if (context->isHardenedContext()) { mCachedBufferPropertyTransformFeedbackConflict.set( bindingIndex, boundBuffer->hasTFBBindingConflict()); @@ -765,7 +765,7 @@ } } - if (context->isWebGL() || context->isHardenedContext()) + if (context->isHardenedContext()) { for (size_t bindingIndex : bufferBindingMask) { @@ -868,7 +868,7 @@ } } - if (context->isWebGL() || context->isHardenedContext()) + if (context->isHardenedContext()) { if (buffer->hasTFBBindingConflict()) { @@ -918,7 +918,7 @@ break; case angle::SubjectMessage::BindingChanged: - if (context->isWebGL() || context->isHardenedContext()) + if (context->isHardenedContext()) { bufferBindingMask.reset(kElementArrayBufferIndex); diff --git a/src/libANGLE/renderer/d3d/FramebufferD3D.cpp b/src/libANGLE/renderer/d3d/FramebufferD3D.cpp index 1a3460b..204b3a7 100644 --- a/src/libANGLE/renderer/d3d/FramebufferD3D.cpp +++ b/src/libANGLE/renderer/d3d/FramebufferD3D.cpp @@ -265,7 +265,7 @@ // D3D11 does not allow for overlapping RenderTargetViews. // If WebGL compatibility (or hardened context) is enabled, this has already been checked at a // higher level. - if (!context->isWebGL() && !context->isHardenedContext()) + if (!context->isHardenedContext()) { if (!mState.colorAttachmentsAreUniqueImages()) { diff --git a/src/libANGLE/renderer/d3d/ShaderD3D.cpp b/src/libANGLE/renderer/d3d/ShaderD3D.cpp index 33351fd..8aaa5ab 100644 --- a/src/libANGLE/renderer/d3d/ShaderD3D.cpp +++ b/src/libANGLE/renderer/d3d/ShaderD3D.cpp @@ -286,8 +286,7 @@ } #endif - const bool isHardened = context->isWebGL() || context->isHardenedContext(); - if (isHardened) + if (context->isHardenedContext()) { options->clampIndirectArrayBounds = true; } diff --git a/src/libANGLE/renderer/gl/ShaderGL.cpp b/src/libANGLE/renderer/gl/ShaderGL.cpp index ab40bee..a58eb4e 100644 --- a/src/libANGLE/renderer/gl/ShaderGL.cpp +++ b/src/libANGLE/renderer/gl/ShaderGL.cpp @@ -128,7 +128,7 @@ options->initGLPosition = true; - const bool isHardened = context->isWebGL() || context->isHardenedContext(); + const bool isHardened = context->isHardenedContext(); if (isHardened || (features.initFragmentOutputVariables.enabled && mState.getShaderType() == gl::ShaderType::Fragment)) { diff --git a/src/libANGLE/renderer/metal/ShaderMtl.mm b/src/libANGLE/renderer/metal/ShaderMtl.mm index fd75a48..5fb6ca4 100644 --- a/src/libANGLE/renderer/metal/ShaderMtl.mm +++ b/src/libANGLE/renderer/metal/ShaderMtl.mm @@ -70,8 +70,7 @@ options->forceDeferNonConstGlobalInitializers = true; - const bool isHardened = context->isWebGL() || context->isHardenedContext(); - if (isHardened && mState.getShaderType() != gl::ShaderType::Compute) + if (context->isHardenedContext() && mState.getShaderType() != gl::ShaderType::Compute) { options->initOutputVariables = true; } diff --git a/src/libANGLE/renderer/vulkan/ShaderVk.cpp b/src/libANGLE/renderer/vulkan/ShaderVk.cpp index 352282e..b2103a4 100644 --- a/src/libANGLE/renderer/vulkan/ShaderVk.cpp +++ b/src/libANGLE/renderer/vulkan/ShaderVk.cpp @@ -25,7 +25,7 @@ { ContextVk *contextVk = vk::GetImpl(context); - if (context->isWebGL() || context->isHardenedContext()) + if (context->isHardenedContext()) { // Only WebGL requires initialization of local variables, others don't. // Extra initialization in spirv shader may affect performance. diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp index 7b3f9e1..c5a6a2e 100644 --- a/src/libANGLE/validationES.cpp +++ b/src/libANGLE/validationES.cpp @@ -615,8 +615,7 @@ } } - if (ANGLE_UNLIKELY(context->isWebGL() || context->isBufferAccessValidationEnabled() || - context->isHardenedContext())) + if (ANGLE_UNLIKELY(context->isHardenedContext() || context->isBufferAccessValidationEnabled())) { // Uniform buffer validation for (size_t uniformBlockIndex = 0; uniformBlockIndex < executable.getUniformBlocks().size(); @@ -628,8 +627,7 @@ const OffsetBindingPointer<Buffer> &uniformBuffer = state.getIndexedUniformBuffer(blockBinding); - if (uniformBuffer.get() == nullptr && - (context->isWebGL() || context->isHardenedContext())) + if (uniformBuffer.get() == nullptr && context->isHardenedContext()) { // undefined behaviour return gl::err::kUniformBufferUnbound; @@ -1754,8 +1752,7 @@ return false; } - if ((context->isWebGL() || context->isHardenedContext()) &&
Original Bug Report
Potential info leak due to premature robust-resource-init state commitment in ANGLE
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: ANGLE’s front-end robust resource initialization logic marks textures and framebuffers as initialized before executing the underlying backend operation. If the backend operation fails, the resource’s state remains marked as initialized despite the GPU memory being unwritten. This could allow an attacker to sample uninitialized GPU memory, leading to a potential cross-origin information leak.
Affected files:
third_party/angle/src/libANGLE/Texture.cppthird_party/angle/src/libANGLE/Framebuffer.cppthird_party/angle/src/libANGLE/Context.cpp
Estimated timestamp from git blame: Unknown (Google3 checkout)
Root Cause Analysis
In ANGLE’s front-end, the robust-resource-init feature tracks whether a resource (like a texture level or a framebuffer attachment) has been initialized to prevent reading uninitialized GPU memory. A logic flaw exists where the front-end commits the InitState::Initialized state before the backend implementation executes the write. If the backend operation subsequently fails, the resource is falsely flagged as initialized.
Texture Initialization
In third_party/angle/src/libANGLE/Texture.cpp, ensureSubImageInitialized is called before dispatching to the backend at sub-image entry points (e.g., setSubImage).
angle::Result Texture::ensureSubImageInitialized(..., const Box &area) {
if (doesSubImageNeedInit(context, imageIndex, area)) {
ANGLE_TRY(initializeContents(context, GL_NONE, imageIndex));
}
setInitState(GL_NONE, imageIndex, InitState::Initialized); // Always committed
return angle::Result::Continue;
}
When an attacker issues an upload that exactly covers the full extent of the resource, doesSubImageNeedInit returns false, bypassing explicit zero-fill initialization. setInitState(Initialized) is then unconditionally called, expecting the backend to fully overwrite the texture.
Subsequently, Texture::setSubImage calls mTexture->setSubImage(...). If this backend operation fails (returning an error), the ANGLE_TRY macro aborts the function. However, the InitState::Initialized state is never rolled back.
Framebuffer Initialization
A similar pattern exists in third_party/angle/src/libANGLE/Framebuffer.cpp within ensureClearAttachmentsInitialized. The front-end marks attachments as initialized before calling the backend’s clear method. A comment in the code explicitly acknowledges this:
> // If the impl encounters an error during a a full (non-partial) clear, the attachments will
> // still be marked initialized. This simplifies design, allowing this method to be called before
> // the clear.
Impact
If an attacker can induce a backend failure during a full-resource upload or clear, the resource’s GPU memory remains completely uninitialized but ANGLE considers it initialized. Subsequent sampling or readPixels operations will bypass zero-fill mitigations, allowing the attacker to read recycled GPU memory. This memory may contain cross-origin data, leading to an information leak.
Crucially, while an Out-Of-Memory (OOM) error typically triggers context loss (mitigating the leak), a non-OOM error does not. For example, in the Metal backend (TextureMtl.mm), ANGLE_CHECK_GL_MATH is used to validate pitch and skip bytes. If these calculations overflow, a GL_INVALID_OPERATION is generated. According to ErrorSet::handleError in Context.cpp, GL_INVALID_OPERATION does not trigger context loss. The WebGL context survives, allowing the attacker to read the uninitialized texture.
Suggested Steps to Trigger (Potential)
Note: These are potential steps as our tooling agent cannot execute code to verify them.
- Establish a WebGL context in a browser window.
- Create a texture and allocate its storage using
gl.texStorage2D(). Its state isMayNeedInit. - Issue a
gl.texSubImage2D()call where the upload area exactly matches the entire texture dimensions. - Craft the parameters (e.g., unpack alignment, row length, or offsets) such that they pass front-end validation but cause an integer overflow or validation failure in the specific backend implementation (e.g., triggering
ANGLE_CHECK_GL_MATHin Metal’ssetSubImageImpl). - The front-end marks the texture as
Initialized, but the backend fails and returnsGL_INVALID_OPERATION. - Because the error is not an OOM, the context survives.
- Execute
gl.readPixels()or sample the texture in a shader. Because the state isInitialized, ANGLE permits the read without zeroing the memory, exposing uninitialized GPU memory.
Proposed Fix
Do not commit the InitState::Initialized state until the backend operation has successfully completed. This could be achieved by modifying ensureSubImageInitialized and ensureClearAttachmentsInitialized to return a state update token or an RAII helper that only applies the Initialized state upon successful return of the surrounding function. Alternatively, move the setInitState calls to occur immediately after the ANGLE_TRY(mTexture->setSubImage(...)) and ANGLE_CONTEXT_TRY(mState.getDrawFramebuffer()->clear(...)) calls succeed.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.