CVE-2026-110915
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcc/paint/paint_op_reader.cc |
modified | |
TESTcc/paint/paint_op_buffer_unittest.cc |
modified | |
forcc/paint/paint_op_buffer_unittest.cc |
modified |
Files Changed
gpu/command_buffer/service/service_transfer_cache_unittest.cccc/paint/paint_op_buffer_unittest.ccgpu/command_buffer/service/service_transfer_cache.hgpu/command_buffer/service/raster_decoder.ccgpu/command_buffer/service/service_transfer_cache.cccc/paint/paint_shader.hcc/paint/transfer_cache_deserialize_helper.hcc/test/transfer_cache_test_helper.hcc/paint/paint_op_reader.cccc/paint/paint_shader.cccc/paint/paint_op_writer.cccc/paint/paint_op_writer.hcc/paint/paint_op_helper_unittest.cccc/test/transfer_cache_test_helper.cc
Patch
From f06e60e56d7f62632ed78bdfd414d89097669734 Mon Sep 17 00:00:00 2001 From: Jonathan Ross <[email protected]> Date: Mon, 21 Sep 2026 16:27:58 -0700 Subject: [PATCH] Widen cc::PaintShader record shader IDs to 64 bits Record shader IDs were previously minted from a 32-bit counter, which could roll over in long-lived renderer processes. When the counter wrapped around, ID collisions could cause the service-side transfer cache to reuse an incorrect cached picture or generate kInvalidRecordShaderId, resulting in raster failures. Widen RecordShaderId and the sequence counter to uint64_t to prevent counter rollover. uint64_t serialization in PaintOpWriter and PaintOpReader is split into two uint32_t fields to maintain 4-byte alignment guarantees, and the transfer cache lookup helper is updated to accept 64-bit entry IDs. TAG=agy Bug: 556272421 Change-Id: Id363ac7bebd8f7d5c30f1dd16cb2e60c98744e29 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8408827 Commit-Queue: Jonathan Ross <[email protected]> Reviewed-by: Zhenyao Mo <[email protected]> Cr-Commit-Position: refs/heads/main@{#1702187} --- diff --git a/cc/paint/paint_op_reader.cc b/cc/paint/paint_op_reader.cc index f4eac2c5..006362e 100644 --- a/cc/paint/paint_op_reader.cc +++ b/cc/paint/paint_op_reader.cc @@ -246,7 +246,14 @@ } void PaintOpReader::Read(uint64_t* data) { - ReadSimple(data); + // uint64_t is serialized as two uint32_ts to meet the 4-byte minimum + // alignment requirement of the serialization format (see + // PaintOpWriter::Write(uint64_t)). + uint32_t lo = 0u; + uint32_t hi = 0u; + ReadSimple(&lo); + ReadSimple(&hi); + *data = static_cast<uint64_t>(hi) << 32 | lo; } void PaintOpReader::Read(int32_t* data) { @@ -731,7 +738,7 @@ Read(&ref.image_); bool has_record = false; Read(&has_record); - uint32_t shader_id = PaintShader::kInvalidRecordShaderId; + PaintShader::RecordShaderId shader_id = PaintShader::kInvalidRecordShaderId; size_t shader_size = 0; if (has_record) { if (shader_type != PaintShader::Type::kPaintRecord) { diff --git a/cc/paint/paint_op_writer.cc b/cc/paint/paint_op_writer.cc index 4e9f982..706d314 100644 --- a/cc/paint/paint_op_writer.cc +++ b/cc/paint/paint_op_writer.cc @@ -289,6 +289,14 @@ static_cast<uint32_t>(static_cast<uint64_t>(size) >> 32); } +void PaintOpWriter::Write(uint64_t data) { + // uint64_t is serialized as two uint32_ts to meet the 4-byte minimum + // alignment requirement of PaintOpWriter (https://crbug.com/1429994 and + // https://crbug.com/1440013). + Write(static_cast<uint32_t>(data)); + Write(static_cast<uint32_t>(data >> 32)); +} + void PaintOpWriter::Write(const SkPath& path, UsePaintCache use_paint_cache) { auto id = path.getGenerationID(); Write(id); diff --git a/cc/paint/paint_op_writer.h b/cc/paint/paint_op_writer.h index 57a0a6af..8c3ff25 100644 --- a/cc/paint/paint_op_writer.h +++ b/cc/paint/paint_op_writer.h @@ -248,6 +248,7 @@ void Write(uint8_t data) { WriteSimple(data); } void Write(uint16_t data) { WriteSimple(data); } void Write(uint32_t data) { WriteSimple(data); } + void Write(uint64_t data); void Write(int32_t data) { WriteSimple(data); } void Write(const SkPoint& point) { WriteSimple(point); } void Write(const SkRect& rect) { WriteSimple(rect); } diff --git a/cc/paint/paint_shader.cc b/cc/paint/paint_shader.cc index f18cc6fc..ad7814f6 100644 --- a/cc/paint/paint_shader.cc +++ b/cc/paint/paint_shader.cc @@ -28,7 +28,7 @@ namespace cc { namespace { -base::AtomicSequenceNumber g_next_shader_id; +base::AtomicSequenceNumberT<uint64_t> g_next_shader_id; sk_sp<SkPicture> ToSkPicture(const PaintRecord& record, const SkRect& bounds, diff --git a/cc/paint/paint_shader.h b/cc/paint/paint_shader.h index 29dc5c2..58fd2c7 100644 --- a/cc/paint/paint_shader.h +++ b/cc/paint/paint_shader.h @@ -45,7 +45,7 @@ kShaderCount }; - using RecordShaderId = uint32_t; + using RecordShaderId = uint64_t; static const RecordShaderId kInvalidRecordShaderId; // Scaling behavior dictates how a PaintRecord shader will behave. Use diff --git a/cc/paint/transfer_cache_deserialize_helper.h b/cc/paint/transfer_cache_deserialize_helper.h index 9acc015..40ea0ca 100644 --- a/cc/paint/transfer_cache_deserialize_helper.h +++ b/cc/paint/transfer_cache_deserialize_helper.h @@ -26,7 +26,7 @@ // Type safe access to an entry in the transfer cache. Returns null if the // entry is missing or of the wrong type. template <typename T> - T* GetEntryAs(uint32_t id) { + T* GetEntryAs(uint64_t id) { // There is a bit of a weirdness if we use T::kType directly in the DCHECK // below. Specifically, the linker can't seem to find that symbol ¯\_(ツ)_/¯ // so instead save off the type into a local variable and use that. @@ -46,7 +46,7 @@ // Creates an entry directly. If an entry exists, it will be clobbered. virtual void CreateLocalEntry( - uint32_t id, + uint64_t id, std::unique_ptr<ServiceTransferCacheEntry> entry) = 0; size_t GetTotalEntrySizes() const { return total_size_; } @@ -54,7 +54,7 @@ private: virtual ServiceTransferCacheEntry* GetEntryInternal( TransferCacheEntryType entry_type, - uint32_t entry_id) = 0; + uint64_t entry_id) = 0; size_t total_size_ = 0; }; diff --git a/gpu/command_buffer/service/raster_decoder.cc b/gpu/command_buffer/service/raster_decoder.cc index 5d1bfde..b9a44b8 100644 --- a/gpu/command_buffer/service/raster_decoder.cc +++ b/gpu/command_buffer/service/raster_decoder.cc @@ -2761,7 +2761,7 @@ ~TransferCacheDeserializeHelperImpl() override = default; void CreateLocalEntry( - uint32_t id, + uint64_t id, std::unique_ptr<cc::ServiceTransferCacheEntry> entry) override { auto type = entry->Type(); transfer_cache_->CreateLocalEntry( @@ -2772,7 +2772,7 @@ private: cc::ServiceTransferCacheEntry* GetEntryInternal( cc::TransferCacheEntryType entry_type, - uint32_t entry_id) override { + uint64_t entry_id) override { return transfer_cache_->GetEntry(ServiceTransferCache::EntryKey( raster_decoder_id_, entry_type, entry_id)); } diff --git a/gpu/command_buffer/service/service_transfer_cache.cc b/gpu/command_buffer/service/service_transfer_cache.cc index 663ee62..00e4494 100644 --- a/gpu/command_buffer/service/service_transfer_cache.cc +++ b/gpu/command_buffer/service/service_transfer_cache.cc @@ -493,7 +493,7 @@ ServiceTransferCache::EntryKey::EntryKey(int decoder_id, cc::TransferCacheEntryType entry_type, - uint32_t entry_id) + uint64_t entry_id) : decoder_id(decoder_id), entry_type(entry_type), entry_id(entry_id) {} } // namespace gpu diff --git a/gpu/command_buffer/service/service_transfer_cache.h b/gpu/command_buffer/service/service_transfer_cache.h index c74a3fa2..0f5fefc 100644 --- a/gpu/command_buffer/service/service_transfer_cache.h +++ b/gpu/command_buffer/service/service_transfer_cache.h @@ -48,10 +48,10 @@ struct GPU_GLES2_EXPORT EntryKey { EntryKey(int decoder_id, cc::TransferCacheEntryType entry_type, - uint32_t entry_id); + uint64_t entry_id); int decoder_id; cc::TransferCacheEntryType entry_type; - uint32_t entry_id; + uint64_t entry_id; }; ServiceTransferCache(const GpuPreferences& preferences, diff --git a/cc/paint/paint_op_buffer_unittest.cc b/cc/paint/paint_op_buffer_unittest.cc index 8e9773c..bc22ed0 100644 --- a/cc/paint/paint_op_buffer_unittest.cc +++ b/cc/paint/paint_op_buffer_unittest.cc @@ -140,6 +140,15 @@ static void ResetShaderType(PaintShader* shader, PaintShader::Type type) { shader->shader_type_ = type; } + + static void SetRecordShaderId(PaintShader* shader, + PaintShader::RecordShaderId id) { + shader->id_ = id; + } + + static sk_sp<SkPicture> GetCachedPicture(const PaintShader* shader) { + return shader->sk_cached_picture_; + } }; TEST(PaintOpBufferTest, Empty) { @@ -4125,6 +4134,135 @@ EXPECT_GT(shader_size, estimated_image_size); } +TEST(PaintOpBufferTest, RecordShaderIdSerialization) { + PaintOpBuffer shader_buffer; + shader_buffer.push<DrawRectOp>(SkRect::MakeWH(10.f, 10.f), PaintFlags()); + auto shader = PaintShader::MakePaintRecord( + shader_buffer.ReleaseAsRecord(), SkRect::MakeWH(10.f, 10.f), + SkTileMode::kClamp, SkTileMode::kClamp, nullptr); + // Record shader ids are minted by a 64-bit counter, so ids wider than 32 + // bits must round-trip through serialization at full width. + const uint64_t shader_id = + uint64_t{shader->paint_record_shader_id()} + (uint64_t{1} << 32); + PaintOpSerializationTestUtils::SetRecordShaderId(shader.get(), shader_id); + + PaintOpBuffer buffer; + PaintFlags flags; + flags.setShader(shader); + buffer.push<DrawRectOp>(SkRect::MakeWH(10.f, 10.f), flags); + + auto deserialized = SerializeAndDeserialize(buffer); + ASSERT_TRUE(deserialized); + bool found_draw_rect = false; + for (const PaintOp& base_op : *deserialized) { + if (base_op.GetType() != PaintOpType::kDrawRect) { + continue; + } + found_draw_rect = true; + const auto& op = static_cast<const DrawRectOp&>(base_op); + ASSERT_TRUE(op.flags.getShader()); + EXPECT_EQ(shader_id, op.flags.getShader()->paint_record_shader_id()); + } + EXPECT_TRUE(found_draw_rect); +} + +TEST(PaintOpBufferTest, RecordShadersWithDistinctIdsCachedSeparately) { + auto make_solid_color_record_shader = [](SkColor4f color) { + PaintOpBuffer shader_buffer; + PaintFlags record_flags; + record_flags.setColor(color); + shader_buffer.push<DrawRectOp>(SkRect::MakeWH(10.f, 10.f), record_flags); + return PaintShader::MakePaintRecord( + shader_buffer.ReleaseAsRecord(), SkRect::MakeWH(10.f, 10.f), + SkTileMode::kClamp, SkTileMode::kClamp, nullptr); + }; + + auto shader1 = make_solid_color_record_shader(SkColors::kRed); + auto shader2 = make_solid_color_record_shader(SkColors::kBlue); + const uint64_t shader1_id = shader1->paint_record_shader_id(); + // Give the second shader an id that matches the first shader's id in the + // low 32 bits. The shaders have different contents, so they must not share + // cached pictures. + const uint64_t shader2_id = shader1_id + (uint64_t{1} << 32); + PaintOpSerializationTestUtils::SetRecordShaderId(shader2.get(), shader2_id); + + TestOptionsProvider options_provider; + auto* transfer_cache = options_provider.transfer_cache_helper(); + + auto serialize_draw_rect_with_shader = [&](sk_sp<PaintShader> shader) { + PaintOpBuffer buffer; + PaintFlags flags; + flags.setShader(std::move(shader)); + buffer.push<DrawRectOp>(SkRect::MakeWH(10.f, 10.f), flags); + auto memory = AllocateSerializedBuffer(); + SimpleBufferSerializer serializer(memory.as_span(), + options_provider.serialize_options()); + serializer.Serialize(buffer); + CHECK(serializer.valid()); + CHECK_GT(serializer.written(), 0u); + return std::make_pair(std::move(memory), serializer.written()); + }; + + auto [memory1, written1] = serialize_draw_rect_with_shader(shader1); + auto [memory2, written2] = serialize_draw_rect_with_shader(shader2); + + auto deserialized1 = + PaintOpBuffer::MakeFromMemory(memory1.as_span().first(written1), + options_provider.deserialize_options()); + auto deserialized2 = + PaintOpBuffer::MakeFromMemory(memory2.as_span().first(written2), + options_provider.deserialize_options()); + ASSERT_TRUE(deserialized1); + ASSERT_TRUE(deserialized2); + + auto find_draw_rect_shader = + [](const PaintOpBuffer& buffer) -> const PaintShader* { + for (const PaintOp& base_op : buffer) { + if (base_op.GetType() != PaintOpType::kDrawRect) { + continue; + } + return static_cast<const DrawRectOp&>(base_op).flags.getShader(); + } + return nullptr; + }; + + const PaintShader* deserialized_shader1 = + find_draw_rect_shader(*deserialized1); + const PaintShader* deserialized_shader2 = + find_draw_rect_shader(*deserialized2); + ASSERT_TRUE(deserialized_shader1); + ASSERT_TRUE(deserialized_shader2); + + // Each shader gets its own transfer cache entry. + auto* entry1 = + transfer_cache->GetEntryAs<ServiceShaderTransferCacheEntry>(shader1_id); + auto* entry2 = + transfer_cache->GetEntryAs<ServiceShaderTransferCacheEntry>(shader2_id); + ASSERT_TRUE(entry1); + ASSERT_TRUE(entry2); + EXPECT_NE(entry1, entry2); + + sk_sp<SkPicture> picture1 = + PaintOpSerializationTestUtils::GetCachedPicture(deserialized_shader1); + sk_sp<SkPicture> picture2 = + PaintOpSerializationTestUtils::GetCachedPicture(deserialized_shader2); + ASSERT_TRUE(picture1); + ASSERT_TRUE(picture2); + EXPECT_NE(picture1.get(), picture2.get()); + + // Each deserialized shader draws its own contents. + auto center_pixel_color = [](const sk_sp<SkPicture>& picture) { + SkBitmap bitmap; + bitmap.allocN32Pixels(10, 10); + bitmap.eraseColor(SK_ColorTRANSPARENT); + SkCanvas canvas(bitmap); + canvas.drawPicture(picture); + return bitmap.getColor(5, 5); + }; + EXPECT_EQ(SK_ColorRED, center_pixel_color(picture1)); + EXPECT_EQ(SK_ColorBLUE, center_pixel_color(picture2)); +} + TEST(PaintOpBufferTest, RecordFilterSerializeScaledImages) { PaintOpBuffer filter_buffer; filter_buffer.push<DrawImageOp>( diff --git a/cc/paint/paint_op_helper_unittest.cc b/cc/paint/paint_op_helper_unittest.cc index c1748b5..225964db 100644 --- a/cc/paint/paint_op_helper_unittest.cc +++ b/cc/paint/paint_op_helper_unittest.cc @@ -622,7 +622,8 @@ "local_matrix=(nil), center=[0.000,0.000], tile=[0.000,0.000 " "0.000x0.000], start_point=[0.000,0.000], end_point=[0.000,0.000], " "start_degrees=0, end_degrees=0, image=<paint image>, record=(nil), " - "id=4294967295, tile_scale=(nil), colors=(nil), positions=(nil)], " + "id=18446744073709551615, tile_scale=(nil), colors=(nil), " + "positions=(nil)], " "alpha=1.000, filter_quality=kMedium_SkFilterQuality, dither=kYes, " "crop_rect=[0.000,0.000 100.000x100.000])"); } diff --git a/cc/test/transfer_cache_test_helper.cc b/cc/test/transfer_cache_test_helper.cc index 26e253f..c1d0ba9 100644 --- a/cc/test/transfer_cache_test_helper.cc +++ b/cc/test/transfer_cache_test_helper.cc @@ -58,7 +58,7 @@ } void TransferCacheTestHelper::CreateLocalEntry( - uint32_t id, + uint64_t id, std::unique_ptr<ServiceTransferCacheEntry> entry) { auto key = std::make_pair(entry->Type(), id); @@ -77,7 +77,7 @@ EnforceLimits(); } -void TransferCacheTestHelper::DeleteEntryDirect(const EntryKey& key) { +void TransferCacheTestHelper::DeleteEntryDirect(const ServiceEntryKey& key) { locked_entries_.erase(key); local_entries_.erase(key); entries_.erase(key); @@ -90,7 +90,7 @@ ServiceTransferCacheEntry* TransferCacheTestHelper::GetEntryInternal( TransferCacheEntryType type, - uint32_t id) { + uint64_t id) { auto key = std::make_pair(type, id); if (locked_entries_.count(key) + local_entries_.count(key) == 0) { return nullptr; diff --git a/cc/test/transfer_cache_test_helper.h b/cc/test/transfer_cache_test_helper.h index 2e655a2..5205fca 100644 --- a/cc/test/transfer_cache_test_helper.h +++ b/cc/test/transfer_cache_test_helper.h ... (file diff truncated)
Regression Test / PoC
diff --git a/cc/paint/paint_op_buffer_unittest.cc b/cc/paint/paint_op_buffer_unittest.cc
index 8e9773c..bc22ed0 100644
--- a/cc/paint/paint_op_buffer_unittest.cc
+++ b/cc/paint/paint_op_buffer_unittest.cc
@@ -140,6 +140,15 @@
static void ResetShaderType(PaintShader* shader, PaintShader::Type type) {
shader->shader_type_ = type;
}
+
+ static void SetRecordShaderId(PaintShader* shader,
+ PaintShader::RecordShaderId id) {
+ shader->id_ = id;
+ }
+
+ static sk_sp<SkPicture> GetCachedPicture(const PaintShader* shader) {
+ return shader->sk_cached_picture_;
+ }
};
TEST(PaintOpBufferTest, Empty) {
@@ -4125,6 +4134,135 @@
EXPECT_GT(shader_size, estimated_image_size);
}
+TEST(PaintOpBufferTest, RecordShaderIdSerialization) {
+ PaintOpBuffer shader_buffer;
+ shader_buffer.push<DrawRectOp>(SkRect::MakeWH(10.f, 10.f), PaintFlags());
+ auto shader = PaintShader::MakePaintRecord(
+ shader_buffer.ReleaseAsRecord(), SkRect::MakeWH(10.f, 10.f),
+ SkTileMode::kClamp, SkTileMode::kClamp, nullptr);
+ // Record shader ids are minted by a 64-bit counter, so ids wider than 32
+ // bits must round-trip through serialization at full width.
+ const uint64_t shader_id =
+ uint64_t{shader->paint_record_shader_id()} + (uint64_t{1} << 32);
+ PaintOpSerializationTestUtils::SetRecordShaderId(shader.get(), shader_id);
+
+ PaintOpBuffer buffer;
+ PaintFlags flags;
+ flags.setShader(shader);
+ buffer.push<DrawRectOp>(SkRect::MakeWH(10.f, 10.f), flags);
+
+ auto deserialized = SerializeAndDeserialize(buffer);
+ ASSERT_TRUE(deserialized);
+ bool found_draw_rect = false;
+ for (const PaintOp& base_op : *deserialized) {
+ if (base_op.GetType() != PaintOpType::kDrawRect) {
+ continue;
+ }
+ found_draw_rect = true;
+ const auto& op = static_cast<const DrawRectOp&>(base_op);
+ ASSERT_TRUE(op.flags.getShader());
+ EXPECT_EQ(shader_id, op.flags.getShader()->paint_record_shader_id());
+ }
+ EXPECT_TRUE(found_draw_rect);
+}
+
+TEST(PaintOpBufferTest, RecordShadersWithDistinctIdsCachedSeparately) {
+ auto make_solid_color_record_shader = [](SkColor4f color) {
+ PaintOpBuffer shader_buffer;
+ PaintFlags record_flags;
+ record_flags.setColor(color);
+ shader_buffer.push<DrawRectOp>(SkRect::MakeWH(10.f, 10.f), record_flags);
+ return PaintShader::MakePaintRecord(
+ shader_buffer.ReleaseAsRecord(), SkRect::MakeWH(10.f, 10.f),
+ SkTileMode::kClamp, SkTileMode::kClamp, nullptr);
+ };
+
+ auto shader1 = make_solid_color_record_shader(SkColors::kRed);
+ auto shader2 = make_solid_color_record_shader(SkColors::kBlue);
+ const uint64_t shader1_id = shader1->paint_record_shader_id();
+ // Give the second shader an id that matches the first shader's id in the
+ // low 32 bits. The shaders have different contents, so they must not share
+ // cached pictures.
+ const uint64_t shader2_id = shader1_id + (uint64_t{1} << 32);
+ PaintOpSerializationTestUtils::SetRecordShaderId(shader2.get(), shader2_id);
+
+ TestOptionsProvider options_provider;
+ auto* transfer_cache = options_provider.transfer_cache_helper();
+
+ auto serialize_draw_rect_with_shader = [&](sk_sp<PaintShader> shader) {
+ PaintOpBuffer buffer;
+ PaintFlags flags;
+ flags.setShader(std::move(shader));
+ buffer.push<DrawRectOp>(SkRect::MakeWH(10.f, 10.f), flags);
+ auto memory = AllocateSerializedBuffer();
+ SimpleBufferSerializer serializer(memory.as_span(),
+ options_provider.serialize_options());
+ serializer.Serialize(buffer);
+ CHECK(serializer.valid());
+ CHECK_GT(serializer.written(), 0u);
+ return std::make_pair(std::move(memory), serializer.written());
+ };
+
+ auto [memory1, written1] = serialize_draw_rect_with_shader(shader1);
+ auto [memory2, written2] = serialize_draw_rect_with_shader(shader2);
+
+ auto deserialized1 =
+ PaintOpBuffer::MakeFromMemory(memory1.as_span().first(written1),
+ options_provider.deserialize_options());
+ auto deserialized2 =
+ PaintOpBuffer::MakeFromMemory(memory2.as_span().first(written2),
+ options_provider.deserialize_options());
+ ASSERT_TRUE(deserialized1);
+ ASSERT_TRUE(deserialized2);
+
+ auto find_draw_rect_shader =
+ [](const PaintOpBuffer& buffer) -> const PaintShader* {
+ for (const PaintOp& base_op : buffer) {
+ if (base_op.GetType() != PaintOpType::kDrawRect) {
+ continue;
+ }
+ return static_cast<const DrawRectOp&>(base_op).flags.getShader();
+ }
+ return nullptr;
+ };
+
+ const PaintShader* deserialized_shader1 =
+ find_draw_rect_shader(*deserialized1);
+ const PaintShader* deserialized_shader2 =
+ find_draw_rect_shader(*deserialized2);
+ ASSERT_TRUE(deserialized_shader1);
+ ASSERT_TRUE(deserialized_shader2);
+
+ // Each shader gets its own transfer cache entry.
+ auto* entry1 =
+ transfer_cache->GetEntryAs<ServiceShaderTransferCacheEntry>(shader1_id);
+ auto* entry2 =
+ transfer_cache->GetEntryAs<ServiceShaderTransferCacheEntry>(shader2_id);
+ ASSERT_TRUE(entry1);
+ ASSERT_TRUE(entry2);
+ EXPECT_NE(entry1, entry2);
+
+ sk_sp<SkPicture> picture1 =
+ PaintOpSerializationTestUtils::GetCachedPicture(deserialized_shader1);
+ sk_sp<SkPicture> picture2 =
+ PaintOpSerializationTestUtils::GetCachedPicture(deserialized_shader2);
+ ASSERT_TRUE(picture1);
+ ASSERT_TRUE(picture2);
+ EXPECT_NE(picture1.get(), picture2.get());
+
+ // Each deserialized shader draws its own contents.
+ auto center_pixel_color = [](const sk_sp<SkPicture>& picture) {
+ SkBitmap bitmap;
+ bitmap.allocN32Pixels(10, 10);
+ bitmap.eraseColor(SK_ColorTRANSPARENT);
+ SkCanvas canvas(bitmap);
+ canvas.drawPicture(picture);
+ return bitmap.getColor(5, 5);
+ };
+ EXPECT_EQ(SK_ColorRED, center_pixel_color(picture1));
+ EXPECT_EQ(SK_ColorBLUE, center_pixel_color(picture2));
+}
+
TEST(PaintOpBufferTest, RecordFilterSerializeScaledImages) {
PaintOpBuffer filter_buffer;
filter_buffer.push<DrawImageOp>(
diff --git a/cc/paint/paint_op_helper_unittest.cc b/cc/paint/paint_op_helper_unittest.cc
index c1748b5..225964db 100644
--- a/cc/paint/paint_op_helper_unittest.cc
+++ b/cc/paint/paint_op_helper_unittest.cc
@@ -622,7 +622,8 @@
"local_matrix=(nil), center=[0.000,0.000], tile=[0.000,0.000 "
"0.000x0.000], start_point=[0.000,0.000], end_point=[0.000,0.000], "
"start_degrees=0, end_degrees=0, image=<paint image>, record=(nil), "
- "id=4294967295, tile_scale=(nil), colors=(nil), positions=(nil)], "
+ "id=18446744073709551615, tile_scale=(nil), colors=(nil), "
+ "positions=(nil)], "
"alpha=1.000, filter_quality=kMedium_SkFilterQuality, dither=kYes, "
"crop_rect=[0.000,0.000 100.000x100.000])");
}
diff --git a/cc/test/transfer_cache_test_helper.cc b/cc/test/transfer_cache_test_helper.cc
index 26e253f..c1d0ba9 100644
--- a/cc/test/transfer_cache_test_helper.cc
+++ b/cc/test/transfer_cache_test_helper.cc
@@ -58,7 +58,7 @@
}
void TransferCacheTestHelper::CreateLocalEntry(
- uint32_t id,
+ uint64_t id,
std::unique_ptr<ServiceTransferCacheEntry> entry) {
auto key = std::make_pair(entry->Type(), id);
@@ -77,7 +77,7 @@
EnforceLimits();
}
-void TransferCacheTestHelper::DeleteEntryDirect(const EntryKey& key) {
+void TransferCacheTestHelper::DeleteEntryDirect(const ServiceEntryKey& key) {
locked_entries_.erase(key);
local_entries_.erase(key);
entries_.erase(key);
@@ -90,7 +90,7 @@
ServiceTransferCacheEntry* TransferCacheTestHelper::GetEntryInternal(
TransferCacheEntryType type,
- uint32_t id) {
+ uint64_t id) {
auto key = std::make_pair(type, id);
if (locked_entries_.count(key) + local_entries_.count(key) == 0) {
return nullptr;
diff --git a/cc/test/transfer_cache_test_helper.h b/cc/test/transfer_cache_test_helper.h
index 2e655a2..5205fca 100644
--- a/cc/test/transfer_cache_test_helper.h
+++ b/cc/test/transfer_cache_test_helper.h
@@ -9,6 +9,7 @@
#include <map>
#include <memory>
#include <set>
+#include <utility>
#include <vector>
#include "base/containers/span.h"
@@ -22,6 +23,11 @@
class TransferCacheTestHelper : public TransferCacheDeserializeHelper,
public TransferCacheSerializeHelper {
public:
+ // Entries created locally by the service side during deserialization (e.g.
+ // record shader entries) are keyed by 64-bit ids, while client entries use
+ // 32-bit ids.
+ using ServiceEntryKey = std::pair<TransferCacheEntryType, uint64_t>;
+
TransferCacheTestHelper();
~TransferCacheTestHelper() override;
@@ -32,16 +38,16 @@
void CreateEntryDirect(const EntryKey& key, base::span<uint8_t> data);
void UnlockEntriesDirect(const std::vector<EntryKey>& keys);
- void DeleteEntryDirect(const EntryKey& key);
+ void DeleteEntryDirect(const ServiceEntryKey& key);
// Deserialization helpers.
ServiceTransferCacheEntry* GetEntryInternal(TransferCacheEntryType type,
- uint32_t id) override;
+ uint64_t id) override;
- const EntryKey& GetLastAddedEntry() const { return last_added_entry_; }
+ const ServiceEntryKey& GetLastAddedEntry() const { return last_added_entry_; }
void CreateLocalEntry(
- uint32_t id,
+ uint64_t id,
std::unique_ptr<ServiceTransferCacheEntry> entry) override;
size_t num_of_entries() const { return entries_.size(); }
@@ -61,10 +67,11 @@
// entries_ may reference owned_context_ so must be destroyed before the
// context to avoid dangling ptrs.
- std::map<EntryKey, std::unique_ptr<ServiceTransferCacheEntry>> entries_;
- std::set<EntryKey> local_entries_;
- std::set<EntryKey> locked_entries_;
- EntryKey last_added_entry_ = {TransferCacheEntryType::kRawMemory, ~0};
+ std::map<ServiceEntryKey, std::unique_ptr<ServiceTransferCacheEntry>>
+ entries_;
+ std::set<ServiceEntryKey> local_entries_;
+ std::set<ServiceEntryKey> locked_entries_;
+ ServiceEntryKey last_added_entry_ = {TransferCacheEntryType::kRawMemory, ~0};
size_t cached_items_limit_ = std::numeric_limits<size_t>::max();
};
diff --git a/gpu/command_buffer/service/service_transfer_cache_unittest.cc b/gpu/command_buffer/service/service_transfer_cache_unittest.cc
index 1e39295..0afcebe 100644
--- a/gpu/command_buffer/service/service_transfer_cache_unittest.cc
+++ b/gpu/command_buffer/service/service_transfer_cache_unittest.cc
@@ -135,6 +135,34 @@
decoder2, kEntryType, entry_id)));
}
+TEST(ServiceTransferCache, EntryIdsBeyond32Bits) {
+ ServiceTransferCache cache{GpuPreferences(), base::RepeatingClosure()};
+ const size_t entry_size = 1024u;
+
+ // Two entry ids that only match in the low 32 bits address distinct
+ // entries.
+ const uint64_t low_entry_id = 7u;
+ const uint64_t high_entry_id = low_entry_id + (uint64_t{1} << 32);
+
+ auto low_entry = CreateEntry(entry_size);
+ auto* low_entry_ptr = low_entry.get();
+ cache.CreateLocalEntry(
+ ServiceTransferCache::EntryKey(kDecoderId, kEntryType, low_entry_id),
+ std::move(low_entry));
+
+ auto high_entry = CreateEntry(entry_size);
+ auto* high_entry_ptr = high_entry.get();
+ cache.CreateLocalEntry(
+ ServiceTransferCache::EntryKey(kDecoderId, kEntryType, high_entry_id),
+ std::move(high_entry));
+
+ EXPECT_EQ(cache.entries_count_for_testing(), 2u);
+ EXPECT_EQ(low_entry_ptr, cache.GetEntry(ServiceTransferCache::EntryKey(
+ kDecoderId, kEntryType, low_entry_id)));
+ EXPECT_EQ(high_entry_ptr, cache.GetEntry(ServiceTransferCache::EntryKey(
+ kDecoderId, kEntryType, high_entry_id)));
+}
+
TEST(ServiceTransferCache, DeleteEntriesForDecoder) {
ServiceTransferCache cache{GpuPreferences(), base::RepeatingClosure()};
... (truncated)
Original Bug Report
Canvas Taint Bypass via RecordShaderId Wraparound in GPU ServiceTransferCache
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: A potential cross-origin information disclosure vulnerability exists in Chromium’s GPU transfer cache handling of PaintRecord shaders. When 32-bit RecordShaderId values wrap around, the GPU process can alias ServiceShaderTransferCacheEntry records across different drawing operations with matching tile rectangles. If an attacker triggers an ID collision between a cross-origin tainted draw and a subsequent clean canvas draw, rendered cross-origin SVG pixels can be substituted and read back via an origin-clean canvas.
Affected files:
cc/paint/paint_op_reader.cccc/paint/paint_shader.cccc/paint/paint_shader.h
Estimated timestamp from git blame: 2018-05-08
Summary
A potential logic flaw in cc::PaintShader and GPU ServiceShaderTransferCacheEntry handling allows 32-bit shader IDs (RecordShaderId) to wrap around without validation. In the GPU process, PaintOpReader reuses cached SkPicture records based solely on the 32-bit shader ID and tile dimension equality. This enables a potential canvas origin-tainting bypass where rendered pixels of a cross-origin, credentialed SVG can be substituted into an origin-clean 2D canvas and disclosed via getImageData().
Root Cause Analysis
- 32-Bit Sequence Generator with Wrap-Around (
cc/paint/paint_shader.cc:31, 220):
base::AtomicSequenceNumber g_next_shader_id;
// ...
shader->id_ = g_next_shader_id.GetNext();
RecordShaderId is defined as uint32_t (cc/paint/paint_shader.h:48), while base::AtomicSequenceNumber::GetNext() wraps around via std::atomic<int>::fetch_add (base/atomic_sequence_num.h:26). After $2^{32}$ mints, IDs repeat.
- Transfer Cache Aliasing Without Content/Origin Verification (
cc/paint/paint_op_reader.cc:886-901):
auto* entry = options_.transfer_cache->GetEntryAs<ServiceShaderTransferCacheEntry>(shader_id);
if (entry && entry->shader()->tile_ == ref.tile_) {
DCHECK(!ref.sk_cached_picture_);
ref.sk_cached_picture_ = entry->shader()->sk_cached_picture_;
} else {
ref.ResolveSkObjects();
options_.transfer_cache->CreateLocalEntry(
shader_id, std::make_unique<ServiceShaderTransferCacheEntry>(*shader, shader_size));
}
In the GPU process, PaintOpReader treats shader_id as the primary key. If an entry exists and entry->shader()->tile_ == ref.tile_, it substitutes the cached sk_cached_picture_ into the current shader without validating content hash or context origin.
- Decoupled Origin Tainting & Deferred GPU Rasterization:
Canvas origin tainting is evaluated renderer-side at record time (
third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc:2616-2618), andBaseRenderingContext2D::getImageDataInternal(third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.cc:377) validatesOriginClean()before triggeringFinalizeFrame()(line 427). Because the substitution occurs GPU-side during rasterization, a clean canvas can rasterize and return tainted pixel data without failing the renderer-side origin check.
Potential / Suggested Steps to Reproduce
(Note: These are potential steps derived from static code analysis, as our tooling environment cannot execute code directly.)
- Enable the
Canvas2dMeshfeature (e.g., via Origin Trial token or--enable-blink-features=Canvas2dMesh). - Inspect the
naturalWidthandnaturalHeight($W, H$) of a target cross-origin SVG imagevictim.svgembedded via<img>. - Create an origin-clean canvas
canvasClean, and calldrawMesh(...)with a same-origin SVG matching size $(W, H)$. This mints a shader with ID $X$ and records the op into memory while remaining unflushed and origin-clean. - On a detached scratch canvas, loop $2^{32} - 1$ times calling
drawMesh(tinySvg)followed byctx.reset(). Thereset()call discards recordings viaMemoryManagedPaintRecorder::ReleaseMainRecording()without issuing GPU commands, advancingg_next_shader_iduntil it wraps around back to $X$. - Draw
victim.svgviadrawMesh(...)on a separate canvascanvasTainted. This markscanvasTaintedas tainted and mints ID $X$ for the victim shader. - Rasterize
canvasTaintedto populateServiceShaderTransferCacheEntrywith key $X$ holding the rendered victimSkPicturein the GPU process. - Within 25 seconds (before transfer cache age pruning), call
canvasClean.getContext('2d').getImageData(0, 0, W, H). The renderer origin check passes becausecanvasCleanis clean; the GPU process deserializes ID $X$, hits the transfer cache, substitutes the victimSkPicture, and returns the victim pixels to JavaScript.
Suggested Fix
- Widen RecordShaderId: Change
RecordShaderIdfromuint32_ttouint64_torbase::IdType64, preventing wraparound in practice. - Add Content Fingerprinting: Store a content hash or unique token in
ServiceShaderTransferCacheEntryand verify that the entry’s content matches the deserialized shader before substitutingsk_cached_picture_.
Evaluated with Chrome root at commit: 53c437ac4ea52cc89d0265eb9e6d68666fdb303e
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.