diff --git a/skia/BUILD.gn b/skia/BUILD.gn index baa11b3c0e6ea..9bce3d103ebf4 100644 --- a/skia/BUILD.gn +++ b/skia/BUILD.gn @@ -1180,6 +1180,7 @@ test("skia_unittests") { "ext/skcolorspace_trfn_unittest.cc", "ext/skia_memory_dump_provider_unittest.cc", "ext/skia_utils_base_unittest.cc", + "ext/sktdarray_overflow_unittest.cc", ] if (is_ios) { sources += [ "ext/skia_utils_ios_unittest.mm" ] diff --git a/skia/ext/sktdarray_overflow_unittest.cc b/skia/ext/sktdarray_overflow_unittest.cc new file mode 100644 index 0000000000000..a43006392f07a --- /dev/null +++ b/skia/ext/sktdarray_overflow_unittest.cc @@ -0,0 +1,79 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Demonstrates an integer-overflow -> heap-buffer-overflow in +// SkTDStorage::reserve()/bytes(). +// +// SkTDStorage::bytes(int n) computes `n * fSizeOfT` as a 32-bit int*int +// product before the SkToSizeT widening. SkToSizeT's range check is an +// SkASSERT, i.e. debug-only, so in release builds the wrapped 32-bit +// product is silently widened to size_t. +// +// SkTDStorage::reserve() stores the *element* count into fCapacity and +// passes bytes(fCapacity) to sk_realloc_throw(). When the int product +// wraps to a small *positive* value, the allocator returns a tiny buffer +// while fCapacity records ~1e9 elements. Every subsequent append() takes +// the (fSize < fCapacity) fast path and writes linearly past the end of +// the undersized allocation. +// +// This is the root cause behind the latent GPU-process overflow in +// GrAAConvexTessellator::extractFromPath(), which calls +// reservePts(5 * path.countPoints()) +// on SkTDArray (sizeof = 8). With countPoints() = 171,798,692, +// reserve(858,993,460) yields expandedReserve = 1,073,741,830 and +// 1,073,741,830 * 8 mod 2^32 = 48 — a 48-byte buffer with a 1.07B-element +// capacity. +// +// The Chrome-level path is currently gated by the 24-bit serialized-size +// field in the cc::PaintOp header (max 16 MiB per op on the GPU reader +// side), so a 172M-point SkPath cannot be delivered across the +// renderer→GPU boundary today. This unit test exercises the underlying +// Skia primitive directly. + +#include + +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/skia/include/private/base/SkTDArray.h" + +namespace { + +// Mirrors GrAAConvexTessellator::extractFromPath(): +// reservePts(5 * path.countPoints()) -> fPts.reserve(N) +// where fPts is SkTDArray, sizeof(SkPoint) == 8. +// +// For kCountPoints below: +// newCapacity = 5 * 171,798,692 = 858,993,460 +// growth = 4 + ((858,993,460+4) >> 2) = 214,748,370 +// expandedReserve = 858,993,460 + 214,748,370 = 1,073,741,830 +// bytes() = 1,073,741,830 * 8 = 8,589,934,640 +// 8,589,934,640 mod 2^32 = 48 (positive int) +// +// sk_realloc_throw() therefore allocates 48 bytes while fCapacity is set +// to 1,073,741,830. +constexpr int kCountPoints = 171798692; +constexpr int kReserve = 5 * kCountPoints; // 858,993,460 +static_assert(kReserve == 858993460); + +TEST(SkTDArrayOverflow, ReserveBytesPositiveWrapHeapOverflow) { + // int64_t stands in for SkPoint (both are 8 bytes). + static_assert(sizeof(int64_t) == 8); + SkTDArray pts; + + // Triggers the int*int overflow in SkTDStorage::bytes(). + pts.reserve(kReserve); + + // After the overflow, capacity() reports ~1.07B elements, but the + // backing store is only 48 bytes (room for 6 elements). + // The 7th append() is a heap-buffer-overflow WRITE. + for (int i = 0; i < 64; ++i) { + *pts.append() = 0x4141414141414141LL; + } + + // Never reached under ASAN. + ADD_FAILURE() << "expected ASAN heap-buffer-overflow on append() past the " + "undersized 48-byte buffer; capacity()=" + << pts.capacity(); +} + +} // namespace