CVE-2026-103629
Overview
Files Changed
gpu/command_buffer/common/gles2_cmd_format_test_autogen.hgpu/command_buffer/raster_cmd_buffer_functions.txtgpu/command_buffer/common/raster_cmd_format_autogen.hgpu/command_buffer/service/raster_decoder.cccc/paint/paint_op_reader.ccgpu/command_buffer/service/raster_decoder_autogen.hgpu/command_buffer/build_cmd_buffer_lib.pygpu/command_buffer/client/raster_cmd_helper_autogen.hcc/paint/paint_op_writer.cccc/paint/paint_cache_unittest.ccgpu/command_buffer/client/raster_implementation.cccc/paint/paint_cache.hgpu/command_buffer/common/raster_cmd_format_test_autogen.h
Patch
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)
Regression Test / PoC
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);
Original Bug Report
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:
third_party/skia/src/core/SkPathData.cpp:33-46— eager 30-bit wrapping generation-ID allocator, consumed at path constructionthird_party/skia/src/gpu/graphite/geom/Shape.cpp:238-246— generation ID is the sole geometry discriminator for >10-verb pathsthird_party/skia/src/gpu/graphite/RasterPathUtils.cpp:135-174— remaining mask-key components are attacker-reproduciblethird_party/skia/src/gpu/graphite/PathAtlas.cpp:116-124— cache hit returns the cached atlas slot with no content checkthird_party/skia/src/gpu/graphite/PathAtlas.cpp:193-205— entries removed only by plot eviction; no invalidation tied to path lifetimethird_party/skia/src/gpu/graphite/RasterPathAtlas.cpp:59-124— cache routing; atlas-full fallback aliases identically viaProxyCachewith the same keygpu/command_buffer/service/raster_decoder.cc:628-630— one gpu-main recorder (and thus one atlas/cache) shared by all renderer channelsgpu/command_buffer/service/skia_utils.cc:149-169— configurations that route path draws into the atlas
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 —
kSkiaGraphiteis default-enabled on Apple (gpu/config/gpu_finch_features.cc:378-384), andmsaa_is_slow/msaa_is_slow_2(gpu/config/gpu_driver_bug_list.jsonentries 132/398) forceavoidMSAA()→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:
SkiaGraphiteSmallPathAtlason 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/SkiaGraphiteWinInteltrials extend the Windows/Android population.
Root Cause Analysis
- Eager 30-bit wrapping ID:
third_party/skia/src/core/SkPathData.cpp:33-46,148-149— every non-emptySkPathmaterialization 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); - 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 thankMaxKeyFromDataVerbCnt = 10verbs skip the key-from-data encoding and write onlythis->path().getGenerationID()into the key. - Every other key component is attacker-reproducible:
third_party/skia/src/gpu/graphite/RasterPathUtils.cpp:135-174—GeneratePathMaskKeyadds 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). - 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 cachedAtlasLocatorand page proxy with no geometry comparison. Entries are removed only by plot eviction (PathAtlas.cpp:193-205); noSkIDChangeListenerexists 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 transientSkPathper 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 } - One cache for all origins:
gpu/command_buffer/service/raster_decoder.cc:628-630— every renderer channel rasters throughSharedContextState::gpu_main_graphite_recorder(); the per-recorderAtlasProviderowns oneRasterPathAtlas(AtlasProvider.cpp:37-45), andfRequireOrderedRecordings = true(skia_utils.cc:157-164) keeps atlas pages andfShapeCachealive 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. - Web-content attack primitives: ID burn — implicit-path
ctx.clip()serializes askInlinedDoNotCache(canvas_2d_recorder_context.cc:1839-1841,paint_op_writer.cc:292-315); GPU-side deserialization builds one fresh volatileSkPathData(one ID) that never touches the cached managers (paint_op_reader.cc:317-349,RasterPathAtlas.cpp:59). Probes/keepers — retainedPath2Ddraws (under the activePath2DPaintCachetrial) or document-painted SVG drawn into the canvas (paint_canvas.h:170defaultsUsePaintCache::kEnabled) produce non-volatile paths that enterfShapeCacheand 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. - 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 andgetImageData()reads it. The atlas-full fallback (RasterPathAtlas.cpp:100-124) keysProxyCacheentries with the sameGeneratePathMaskKeyand registers no listener, so the identical aliasing exists there under a byte-budget lifetime.
Reproduction
- On an affected config (pre-Gen11-Intel default, or the Mac
SkiaGraphiteSmallPathAtlastrial config), the attacker page draws ~8 retained >10-verbPath2Dmarkers (~30x30 px, distinctive sparse geometry) into an accelerated canvas, then redraws them every frame — stable genIDs give guaranteed cache hits whosesetLastUseTokenpins their plots againstDrawAtlas::compact()aging and pressure eviction. 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.- 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. - Sweep fresh probe
Path2Ds (solid blobs, marker-matching maskSize/transform), reading back each viagetImageData(). 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). - 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.
- 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:
- plants marker entries in the shared GPU-process cache (retained
Path2Dobjects → stable generation IDs), - loads a cross-origin victim page (different site ⇒ different renderer process) whose secret SVG figures enter the same cache,
- burns generation IDs with implicit-path
canvas.clip()operations (each op = one fresh GPU-sideSkPath= one ID; volatile, zero cache residue) until the 30-bit counter wraps, - 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.pybinds 127.0.0.1:8080 (attacker) and 127.0.0.1:8081 (victim);--host-resolver-rulesmapsattacker.test/victim.testto 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
SkiaGraphiteis default-enabled on Apple;SkiaGraphiteSmallPathAtlas(min_path_size_for_msaa=32, mac) andPath2DPaintCache(mac/windows/android/chromeos) are active field-trial rollout candidates intesting/variations/fieldtrial_testing_config.json— this POC enables the same configuration on Linux. The resulting path-renderer strategy iskTessellationAndSmallAtlas(GPU log:strategy=1), identical to the Mac rollout. On default-shipping pre-Gen11-Intel machines the strategy iskRasterAtlas(every path atlased, no flags needed), which is strictly easier to attack.Path2DPaintCacheonly provides the cleanest probe primitive; document painting (PaintCanvas::drawPathdefaultsUsePaintCache::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=1are 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 againstDrawAtlas::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:
third_party/skia/src/gpu/graphite/PathAtlas.h— cache value becomes{AtlasLocator, uint64_t contentHash}.third_party/skia/src/gpu/graphite/PathAtlas.cpp— hash computed for paths with >10 verbs, checked on hit,evictCacheEntry()on mismatch.
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.
- http://attacker.test:8080/poc.html
- http://attacker.test:8080/rate.html
- https://source.chromium.org/chromium/chromium/src/+/main:gpu/command_buffer/service/raster_decoder.cc
- https://source.chromium.org/chromium/chromium/src/+/main:gpu/command_buffer/service/raster_decoder.cc;l=628
- https://source.chromium.org/chromium/chromium/src/+/main:gpu/command_buffer/service/skia_utils.cc;l=149
- https://source.chromium.org/chromium/chromium/src/+/main:gpu/config/gpu_finch_features.cc;l=378
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/core/SkPathData.cpp
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/core/SkPathData.cpp;l=33
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/core/SkPathData.cpp;l=38
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/PathAtlas.cpp
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/PathAtlas.cpp;l=116
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/PathAtlas.cpp;l=118
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/PathAtlas.cpp;l=193
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/PathAtlas.h
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/RasterPathAtlas.cpp;l=59
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/RasterPathUtils.cpp;l=135
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/geom/Shape.cpp;l=238
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/geom/Shape.cpp;l=24
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/src/gpu/graphite/render/CoverageMaskRenderStep.cpp;l=172