Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in GPU
DescriptionBuffer overflow in GPU
ComponentGPU
Sub-componentGles2_implementation
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker551593376
Fix commit6454cd98685d (chromium/src) +447/-46
CWECWE-121 (Stack buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedCassio Lima
Reported2026-08-24 (per vendor advisory)
Fix landed2026-08-26 (public git)
Disclosed2026-09-01
Patch gap6 days — fix public in source before the advisory

Background

Command buffer
Chrome’s GPU IPC layer: a client library in the renderer/GPU-client process issues commands to a separate, sandboxed GPU service process, which executes real GL and returns results.
`GLES2Util::GLGetNumValuesReturned(pname)`
a client-side lookup that maps each query enum to the fixed number of values it is defined to return, used to size the caller’s params buffer.
`build_cmd_buffer_lib.py` / codegen
the autogen templates that emit the client-side glGet* handlers into gles2_implementation_impl_autogen.h.

Root Cause Analysis

The autogenerated client-side glGet* handlers in gles2_implementation_impl_autogen.h issued the query command, waited for the service to respond via WaitForCmd(), then called result->CopyResult(params) to copy result->GetNumResults() values into the caller’s params buffer. That GetNumResults() count is written into the shared result memory by the GPU service process and was trusted verbatim by the client. Because the caller’s params buffer is sized only for the number of values the given pname is defined to return, a service that reported a larger count than the pname warrants caused CopyResult to write past the end of params. The client never cross-checked the returned count against the known per-enum count from GLES2Util::GLGetNumValuesReturned(pname).

The fix clamps the copied count with result->SetNumResults(std::min(result->GetNumResults(), expected_num_results)) before CopyResult, and adds a DCHECK that the expected count is nonzero. Supporting changes add missing enum cases to GLGetNumValuesReturned (in build_gles2_cmd_buffer.py) and give PixelLocalStorage params a named enum set so their expected counts resolve correctly.

Key insight
The client trusted a length field controlled by the separate, potentially compromised GPU service process and used it to bound a copy into a fixed-size buffer, instead of deriving the count from the pname it already knew locally.

Attack Path

  1. Compromise the service An attacker gains code execution in, or otherwise controls the output of, the GPU service process (the process that fills the shared result buffer).
  2. Inflate the result count The service writes a GetNumResults() value into the shared result memory larger than what the queried pname legitimately returns.
  3. Trigger a query Client code (or an attacker-influenced code path in the renderer) calls a glGet* such as glGetIntegerv/glGetShaderiv with a small, correctly-sized params buffer.
  4. Overflow on copy The generated handler calls result->CopyResult(params), copying the attacker-controlled number of values past the end of params, overflowing the caller’s buffer in the client process.

Impact Assessment

An out-of-bounds write in the GPU-client process (renderer / GPU client) driven by a count controlled by the GPU service, giving a heap/stack buffer overflow beyond the fixed-size params output buffer. This enables corruption of adjacent memory and can escalate to code execution in the client process, useful as a component in a sandbox-escape or cross-process exploit chain originating from a compromised GPU service.

Changed Functions

FunctionChangeNotes
TEST_F
gpu/command_buffer/client/gles2_implementation_unittest.cc
modified

Files Changed

  • gpu/command_buffer/gles2_cmd_buffer_functions.txt
  • gpu/command_buffer/build_gles2_cmd_buffer.py
  • gpu/command_buffer/common/gles2_cmd_utils_implementation_autogen.h
  • gpu/command_buffer/client/gles2_implementation_unittest_autogen.h
  • gpu/command_buffer/service/gles2_cmd_decoder_autogen.h
  • gpu/command_buffer/common/gles2_cmd_utils.cc
  • gpu/command_buffer/build_cmd_buffer_lib.py
  • gpu/command_buffer/common/gles2_cmd_utils_autogen.h
  • gpu/command_buffer/client/gles2_implementation_unittest.cc
  • gpu/command_buffer/client/gles2_implementation_impl_autogen.h
  • gpu/command_buffer/service/gles2_cmd_validation_implementation_autogen.h
  • gpu/command_buffer/service/gles2_cmd_validation_autogen.h

Audit Directions

  • Service-controlled length fields
    Audit every client-side consumer of shared/IPC result memory (GetNumResults, size, or count fields written by the GPU service) for copies into fixed-size buffers without a locally-derived bound.
  • Codegen-emitted copies
    Review the command buffer autogen templates in build_cmd_buffer_lib.py/build_gles2_cmd_buffer.py for other emitted CopyResult/memcpy patterns that trust remote counts, since one template flaw replicates across many generated glGet* handlers.
  • Enum-to-count completeness
    Verify GLES2Util::GLGetNumValuesReturned covers every pname reachable by a glGet* (no 0/default fall-through), since a missing case would silently defeat the clamp; the DCHECK(expected_num_results != 0) flags gaps only in debug builds.
From 6454cd98685d08547fdf80ae56c31267381050ed Mon Sep 17 00:00:00 2001
From: Corentin Wallez <cwallez@chromium.org>
Date: Wed, 26 Aug 2026 09:58:29 -0700
Subject: [PATCH] gles2_implementation: Clamp the number of values returned by glGet*

glGet*'s autogen implementation reads the number of bytes returned by
the GPU process and writes them directly to the return pointer, without
checking that it matches the number of parameters for the given `pname`.

Modify build_cmd_buffer_lib so that in gles2_implementation_impl_autogen.cc,
GLES2Util::GLGetNumValuesReturned is used to clamp the number of returned
results.

Other required changes:
 - Add more cases to GLES2Util::GLGetNumValuesReturned to cover all the
   enums that the gles2_cmd_decoder may handle.
 - Modify the codegen of gles2_implementation_unittest_autogen.h to use
   a real uncached pname for "ValidNonCached" parameters.
 - Update PixelLocalStorage function definition in the GLES2 command
   decoder to use a "named enum set" similarly to all other glGet*. This
   also changes the gles2_cmd_decoder autogen files in a noop way
   because PLS is only exposed via the passthrough command decoder.
 - Adds a test of glGetShaderiv that would fail without the fix.

Fixed: 551593376
Change-Id: I0db53a77709f2a51de203923cb306a2f414039c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8285102
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1686513}
---
diff --git a/gpu/command_buffer/build_cmd_buffer_lib.py b/gpu/command_buffer/build_cmd_buffer_lib.py
index 7c8b303..f6f420f 100644
--- a/gpu/command_buffer/build_cmd_buffer_lib.py
+++ b/gpu/command_buffer/build_cmd_buffer_lib.py
@@ -2883,6 +2883,12 @@
   if (!WaitForCmd()) {
     return;
   }
+
+  int expected_num_results = util_.GLGetNumValuesReturned(pname);
+  DCHECK(expected_num_results != 0) << pname;
+  result->SetNumResults(
+    std::min(result->GetNumResults(), expected_num_results));
+
   result->CopyResult(%(last_arg_name)s);
   GPU_CLIENT_LOG_CODE_BLOCK({
     for (int32_t i = 0; i < result->GetNumResults(); ++i) {
@@ -5009,6 +5015,14 @@
     index = func.GetCmdArgs().index(self)
     return str(index + 1)
 
+  def GetValidNonCachedClientSideArg(self, _):
+    """Return a glGet pname so that GLES2Util::GLGetNumValuesReturned returns
+    1, but that is also not cached on the GLES2Implementation"""
+    return 'GL_COMPILE_STATUS'
+
+  def GetValidNonCachedClientSideCmdArg(self, func):
+    return self.GetValidNonCachedClientSideArg(func)
+
   def GetValidGLArg(self, func):
     """Gets a valid value for this argument."""
     return self.GetValidArg(func)
diff --git a/gpu/command_buffer/build_gles2_cmd_buffer.py b/gpu/command_buffer/build_gles2_cmd_buffer.py
index 7a161dd..3510b07 100755
--- a/gpu/command_buffer/build_gles2_cmd_buffer.py
+++ b/gpu/command_buffer/build_gles2_cmd_buffer.py
@@ -1549,6 +1549,22 @@
       'GL_SHARED_IMAGE_ACCESS_MODE_READ_CHROMIUM',
     ],
   },
+  'FramebufferPixelLocalStorageParameter': {
+    'type': 'GLenum',
+    'valid': [
+      'GL_MAX_PIXEL_LOCAL_STORAGE_PLANES_ANGLE',
+      'GL_MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES_ANGLE',
+      'GL_PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_ANGLE',
+      'GL_PIXEL_LOCAL_INTERNAL_FORMAT_ANGLE',
+      'GL_PIXEL_LOCAL_TEXTURE_NAME_ANGLE',
+      'GL_PIXEL_LOCAL_TEXTURE_LEVEL_ANGLE',
+      'GL_PIXEL_LOCAL_TEXTURE_LAYER_ANGLE',
+      'GL_PIXEL_LOCAL_USAGE_ANGLE',
+      'GL_PIXEL_LOCAL_CLEAR_VALUE_FLOAT_ANGLE',
+      'GL_PIXEL_LOCAL_CLEAR_VALUE_INT_ANGLE',
+      'GL_PIXEL_LOCAL_CLEAR_VALUE_UNSIGNED_INT_ANGLE',
+    ],
+  },
 }
 
 # A function info object specifies the type and other special data for the
diff --git a/gpu/command_buffer/common/gles2_cmd_utils.cc b/gpu/command_buffer/common/gles2_cmd_utils.cc
index f7583385..684f3b2a 100644
--- a/gpu/command_buffer/common/gles2_cmd_utils.cc
+++ b/gpu/command_buffer/common/gles2_cmd_utils.cc
@@ -110,6 +110,8 @@
       return 1;
     case GL_LINE_WIDTH:
       return 1;
+    case GL_MAJOR_VERSION:
+      return 1;
     case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS:
       return 1;
     case GL_MAX_CUBE_MAP_TEXTURE_SIZE:
@@ -132,8 +134,12 @@
       return 1;
     case GL_MAX_VIEWPORT_DIMS:
       return 2;
+    case GL_MINOR_VERSION:
+      return 1;
     case GL_NUM_COMPRESSED_TEXTURE_FORMATS:
       return 1;
+    case GL_NUM_PROGRAM_BINARY_FORMATS:
+      return 1;
     case GL_NUM_SHADER_BINARY_FORMATS:
       return 1;
     case GL_PACK_ALIGNMENT:
@@ -150,10 +156,14 @@
       return 1;
     case GL_SAMPLE_BUFFERS:
       return 1;
+    case GL_SAMPLE_COVERAGE:
+      return 1;
     case GL_SAMPLE_COVERAGE_INVERT:
       return 1;
     case GL_SAMPLE_COVERAGE_VALUE:
       return 1;
+    case GL_SAMPLE_ALPHA_TO_COVERAGE:
+      return 1;
     case GL_SAMPLES:
       return 1;
     case GL_SCISSOR_BOX:
@@ -202,6 +212,10 @@
       return 1;
     case GL_TEXTURE_BINDING_2D:
       return 1;
+    case GL_TEXTURE_BINDING_2D_ARRAY:
+      return 1;
+    case GL_TEXTURE_BINDING_3D:
+      return 1;
     case GL_TEXTURE_BINDING_CUBE_MAP:
       return 1;
     case GL_TEXTURE_BINDING_EXTERNAL_OES:
@@ -222,12 +236,16 @@
       return 1;
     case GL_MAX_ARRAY_TEXTURE_LAYERS:
       return 1;
+    case GL_MAX_COLOR_ATTACHMENTS_EXT:
+      return 1;
     case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS:
       return 1;
     case GL_MAX_COMBINED_UNIFORM_BLOCKS:
       return 1;
     case GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS:
       return 1;
+    case GL_MAX_DRAW_BUFFERS:
+      return 1;
     case GL_MAX_ELEMENT_INDEX:
       return 1;
     case GL_MAX_ELEMENTS_INDICES:
@@ -266,10 +284,32 @@
       return 1;
     case GL_MIN_PROGRAM_TEXEL_OFFSET:
       return 1;
+    case GL_NUM_EXTENSIONS:
+      return 1;
+    case GL_PACK_ROW_LENGTH:
+      return 1;
+    case GL_PACK_SKIP_PIXELS:
+      return 1;
+    case GL_PACK_SKIP_ROWS:
+      return 1;
     case GL_PIXEL_PACK_BUFFER_BINDING:
       return 1;
     case GL_PIXEL_UNPACK_BUFFER_BINDING:
       return 1;
+    case GL_PRIMITIVE_RESTART_FIXED_INDEX:
+      return 1;
+    case GL_RASTERIZER_DISCARD:
+      return 1;
+    case GL_READ_BUFFER:
+      return 1;
+    case GL_READ_FRAMEBUFFER_BINDING:
+      return 1;
+    case GL_SAMPLER_BINDING:
+      return 1;
+    case GL_TRANSFORM_FEEDBACK_ACTIVE:
+      return 1;
+    case GL_TRANSFORM_FEEDBACK_BINDING:
+      return 1;
     case GL_TRANSFORM_FEEDBACK_BUFFER_BINDING:
       return 1;
     case GL_TRANSFORM_FEEDBACK_BUFFER_MODE:
@@ -278,6 +318,8 @@
       return 1;
     case GL_TRANSFORM_FEEDBACK_BUFFER_START:
       return 1;
+    case GL_TRANSFORM_FEEDBACK_PAUSED:
+      return 1;
     case GL_UNIFORM_BUFFER_BINDING:
       return 1;
     case GL_UNIFORM_BUFFER_SIZE:
@@ -286,6 +328,16 @@
       return 1;
     case GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT:
       return 1;
+    case GL_UNPACK_ROW_LENGTH:
+      return 1;
+    case GL_UNPACK_IMAGE_HEIGHT:
+      return 1;
+    case GL_UNPACK_SKIP_PIXELS:
+      return 1;
+    case GL_UNPACK_SKIP_ROWS:
+      return 1;
+    case GL_UNPACK_SKIP_IMAGES:
+      return 1;
 
     // ES31
     case GL_ATOMIC_COUNTER_BUFFER_BINDING:
@@ -326,6 +378,8 @@
       return 1;
     case GL_NUM_WINDOW_RECTANGLES_EXT:
       return 1;
+    case GL_WINDOW_RECTANGLE_EXT:
+      return 4;
 
     // -- glGetBufferParameteriv
     case GL_BUFFER_SIZE:
@@ -510,6 +564,98 @@
     case GL_FRAGMENT_SHADER_DERIVATIVE_HINT_OES:
       return 1;
 
+    // -- GL_ANGLE_shader_pixel_local_storage
+    case GL_MAX_PIXEL_LOCAL_STORAGE_PLANES_ANGLE:
+    case GL_MAX_COMBINED_DRAW_BUFFERS_AND_PIXEL_LOCAL_STORAGE_PLANES_ANGLE:
+    case GL_PIXEL_LOCAL_STORAGE_ACTIVE_PLANES_ANGLE:
+    case GL_PIXEL_LOCAL_INTERNAL_FORMAT_ANGLE:
+    case GL_PIXEL_LOCAL_TEXTURE_NAME_ANGLE:
+    case GL_PIXEL_LOCAL_TEXTURE_LEVEL_ANGLE:
+    case GL_PIXEL_LOCAL_TEXTURE_LAYER_ANGLE:
+    case GL_PIXEL_LOCAL_USAGE_ANGLE:
+      return 1;
+    case GL_PIXEL_LOCAL_CLEAR_VALUE_FLOAT_ANGLE:
+    case GL_PIXEL_LOCAL_CLEAR_VALUE_INT_ANGLE:
+    case GL_PIXEL_LOCAL_CLEAR_VALUE_UNSIGNED_INT_ANGLE:
+      return 4;
+
+    // -- GL_EXT_clip_control
+    case GL_CLIP_ORIGIN_EXT:
+    case GL_CLIP_DEPTH_MODE_EXT:
+      return 1;
+
+    // -- GL_EXT_depth_clamp
+    case GL_DEPTH_CLAMP_EXT:
+      return 1;
+
+    // -- GL_EXT_polygon_offset_clamp
+    case GL_POLYGON_OFFSET_CLAMP_EXT:
+      return 1;
+
+    // -- GL_KHR_parallel_shader_compile
+    case GL_COMPLETION_STATUS_KHR:
+      return 1;
+
+    // -- GL_ANGLE_polygon_mode
+    case GL_POLYGON_MODE_ANGLE:
+    case GL_POLYGON_OFFSET_LINE_ANGLE:
+      return 1;
+
+    // -- GL_OES_shader_multisample_interpolation
+    case GL_MIN_FRAGMENT_INTERPOLATION_OFFSET_OES:
+    case GL_MAX_FRAGMENT_INTERPOLATION_OFFSET_OES:
+    case GL_FRAGMENT_INTERPOLATION_OFFSET_BITS_OES:
+      return 1;
+
+    // -- GL_EXT_clip_cull_distance
+    case GL_MAX_CLIP_DISTANCES_ANGLE:
+    case GL_MAX_CULL_DISTANCES_ANGLE:
+    case GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES_ANGLE:
+    case GL_CLIP_DISTANCE0_ANGLE:
+    case GL_CLIP_DISTANCE1_ANGLE:
+    case GL_CLIP_DISTANCE2_ANGLE:
+    case GL_CLIP_DISTANCE3_ANGLE:
+    case GL_CLIP_DISTANCE4_ANGLE:
+    case GL_CLIP_DISTANCE5_ANGLE:
+    case GL_CLIP_DISTANCE6_ANGLE:
+    case GL_CLIP_DISTANCE7_ANGLE:
+      return 1;
+
+    // -- GL_ANGLE_provoking_vertex
+    case GL_PROVOKING_VERTEX_ANGLE:
+      return 1;
+
+    // -- GL_ANGLE_stencil_texturing
+    case GL_DEPTH_STENCIL_TEXTURE_MODE_ANGLE:
+      return 1;
+
+    // -- GL_EXT_framebuffer_sRGB
+    case GL_FRAMEBUFFER_SRGB_EXT:
+      return 1;
+
+    // -- GL_EXT_disjoint_timer_query
+    case GL_GPU_DISJOINT_EXT:
+      return 1;
+
+    // -- GL_EXT_blend_func_extended
+    case GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT:
+      return 1;
+
+    // -- GL_EXT_multisample_compatibility
+    case GL_MULTISAMPLE_EXT:
+    case GL_SAMPLE_ALPHA_TO_ONE_EXT:
+      return 1;
+
+    // -- GL_OES_vertex_array_object
+    case GL_VERTEX_ARRAY_BINDING_OES:
+      return 1;
+
+    // -- GL_OVR_multiview2
+    case GL_MAX_VIEWS_OVR:
+    case GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_NUM_VIEWS_OVR:
+    case GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_BASE_VIEW_INDEX_OVR:
+      return 1;
+
     // bad enum
     default:
       return 0;
diff --git a/gpu/command_buffer/gles2_cmd_buffer_functions.txt b/gpu/command_buffer/gles2_cmd_buffer_functions.txt
index e1d9d782..4e38675 100644
--- a/gpu/command_buffer/gles2_cmd_buffer_functions.txt
+++ b/gpu/command_buffer/gles2_cmd_buffer_functions.txt
@@ -378,9 +378,9 @@
 GL_APICALL void         GL_APIENTRY glPixelLocalStorageBarrierANGLE (void);
 GL_APICALL void         GL_APIENTRY glFramebufferPixelLocalStorageInterruptANGLE (void);
 GL_APICALL void         GL_APIENTRY glFramebufferPixelLocalStorageRestoreANGLE (void);
-GL_APICALL void         GL_APIENTRY glGetFramebufferPixelLocalStorageParameterfvANGLE (GLint plane, GLenum pname, GLfloat* params);
-GL_APICALL void         GL_APIENTRY glGetFramebufferPixelLocalStorageParameterivANGLE (GLint plane, GLenum pname, GLint* params);
-GL_APICALL void         GL_APIENTRY glGetFramebufferPixelLocalStorageParameteruivANGLE (GLint plane, GLenum pname, GLuint* params);
+GL_APICALL void         GL_APIENTRY glGetFramebufferPixelLocalStorageParameterfvANGLE (GLint plane, GLenumFramebufferPixelLocalStorageParameter pname, GLfloat* params);
+GL_APICALL void         GL_APIENTRY glGetFramebufferPixelLocalStorageParameterivANGLE (GLint plane, GLenumFramebufferPixelLocalStorageParameter pname, GLint* params);
+GL_APICALL void         GL_APIENTRY glGetFramebufferPixelLocalStorageParameteruivANGLE (GLint plane, GLenumFramebufferPixelLocalStorageParameter pname, GLuint* params);
 
 // Extension EXT_clip_control
 GL_APICALL void         GL_APIENTRY glClipControlEXT (GLenum origin, GLenum depth);
diff --git a/gpu/command_buffer/client/gles2_implementation_unittest.cc b/gpu/command_buffer/client/gles2_implementation_unittest.cc
index 0de18a57..610de552 100644
--- a/gpu/command_buffer/client/gles2_implementation_unittest.cc
+++ b/gpu/command_buffer/client/gles2_implementation_unittest.cc
@@ -810,6 +810,31 @@
   EXPECT_EQ(buf[sizeof(kString)], kBad);
 }
 
+// Check that for GETn functions (like glShaderiv) the writes to the result
+// pointer are clamped even if the service side returns more data. See
+// http://issues.chromium.org/issues/551593376.
+TEST_F(GLES2ImplementationTest, GetShaderiv_OversizeReturn) {
+  // Make the SizedResult hold two GLints instead of the single one for
+  // GL_SHADER_TYPE.
+  struct TwoInts {
+    GLint first = 0xBEEF;
+    GLint second = 0xCAFE;
+  };
+
+  ExpectedMemoryInfo resultBuffer =
+      GetExpectedResultMemory(sizeof(uint32_t) + sizeof(TwoInts));
+  EXPECT_CALL(*command_buffer(), OnFlush())
+      .WillOnce(SetMemory(resultBuffer.ptr, SizedResultHelper<TwoInts>({})))
+      .RetiresOnSaturation();
+
+  std::array<GLint, 2> resultAndGuard = {{0, 0}};
+  gl_->GetShaderiv(123, GL_SHADER_TYPE, resultAndGuard.data());
+
+  // Only the first GLint should be written to the output.
+  EXPECT_EQ(0xBEEF, resultAndGuard[0]);
+  EXPECT_EQ(0, resultAndGuard[1]);
+}
+
 TEST_F(GLES2ImplementationTest, ReadPixels2Reads) {
   struct Cmds {
     cmds::ReadPixels read1;
diff --git a/gpu/command_buffer/client/gles2_implementation_impl_autogen.h b/gpu/command_buffer/client/gles2_implementation_impl_autogen.h
index f84de15..4c9d45d 100644
--- a/gpu/command_buffer/client/gles2_implementation_impl_autogen.h
+++ b/gpu/command_buffer/client/gles2_implementation_impl_autogen.h
@@ -884,6 +884,12 @@
   if (!WaitForCmd()) {
     return;
   }
+
+  int expected_num_results = util_.GLGetNumValuesReturned(pname);
+  DCHECK(expected_num_results != 0) << pname;
+  result->SetNumResults(
+      std::min(result->GetNumResults(), expected_num_results));
+
   result->CopyResult(params);
   GPU_CLIENT_LOG_CODE_BLOCK({
     for (int32_t i = 0; i < result->GetNumResults(); ++i) {
@@ -915,6 +921,12 @@
   if (!WaitForCmd()) {
     return;
   }
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/client/gles2_implementation_unittest.cc b/gpu/command_buffer/client/gles2_implementation_unittest.cc
index 0de18a57..610de552 100644
--- a/gpu/command_buffer/client/gles2_implementation_unittest.cc
+++ b/gpu/command_buffer/client/gles2_implementation_unittest.cc
@@ -810,6 +810,31 @@
   EXPECT_EQ(buf[sizeof(kString)], kBad);
 }
 
+// Check that for GETn functions (like glShaderiv) the writes to the result
+// pointer are clamped even if the service side returns more data. See
+// http://issues.chromium.org/issues/551593376.
+TEST_F(GLES2ImplementationTest, GetShaderiv_OversizeReturn) {
+  // Make the SizedResult hold two GLints instead of the single one for
+  // GL_SHADER_TYPE.
+  struct TwoInts {
+    GLint first = 0xBEEF;
+    GLint second = 0xCAFE;
+  };
+
+  ExpectedMemoryInfo resultBuffer =
+      GetExpectedResultMemory(sizeof(uint32_t) + sizeof(TwoInts));
+  EXPECT_CALL(*command_buffer(), OnFlush())
+      .WillOnce(SetMemory(resultBuffer.ptr, SizedResultHelper<TwoInts>({})))
+      .RetiresOnSaturation();
+
+  std::array<GLint, 2> resultAndGuard = {{0, 0}};
+  gl_->GetShaderiv(123, GL_SHADER_TYPE, resultAndGuard.data());
+
+  // Only the first GLint should be written to the output.
+  EXPECT_EQ(0xBEEF, resultAndGuard[0]);
+  EXPECT_EQ(0, resultAndGuard[1]);
+}
+
 TEST_F(GLES2ImplementationTest, ReadPixels2Reads) {
   struct Cmds {
     cmds::ReadPixels read1;
diff --git a/gpu/command_buffer/client/gles2_implementation_unittest_autogen.h b/gpu/command_buffer/client/gles2_implementation_unittest_autogen.h
index 3a6b1e1..f91f16a 100644
--- a/gpu/command_buffer/client/gles2_implementation_unittest_autogen.h
+++ b/gpu/command_buffer/client/gles2_implementation_unittest_autogen.h
@@ -662,11 +662,11 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, result1.id, result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetBooleanv(123, &result);
+  gl_->GetBooleanv(GL_COMPILE_STATUS, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -680,11 +680,12 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, GL_BUFFER_SIZE, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, GL_BUFFER_SIZE, result1.id,
+                    result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetBufferParameteri64v(123, GL_BUFFER_SIZE, &result);
+  gl_->GetBufferParameteri64v(GL_COMPILE_STATUS, GL_BUFFER_SIZE, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -698,11 +699,12 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, GL_BUFFER_SIZE, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, GL_BUFFER_SIZE, result1.id,
+                    result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetBufferParameteriv(123, GL_BUFFER_SIZE, &result);
+  gl_->GetBufferParameteriv(GL_COMPILE_STATUS, GL_BUFFER_SIZE, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -716,11 +718,11 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, result1.id, result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetFloatv(123, &result);
+  gl_->GetFloatv(GL_COMPILE_STATUS, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -734,15 +736,15 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, GL_COLOR_ATTACHMENT0,
+  expected.cmd.Init(GL_COMPILE_STATUS, GL_COLOR_ATTACHMENT0,
                     GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, result1.id,
                     result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
   gl_->GetFramebufferAttachmentParameteriv(
-      123, GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
-      &result);
+      GL_COMPILE_STATUS, GL_COLOR_ATTACHMENT0,
+      GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -756,11 +758,11 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, result1.id, result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetInteger64v(123, &result);
+  gl_->GetInteger64v(GL_COMPILE_STATUS, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -774,11 +776,11 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, 2, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, 2, result1.id, result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetIntegeri_v(123, 2, &result);
+  gl_->GetIntegeri_v(GL_COMPILE_STATUS, 2, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -792,11 +794,11 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, 2, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, 2, result1.id, result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetInteger64i_v(123, 2, &result);
+  gl_->GetInteger64i_v(GL_COMPILE_STATUS, 2, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -810,11 +812,11 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, result1.id, result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetIntegerv(123, &result);
+  gl_->GetIntegerv(GL_COMPILE_STATUS, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -846,11 +848,13 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, GL_RENDERBUFFER_RED_SIZE, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, GL_RENDERBUFFER_RED_SIZE, result1.id,
+                    result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetRenderbufferParameteriv(123, GL_RENDERBUFFER_RED_SIZE, &result);
+  gl_->GetRenderbufferParameteriv(GL_COMPILE_STATUS, GL_RENDERBUFFER_RED_SIZE,
+                                  &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -937,11 +941,12 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, GL_TEXTURE_MAG_FILTER, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, GL_TEXTURE_MAG_FILTER, result1.id,
+                    result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetTexParameterfv(123, GL_TEXTURE_MAG_FILTER, &result);
+  gl_->GetTexParameterfv(GL_COMPILE_STATUS, GL_TEXTURE_MAG_FILTER, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -955,11 +960,12 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, GL_TEXTURE_MAG_FILTER, result1.id, result1.offset);
+  expected.cmd.Init(GL_COMPILE_STATUS, GL_TEXTURE_MAG_FILTER, result1.id,
+                    result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetTexParameteriv(123, GL_TEXTURE_MAG_FILTER, &result);
+  gl_->GetTexParameteriv(GL_COMPILE_STATUS, GL_TEXTURE_MAG_FILTER, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -3083,11 +3089,13 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, 2, result1.id, result1.offset);
+  expected.cmd.Init(123, GL_MAX_PIXEL_LOCAL_STORAGE_PLANES_ANGLE, result1.id,
+                    result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetFramebufferPixelLocalStorageParameterfvANGLE(123, 2, &result);
+  gl_->GetFramebufferPixelLocalStorageParameterfvANGLE(
+      123, GL_MAX_PIXEL_LOCAL_STORAGE_PLANES_ANGLE, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -3103,11 +3111,13 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, 2, result1.id, result1.offset);
+  expected.cmd.Init(123, GL_MAX_PIXEL_LOCAL_STORAGE_PLANES_ANGLE, result1.id,
+                    result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetFramebufferPixelLocalStorageParameterivANGLE(123, 2, &result);
+  gl_->GetFramebufferPixelLocalStorageParameterivANGLE(
+      123, GL_MAX_PIXEL_LOCAL_STORAGE_PLANES_ANGLE, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
@@ -3123,11 +3133,13 @@
   Cmds expected;
   ExpectedMemoryInfo result1 =
       GetExpectedResultMemory(sizeof(uint32_t) + sizeof(ResultType));
-  expected.cmd.Init(123, 2, result1.id, result1.offset);
+  expected.cmd.Init(123, GL_MAX_PIXEL_LOCAL_STORAGE_PLANES_ANGLE, result1.id,
+                    result1.offset);
   EXPECT_CALL(*command_buffer(), OnFlush())
       .WillOnce(SetMemory(result1.ptr, SizedResultHelper<ResultType>(1)))
       .RetiresOnSaturation();
-  gl_->GetFramebufferPixelLocalStorageParameteruivANGLE(123, 2, &result);
+  gl_->GetFramebufferPixelLocalStorageParameteruivANGLE(
+      123, GL_MAX_PIXEL_LOCAL_STORAGE_PLANES_ANGLE, &result);
   EXPECT_EQ(0, UNSAFE_TODO(memcmp(&expected, commands_, sizeof(expected))));
   EXPECT_EQ(static_cast<ResultType>(1), result);
 }
Loading diff…

Original Bug Report

reported by ca...@gmail.com

Hostile GPU result count causes a browser-process stack-buffer overflow


Report description

Hostile GPU result count causes a browser-process stack-buffer overflow


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://chromium.googlesource.com/chromium/src/+/cd97647436f2c251cc6cd74d9302d9ce79a7957f/


The problem

Please describe the technical details of the vulnerability

A compromised, sandboxed Chrome GPU process can return a hostile result count that is later trusted by the unsandboxed browser process.

Root cause

The browser-owned WebXR UI compiles shaders through vr::CompileShader() and supplies a scalar stack destination for GL_COMPILE_STATUS:

GLint status = GL_FALSE;
glGetShaderiv(shader_handle, GL_COMPILE_STATUS, &status);

GLES2Implementation::GetShaderiv() reserves a general shared result buffer, sends the command to the GPU process, waits for completion, and then executes:

result->CopyResult(params);

SizedResult<T>::CopyResult() does not know the capacity of the destination supplied by the caller:

void CopyResult(void* dst) const {
  memcpy(dst, &data, size);
}

The size field is stored in shared command-buffer memory and is derived from a result count produced by the GPU process.

On Windows, the passthrough decoder calls ANGLE’s robust query using a bufsize derived from the remaining shared-memory allocation:

GLsizei written_values = 0;
DoGetShaderiv(shader, pname, bufsize, &written_values, params);
if (written_values > bufsize)
  return error::kOutOfBounds;
result->SetNumResults(written_values);

The decoder verifies only that the returned values fit inside the general transfer-buffer allocation. It does not verify the semantic cardinality of the query. In particular, it does not require exactly one result for scalar queries such as GL_COMPILE_STATUS.

A compromised GPU process can therefore return written_values = 2. Both integers remain inside the legitimate shared result allocation, but the browser later performs the equivalent of:

memcpy(&status, &result->data, 8);

This writes four bytes beyond the four-byte browser stack variable.

The relevant source files are:

  • device/vr/vr_gl_util.cc
  • gpu/command_buffer/client/gles2_implementation_impl_autogen.h
  • gpu/command_buffer/common/common_cmd_format.h
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_handlers_autogen.cc
  • gpu/command_buffer/service/gles2_cmd_decoder_passthrough_doers.cc

Runtime validation

I reproduced the boundary violation in a disposable Windows 11 VM using the official Windows x64 ASAN build:

  • Chrome 154.0.8019.0
  • Chromium commit dfcfe0f11f7aa49f7ee492505f9a6e84fd12a542
  • stock Windows WARP adapter at D3D feature level 11.1
  • OpenXR selected through the standard HKLM\SOFTWARE\Khronos\OpenXR\1\ActiveRuntime registry value
  • XR_RUNTIME_JSON unset
  • no --no-sandbox
  • no --disable-gpu-sandbox
  • no unsafe SwiftShader
  • no DevTools or remote debugging
  • no OpenXR feature-forcing flags

The page requests a normal immersive-vr session using a user gesture and deliberately submits no XR frame. This causes Chrome’s browser-owned WebXR timeout UI to initialize and compile its shaders.

The matched test arms were:

  • count = 1: the immersive session resolved and ended normally. Chrome remained alive and no ASAN report was produced.
  • count = 2: the browser process terminated with a symbolized stack-buffer-overflow, WRITE of size 8.

The GPU process had LOW integrity RID 4096, a restricted token, was not AppContainer, and was inside a job. A separate counter confirmed that the controlled query modification executed once in the GPU process and zero times in the browser process.

The relevant ASAN result was:

ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 8

#1 gpu::SizedResult<int>::CopyResult
   gpu/command_buffer/common/common_cmd_format.h:73
#2 gpu::gles2::GLES2Implementation::GetShaderiv
   gpu/command_buffer/client/gles2_implementation_impl_autogen.h:1310
#3 vr::CompileShader
   device/vr/vr_gl_util.cc:31

[64, 68) 'status'
Memory access at offset 68 overflows this variable

The runtime-confirmed official-ASAN replay used a byte-equivalent offline detour to modify the native ANGLE query result. The attached source patch is the reviewable equivalent. It is explicitly restricted to --type=gpu-process, fingerprints only the browser WebXR UI shader, and changes only that shader’s GL_COMPILE_STATUS result count from one to two.

Affected versions

Source inspection confirmed the same vulnerable behavior in:

  • Stable 152.0.7977.54 — 24072c1aa400ec4a89dc738b6b6acd12a8589b6f
  • Beta 153.0.8010.5 — cf211f8742b58940918596ab3bf0f570f9546085
  • Dev 154.0.8013.2 — cd97647436f2c251cc6cd74d9302d9ce79a7957f

The relevant browser caller, GLES2 client, SizedResult, passthrough handler, and GPU doer source files are byte-identical across these released snapshots.

Reproduction steps

  1. On a clean Windows 11 x64 VM, check out the affected Dev revision and apply the attached patch:
git checkout cd97647436f2c251cc6cd74d9302d9ce79a7957f
git apply C:\gpu-poc\gpu-process-sized-result.patch

gn gen out\vrp_asan --args='is_asan=true is_debug=false is_component_build=true is_clang=true target_cpu="x64" symbol_level=1 blink_symbol_level=0
v8_symbol_level=0 dcheck_always_on=false use_remoteexec=false use_reclient=false use_siso=true'

autoninja -C out\vrp_asan -j2 chrome
  1. Place the attached runtime files and poc.html in C:\gpu-poc.

  2. Rename the runtime DLL:

Rename-Item C:\gpu-poc\openxr_runtime.dll.bin openxr_simulator.dll
  1. From an elevated PowerShell, grant the OpenXR isolated service read access and register the runtime:
icacls.exe C:\gpu-poc /grant '*S-1-15-2-1:(OI)(CI)RX' /T /C

reg.exe add 'HKLM\SOFTWARE\Khronos\OpenXR\1' `
  /v ActiveRuntime `
  /t REG_SZ `
  /d 'C:\gpu-poc\openxr-runtime.json' `
  /f

If the VM already has an ActiveRuntime value, save it before this step and restore it after testing.

  1. From a normal, non-elevated PowerShell, serve the attached page:
$env:XR_RUNTIME_JSON = $null
py -3 -m http.server 8000 --directory C:\gpu-poc
  1. In another normal, non-elevated PowerShell, launch the ASAN build directly:
$env:XR_RUNTIME_JSON = $null
$env:ASAN_OPTIONS = 'log_path=C:\gpu-poc\asan'

& C:\src\src\out\vrp_asan\chrome.exe `
  --user-data-dir=C:\gpu-poc\profile-count2 `
  --no-first-run `
  --no-default-browser-check `
  http://localhost:8000/poc.html
  1. Click Request immersive VR and approve the permission prompt if shown.

  2. Do not submit an XR frame. The browser-owned timeout UI initializes after approximately two seconds. The browser process should terminate with the attached WRITE of size 8 ASAN report.

Negative control

Change only the following line in gles2_cmd_decoder_passthrough_doers.cc:

*length = 2;

to:

*length = 1;

Rebuild:

autoninja -C out\vrp_asan -j2 chrome

Launch Chrome with a fresh profile. The browser should remain alive, the page should record session resolved followed by session ended, and no ASAN report should be generated.

Impact analysis

This is a direct memory-corruption primitive from an already-compromised, sandboxed GPU process into the unsandboxed browser process.

The minimal validated case gives the GPU process control over an additional four-byte browser stack write: eight bytes are copied into a four-byte GLint. The browser-process ASAN report proves that the GPU sandbox boundary is crossed.

The general shared result allocation can hold larger result counts, but this report limits the runtime claim to the demonstrated eight-byte copy. I did not demonstrate control-flow takeover, arbitrary code execution, or a complete sandbox escape.

The appropriate classification is therefore High-severity browser-process memory corruption, with the explicit prerequisite of existing GPU-process code execution.


The cause

What version of Chrome have you found the security issue in?

152.0.7977.54 stable + 153.0.8010.5 beta + 154.0.8013.2 dev

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a non-sandboxed process)

How would you like to be publicly acknowledged for your report?

Cassio Lima

View on issue tracker