Overview

High
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in ANGLE
DescriptionOut of bounds write in ANGLE
ComponentANGLE
Bug ClassOOB
Tracker489071023
Fix commit31151bbc3505 (angle/angle) +247/-29
CISA KEVNot listed
Creditedsweetchip
Disclosed2026-06-02

Files Changed

  • src/libANGLE/TransformFeedback.cpp
  • src/libANGLE/TransformFeedback.h
  • src/libANGLE/capture/capture_gles_ext_params.cpp
  • src/libANGLE/validationES.cpp
  • src/libANGLE/validationES.h
From 31151bbc3505a9fe84e64529c7f105a7cd8c40a6 Mon Sep 17 00:00:00 2001
From: Geoff Lang <[email protected]>
Date: Mon, 13 Apr 2026 14:48:57 -0400
Subject: [PATCH] Validate TF buffer size for multidraw calls.

Validation for transform feedback output buffer size was done per draw
call and the space available was accumulated after the draw finished.
For multidraw calls, this did not correctly calculate the counts.

Refactor this validation out of the common draw call validation and do
it in the higher level validation functions which are aware of how many
draws will be submitted.

Bug: chromium:489071023
Change-Id: Id1976906235a174688cf3c586ef7af49ae81dff9
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7758139
Commit-Queue: Geoff Lang <[email protected]>
Reviewed-by: Shahbaz Youssefi <[email protected]>
Reviewed-by: Kenneth Russell <[email protected]>
---

diff --git a/src/libANGLE/TransformFeedback.cpp b/src/libANGLE/TransformFeedback.cpp
index 175f0bc..05608a2 100644
--- a/src/libANGLE/TransformFeedback.cpp
+++ b/src/libANGLE/TransformFeedback.cpp
@@ -194,10 +194,17 @@
     return mState.mPrimitiveMode;
 }
 
-bool TransformFeedback::checkBufferSpaceForDraw(GLsizei count, GLsizei primcount) const
+bool TransformFeedback::checkBufferSpaceForDraw(const GLsizei *counts,
+                                                const GLsizei *primcounts,
+                                                GLsizei drawcount) const
 {
-    auto vertices =
-        mState.mVerticesDrawn + GetVerticesNeededForDraw(mState.mPrimitiveMode, count, primcount);
+    auto vertices = angle::CheckedNumeric<GLsizeiptr>(mState.mVerticesDrawn);
+    for (GLsizei drawID = 0; drawID < drawcount; ++drawID)
+    {
+        GLsizei primcount = ANGLE_UNSAFE_BUFFERS(primcounts ? primcounts[drawID] : 1);
+        GLsizei count     = ANGLE_UNSAFE_BUFFERS(counts[drawID]);
+        vertices += GetVerticesNeededForDraw(mState.mPrimitiveMode, count, primcount);
+    }
     return vertices.IsValid() && vertices.ValueOrDie() <= mState.mVertexCapacity;
 }
 
diff --git a/src/libANGLE/TransformFeedback.h b/src/libANGLE/TransformFeedback.h
index 9eb3479..a56c62e 100644
--- a/src/libANGLE/TransformFeedback.h
+++ b/src/libANGLE/TransformFeedback.h
@@ -77,8 +77,10 @@
     bool isPaused() const;
     PrimitiveMode getPrimitiveMode() const;
     // Validates that the vertices produced by a draw call will fit in the bound transform feedback
-    // buffers.
-    bool checkBufferSpaceForDraw(GLsizei count, GLsizei primcount) const;
+    // buffers. primcounts may be nullptr for non-instanced draw calls.
+    bool checkBufferSpaceForDraw(const GLsizei *counts,
+                                 const GLsizei *primcounts,
+                                 GLsizei drawcount) const;
     // This must be called after each draw call when transform feedback is enabled to keep track of
     // how many vertices have been written to the buffers. This information is needed by
     // checkBufferSpaceForDraw because each draw call appends vertices to the buffers starting just
diff --git a/src/libANGLE/capture/capture_gles_ext_params.cpp b/src/libANGLE/capture/capture_gles_ext_params.cpp
index 9e67858..89734a1 100644
--- a/src/libANGLE/capture/capture_gles_ext_params.cpp
+++ b/src/libANGLE/capture/capture_gles_ext_params.cpp
@@ -508,7 +508,7 @@
                                         GLsizei drawcount,
                                         ParamCapture *paramCapture)
 {
-    UNIMPLEMENTED();
+    CaptureArray(firsts, drawcount, paramCapture);
 }
 
 void CaptureMultiDrawArraysANGLE_counts(const State &glState,
@@ -518,7 +518,7 @@
                                         GLsizei drawcount,
                                         ParamCapture *paramCapture)
 {
-    UNIMPLEMENTED();
+    CaptureArray(counts, drawcount, paramCapture);
 }
 
 void CaptureMultiDrawArraysInstancedANGLE_firsts(const State &glState,
@@ -529,7 +529,7 @@
                                                  GLsizei drawcount,
                                                  ParamCapture *paramCapture)
 {
-    UNIMPLEMENTED();
+    CaptureArray(firsts, drawcount, paramCapture);
 }
 
 void CaptureMultiDrawArraysInstancedANGLE_counts(const State &glState,
@@ -540,7 +540,7 @@
                                                  GLsizei drawcount,
                                                  ParamCapture *paramCapture)
 {
-    UNIMPLEMENTED();
+    CaptureArray(counts, drawcount, paramCapture);
 }
 
 void CaptureMultiDrawArraysInstancedANGLE_instanceCounts(const State &glState,
@@ -551,7 +551,7 @@
                                                          GLsizei drawcount,
                                                          ParamCapture *paramCapture)
 {
-    UNIMPLEMENTED();
+    CaptureArray(instanceCounts, drawcount, paramCapture);
 }
 
 void CaptureMultiDrawElementsANGLE_counts(const State &glState,
diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index bd51e02..f9f902f 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -1525,6 +1525,27 @@
     return true;
 }
 
+bool ValidateDrawArraysTransformFeedbackBufferSize(const Context *context,
+                                                   angle::EntryPoint entryPoint,
+                                                   const GLsizei *counts,
+                                                   const GLsizei *primcounts,
+                                                   GLsizei drawcount)
+{
+    if (ANGLE_UNLIKELY(context->getStateCache().isTransformFeedbackActiveUnpaused()) &&
+        ANGLE_UNLIKELY(!context->supportsGeometryOrTesselation()))
+    {
+        const State &state                      = context->getState();
+        TransformFeedback *curTransformFeedback = state.getCurrentTransformFeedback();
+        if (!curTransformFeedback->checkBufferSpaceForDraw(counts, primcounts, drawcount))
+        {
+            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, err::kTransformFeedbackBufferTooSmall);
+            return false;
+        }
+    }
+
+    return true;
+}
+
 Program *GetValidProgramNoResolve(const Context *context,
                                   angle::EntryPoint entryPoint,
                                   ShaderProgramID id)
@@ -4496,6 +4517,11 @@
         return false;
     }
 
+    if (!ValidateDrawArraysTransformFeedbackBufferSize(context, entryPoint, &count, &primcount, 1))
+    {
+        return false;
+    }
+
     return ValidateDrawInstancedANGLE(context, entryPoint);
 }
 
@@ -4511,6 +4537,11 @@
         return false;
     }
 
+    if (!ValidateDrawArraysTransformFeedbackBufferSize(context, entryPoint, &count, &primcount, 1))
+    {
+        return false;
+    }
+
     return true;
 }
 
diff --git a/src/libANGLE/validationES.h b/src/libANGLE/validationES.h
index ce18079..4eaad41 100644
--- a/src/libANGLE/validationES.h
+++ b/src/libANGLE/validationES.h
@@ -98,6 +98,15 @@
                                       const void *ptr,
                                       bool pureInteger);
 
+// Validation of transform feedback buffer output size for various DrawArrays calls.
+// `primcounts` can be null for non-instanced calls.
+// If this function returns false, an error has been generated.
+bool ValidateDrawArraysTransformFeedbackBufferSize(const Context *context,
+                                                   angle::EntryPoint entryPoint,
+                                                   const GLsizei *counts,
+                                                   const GLsizei *primcounts,
+                                                   GLsizei drawcount);
+
 // Returns valid program if id is a valid program name
 // Errors INVALID_OPERATION if valid shader is given and returns NULL
 // Errors INVALID_VALUE otherwise and returns NULL
@@ -941,18 +950,6 @@
         return false;
     }
 
-    if (ANGLE_UNLIKELY(context->getStateCache().isTransformFeedbackActiveUnpaused()) &&
-        ANGLE_UNLIKELY(!context->supportsGeometryOrTesselation()))
-    {
-        const State &state                      = context->getState();
-        TransformFeedback *curTransformFeedback = state.getCurrentTransformFeedback();
-        if (!curTransformFeedback->checkBufferSpaceForDraw(count, primcount))
-        {
-            ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, err::kTransformFeedbackBufferTooSmall);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/src/tests/gl_tests/TransformFeedbackTest.cpp b/src/tests/gl_tests/TransformFeedbackTest.cpp
index 387971e..b4fdec1 100644
--- a/src/tests/gl_tests/TransformFeedbackTest.cpp
+++ b/src/tests/gl_tests/TransformFeedbackTest.cpp
@@ -4736,6 +4736,124 @@
     glEndTransformFeedback();
 }
 
+// Test validation of buffer bounds checking for transform feedback with multidraw commands
+TEST_P(WebGLTransformFeedbackTest, TooSmallBuffersMultiDraw)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_multi_draw"));
+
+    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    glEnable(GL_RASTERIZER_DISCARD);
+
+    // Set the program's transform feedback varyings (just gl_Position)
+    std::vector<std::string> tfVaryings;
+    tfVaryings.push_back("gl_Position");
+    compileDefaultProgram(tfVaryings, GL_INTERLEAVED_ATTRIBS);
+    GLint positionLocation = glGetAttribLocation(mProgram, essl1_shaders::PositionAttrib());
+
+    glUseProgram(mProgram);
+
+    const GLfloat vertices[] = {
+        -1.0f, 1.0f, 0.5f, -1.0f, -1.0f, 0.5f, 1.0f, -1.0f, 0.5f,
+        -1.0f, 1.0f, 0.5f, 1.0f,  -1.0f, 0.5f, 1.0f, 1.0f,  0.5f,
+    };
+
+    GLBuffer buffer;
+    glBindBuffer(GL_ARRAY_BUFFER, buffer);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+    glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(positionLocation);
+
+    const GLsizei verticesToDraw = 3;
+    const size_t stride          = sizeof(float) * 4;
+    const GLsizei drawcount      = 2;
+    const size_t bytesNeeded     = stride * verticesToDraw * drawcount;
+
+    const GLsizei firsts[drawcount] = {0, 0};
+    const GLsizei counts[drawcount] = {verticesToDraw, verticesToDraw};
+
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, mTransformFeedbackBuffer);
+
+    // Set up the buffer to be the right size
+    uint8_t tfData[bytesNeeded] = {0};
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, bytesNeeded, &tfData, GL_STATIC_DRAW);
+
+    glBeginTransformFeedback(GL_POINTS);
+    glMultiDrawArraysANGLE(GL_POINTS, firsts, counts, drawcount);
+    EXPECT_GL_NO_ERROR();
+    glEndTransformFeedback();
+
+    // Set up the buffer to be too small
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, bytesNeeded - 1, &tfData, GL_STATIC_DRAW);
+
+    glBeginTransformFeedback(GL_POINTS);
+    EXPECT_GL_NO_ERROR();
+    glMultiDrawArraysANGLE(GL_POINTS, firsts, counts, drawcount);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+    glEndTransformFeedback();
+}
+
+// Test validation of buffer bounds checking for transform feedback with multidraw instanced
+// commands
+TEST_P(WebGLTransformFeedbackTest, TooSmallBuffersMultiDrawInstanced)
+{
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_multi_draw"));
+    ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_ANGLE_instanced_arrays"));
+
+    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    glClear(GL_COLOR_BUFFER_BIT);
+    glEnable(GL_RASTERIZER_DISCARD);
+
+    // Set the program's transform feedback varyings (just gl_Position)
+    std::vector<std::string> tfVaryings;
+    tfVaryings.push_back("gl_Position");
+    compileDefaultProgram(tfVaryings, GL_INTERLEAVED_ATTRIBS);
+    GLint positionLocation = glGetAttribLocation(mProgram, essl1_shaders::PositionAttrib());
+
+    glUseProgram(mProgram);
+
+    const GLfloat vertices[] = {
+        -1.0f, 1.0f, 0.5f, -1.0f, -1.0f, 0.5f, 1.0f, -1.0f, 0.5f,
+        -1.0f, 1.0f, 0.5f, 1.0f,  -1.0f, 0.5f, 1.0f, 1.0f,  0.5f,
+    };
+
+    GLBuffer buffer;
+    glBindBuffer(GL_ARRAY_BUFFER, buffer);
+    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+    glVertexAttribPointer(positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
+    glEnableVertexAttribArray(positionLocation);
+
+    const GLsizei verticesToDraw = 3;
+    const size_t stride          = sizeof(float) * 4;
+    const GLsizei drawcount      = 2;
+    const GLsizei instanceCount  = 2;
+    const size_t bytesNeeded     = stride * verticesToDraw * drawcount * instanceCount;
+
+    const GLsizei firsts[drawcount]         = {0, 0};
+    const GLsizei counts[drawcount]         = {verticesToDraw, verticesToDraw};
+    const GLsizei instanceCounts[drawcount] = {instanceCount, instanceCount};
+
+    glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, mTransformFeedbackBuffer);
+
+    // Set up the buffer to be the right size
+    uint8_t tfData[bytesNeeded] = {0};
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, bytesNeeded, &tfData, GL_STATIC_DRAW);
+
+    glBeginTransformFeedback(GL_POINTS);
+    glMultiDrawArraysInstancedANGLE(GL_POINTS, firsts, counts, instanceCounts, drawcount);
+    EXPECT_GL_NO_ERROR();
+    glEndTransformFeedback();
+
+    // Set up the buffer to be too small
+    glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, bytesNeeded - 1, &tfData, GL_STATIC_DRAW);
+
+    glBeginTransformFeedback(GL_POINTS);
+    EXPECT_GL_NO_ERROR();
+    glMultiDrawArraysInstancedANGLE(GL_POINTS, firsts, counts, instanceCounts, drawcount);
+    EXPECT_GL_ERROR(GL_INVALID_OPERATION);
+    glEndTransformFeedback();
+}
+
 // Test that deleting a buffer bound to a transform feedback slot that is not used by the current
 // program.
 TEST_P(TransformFeedbackTest, StaleBufferBinding)
Loading diff…

Original Bug Report

reported by [email protected]

Transform Feedback OOB Write via Multi-Draw Cumulative Overflow


Report description

Transform Feedback OOB Write via Multi-Draw Cumulative Overflow


Bug location

Where do you want to report your vulnerability?

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


The problem

Please describe the technical details of the vulnerability

Summary

ANGLE’s ValidateMultiDrawArraysANGLE() checks each sub-draw’s transform feedback (XFB) buffer space against a stale mVerticesDrawn counter that is never updated between validation iterations. Sub-draws that individually fit within the XFB buffer but collectively exceed its capacity all pass validation. The backend then executes all sub-draws sequentially, writing past the XFB buffer boundary.

Root Cause

The Bug: Stale Accumulator in Multi-Draw Validation Loop

The root cause is a stale accumulator leading to cumulative overflow β€” the validation loop reads mVerticesDrawn without advancing it between iterations, so every sub-draw is checked against the same base offset. This gap is deterministic and single-threaded: the validation phase simply never updates the counter that the execution phase later advances correctly. Each sub-draw individually fits within the buffer, but their cumulative writes exceed its capacity.

// validationES2.cpp β€” ValidateMultiDrawArraysANGLE
bool ValidateMultiDrawArraysANGLE(const Context *context,
                                   angle::EntryPoint entryPoint,
                                   PrimitiveMode mode,
                                   const GLint *firsts,
                                   const GLsizei *counts,
                                   GLsizei drawcount)
{
    if (drawcount < 0) { ... return false; }
    for (GLsizei drawID = 0; drawID < drawcount; ++drawID)
    {
        if (!ValidateDrawArrays(context, entryPoint, mode,
                                firsts[drawID], counts[drawID]))
            return false;
        // *** BUG: mVerticesDrawn NOT updated between iterations ***
        // Each sub-draw validates against the same stale counter
    }
    return true;
}

The same stale-accumulator pattern exists in all multi-draw validation functions β€” each loops over sub-draws calling per-draw validation without advancing mVerticesDrawn between iterations:

// validationES2.cpp β€” ValidateMultiDrawElementsANGLE (same pattern)
for (GLsizei drawID = 0; drawID < drawcount; ++drawID)
{
    if (!ValidateDrawElements(context, entryPoint, mode, counts[drawID], type, indices[drawID]))
        return false;
    // BUG: mVerticesDrawn not updated between iterations
}
// validationES3.cpp β€” ValidateMultiDrawArraysInstancedANGLE (same pattern)
for (GLsizei drawID = 0; drawID < drawcount; ++drawID)
{
    if (!ValidateDrawArraysInstancedBase(context, entryPoint, mode, firsts[drawID],
                                         counts[drawID], instanceCounts[drawID], 0))
        return false;
    // BUG: mVerticesDrawn not updated β€” instanceCounts amplifies overflow
}
// validationES3.cpp β€” ValidateMultiDrawElementsInstancedANGLE (same pattern)
for (GLsizei drawID = 0; drawID < drawcount; ++drawID)
{
    if (!ValidateDrawElementsInstancedBase(context, entryPoint, mode, counts[drawID], type,
                                           indices[drawID], instanceCounts[drawID], 0))
        return false;
    // BUG: mVerticesDrawn not updated β€” instanceCounts amplifies overflow
}

The XFB Space Check (called per sub-draw via ValidateDrawArraysCommon)

// validationES.h β€” inside ValidateDrawArraysCommon
if (ANGLE_UNLIKELY(context->getStateCache().isTransformFeedbackActiveUnpaused()) &&
    ANGLE_UNLIKELY(!context->supportsGeometryOrTesselation()))
{
    const State &state = context->getState();
    TransformFeedback *curTransformFeedback = state.getCurrentTransformFeedback();
    if (!curTransformFeedback->checkBufferSpaceForDraw(count, primcount))
    {
        ANGLE_VALIDATION_ERROR(GL_INVALID_OPERATION, err::kTransformFeedbackBufferTooSmall);
        return false;
    }
}

The Counter Check (reads stale mVerticesDrawn)

// TransformFeedback.cpp β€” checkBufferSpaceForDraw
bool TransformFeedback::checkBufferSpaceForDraw(GLsizei count, GLsizei primcount) const
{
    auto vertices = mState.mVerticesDrawn +  // <-- STALE: never advanced during validation
        GetVerticesNeededForDraw(mState.mPrimitiveMode, count, primcount);
    return vertices.IsValid() && vertices.ValueOrDie() <= mState.mVertexCapacity;
}

The Counter Update (only called AFTER execution, not during validation)

// TransformFeedback.cpp β€” onVerticesDrawn
void TransformFeedback::onVerticesDrawn(const Context *context, GLsizei count, GLsizei primcount)
{
    ASSERT(mState.mActive && !mState.mPaused);
    mState.mVerticesDrawn =
        (mState.mVerticesDrawn + GetVerticesNeededForDraw(mState.mPrimitiveMode, count, primcount))
            .ValueOrDie();
    // ...
}

Execution Flow

Validation phase (all pass β€” stale mVerticesDrawn = 0):
  Sub-draw 0: checkBufferSpaceForDraw(400,1) β†’ 0+400 <= 1024 βœ“
  Sub-draw 1: checkBufferSpaceForDraw(400,1) β†’ 0+400 <= 1024 βœ“ (STALE!)
  ... (all 10 sub-draws pass)

Execution phase (MULTI_DRAW_BLOCK macro in renderer_utils.cpp):
  Sub-draw 0: writes 400 vertices at offset 0,     mVerticesDrawn = 400
  Sub-draw 1: writes 400 vertices at offset 1600,   mVerticesDrawn = 800
  Sub-draw 2: writes 400 vertices at offset 3200,   mVerticesDrawn = 1200 ← OVERFLOW START
  Sub-draw 3: writes 400 vertices at offset 4800,   mVerticesDrawn = 1600
  ...
  Sub-draw 9: writes 400 vertices at offset 14400,  mVerticesDrawn = 4000
  β†’ 2976 vertices Γ— 4 bytes = 11,904 bytes OOB write

Reproduction Steps

Cross-Tab Rendering Corruption

This PoC demonstrates that an attacker webpage can corrupt the GPU buffer data of a victim webpage open in a separate tab. Both tabs share Chrome’s GPU process.

  1. Open Chrome.
  2. Open poc_render_verify_victim_min.html in Tab 1 β€” it renders 3 colored triangles (red, green, blue), sprays 4096 Γ— 4KB GPU buffers with tagged sentinel data, then starts a rendering integrity monitoring loop
  3. Open poc_render_verify_attacker_min.html in Tab 2 β€” click “Attack (100 rounds)”
  4. The attacker page will:
    • Force ANGLE BufferManager GC eviction (60 temporary contexts destroyed + 3Γ—600 buffer spray/free cycles + 256MB allocation pressure)
    • Allocate a 4KB XFB buffer (same size class as victim’s buffers)
    • Execute 100 XFB overflow rounds via multiDrawArraysWEBGL(POINTS, ...) with 10 sub-draws of 400 vertices each
    • Each sub-draw passes validation individually (400 < 1024) because mVerticesDrawn is never updated between iterations
    • Each round writes 12KB past the 4KB buffer boundary into adjacent GPU memory
  5. Expected victim behavior (if no bug): Rendering stays identical, all buffers clean
  6. Actual (confirmed on Apple M4 Pro, macOS, Metal backend): “GPU RENDERING CORRUPTION DETECTED” β€” victim tab’s triangles disappear or move to wrong positions, spray buffer sentinel data overwritten

Note: The cross-tab attack relies on GPU heap status (GC eviction + buffer spray) to place victim and attacker buffers adjacently. (multiple attempts may be required)

How it works:

  1. Both tabs share Chrome’s single GPU process and Metal device
  2. ANGLE’s per-context BufferManager caches freed buffers; GC eviction returns them to Metal
  3. Attacker triggers GC eviction (destroy 60 temp contexts + 3Γ—600 spray/free + 256MB pressure)
  4. Victim allocates 4096 Γ— 4KB buffers β†’ Metal reuses freed regions, placing them adjacent to attacker’s XFB buffer
  5. Attacker executes XFB overflow β†’ writes 12KB past buffer boundary into victim’s VBO/spray buffers
  6. Victim’s rendering loop detects pixel differences β†’ GPU rendering corruption confirmed

Cross-tab attack timeline:

Tab 1 (Victim):  Allocate VBO β†’ Render triangles β†’ Monitor rendering
Tab 2 (Attacker): GC eviction β†’ Allocate XFB buf β†’ multiDrawArrays overflow
                                                      ↓
Tab 1 (Victim):  Re-render β†’ readPixels β†’ PIXEL DIFFS DETECTED
                              β†’ "GPU RENDERING CORRUPTION DETECTED"

Attached Files

File Description
poc_render_verify_attacker_min.html Cross-tab attack PoC β€” executes XFB overflow after GC eviction to corrupt victim’s GPU buffers (CSS-stripped minimal version)
poc_render_verify_victim_min.html Cross-tab victim PoC β€” renders 3 triangles + 4096Γ—4KB buffer spray, monitors rendering integrity (CSS-stripped minimal version)

Impact analysis

Impact

An attacker can corrputs GPU memory. (POC shows that an attacker page corrupts the rendering output of a victim page in a different tab.)


The cause

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

145.0.7632.117 stable

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption

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

sweetchip

View on issue tracker