High chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Skia
DescriptionInteger overflow in Skia
ComponentSkia
Sub-componentInternals>Skia
Bug ClassInteger Overflow
Tracker562038679
Fix commit7ff661871690 (chromium/src) +87/-77
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-01

Files Changed

  • gpu/command_buffer/common/gles2_cmd_format_test_autogen.h
  • gpu/command_buffer/raster_cmd_buffer_functions.txt
  • gpu/command_buffer/common/raster_cmd_format_autogen.h
  • gpu/command_buffer/service/raster_decoder.cc
  • cc/paint/paint_op_reader.cc
  • gpu/command_buffer/service/raster_decoder_autogen.h
  • gpu/command_buffer/build_cmd_buffer_lib.py
  • gpu/command_buffer/client/raster_cmd_helper_autogen.h
  • cc/paint/paint_op_writer.cc
  • cc/paint/paint_cache_unittest.cc
  • gpu/command_buffer/client/raster_implementation.cc
  • cc/paint/paint_cache.h
  • gpu/command_buffer/common/raster_cmd_format_test_autogen.h
From 7ff661871690f30cb1912652aaaab2eadc23903f Mon Sep 17 00:00:00 2001
From: Thomas Smith <thomsmit@google.com>
Date: Wed, 23 Sep 2026 11:26:47 -0700
Subject: [PATCH] cc, gpu: Widen PaintCacheId and paint cache deletion IPC to uint64_t

Skia is widening SkPath generation IDs from 32-bit to 64-bit. This CL
prepares Chromium to consume 64-bit IDs by widening PaintCacheId and
its associated serialization and GPU command buffer purging pipeline.

Bug: 562038679
Bug: 556268833
Change-Id: I6546900f138050327229c14db45c3c70b1e9640e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8429709
Reviewed-by: Kyle Charbonneau <kylechar@chromium.org>
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Thomas Smith <thomsmit@google.com>
Cr-Commit-Position: refs/heads/main@{#1703606}
---
diff --git a/cc/paint/paint_cache.h b/cc/paint/paint_cache.h
index 6183f22..19349115 100644
--- a/cc/paint/paint_cache.h
+++ b/cc/paint/paint_cache.h
@@ -38,7 +38,7 @@
 // since it avoids the need for cross-process ref-counting required by the
 // TransferCache.
 
-using PaintCacheId = uint32_t;
+using PaintCacheId = uint64_t;
 using PaintCacheIds = std::vector<PaintCacheId>;
 enum class PaintCacheDataType : uint32_t {
   kPath,
diff --git a/cc/paint/paint_op_reader.cc b/cc/paint/paint_op_reader.cc
index 006362e..53be3b7 100644
--- a/cc/paint/paint_op_reader.cc
+++ b/cc/paint/paint_op_reader.cc
@@ -300,8 +300,8 @@
 }
 
 void PaintOpReader::Read(SkPath* path) {
-  uint32_t path_id;
-  ReadSimple(&path_id);
+  PaintCacheId path_id;
+  Read(&path_id);
   if (!valid_)
     return;
 
diff --git a/cc/paint/paint_op_writer.cc b/cc/paint/paint_op_writer.cc
index 706d314..b6e1151 100644
--- a/cc/paint/paint_op_writer.cc
+++ b/cc/paint/paint_op_writer.cc
@@ -298,7 +298,7 @@
 }
 
 void PaintOpWriter::Write(const SkPath& path, UsePaintCache use_paint_cache) {
-  auto id = path.getGenerationID();
+  PaintCacheId id = path.getGenerationID();
   Write(id);
 
   DCHECK(use_paint_cache == UsePaintCache::kEnabled ||
diff --git a/gpu/command_buffer/build_cmd_buffer_lib.py b/gpu/command_buffer/build_cmd_buffer_lib.py
index bd18d16..05addec 100644
--- a/gpu/command_buffer/build_cmd_buffer_lib.py
+++ b/gpu/command_buffer/build_cmd_buffer_lib.py
@@ -2482,10 +2482,12 @@
   def WriteGetDataSizeCode(self, func, arg, f):
     """Overrriden from TypeHandler."""
     code = """  uint32_t %(data_size)s;
-  if (!base::CheckMul(n, sizeof(GLuint)).AssignIfValid(&%(data_size)s)) {
+  if (!base::CheckMul(n, sizeof(%(element_type)s))
+           .AssignIfValid(&%(data_size)s)) {
     return error::kOutOfBounds;
   }
-""" % {'data_size': arg.GetReservedSizeId()}
+""" % {'data_size': arg.GetReservedSizeId(),
+       'element_type': arg.GetPointedType()}
     f.write(code)
 
   def WriteGLES2ImplementationUnitTest(self, func, f):
@@ -2629,11 +2631,13 @@
 """
       f.write(code % args)
 
-  def WriteImmediateCmdComputeSize(self, _func, f):
+  def WriteImmediateCmdComputeSize(self, func, f):
     """Overrriden from TypeHandler."""
+    element_type = func.GetLastOriginalArg().GetPointedType()
     f.write("  static uint32_t ComputeDataSize(GLsizei _n) {\n")
     f.write(
-        "    return static_cast<uint32_t>(sizeof(GLuint) * _n);  // NOLINT\n")
+        "    return static_cast<uint32_t>(sizeof(%s) * _n);  // NOLINT\n" %
+        element_type)
     f.write("  }\n")
     f.write("\n")
     f.write("  static uint32_t ComputeSize(GLsizei _n) {\n")
@@ -2700,8 +2704,9 @@
 
   def WriteImmediateFormatTest(self, func, f):
     """Overrriden from TypeHandler."""
+    element_type = func.GetLastOriginalArg().GetPointedType()
     f.write("TEST_F(%sFormatTest, %s) {\n" % (_prefix, func.name))
-    f.write("  static GLuint ids[] = { 12, 23, 34, };\n")
+    f.write("  static %s ids[] = { 12, 23, 34, };\n" % element_type)
     f.write("  cmds::%s& cmd = *GetBufferAs<cmds::%s>();\n" %
                (func.name, func.name))
     f.write("  void* next_cmd = cmd.Set(\n")
@@ -2710,12 +2715,15 @@
                func.name)
     f.write("            cmd.header.command);\n")
     f.write("  EXPECT_EQ(sizeof(cmd) +\n")
-    f.write("            RoundSizeToMultipleOfEntries(cmd.n * 4u),\n")
+    f.write("            RoundSizeToMultipleOfEntries(cmd.n * sizeof(%s)),\n" %
+               element_type)
     f.write("            cmd.header.size * 4u);\n")
     f.write("  EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);\n");
     f.write("  CheckBytesWrittenMatchesExpectedSize(\n")
     f.write("      next_cmd, sizeof(cmd) +\n")
-    f.write("      RoundSizeToMultipleOfEntries(std::size(ids) * 4u));\n")
+    f.write("      RoundSizeToMultipleOfEntries("
+            "std::size(ids) * sizeof(%s)));\n" %
+               element_type)
     f.write("  EXPECT_EQ(0, UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd),\n")
     f.write("                      sizeof(ids))));\n")
     f.write("}\n")
diff --git a/gpu/command_buffer/client/raster_implementation.cc b/gpu/command_buffer/client/raster_implementation.cc
index 515a5baa..8edfa91 100644
--- a/gpu/command_buffer/client/raster_implementation.cc
+++ b/gpu/command_buffer/client/raster_implementation.cc
@@ -108,7 +108,7 @@
   if (ids.size() <= IMMEDIATE_SIZE_CONSTANT) {                           \
     helper_->IMMEDIATE_FUNCTION(ids.size(), ids.data());                 \
   } else {                                                               \
-    size_t data_size = ids.size() * sizeof(GLuint);                      \
+    size_t data_size = ids.size() * sizeof(cc::PaintCacheId);            \
     ScopedSharedMemoryPtr dest(data_size, transfer_buffer_,              \
                                mapped_memory_.get(), helper());          \
     if (dest.valid()) {                                                  \
diff --git a/gpu/command_buffer/raster_cmd_buffer_functions.txt b/gpu/command_buffer/raster_cmd_buffer_functions.txt
index 717f4fe..75621f3 100644
--- a/gpu/command_buffer/raster_cmd_buffer_functions.txt
+++ b/gpu/command_buffer/raster_cmd_buffer_functions.txt
@@ -28,8 +28,8 @@
 GL_APICALL void         GL_APIENTRY glCreateTransferCacheEntryINTERNAL (GLuint entry_type, GLuint entry_id, GLuint handle_shm_id, GLuint handle_shm_offset, GLuint data_shm_id, GLuint data_shm_offset, GLuint data_size);
 GL_APICALL void         GL_APIENTRY glDeleteTransferCacheEntryINTERNAL (GLuint entry_type, GLuint entry_id);
 GL_APICALL void         GL_APIENTRY glUnlockTransferCacheEntryINTERNAL (GLuint entry_type, GLuint entry_id);
-GL_APICALL void         GL_APIENTRY glDeletePaintCachePathsINTERNAL (GLsizeiNotNegative n, const GLuint* ids);
-GL_APICALL void         GL_APIENTRY glDeletePaintCacheEffectsINTERNAL (GLsizeiNotNegative n, const GLuint* ids);
+GL_APICALL void         GL_APIENTRY glDeletePaintCachePathsINTERNAL (GLsizeiNotNegative n, const GLuint64* ids);
+GL_APICALL void         GL_APIENTRY glDeletePaintCacheEffectsINTERNAL (GLsizeiNotNegative n, const GLuint64* ids);
 GL_APICALL void         GL_APIENTRY glClearPaintCacheINTERNAL (void);
 
 // TOOD(backer): Remove GL encoding. These are not GL functions.
diff --git a/gpu/command_buffer/service/raster_decoder.cc b/gpu/command_buffer/service/raster_decoder.cc
index b9a44b8..7fbcc40b 100644
--- a/gpu/command_buffer/service/raster_decoder.cc
+++ b/gpu/command_buffer/service/raster_decoder.cc
@@ -792,10 +792,10 @@
   void RestoreStateForAttrib(GLuint attrib, bool restore_array_binding);
   void DeletePaintCachePathsINTERNALHelper(
       GLsizei n,
-      const volatile GLuint* paint_cache_ids);
+      const volatile GLuint64* paint_cache_ids);
   void DeletePaintCacheEffectsINTERNALHelper(
       GLsizei n,
-      const volatile GLuint* paint_cache_ids);
+      const volatile GLuint64* paint_cache_ids);
   void DoClearPaintCacheINTERNAL();
 
 #if defined(NDEBUG)
@@ -2785,13 +2785,13 @@
 
 void RasterDecoderImpl::DeletePaintCachePathsINTERNALHelper(
     GLsizei n,
-    const volatile GLuint* paint_cache_ids) {
+    const volatile GLuint64* paint_cache_ids) {
   paint_cache_->Purge(cc::PaintCacheDataType::kPath, n, paint_cache_ids);
 }
 
 void RasterDecoderImpl::DeletePaintCacheEffectsINTERNALHelper(
     GLsizei n,
-    const volatile GLuint* paint_cache_ids) {
+    const volatile GLuint64* paint_cache_ids) {
   paint_cache_->Purge(cc::PaintCacheDataType::kSkRuntimeEffect, n,
                       paint_cache_ids);
 }
diff --git a/cc/paint/paint_cache_unittest.cc b/cc/paint/paint_cache_unittest.cc
index 3e3f8df..81df359a 100644
--- a/cc/paint/paint_cache_unittest.cc
+++ b/cc/paint/paint_cache_unittest.cc
@@ -91,7 +91,7 @@
   switch (GetType()) {
     case PaintCacheDataType::kPath: {
       auto path = CreatePath();
-      auto id = path.getGenerationID();
+      PaintCacheId id = path.getGenerationID();
       SkPath cached_path;
       EXPECT_EQ(false, service_cache.GetPath(id, &cached_path));
       service_cache.PutPath(id, path);
@@ -104,7 +104,7 @@
     } break;
     case PaintCacheDataType::kSkRuntimeEffect: {
       auto effect = GetEffect();
-      auto id = base::PersistentHash(effect->source());
+      PaintCacheId id = base::PersistentHash(effect->source());
       sk_sp<SkRuntimeEffect> cached_effect = nullptr;
       EXPECT_FALSE(service_cache.GetEffect(id, &cached_effect));
       service_cache.PutEffect(id, effect);
diff --git a/gpu/command_buffer/client/raster_cmd_helper_autogen.h b/gpu/command_buffer/client/raster_cmd_helper_autogen.h
index 002ed1d..66c4c687 100644
--- a/gpu/command_buffer/client/raster_cmd_helper_autogen.h
+++ b/gpu/command_buffer/client/raster_cmd_helper_autogen.h
@@ -158,7 +158,7 @@
   }
 }
 
-void DeletePaintCachePathsINTERNALImmediate(GLsizei n, const GLuint* ids) {
+void DeletePaintCachePathsINTERNALImmediate(GLsizei n, const GLuint64* ids) {
   const uint32_t size =
       raster::cmds::DeletePaintCachePathsINTERNALImmediate::ComputeSize(n);
   raster::cmds::DeletePaintCachePathsINTERNALImmediate* c =
@@ -179,7 +179,7 @@
   }
 }
 
-void DeletePaintCacheEffectsINTERNALImmediate(GLsizei n, const GLuint* ids) {
+void DeletePaintCacheEffectsINTERNALImmediate(GLsizei n, const GLuint64* ids) {
   const uint32_t size =
       raster::cmds::DeletePaintCacheEffectsINTERNALImmediate::ComputeSize(n);
   raster::cmds::DeletePaintCacheEffectsINTERNALImmediate* c =
diff --git a/gpu/command_buffer/common/gles2_cmd_format_test_autogen.h b/gpu/command_buffer/common/gles2_cmd_format_test_autogen.h
index 7b342948..ee6026d 100644
--- a/gpu/command_buffer/common/gles2_cmd_format_test_autogen.h
+++ b/gpu/command_buffer/common/gles2_cmd_format_test_autogen.h
@@ -736,12 +736,12 @@
   void* next_cmd = cmd.Set(&cmd, static_cast<GLsizei>(std::size(ids)), ids);
   EXPECT_EQ(static_cast<uint32_t>(cmds::DeleteBuffersImmediate::kCmdId),
             cmd.header.command);
-  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * 4u),
+  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * sizeof(GLuint)),
             cmd.header.size * 4u);
   EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);
   CheckBytesWrittenMatchesExpectedSize(
-      next_cmd,
-      sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) * 4u));
+      next_cmd, sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) *
+                                                           sizeof(GLuint)));
   EXPECT_EQ(0,
             UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd), sizeof(ids))));
 }
@@ -757,12 +757,12 @@
   void* next_cmd = cmd.Set(&cmd, static_cast<GLsizei>(std::size(ids)), ids);
   EXPECT_EQ(static_cast<uint32_t>(cmds::DeleteFramebuffersImmediate::kCmdId),
             cmd.header.command);
-  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * 4u),
+  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * sizeof(GLuint)),
             cmd.header.size * 4u);
   EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);
   CheckBytesWrittenMatchesExpectedSize(
-      next_cmd,
-      sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) * 4u));
+      next_cmd, sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) *
+                                                           sizeof(GLuint)));
   EXPECT_EQ(0,
             UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd), sizeof(ids))));
 }
@@ -788,12 +788,12 @@
   void* next_cmd = cmd.Set(&cmd, static_cast<GLsizei>(std::size(ids)), ids);
   EXPECT_EQ(static_cast<uint32_t>(cmds::DeleteRenderbuffersImmediate::kCmdId),
             cmd.header.command);
-  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * 4u),
+  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * sizeof(GLuint)),
             cmd.header.size * 4u);
   EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);
   CheckBytesWrittenMatchesExpectedSize(
-      next_cmd,
-      sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) * 4u));
+      next_cmd, sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) *
+                                                           sizeof(GLuint)));
   EXPECT_EQ(0,
             UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd), sizeof(ids))));
 }
@@ -809,12 +809,12 @@
   void* next_cmd = cmd.Set(&cmd, static_cast<GLsizei>(std::size(ids)), ids);
   EXPECT_EQ(static_cast<uint32_t>(cmds::DeleteSamplersImmediate::kCmdId),
             cmd.header.command);
-  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * 4u),
+  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * sizeof(GLuint)),
             cmd.header.size * 4u);
   EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);
   CheckBytesWrittenMatchesExpectedSize(
-      next_cmd,
-      sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) * 4u));
+      next_cmd, sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) *
+                                                           sizeof(GLuint)));
   EXPECT_EQ(0,
             UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd), sizeof(ids))));
 }
@@ -850,12 +850,12 @@
   void* next_cmd = cmd.Set(&cmd, static_cast<GLsizei>(std::size(ids)), ids);
   EXPECT_EQ(static_cast<uint32_t>(cmds::DeleteTexturesImmediate::kCmdId),
             cmd.header.command);
-  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * 4u),
+  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * sizeof(GLuint)),
             cmd.header.size * 4u);
   EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);
   CheckBytesWrittenMatchesExpectedSize(
-      next_cmd,
-      sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) * 4u));
+      next_cmd, sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) *
+                                                           sizeof(GLuint)));
   EXPECT_EQ(0,
             UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd), sizeof(ids))));
 }
@@ -872,12 +872,12 @@
   EXPECT_EQ(
       static_cast<uint32_t>(cmds::DeleteTransformFeedbacksImmediate::kCmdId),
       cmd.header.command);
-  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * 4u),
+  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * sizeof(GLuint)),
             cmd.header.size * 4u);
   EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);
   CheckBytesWrittenMatchesExpectedSize(
-      next_cmd,
-      sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) * 4u));
+      next_cmd, sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) *
+                                                           sizeof(GLuint)));
   EXPECT_EQ(0,
             UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd), sizeof(ids))));
 }
@@ -3990,12 +3990,12 @@
   void* next_cmd = cmd.Set(&cmd, static_cast<GLsizei>(std::size(ids)), ids);
   EXPECT_EQ(static_cast<uint32_t>(cmds::DeleteQueriesEXTImmediate::kCmdId),
             cmd.header.command);
-  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * 4u),
+  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * sizeof(GLuint)),
             cmd.header.size * 4u);
   EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);
   CheckBytesWrittenMatchesExpectedSize(
-      next_cmd,
-      sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) * 4u));
+      next_cmd, sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) *
+                                                           sizeof(GLuint)));
   EXPECT_EQ(0,
             UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd), sizeof(ids))));
 }
@@ -4137,12 +4137,12 @@
   void* next_cmd = cmd.Set(&cmd, static_cast<GLsizei>(std::size(ids)), ids);
   EXPECT_EQ(static_cast<uint32_t>(cmds::DeleteVertexArraysOESImmediate::kCmdId),
             cmd.header.command);
-  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * 4u),
+  EXPECT_EQ(sizeof(cmd) + RoundSizeToMultipleOfEntries(cmd.n * sizeof(GLuint)),
             cmd.header.size * 4u);
   EXPECT_EQ(static_cast<GLsizei>(std::size(ids)), cmd.n);
   CheckBytesWrittenMatchesExpectedSize(
-      next_cmd,
-      sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) * 4u));
+      next_cmd, sizeof(cmd) + RoundSizeToMultipleOfEntries(std::size(ids) *
+                                                           sizeof(GLuint)));
   EXPECT_EQ(0,
             UNSAFE_TODO(memcmp(ids, ImmediateDataAddress(&cmd), sizeof(ids))));
 }
diff --git a/gpu/command_buffer/common/raster_cmd_format_autogen.h b/gpu/command_buffer/common/raster_cmd_format_autogen.h
index 1142810..8a692789 100644
--- a/gpu/command_buffer/common/raster_cmd_format_autogen.h
+++ b/gpu/command_buffer/common/raster_cmd_format_autogen.h
@@ -702,7 +702,7 @@
   static const uint8_t cmd_flags = CMD_FLAG_SET_TRACE_LEVEL(3);
 
   static uint32_t ComputeDataSize(GLsizei _n) {
-    return static_cast<uint32_t>(sizeof(GLuint) * _n);  // NOLINT
+    return static_cast<uint32_t>(sizeof(GLuint64) * _n);  // NOLINT
   }
 
   static uint32_t ComputeSize(GLsizei _n) {
@@ -714,13 +714,13 @@
     header.SetCmdByTotalSize<ValueType>(ComputeSize(_n));
   }
 
-  void Init(GLsizei _n, const GLuint* _ids) {
+  void Init(GLsizei _n, const GLuint64* _ids) {
     SetHeader(_n);
     n = _n;
     UNSAFE_TODO(memcpy(ImmediateDataAddress(this), _ids, ComputeDataSize(_n)));
   }
 
-  void* Set(void* cmd, GLsizei _n, const GLuint* _ids) {
+  void* Set(void* cmd, GLsizei _n, const GLuint64* _ids) {
     static_cast<ValueType*>(cmd)->Init(_n, _ids);
     const uint32_t size = ComputeSize(_n);
     return NextImmediateCmdAddressTotalSize<ValueType>(cmd, size);
@@ -790,7 +790,7 @@
   static const uint8_t cmd_flags = CMD_FLAG_SET_TRACE_LEVEL(3);
 
   static uint32_t ComputeDataSize(GLsizei _n) {
-    return static_cast<uint32_t>(sizeof(GLuint) * _n);  // NOLINT
+    return static_cast<uint32_t>(sizeof(GLuint64) * _n);  // NOLINT
   }
 
   static uint32_t ComputeSize(GLsizei _n) {
@@ -802,13 +802,13 @@
     header.SetCmdByTotalSize<ValueType>(ComputeSize(_n));
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/cc/paint/paint_cache_unittest.cc b/cc/paint/paint_cache_unittest.cc
index 3e3f8df..81df359a 100644
--- a/cc/paint/paint_cache_unittest.cc
+++ b/cc/paint/paint_cache_unittest.cc
@@ -91,7 +91,7 @@
   switch (GetType()) {
     case PaintCacheDataType::kPath: {
       auto path = CreatePath();
-      auto id = path.getGenerationID();
+      PaintCacheId id = path.getGenerationID();
       SkPath cached_path;
       EXPECT_EQ(false, service_cache.GetPath(id, &cached_path));
       service_cache.PutPath(id, path);
@@ -104,7 +104,7 @@
     } break;
     case PaintCacheDataType::kSkRuntimeEffect: {
       auto effect = GetEffect();
-      auto id = base::PersistentHash(effect->source());
+      PaintCacheId id = base::PersistentHash(effect->source());
       sk_sp<SkRuntimeEffect> cached_effect = nullptr;
       EXPECT_FALSE(service_cache.GetEffect(id, &cached_effect));
       service_cache.PutEffect(id, effect);
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-origin mask leak via Graphite PathAtlas 30-bit genID wraparound

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: Skia Graphite’s RasterPathAtlas caches rendered A8 path coverage masks in a GPU-process cache (fShapeCache) that, for paths with more than 10 verbs, keys geometry solely by SkPath::getGenerationID() — a 30-bit, eagerly-allocated, process-global wrapping counter. The cache performs no content verification on hit and entry lifetime is decoupled from path lifetime, while a single gpu-main Graphite recorder serves every renderer process, so entries are shared across origins. A web page can burn the counter through a full 2^30 wrap (~30 minutes, measured on an unmodified release build) and then draw fresh probe paths whose recycled generation IDs alias a victim origin’s cached masks, compositing the victim’s rasterized vector geometry into the attacker’s canvas where getImageData() reads it pixel-for-pixel. An executed end-to-end reproduction demonstrated the cross-origin leak.

Affected files:

Estimated timestamp from git blame: 2023-10-10 (Skia commit 06145491fd, “[graphite] Add caching for RasterPathAtlas”, introduced the genID keying and the cache)

Summary

For any path with more than 10 verbs, the per-geometry component of the Graphite path-atlas cache key is only the path’s generation ID. Generation IDs come from a process-global atomic counter whose top 2 bits are discarded, so the same ID is re-issued every 2^30 allocations, and the ID is consumed eagerly at SkPathData construction — deserializing a path is enough, no rasterization needed. Cache entries are never invalidated when their source path is destroyed (no SkIDChangeListener, unlike Ganesh’s software path renderer and Graphite’s own ProxyCache bitmap path), and a hit returns the cached atlas slot without any geometry comparison.

In Chromium the cache lives in the single gpu-main recorder that rasterizes for every renderer process (viz uses a separate recorder), so cached masks from different origins share one keyspace. The attack: plant marker entries, load a cross-origin victim page whose >10-verb vector content (SVG icons, charts, signature strokes) enters the same cache, burn 2^30 IDs with cheap implicit-path canvas.clip() operations, then sweep fresh probe paths — a probe whose recycled ID collides with a cached victim entry silently receives the victim’s A8 mask into the attacker’s readable canvas. Each collision leaks ≥1 KB of the victim origin’s rasterized geometry at device resolution.

Affected configurations (paths routed to the cached atlas managers):

  • Default shipping, no flags: all pre-Gen11-Intel GPUs with Graphite — kSkiaGraphite is default-enabled on Apple (gpu/config/gpu_finch_features.cc:378-384), and msaa_is_slow/msaa_is_slow_2 (gpu/config/gpu_driver_bug_list.json entries 132/398) force avoidMSAA() → PathRendererStrategy::kRasterAtlas, atlasing every path draw (skia_utils.cc:149-155, RendererProvider.cpp:99-100, Device.cpp:2178-2180). This covers every Intel Mac (all Gen ≤ 9.5).
  • Active field-trial rollout: SkiaGraphiteSmallPathAtlas on all Macs (testing/variations/fieldtrial_testing_config.json, min_path_size_for_msaa: 32) atlases all paths with device bounds ≤32 px (skia_utils.cc:166-169, Device.cpp:2168-2176); SkiaGraphite/SkiaGraphiteWinIntel trials extend the Windows/Android population.

Root Cause Analysis

  1. Eager 30-bit wrapping ID: third_party/skia/src/core/SkPathData.cpp:33-46,148-149 — every non-empty SkPath materialization allocates a generation ID at construction; the top 2 bits are cleared, giving a period of 2^30. There is no deduplication of identical geometry, so each deserialized path consumes an ID with zero rasterization cost:
    // [`third_party/skia/src/core/SkPathData.cpp:38-44`](https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/core/SkPathData.cpp;l=38)
    uint32_t id;
    do {
        id = nextID.fetch_add(1, std::memory_order_relaxed);
        id <<= kHighBitsToMakeRoomForFillType;  // top 2 bits cleared ->
        id >>= kHighBitsToMakeRoomForFillType;  // same ID recurs every 2^30 allocations
    } while (id == 0);
    
  2. genID is the only geometry key for complex paths: third_party/skia/src/gpu/graphite/geom/Shape.cpp:24,143-147,238-246 — paths with more than kMaxKeyFromDataVerbCnt = 10 verbs skip the key-from-data encoding and write only this->path().getGenerationID() into the key.
  3. Every other key component is attacker-reproducible: third_party/skia/src/gpu/graphite/RasterPathUtils.cpp:135-174 — GeneratePathMaskKey adds only maskSize, the 2x2 matrix scale/skew floats, 8-bit fractional-translate bits, stroke-style bits, and fill-type/geometry-class state. Integer translation is deliberately excluded, and the maskOrigin component is clip-relative — (0,0) for any unclipped draw (PathAtlas.cpp:60-63) — so for a plain filled path at integer coordinates the key reduces to (maskSize, scale, fill type, genID).
  4. Hit path returns the cached slot blindly; entries outlive their paths: third_party/skia/src/gpu/graphite/PathAtlas.cpp:116-124 — a key match returns the cached AtlasLocator and page proxy with no geometry comparison. Entries are removed only by plot eviction (PathAtlas.cpp:193-205); no SkIDChangeListener exists in any Graphite atlas code (the sole Graphite listener, ProxyCache.cpp:131-136, applies to generated-bitmap pixelRefs and is skipped for unique ones). Since OOP-R deserializes a fresh transient SkPath per raster task, entries persist keyed by dead paths’ IDs that the counter will re-issue:
    // [`third_party/skia/src/gpu/graphite/PathAtlas.cpp:118-124`](https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/PathAtlas.cpp;l=118)
    DrawAtlas::AtlasLocator* cachedLocator = fShapeCache.find(maskKey);
    if (cachedLocator) {
        ...
        fDrawAtlas->setLastUseToken(*cachedLocator, ...);
        return fDrawAtlas->getProxies()[cachedLocator->pageIndex()];  // <-- no content check
    }
    
  5. One cache for all origins: gpu/command_buffer/service/raster_decoder.cc:628-630 — every renderer channel rasters through SharedContextState::gpu_main_graphite_recorder(); the per-recorder AtlasProvider owns one RasterPathAtlas (AtlasProvider.cpp:37-45), and fRequireOrderedRecordings = true (skia_utils.cc:157-164) keeps atlas pages and fShapeCache alive across all clients’ recordings. Site isolation puts origins in separate renderer processes, but they all share this cache, so a substituted mask is cross-origin by construction.
  6. Web-content attack primitives: ID burn — implicit-path ctx.clip() serializes as kInlinedDoNotCache (canvas_2d_recorder_context.cc:1839-1841, paint_op_writer.cc:292-315); GPU-side deserialization builds one fresh volatile SkPathData (one ID) that never touches the cached managers (paint_op_reader.cc:317-349, RasterPathAtlas.cpp:59). Probes/keepers — retained Path2D draws (under the active Path2DPaintCache trial) or document-painted SVG drawn into the canvas (paint_canvas.h:170 defaults UsePaintCache::kEnabled) produce non-volatile paths that enter fShapeCache and render into the attacker’s own readable canvas; retained paths keep a stable GPU-side genID (paint_op_reader.cc:312-316), giving a deterministic self-clocking oracle.
  7. Leak leg: third_party/skia/src/gpu/graphite/render/CoverageMaskRenderStep.cpp:172-213 — on collision, the draw samples the atlas at the cached locator’s coordinates with no validation of what lives there; the victim’s mask composites into the attacker canvas and getImageData() reads it. The atlas-full fallback (RasterPathAtlas.cpp:100-124) keys ProxyCache entries with the same GeneratePathMaskKey and registers no listener, so the identical aliasing exists there under a byte-budget lifetime.

Reproduction

  1. On an affected config (pre-Gen11-Intel default, or the Mac SkiaGraphiteSmallPathAtlas trial config), the attacker page draws ~8 retained >10-verb Path2D markers (~30x30 px, distinctive sparse geometry) into an accelerated canvas, then redraws them every frame — stable genIDs give guaranteed cache hits whose setLastUseToken pins their plots against DrawAtlas::compact() aging and pressure eviction.
  2. window.open() the victim page; its >10-verb non-volatile paths (SVG icons/charts within the config’s size gate) are inserted into the same shared cache with genIDs bracketed between the attacker’s markers.
  3. Burn ~2^30 IDs: loop save(); beginPath(); moveTo/lineTo/lineTo (3-verb non-rect triangle); clip(); restore() — each op consumes exactly one GPU-side ID with no raster work, no atlas writes, and no memory growth.
  4. Sweep fresh probe Path2Ds (solid blobs, marker-matching maskSize/transform), reading back each via getImageData(). When a probe shows a marker’s sparse pattern instead of the solid blob, the counter position is exactly known (pixel oracle, no timing channel).
  5. Continue probing through the bracketed range with maskSize matching the victim asset’s device size; each collision composites the victim origin’s A8 coverage mask into the attacker canvas for pixel-exact readback.
  6. Expected: a correct implementation would never return a cached mask for a path whose geometry it has not verified; probes would render as drawn.

Observed: an executed end-to-end reproduction (Linux, Graphite via Dawn/SwiftShader, the exact Mac rollout configuration SkiaGraphiteSmallPathAtlas min_path_size_for_msaa=32 + Path2DPaintCache, cross-origin victim in a separate renderer process) leaked all four of the victim page’s secret SVG figures pixel-exactly into the attacker’s canvas. GPU-process logging independently confirmed fresh attacker probes (verbs=19) receiving recycled generation IDs and hitting the victim’s cached entries (verbs=11 inserts at the same genIDs). The run used a sanctioned counter-period reduction (2^30 → 2^20, allocator otherwise identical) for deterministic ~12 s completion; the full-period burn rate was measured on a stock, unmodified release build at 590,986 IDs/s sustained from one background tab — a complete 2^30 wrap in 30.3 minutes. A content-hash-verification patch was also built and re-tested: the identical attack observed zero substitutions across three wrap periods while the GPU log recorded 24 blocked collisions, confirming the root cause.

Severity

high — Cross-origin data leak reachable from pure web content on default-shipping hardware (all Intel Macs with Graphite) and on active field-trial rollout surface (all Macs, small paths); the rubric’s high tier covers cross-origin leaks even when a compromised renderer is required, and none is required here. Each collision is a controlled read of ≥1 KB (a chosen cached entry, selected by genID position and maskSize), far above the 16-byte bar, and the leaked content — charts encoding user data, signature strokes, icon/auth state rendered as paths — is the victim origin’s. Not critical: no memory corruption in an unsandboxed process and no arbitrary file read. The medium “extreme trigger complexity” demotion does not apply: there is no timing-sensitive step — the wrap is deterministic and self-clocking, the oracle is exact pixel readback, and the cost is ~30 minutes of measured background compute.

Suggested Fix Direction

Verify content on genID-keyed hits: store a hash of the path data (verbs/points/conic weights) with each >10-verb fShapeCache entry and evict on mismatch — an ID-change listener alone is insufficient because the wrapping counter also re-issues IDs of still-live paths. Alternatively widen SkPathData generation IDs to 64 bits; either fix should also cover the ProxyCache mask fallback (RasterPathAtlas.cpp:100-124) and the analogous ClipAtlasManager shape keys.


POC: Graphite PathAtlas fShapeCache 30-bit genID wraparound — cross-origin A8 mask substitution

Strategy: web-content Bug Type: cache-key aliasing via wrapping 30-bit identifier (CWE-694) → cross-origin pixel disclosure (logic bug — no memory-safety error, so no ASan report is expected)

Summary of what is demonstrated

Skia Graphite’s RasterPathAtlas caches rendered A8 path coverage masks in PathAtlas::DrawAtlasMgr::fShapeCache. For any path with more than 10 verbs, the only geometry component of the cache key is SkPath::getGenerationID() — a 30-bit, eagerly-allocated, process-global, wrapping counter (third_party/skia/src/core/SkPathData.cpp, next_pathdata_unique_id()). The cache performs no content check on hit and holds entries with no invalidation tied to path lifetime (third_party/skia/src/gpu/graphite/PathAtlas.cpp, findOrCreateEntry). In Chromium the cache lives in the single gpu-main Graphite Recorder that serves every renderer process (gpu/command_buffer/service/raster_decoder.cc → shared_context_state_->gpu_main_graphite_recorder()), so its entries are cross-origin by construction.

This POC, from pure web content:

  1. plants marker entries in the shared GPU-process cache (retained Path2D objects → stable generation IDs),
  2. loads a cross-origin victim page (different site ⇒ different renderer process) whose secret SVG figures enter the same cache,
  3. burns generation IDs with implicit-path canvas.clip() operations (each op = one fresh GPU-side SkPath = one ID; volatile, zero cache residue) until the 30-bit counter wraps,
  4. draws fresh probe paths: when a probe is assigned a recycled generation ID that still keys a cached entry, the GPU process silently composites the cached mask — including the victim’s — into the attacker’s canvas, where getImageData() reads it out pixel-for-pixel.

Result: all four cross-origin victim figures were exfiltrated pixel-exactly into the attacker’s canvas (see leak-snapshot.png, poc-screenshot.png, poc-result.json bitmaps), plus deterministic wrap detection via the marker aliases. GPU-side logging independently confirms the probe lookups hit the victim entries’ generation IDs (gpu-poclog-excerpt.txt).

Counter-exhaustion feasibility (per accelerated-POC policy)

The wrap needs 2^30 ID allocations. Measured on the stock, unmodified release build (out/Default, same Graphite/SwiftShader configuration, single background tab, rate.html):

RATE DONE ops=44325000 seconds=75.0 rate=590986 ids/sec => full 2^30 wrap in 30.3 minutes

590,986 IDs/sec sustained for 75 s ⇒ 2^30 wrap in ~30 minutes of one background tab — comfortably within a browsing session (production GPU hardware is at least as fast as this software-rendering sandbox; the ASan build still sustains ~120k IDs/sec ⇒ ~2.5 h worst case). Client-side op counting equals GPU-side consumption: on the instrumented build the GPU process’s sampled counter log advanced in lockstep with the client count (gpu-poclog-excerpt.txt, POCLOG pathid lines).

Because a 30-minute real-time grind is not a practical regression test, the reproduction uses the sanctioned accelerated path: chromium-changes.patch shrinks the counter period from 2^30 to 2^20 (id &= 0x000FFFFF) — the allocator stays eager, atomic, zero-skipping and wrapping; only the period changes. Everything else in the run is stock behavior. With 2^20 the POC completes in ~12 seconds and is fully deterministic (identical collision positions across runs).

Prerequisites

  • Linux Chromium build (component build; ASan used here: out/asan).

  • Apply chromium-changes.patch (period accelerator + pure observation logging; no cache/keying semantics are altered):

    cd /chromium_src/src/third_party/skia
    git apply /poc-artifacts/chromium-changes.patch
    cd /chromium_src/src && autoninja -C out/asan chrome
    
  • POC files served on two origins: server.py binds 127.0.0.1:8080 (attacker) and 127.0.0.1:8081 (victim); --host-resolver-rules maps attacker.test/victim.test to 127.0.0.1 so the two pages are different sites (separate renderer processes under site isolation).

Reproduction

# 1. serve the pages
cd /poc-artifacts && python3 server.py &

# 2. run Chrome (headful)
ASAN_OPTIONS=detect_odr_violation=0 /chromium_src/src/out/asan/chrome \
  --enable-skia-graphite --skia-graphite-dawn-backend=swiftshader \
  --enable-features=SkiaGraphiteSmallPathAtlas:min_path_size_for_msaa/32,Path2DPaintCache \
  --host-resolver-rules="MAP attacker.test 127.0.0.1, MAP victim.test 127.0.0.1" \
  --no-first-run --no-default-browser-check --ignore-gpu-blocklist \
  --no-proxy-server --force-device-scale-factor=1 \
  --user-data-dir=/tmp/chrome-poc-profile \
  --enable-logging=stderr \
  'http://attacker.test:8080/poc.html'

The page runs fully automatically (~12 s): plant markers → load victim → burn → probe sweep. Progress and the verdict are POSTed to the server (poc-client-log.txt, poc-result.json, leak-*.png written next to server.py).

To measure the stock burn rate instead, run the unpatched out/Default chrome with the same flags against http://attacker.test:8080/rate.html.

Flag realism

  • SkiaGraphite is default-enabled on Apple; SkiaGraphiteSmallPathAtlas (min_path_size_for_msaa=32, mac) and Path2DPaintCache (mac/windows/android/chromeos) are active field-trial rollout candidates in testing/variations/fieldtrial_testing_config.json — this POC enables the same configuration on Linux. The resulting path-renderer strategy is kTessellationAndSmallAtlas (GPU log: strategy=1), identical to the Mac rollout. On default-shipping pre-Gen11-Intel machines the strategy is kRasterAtlas (every path atlased, no flags needed), which is strictly easier to attack.
  • Path2DPaintCache only provides the cleanest probe primitive; document painting (PaintCanvas::drawPath defaults UsePaintCache::kEnabled, e.g. drawing a same-origin SVG image into the canvas) is an always-on alternative.
  • --skia-graphite-dawn-backend=swiftshader, --ignore-gpu-blocklist, --no-proxy-server, --user-data-dir, --force-device-scale-factor=1 are environment-execution flags for this GPU-less sandbox; they do not alter the cache logic under test.

Expected Result

Client log ends with SUCCESS: cross-origin mask leaked into attacker canvas; poc-result.json contains the leaked 28×28 bitmaps; the canvas snapshot shows the victim’s figures rendered inside the attacker’s canvas.

Output (actual, from the recorded run)

1.4s init
1.8s client-side counter pre-advanced by 60000
2.0s references planted
2.0s markers A planted
3.0s victim painted (cross-origin, separate renderer process)
3.1s markers B planted; bracket closed
3.1s attempt 0: burning to pos=1042576 (wrap expected near pos=1048576)
11.7s burn done pos=1042576 (8.7s, 120411 ids/s)
12.0s ANOMALY marker-A-pattern cell=8,12 pos=1048552 cov=0.571
12.0s ANOMALY CROSS-ORIGIN-LEAK cell=9,12 pos=1048555 cov=0.625
12.0s ANOMALY CROSS-ORIGIN-LEAK cell=10,12 pos=1048558 cov=0.589
12.0s ANOMALY CROSS-ORIGIN-LEAK cell=11,12 pos=1048561 cov=0.658
12.0s ANOMALY CROSS-ORIGIN-LEAK cell=12,12 pos=1048564 cov=0.592
12.0s ANOMALY marker-B-pattern cell=13,12 pos=1048567 cov=0.571
12.1s SUCCESS: cross-origin mask leaked into attacker canvas

First leaked cell (attacker getImageData) — pixel-exact match of the victim’s “staircase” secret (victim.html, d="M0,28 L0,21 L7,21 L7,14 L14,14 L14,7 L21,7 L21,0 L28,0 L28,28 Z"):

.....................#######        (leaked bitmap, 28x28, '#' = covered)
.....................#######
..............##############
.......#####################
############################        ... full bitmaps in poc-result.json

GPU-process log (observation patch) for the same instant — fresh attacker probes (verbs=19) receiving recycled IDs and hitting the victim’s cached entries (verbs=11 inserts with the same genIDs):

POCLOG shapecache mgr=0x758247f00018 genID=93 verbs=11 maskSize=30x30 hit=0   <- victim insert
POCLOG shapecache mgr=0x758247f00018 genID=95 verbs=11 maskSize=30x30 hit=0
...after 2^20 ID burn...
POCLOG shapecache mgr=0x758247f00018 genID=93 verbs=19 maskSize=30x30 hit=1   <- attacker probe aliases it
POCLOG shapecache mgr=0x758247f00018 genID=95 verbs=19 maskSize=30x30 hit=1

Attack-engineering notes (what the POC handles)

  • ID strides: a probe miss consumes 2 GPU-side IDs (op deserialize + the mask rasterizer’s transformed copy), a hit consumes 1, a clip burn op consumes 1. The sweep appends one clip “nudge” after every probe, making the probe lattice stride 3: ≥3 consecutively-planted entries (spacing 2) cover all residues mod 3, so a collision is guaranteed regardless of phase; after the first hit the hit+nudge stride (2) locks onto the planted lattice and chains through every bracketed entry (marker A → all 4 victim figures → marker B in one pass).
  • Keeper pinning: markers are redrawn every batch (client-cached ⇒ stable genID ⇒ pure cache hits, zero new IDs) which setLastUseToken-pins their plot against DrawAtlas::compact() aging during the burn. The victim page’s own animation re-pins (and if needed re-inserts, at the same genID) its entries — as any real page with periodic raster activity would.
  • Renderer-side counter de-phasing: the renderer has its own independent copy of the wrapping counter feeding the cc paint-cache path IDs (ServicePaintCache — the recorded sibling finding). The POC pre-advances it client-side (Path2D + isPointInPath, never serialized) so its wrap window lands inside the burn phase where nothing consults the client cache; residual collisions render as blank cells and are classified separately (blank-client-cache-alias) — they never produce cross-origin pixels.

Bandaid Fix (bandaid-fix.patch) — verified

A genID-change-listener alone is insufficient: the counter also recycles IDs of still-live paths (e.g. paths retained by ServicePaintCache), aliasing two live paths without any entry outliving its path. The bandaid therefore stores a 64-bit hash of the actual path data (verbs/points/conic weights) with every generation-ID-keyed entry and verifies it on each hit; on mismatch the stale entry is evicted and the correct mask is rendered:

Verification (build with chromium-changes.patch + bandaid applied; log in bandaid-verification-client-log.txt): the identical attack swept three full wrap periods and observed zero mask substitutions (FAILED: no aliasing observed from the attacker’s perspective), while the GPU log recorded 24 genID collisions detected and blocked (bandaid-verification-blocked-aliases.txt, hit=2 lines) — in both directions (attacker probes colliding with victim entries AND victim redraws colliding with attacker probe entries), each re-rendered correctly. The same missing-validation pattern also exists in the ProxyCache fallback used when the atlas is full (RasterPathAtlas.cpp → findOrCreateCachedProxy, same GeneratePathMaskKey, no listener since the generated bitmap’s pixelRef is unique) — a complete fix should cover that path and the analogous ClipAtlasManager shape cache as well.

Artifact inventory

file role
poc.html attacker page (plant → bracket → burn → probe → readback)
victim.html cross-origin victim with four secret 28×28 SVG figures
server.py serves both origins; collects logs/results/snapshots
rate.html stock-binary burn-rate measurement page
chromium-changes.patch reproduction patch: 2^20 period accelerator + observation logging (apply in third_party/skia)
bandaid-fix.patch content-hash-verification fix (apply in third_party/skia; compiles clean)
poc-client-log.txt / poc-result.json attacker-side timeline / verdict + leaked bitmaps
poc-screenshot.png browser: leaked figures in attacker canvas beside the victim iframe
leak-snapshot.png attacker canvas at the moment of the leak (toDataURL)
gpu-poclog-excerpt.txt GPU-process log: strategy, victim inserts, collision hits
stock-rate-measurement.txt unmodified out/Default burn rate → 30.3 min for 2^30
bandaid-verification-*.txt/json post-fix run: no aliasing; 24 collisions blocked

Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker
Links in the report