Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in WebGL
DescriptionBuffer overflow in WebGL
ComponentWebGL
Sub-componentGPU
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.
Tracker553172761
Fix commit0ee892c1254d (chromium/src) +360/-51
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-08-27 (per vendor advisory)
Fix landed2026-09-01 (public git)
Disclosed2026-09-22
Patch gap21 days — fix public in source before the advisory

Background

Host-twiddled format
a texture format whose pixel data the driver reorders (“twiddles”) on the host before/during upload; here GL_RGB10_A2/GL_UNSIGNED_INT_2_10_10_10_REV and GL_SRGB8_ALPHA8/GL_UNSIGNED_BYTE.
`TexImage2D` vs `TexSubImage2D`
TexImage2D allocates the texture storage and can upload data in one call; TexSubImage2D uploads into already-allocated storage. Splitting into allocate-then-upload avoids the buggy single-call path.
NPOT (non-power-of-two)
a texture dimension that is not a power of two, detected here via has_single_bit(width)/has_single_bit(height).
PowerVR
the GPU/driver family whose upload path mishandles NPOT host-twiddled data, gated by the use_tex_sub_image_for_host_twiddled_npot_uploads workaround.

Root Cause Analysis

On affected PowerVR drivers, a single-call glTexImage2D that both allocates storage and uploads client pixel data mishandles the internal layout for host-twiddled formats when the texture dimensions are non-power-of-two. Because these formats are reordered by the driver, the driver computes the wrong stride/extent for NPOT dimensions and writes pixel data past the end of the allocated backing store, producing an out-of-bounds buffer overflow inside the driver. The vulnerable path is reached whenever web content issues a texSubImage2D-style full-image upload (args.command_type == kTexSubImage2D) in one of the host-twiddled formats with an NPOT width or height.

Before the fix, texture_manager.cc took the full_image && !texture->IsImmutable() fast path that funnels such uploads into the single-call driver upload.

The fix adds IsHostTwiddledFormat() plus NPOT and command-type checks and, when the workaround is active, excludes these uploads from the fast path so allocation and upload are performed as separate TexImage2D + TexSubImage2D operations. This sidesteps the driver’s buggy combined allocate-and-upload code and keeps the write within bounds.

Key insight
The overflow lives in the driver’s combined allocate-plus-upload path for NPOT host-twiddled textures; splitting allocation (TexImage2D) from data upload (TexSubImage2D) never exercises that miscomputed layout, so the same pixels land within bounds.

Attack Path

  1. Obtain a WebGL context Malicious web content opens a WebGL context on a device with an affected PowerVR driver.
  2. Choose a triggering format Content selects a host-twiddled format such as GL_RGB10_A2/GL_UNSIGNED_INT_2_10_10_10_REV or GL_SRGB8_ALPHA8/GL_UNSIGNED_BYTE per IsHostTwiddledFormat().
  3. Upload NPOT client data Content issues a full-image texSubImage2D upload with a non-power-of-two width or height, hitting the pre-fix fast path.
  4. Trigger driver OOB write The driver’s single-call upload miscomputes the layout and writes pixel data beyond the allocated backing store.
  5. Corrupt adjacent memory The out-of-bounds write corrupts neighboring GPU-process memory, which an attacker can shape toward exploitation.

Impact Assessment

An attacker-controlled WebGL texture upload causes an out-of-bounds heap write in the GPU process on affected PowerVR devices, corrupting adjacent memory. Because the write size and content are influenced by attacker-chosen texture dimensions and pixel data, this buffer overflow is a strong primitive that can lead to memory corruption and potentially GPU-process compromise. The impact is limited to devices running the affected driver family gated by the workaround.

Changed Functions

FunctionChangeNotes
if
gpu/command_buffer/service/texture_manager.cc
modified
GLNPOTHostTwiddledTextureTest
gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
modified
if
gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
modified
TEST_P
gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
modified
for
gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
modified

Files Changed

  • gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
  • gpu/BUILD.gn
  • gpu/command_buffer/service/texture_manager.h
  • gpu/config/gpu_driver_bug_list.json
  • gpu/command_buffer/service/texture_manager.cc
  • gpu/config/gpu_workaround_list.txt

Audit Directions

  • Combined allocate-and-upload paths
    Review other single-call TexImage2D/TexStorage+upload paths for formats the driver reorders, especially where NPOT dimensions change stride/extent computation.
  • Format/type enumeration gaps
    IsHostTwiddledFormat() lists only two combinations; audit whether other twiddled formats (e.g. other packed or sRGB types) exhibit the same NPOT layout bug and are missed by the narrow workaround.
  • Workaround gating coverage
    Check the gpu_driver_bug_list.json conditions for use_tex_sub_image_for_host_twiddled_npot_uploads to confirm all affected PowerVR driver versions are matched and none slip through the fast path.
From 0ee892c1254deadf629092d0bd11b16d860b80d1 Mon Sep 17 00:00:00 2001
From: Ken Russell <[email protected]>
Date: Tue, 01 Sep 2026 13:14:36 -0700
Subject: [PATCH] GPU: add TexSubImage2D workaround for certain texture formats.

Add a workaround for a bug in the handling of texture uploads of
certain host-twiddled texture formats; use TexImage2D + TexSubImage2D
instead of uploading the data in the TexImage2D call. Apply this
workaround to PowerVR drivers.

Bug: 553172761
Change-Id: Ie28aa469e46a015927488176e8ceef0962056287
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8316468
Auto-Submit: Kenneth Russell <[email protected]>
Reviewed-by: Zhenyao Mo <[email protected]>
Reviewed-by: vikas soni <[email protected]>
Commit-Queue: Zhenyao Mo <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1690048}
---
diff --git a/gpu/command_buffer/service/texture_manager.cc b/gpu/command_buffer/service/texture_manager.cc
index d13a4b9..cd80b903 100644
--- a/gpu/command_buffer/service/texture_manager.cc
+++ b/gpu/command_buffer/service/texture_manager.cc
@@ -15,6 +15,7 @@
 #include <tuple>
 #include <utility>
 
+#include "base/bits.h"
 #include "base/compiler_specific.h"
 #include "base/containers/heap_array.h"
 #include "base/format_macros.h"
@@ -431,6 +432,13 @@
   uint32_t previous_size_;
 };
 
+bool IsHostTwiddledFormat(GLenum internal_format, GLenum format, GLenum type) {
+  return (internal_format == GL_RGB10_A2 && format == GL_RGBA &&
+          type == GL_UNSIGNED_INT_2_10_10_10_REV) ||
+         (internal_format == GL_SRGB8_ALPHA8 && format == GL_RGBA &&
+          type == GL_UNSIGNED_BYTE);
+}
+
 }  // namespace anonymous
 
 DecoderTextureState::DecoderTextureState(
@@ -448,7 +456,9 @@
       split_level_0_pbo_full_sub_image_2d(
           workarounds.split_level_0_pbo_full_sub_image_2d),
       upload_oversized_mip_levels_via_unpack_buffer(
-          workarounds.upload_oversized_mip_levels_via_unpack_buffer) {}
+          workarounds.upload_oversized_mip_levels_via_unpack_buffer),
+      use_tex_sub_image_for_host_twiddled_npot_uploads(
+          workarounds.use_tex_sub_image_for_host_twiddled_npot_uploads) {}
 
 TextureManager::DestructionObserver::DestructionObserver() = default;
 
@@ -2978,13 +2988,21 @@
     uploaded = true;
   }
 
+  GLenum internal_format = 0;
+  GLenum tex_type = 0;
+  texture->GetLevelType(args.target, args.level, &tex_type, &internal_format);
+
   if (uploaded) {
     // Upload was performed by one of the workarounds above.
-  } else if (full_image && !texture->IsImmutable()) {
+  } else if (full_image && !texture->IsImmutable() &&
+             !(texture_state
+                   ->use_tex_sub_image_for_host_twiddled_npot_uploads &&
+               args.command_type ==
+                   DoTexSubImageArguments::CommandType::kTexSubImage2D &&
+               (!std::has_single_bit(static_cast<uint32_t>(args.width)) ||
+                !std::has_single_bit(static_cast<uint32_t>(args.height))) &&
+               IsHostTwiddledFormat(internal_format, args.format, args.type))) {
     TRACE_EVENT0("gpu", "FullImage");
-    GLenum internal_format;
-    GLenum tex_type;
-    texture->GetLevelType(args.target, args.level, &tex_type, &internal_format);
     // NOTE: In OpenGL ES 2/3 border is always zero. If that changes we'll need
     // to look it up.
     if (args.command_type ==
@@ -3438,57 +3456,73 @@
                    AdjustTexFormat(feature_info_.get(), args.format), args.type,
                    args.pixels);
     } else {
-      bool handled = false;
-      if (texture_state->upload_oversized_mip_levels_via_unpack_buffer &&
-          args.target == GL_TEXTURE_2D && args.level > 0 &&
-          !unpack_buffer_bound &&
-          !(GLES2Util::GetChannelsForFormat(args.format) &
-            (GLES2Util::kDepth | GLES2Util::kStencil))) {
-        GLsizei level0_width = 0;
-        GLsizei level0_height = 0;
-        GLsizei level0_depth = 0;
-        if (texture->GetLevelSize(args.target, 0, &level0_width, &level0_height,
-                                  &level0_depth) &&
-            level0_width > 0 && level0_height > 0) {
-          const int slot_w = std::max(
-              1, static_cast<int>(
-                     std::bit_ceil(static_cast<uint32_t>(level0_width))) >>
-                     args.level);
-          const int slot_h = std::max(
-              1, static_cast<int>(
-                     std::bit_ceil(static_cast<uint32_t>(level0_height))) >>
-                     args.level);
-          if (args.width > slot_w || args.height > slot_h) {
-            GLuint scratch = 0;
-            glGenBuffersARB(1, &scratch);
-            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, scratch);
-            // Regardless of whether the user supplied data
-            // (args.pixels != nullptr), the pixel unpack buffer must
-            // be allocated with the expected amount of data.
-            if (args.pixels_size > 0) {
-              glBufferData(GL_PIXEL_UNPACK_BUFFER, args.pixels_size,
-                           args.pixels, GL_STREAM_DRAW);
-            }
-            glTexImage2D(
-                args.target, args.level,
-                AdjustTexInternalFormat(feature_info_.get(),
-                                        args.internal_format, args.type),
-                args.width, args.height, args.border,
-                AdjustTexFormat(feature_info_.get(), args.format), args.type,
-                nullptr);
-            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
-            glDeleteBuffersARB(1, &scratch);
-            handled = true;
-          }
-        }
-      }
-      if (!handled) {
+      if (texture_state->use_tex_sub_image_for_host_twiddled_npot_uploads &&
+          (args.pixels != nullptr || unpack_buffer_bound) &&
+          (!std::has_single_bit(static_cast<uint32_t>(args.width)) ||
+           !std::has_single_bit(static_cast<uint32_t>(args.height))) &&
+          IsHostTwiddledFormat(args.internal_format, args.format, args.type)) {
         glTexImage2D(args.target, args.level,
                      AdjustTexInternalFormat(feature_info_.get(),
                                              args.internal_format, args.type),
                      args.width, args.height, args.border,
                      AdjustTexFormat(feature_info_.get(), args.format),
-                     args.type, args.pixels);
+                     args.type, nullptr);
+        glTexSubImage2D(args.target, args.level, 0, 0, args.width, args.height,
+                        AdjustTexFormat(feature_info_.get(), args.format),
+                        args.type, args.pixels);
+      } else {
+        bool handled = false;
+        if (texture_state->upload_oversized_mip_levels_via_unpack_buffer &&
+            args.target == GL_TEXTURE_2D && args.level > 0 &&
+            !unpack_buffer_bound &&
+            !(GLES2Util::GetChannelsForFormat(args.format) &
+              (GLES2Util::kDepth | GLES2Util::kStencil))) {
+          GLsizei level0_width = 0;
+          GLsizei level0_height = 0;
+          GLsizei level0_depth = 0;
+          if (texture->GetLevelSize(args.target, 0, &level0_width,
+                                    &level0_height, &level0_depth) &&
+              level0_width > 0 && level0_height > 0) {
+            const int slot_w = std::max(
+                1, static_cast<int>(
+                       std::bit_ceil(static_cast<uint32_t>(level0_width))) >>
+                       args.level);
+            const int slot_h = std::max(
+                1, static_cast<int>(
+                       std::bit_ceil(static_cast<uint32_t>(level0_height))) >>
+                       args.level);
+            if (args.width > slot_w || args.height > slot_h) {
+              GLuint scratch = 0;
+              glGenBuffersARB(1, &scratch);
+              glBindBuffer(GL_PIXEL_UNPACK_BUFFER, scratch);
+              // Regardless of whether the user supplied data
+              // (args.pixels != nullptr), the pixel unpack buffer must
+              // be allocated with the expected amount of data.
+              if (args.pixels_size > 0) {
+                glBufferData(GL_PIXEL_UNPACK_BUFFER, args.pixels_size,
+                             args.pixels, GL_STREAM_DRAW);
+              }
+              glTexImage2D(
+                  args.target, args.level,
+                  AdjustTexInternalFormat(feature_info_.get(),
+                                          args.internal_format, args.type),
+                  args.width, args.height, args.border,
+                  AdjustTexFormat(feature_info_.get(), args.format), args.type,
+                  nullptr);
+              glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+              glDeleteBuffersARB(1, &scratch);
+              handled = true;
+            }
+          }
+        }
+        if (!handled) {
+          glTexImage2D(args.target, args.level,
+                       AdjustTexInternalFormat(feature_info_.get(),
+                                               args.internal_format, args.type),
+                       args.width, args.height, args.border,
+                       AdjustTexFormat(feature_info_.get(), args.format),
+                       args.type, args.pixels);
+        }
       }
     }
   }
diff --git a/gpu/command_buffer/service/texture_manager.h b/gpu/command_buffer/service/texture_manager.h
index 5b6fcb04c..4b2a333 100644
--- a/gpu/command_buffer/service/texture_manager.h
+++ b/gpu/command_buffer/service/texture_manager.h
@@ -705,6 +705,7 @@
   bool unpack_overlapping_rows_separately_unpack_buffer;
   bool split_level_0_pbo_full_sub_image_2d;
   bool upload_oversized_mip_levels_via_unpack_buffer;
+  bool use_tex_sub_image_for_host_twiddled_npot_uploads;
 };
 
 // This class keeps track of the textures and their sizes so we can do NPOT and
diff --git a/gpu/config/gpu_driver_bug_list.json b/gpu/config/gpu_driver_bug_list.json
index b1c78e16..fbb3c02 100644
--- a/gpu/config/gpu_driver_bug_list.json
+++ b/gpu/config/gpu_driver_bug_list.json
@@ -3889,6 +3889,15 @@
       "features": [
         "finish_before_blit_framebuffer_multi_attachment"
       ]
+    },
+    {
+      "id": 494,
+      "cr_bugs": [553172761],
+      "description": "For texture formats which would be twiddled on the host CPU, upload via TexSubImage2D instead of TexImage2D.",
+      "gl_vendor": "Imagination.*",
+      "features": [
+        "use_tex_sub_image_for_host_twiddled_npot_uploads"
+      ]
     }
   ]
 }
diff --git a/gpu/config/gpu_workaround_list.txt b/gpu/config/gpu_workaround_list.txt
index f3b399d3..d994d5b6 100644
--- a/gpu/config/gpu_workaround_list.txt
+++ b/gpu/config/gpu_workaround_list.txt
@@ -139,6 +139,7 @@
 use_first_valid_ref_for_av1_invalid_ref
 use_gpu_driver_workaround_for_testing
 use_non_zero_size_for_client_side_stream_buffers
+use_tex_sub_image_for_host_twiddled_npot_uploads
 use_virtualized_gl_contexts
 validate_max_per_stage_uniform_blocks_at_compile_time
 wake_up_gpu_before_drawing
diff --git a/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc b/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
new file mode 100644
index 0000000..ea0e632
--- /dev/null
+++ b/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
@@ -0,0 +1,263 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#include <GLES3/gl3.h>
+#include <stdint.h>
+
+#include <vector>
+
+#include "gpu/command_buffer/tests/gl_manager.h"
+#include "gpu/command_buffer/tests/gl_test_utils.h"
+#include "gpu/config/gpu_driver_bug_workarounds.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gpu {
+
+namespace {
+
+void BlitTexture(GLuint program) {
+  glUseProgram(program);
+  const GLuint kVertexPositionAttrib = 0;
+  const GLfloat kQuadVertices[] = {-1.0f, -1.0f, 1.0f,  -1.0f,
+                                   1.0f,  1.0f,  -1.0f, 1.0f};
+  GLuint buffer_id = 0;
+  glGenBuffers(1, &buffer_id);
+  glBindBuffer(GL_ARRAY_BUFFER, buffer_id);
+  glBufferData(GL_ARRAY_BUFFER, sizeof(kQuadVertices), kQuadVertices,
+               GL_STATIC_DRAW);
+
+  glEnableVertexAttribArray(kVertexPositionAttrib);
+  glVertexAttribPointer(kVertexPositionAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
+
+  GLuint sampler_handle = glGetUniformLocation(program, "u_sampler");
+  glUniform1i(sampler_handle, 0);
+
+  glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  glDisableVertexAttribArray(kVertexPositionAttrib);
+  glBindBuffer(GL_ARRAY_BUFFER, 0);
+  glDeleteBuffers(1, &buffer_id);
+}
+
+}  // namespace
+
+class GLNPOTHostTwiddledTextureTest : public testing::TestWithParam<bool> {
+ protected:
+  void SetUp() override {
+    GLManager::Options options;
+    options.context_type = CONTEXT_TYPE_OPENGLES3;
+    options.size = gfx::Size(256, 256);
+
+    GpuDriverBugWorkarounds workarounds;
+    workarounds.use_tex_sub_image_for_host_twiddled_npot_uploads = GetParam();
+    gl_.InitializeWithWorkarounds(options, workarounds);
+
+    if (!IsApplicable()) {
+      return;
+    }
+
+    gl_.MakeCurrent();
+
+    static const char* kVertexShader =
+        "attribute vec2 a_position;\n"
+        "varying mediump vec2 v_uv;\n"
+        "void main(void) {\n"
+        "  gl_Position = vec4(a_position, 0, 1);\n"
+        "  v_uv = 0.5 * (a_position + vec2(1, 1));\n"
+        "}\n";
+    static const char* kFragmentShader =
+        "precision mediump float;\n"
+        "uniform sampler2D u_sampler;\n"
+        "varying mediump vec2 v_uv;\n"
+        "void main(void) {\n"
+        "  gl_FragColor = texture2D(u_sampler, v_uv);\n"
+        "}\n";
+
+    program_ = GLTestHelper::LoadProgram(kVertexShader, kFragmentShader);
+    ASSERT_NE(0u, program_);
+    glBindAttribLocation(program_, 0, "a_position");
+    glLinkProgram(program_);
+
+    glGenFramebuffers(1, &fbo_);
+  }
+
+  void TearDown() override {
+    if (IsApplicable()) {
+      if (program_) {
+        glDeleteProgram(program_);
+      }
+      if (fbo_) {
+        glDeleteFramebuffers(1, &fbo_);
+      }
+    }
+    gl_.Destroy();
+  }
+
+  bool IsApplicable() const { return gl_.IsInitialized(); }
+
+  GLManager gl_;
+  GLuint program_ = 0;
+  GLuint fbo_ = 0;
+};
+
+INSTANTIATE_TEST_SUITE_P(GLNPOTHostTwiddledTextureTests,
+                         GLNPOTHostTwiddledTextureTest,
+                         ::testing::Bool());
+
+// Test that non-power-of-two uploads of RGB10_A2 with
+// UNSIGNED_INT_2_10_10_10_REV data succeed and verify texture contents.
+TEST_P(GLNPOTHostTwiddledTextureTest, RGB10A2) {
+  if (!IsApplicable()) {
+    return;
+  }
+
+  constexpr GLsizei kWidth = 65;
+  constexpr GLsizei kHeight = 64;
+
+  for (int iter = 0; iter < 50; ++iter) {
+    std::vector<uint32_t> data(kWidth * kHeight);
+    for (GLsizei y = 0; y < kHeight; ++y) {
+      for (GLsizei x = 0; x < kWidth; ++x) {
+        uint32_t r = ((x + iter) % 2 == 0) ? 0x3FF : 0;
+        uint32_t g = ((y + iter) % 2 == 0) ? 0x3FF : 0;
+        uint32_t b = ((x + y + iter) % 2 == 0) ? 0x3FF : 0;
+        uint32_t a = 3;  // 1.0 in 2-bit alpha
+        data[y * kWidth + x] = (r & 0x3FF) | ((g & 0x3FF) << 10) |
+                               ((b & 0x3FF) << 20) | ((a & 0x3) << 30);
+      }
+    }
+
+    GLuint tex = 0;
+    glGenTextures(1, &tex);
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, kWidth, kHeight, 0, GL_RGBA,
+                 GL_UNSIGNED_INT_2_10_10_10_REV, data.data());
+    EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+    // Verify texture contents via FBO readback
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc b/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
new file mode 100644
index 0000000..ea0e632
--- /dev/null
+++ b/gpu/command_buffer/tests/gl_npot_host_twiddled_texture_unittest.cc
@@ -0,0 +1,263 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <GLES2/gl2.h>
+#include <GLES2/gl2ext.h>
+#include <GLES3/gl3.h>
+#include <stdint.h>
+
+#include <vector>
+
+#include "gpu/command_buffer/tests/gl_manager.h"
+#include "gpu/command_buffer/tests/gl_test_utils.h"
+#include "gpu/config/gpu_driver_bug_workarounds.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gpu {
+
+namespace {
+
+void BlitTexture(GLuint program) {
+  glUseProgram(program);
+  const GLuint kVertexPositionAttrib = 0;
+  const GLfloat kQuadVertices[] = {-1.0f, -1.0f, 1.0f,  -1.0f,
+                                   1.0f,  1.0f,  -1.0f, 1.0f};
+  GLuint buffer_id = 0;
+  glGenBuffers(1, &buffer_id);
+  glBindBuffer(GL_ARRAY_BUFFER, buffer_id);
+  glBufferData(GL_ARRAY_BUFFER, sizeof(kQuadVertices), kQuadVertices,
+               GL_STATIC_DRAW);
+
+  glEnableVertexAttribArray(kVertexPositionAttrib);
+  glVertexAttribPointer(kVertexPositionAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
+
+  GLuint sampler_handle = glGetUniformLocation(program, "u_sampler");
+  glUniform1i(sampler_handle, 0);
+
+  glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+  EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+  glDisableVertexAttribArray(kVertexPositionAttrib);
+  glBindBuffer(GL_ARRAY_BUFFER, 0);
+  glDeleteBuffers(1, &buffer_id);
+}
+
+}  // namespace
+
+class GLNPOTHostTwiddledTextureTest : public testing::TestWithParam<bool> {
+ protected:
+  void SetUp() override {
+    GLManager::Options options;
+    options.context_type = CONTEXT_TYPE_OPENGLES3;
+    options.size = gfx::Size(256, 256);
+
+    GpuDriverBugWorkarounds workarounds;
+    workarounds.use_tex_sub_image_for_host_twiddled_npot_uploads = GetParam();
+    gl_.InitializeWithWorkarounds(options, workarounds);
+
+    if (!IsApplicable()) {
+      return;
+    }
+
+    gl_.MakeCurrent();
+
+    static const char* kVertexShader =
+        "attribute vec2 a_position;\n"
+        "varying mediump vec2 v_uv;\n"
+        "void main(void) {\n"
+        "  gl_Position = vec4(a_position, 0, 1);\n"
+        "  v_uv = 0.5 * (a_position + vec2(1, 1));\n"
+        "}\n";
+    static const char* kFragmentShader =
+        "precision mediump float;\n"
+        "uniform sampler2D u_sampler;\n"
+        "varying mediump vec2 v_uv;\n"
+        "void main(void) {\n"
+        "  gl_FragColor = texture2D(u_sampler, v_uv);\n"
+        "}\n";
+
+    program_ = GLTestHelper::LoadProgram(kVertexShader, kFragmentShader);
+    ASSERT_NE(0u, program_);
+    glBindAttribLocation(program_, 0, "a_position");
+    glLinkProgram(program_);
+
+    glGenFramebuffers(1, &fbo_);
+  }
+
+  void TearDown() override {
+    if (IsApplicable()) {
+      if (program_) {
+        glDeleteProgram(program_);
+      }
+      if (fbo_) {
+        glDeleteFramebuffers(1, &fbo_);
+      }
+    }
+    gl_.Destroy();
+  }
+
+  bool IsApplicable() const { return gl_.IsInitialized(); }
+
+  GLManager gl_;
+  GLuint program_ = 0;
+  GLuint fbo_ = 0;
+};
+
+INSTANTIATE_TEST_SUITE_P(GLNPOTHostTwiddledTextureTests,
+                         GLNPOTHostTwiddledTextureTest,
+                         ::testing::Bool());
+
+// Test that non-power-of-two uploads of RGB10_A2 with
+// UNSIGNED_INT_2_10_10_10_REV data succeed and verify texture contents.
+TEST_P(GLNPOTHostTwiddledTextureTest, RGB10A2) {
+  if (!IsApplicable()) {
+    return;
+  }
+
+  constexpr GLsizei kWidth = 65;
+  constexpr GLsizei kHeight = 64;
+
+  for (int iter = 0; iter < 50; ++iter) {
+    std::vector<uint32_t> data(kWidth * kHeight);
+    for (GLsizei y = 0; y < kHeight; ++y) {
+      for (GLsizei x = 0; x < kWidth; ++x) {
+        uint32_t r = ((x + iter) % 2 == 0) ? 0x3FF : 0;
+        uint32_t g = ((y + iter) % 2 == 0) ? 0x3FF : 0;
+        uint32_t b = ((x + y + iter) % 2 == 0) ? 0x3FF : 0;
+        uint32_t a = 3;  // 1.0 in 2-bit alpha
+        data[y * kWidth + x] = (r & 0x3FF) | ((g & 0x3FF) << 10) |
+                               ((b & 0x3FF) << 20) | ((a & 0x3) << 30);
+      }
+    }
+
+    GLuint tex = 0;
+    glGenTextures(1, &tex);
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB10_A2, kWidth, kHeight, 0, GL_RGBA,
+                 GL_UNSIGNED_INT_2_10_10_10_REV, data.data());
+    EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+    // Verify texture contents via FBO readback
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                           tex, 0);
+    EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+              glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+    std::vector<uint8_t> readback(kWidth * kHeight * 4);
+    glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE,
+                 readback.data());
+    EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+    for (GLsizei y = 0; y < kHeight; ++y) {
+      for (GLsizei x = 0; x < kWidth; ++x) {
+        uint8_t expected_r = ((x + iter) % 2 == 0) ? 255 : 0;
+        uint8_t expected_g = ((y + iter) % 2 == 0) ? 255 : 0;
+        uint8_t expected_b = ((x + y + iter) % 2 == 0) ? 255 : 0;
+        uint8_t expected_a = 255;
+        size_t idx = (y * kWidth + x) * 4;
+        EXPECT_EQ(readback[idx], expected_r)
+            << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+        EXPECT_EQ(readback[idx + 1], expected_g)
+            << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+        EXPECT_EQ(readback[idx + 2], expected_b)
+            << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+        EXPECT_EQ(readback[idx + 3], expected_a)
+            << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+      }
+    }
+
+    // Also verify sampling via BlitTexture
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    glViewport(0, 0, 256, 256);
+    BlitTexture(program_);
+
+    glDeleteTextures(1, &tex);
+  }
+}
+
+// Test that non-power-of-two uploads of SRGB8_ALPHA8 with UNSIGNED_BYTE data
+// succeed and verify texture contents, including non-default unpack alignment
+// and skip pixels.
+TEST_P(GLNPOTHostTwiddledTextureTest, SRGB8Alpha8) {
+  if (!IsApplicable()) {
+    return;
+  }
+
+  constexpr GLsizei kWidth = 255;
+  constexpr GLsizei kHeight = 256;
+  constexpr GLint kSkipPixels = 1;
+  constexpr GLsizei kRowLength = kWidth + kSkipPixels;
+
+  for (int iter = 0; iter < 50; ++iter) {
+    std::vector<uint8_t> data(kHeight * kRowLength * 4);
+    for (size_t i = 0; i < data.size() / 4; ++i) {
+      data[i * 4] = static_cast<uint8_t>((i * 17 + iter * 3) % 256);
+      data[i * 4 + 1] = static_cast<uint8_t>((i * 31 + iter * 5 + 7) % 256);
+      data[i * 4 + 2] = static_cast<uint8_t>((i * 53 + iter * 11 + 13) % 256);
+      data[i * 4 + 3] = 255;
+    }
+
+    GLuint tex = 0;
+    glGenTextures(1, &tex);
+    glBindTexture(GL_TEXTURE_2D, tex);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+
+    glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, kRowLength);
+    glPixelStorei(GL_UNPACK_SKIP_PIXELS, kSkipPixels);
+
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, kWidth, kHeight, 0, GL_RGBA,
+                 GL_UNSIGNED_BYTE, data.data());
+    EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+    glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+    glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
+
+    // Verify texture contents via FBO readback
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo_);
+    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+                           tex, 0);
+    EXPECT_EQ(static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE),
+              glCheckFramebufferStatus(GL_FRAMEBUFFER));
+
+    std::vector<uint8_t> readback(kWidth * kHeight * 4);
+    glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE,
+                 readback.data());
+    EXPECT_EQ(static_cast<GLenum>(GL_NO_ERROR), glGetError());
+
+    for (GLsizei y = 0; y < kHeight; ++y) {
+      for (GLsizei x = 0; x < kWidth; ++x) {
+        size_t src_idx = (y * kRowLength + x + kSkipPixels) * 4;
+        size_t dst_idx = (y * kWidth + x) * 4;
+        EXPECT_EQ(readback[dst_idx], data[src_idx])
+            << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+        EXPECT_EQ(readback[dst_idx + 1], data[src_idx + 1])
+            << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+        EXPECT_EQ(readback[dst_idx + 2], data[src_idx + 2])
+            << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+        EXPECT_EQ(readback[dst_idx + 3], data[src_idx + 3])
+            << "Mismatch at (" << x << ", " << y << ") on iter " << iter;
+      }
+    }
+
+    // Also verify sampling via BlitTexture
+    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+    glViewport(0, 0, 256, 256);
+    BlitTexture(program_);
+
+    glDeleteTextures(1, &tex);
+  }
+}
+
+}  // namespace gpu
Loading diff…

Original Bug Report

reported by [email protected]

IMG: heap OOB write when call texImage2D with non-power-of-two dimensions and pixel data

  • Attack surface: texImage2D with non-power-of-two dimensions and pixel data, from any untrusted page.
  • Impact: Out-of-bounds write of attacker-chosen bytes in CPU memory inside the GPU process.

Android internal bug: b/553171115

View on issue tracker