Chrome · Skia
CVE-2026-15774
UAF in Skia
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
SubRunInitializersrc/text/gpu/SubRunAllocator.h |
modified | |
AllocateClassMemoryAndArenasrc/text/gpu/SubRunAllocator.h |
modified |
Files Changed
src/text/gpu/SubRunAllocator.h
Patch
From 014a77e8a6cd4abbd5952fd4a1815ec3a521f300 Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni <[email protected]> Date: Fri, 03 Jul 2026 15:03:41 +0000 Subject: [PATCH] Fix Use-After-Free in SubRunAllocator The destruction order of `std::tuple` members is not specified by the C++ standard. This is the root cause of a Use-After-Free (UAF) in SubRunAllocator. Replacing the tuple with a custom struct resolves the issue by guaranteeing the correct destruction order. The Bug: During deserialization of a Slug (specifically in SlugImpl::MakeFromBuffer), if the input buffer is invalid or corrupted, Skia detects this and returns nullptr early. This early return destroys the temporary return value. In the old code (using `std::tuple`), `SubRunInitializer` (index 0) was destructed first and freed the backing memory. `SubRunAllocator` (index 2) was destructed next. Its destructor (~BagOfBytes) then attempted to access fEndByte (which points inside the freed memory), resulting in a UAF (read) followed by a wild-free or double-free. The Fix: We replaced the `std::tuple` with a custom helper struct `AllocateAndArenaResult`: struct AllocateAndArenaResult { SubRunInitializer<T> initializer; // Destructed last int totalMemorySize; SubRunAllocator alloc; // Destructed first }; Since struct members are guaranteed to be destructed in the reverse order of their declaration, declaring `alloc` last guarantees it is destructed before `SubRunInitializer` frees the memory. Additionally, this CL refactors `SubRunInitializer` to use `std::unique_ptr` with a custom deleter to manage the raw memory, removing the need for a manual destructor and making the ownership transfer explicit via `release()`. Bug: skia:530646115 Change-Id: I80cba4fdb9eebfe16e5ec837d70b5646422fbcff Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1284796 Commit-Queue: Kaylee Lubick <[email protected]> Reviewed-by: Kaylee Lubick <[email protected]> --- diff --git a/src/text/gpu/SubRunAllocator.h b/src/text/gpu/SubRunAllocator.h index 921f0f7..dd5c9d7 100644 --- a/src/text/gpu/SubRunAllocator.h +++ b/src/text/gpu/SubRunAllocator.h @@ -188,23 +188,30 @@ template <typename T> class SubRunInitializer { public: - SubRunInitializer(void* memory) : fMemory{memory} { SkASSERT(memory != nullptr); } - ~SubRunInitializer() { - ::operator delete(fMemory); - } + explicit SubRunInitializer(void* memory) : fMemory{memory} { SkASSERT(memory != nullptr); } + template <typename... Args> T* initialize(Args&&... args) { // Warn on more than one initialization. SkASSERT(fMemory != nullptr); - return new (std::exchange(fMemory, nullptr)) T(std::forward<Args>(args)...); + return new (fMemory.release()) T(std::forward<Args>(args)...); } private: - void* fMemory; + struct Deleter { + // Frees the heap memory without calling a destructor. + // We must use `::operator delete(p)` instead of `delete p` because `p` is `void*`. + // Deleting a `void*` is undefined behavior in C++. + // This is paired with the placement `::operator new` in AllocateClassMemoryAndArena. + void operator()(void* p) const { ::operator delete(p); } + }; + std::unique_ptr<void, Deleter> fMemory; }; -// GrSubRunAllocator provides fast allocation where the user takes care of calling the destructors -// of the returned pointers, and GrSubRunAllocator takes care of deleting the storage. The +template <typename T> struct AllocateAndArenaResult; + +// SubRunAllocator provides fast allocation where the user takes care of calling the destructors +// of the returned pointers, and SubRunAllocator takes care of deleting the storage. The // unique_ptrs returned, are to assist in assuring the object's destructor is called. // A note on zero length arrays: according to the standard a pointer must be returned, and it // can't be a nullptr. In such a case, SkArena allocates one byte, but does not initialize it. @@ -234,20 +241,8 @@ SubRunAllocator& operator=(SubRunAllocator&&) = default; template <typename T> - static std::tuple<SubRunInitializer<T>, int, SubRunAllocator> - AllocateClassMemoryAndArena(int allocSizeHint) { - SkASSERT_RELEASE(allocSizeHint >= 0); - // Round the size after the object the optimal amount. - int extraSize = BagOfBytes::PlatformMinimumSizeWithOverhead(allocSizeHint, alignof(T)); - - // Don't overflow or die. - SkASSERT_RELEASE(INT_MAX - SkTo<int>(sizeof(T)) > extraSize); - int totalMemorySize = sizeof(T) + extraSize; - - void* memory = ::operator new (totalMemorySize); - SubRunAllocator alloc{SkTAddOffset<char>(memory, sizeof(T)), extraSize, extraSize/2}; - return {memory, totalMemorySize, std::move(alloc)}; - } + static AllocateAndArenaResult<T> + AllocateClassMemoryAndArena(int allocSizeHint); template <typename T, typename... Args> T* makePOD(Args&&... args) { static_assert(HasNoDestructor<T>, "This is not POD. Use makeUnique."); @@ -328,6 +323,34 @@ BagOfBytes fAlloc; }; +// Members are destroyed in the reverse order of their declaration: +// https://isocpp.org/wiki/faq/dtors#order-dtors-for-members +// `alloc` must be destroyed first because it may contain pointers to memory owned by `initializer`. +// `initializer` must be destroyed last because it owns the backing memory. +// See also: https://issues.skia.org/issues/530646115 +template <typename T> +struct AllocateAndArenaResult { + SubRunInitializer<T> initializer; + int totalMemorySize; + SubRunAllocator alloc; +}; + +template <typename T> +inline AllocateAndArenaResult<T> +SubRunAllocator::AllocateClassMemoryAndArena(int allocSizeHint) { + SkASSERT_RELEASE(allocSizeHint >= 0); + // Round the size after the object the optimal amount. + int extraSize = BagOfBytes::PlatformMinimumSizeWithOverhead(allocSizeHint, alignof(T)); + + // Don't overflow or die. + SkASSERT_RELEASE(INT_MAX - SkTo<int>(sizeof(T)) > extraSize); + int totalMemorySize = sizeof(T) + extraSize; + + void* memory = ::operator new (totalMemorySize); + SubRunAllocator alloc{SkTAddOffset<char>(memory, sizeof(T)), extraSize, extraSize/2}; + return {SubRunInitializer<T>{memory}, totalMemorySize, std::move(alloc)}; +} + // Helper for defining allocators with inline/reserved storage. // For argument declarations, stick to the base type (SubRunAllocator). // Note: Inheriting from the storage first means the storage will outlive the
Loading diff…
Original Bug Report
reported by [email protected]
Use-After-Free in SubRunAllocator
This UAF was discovered during the MiraclePtr rewrite project. It is blocking it, and is also a pre-existing bug, maybe a vulnerability.
Reproducer:
bin/gn gen out/asan --args='is_debug=true extra_cflags=["-fsanitize=address", "-fno-omit-frame-pointer"] extra_ldflags=["-fsanitize=address"]'
autoninja -C out/asan dm
xvfb-run out/asan/dm --match Slug_b520571816
Description
The C++ destruction order of std::tuple is not defined. In practise, clang will destroy <0>, <1>, and <2> and GCC will destroy in the opposite direction: <2>, <1>, <0>.
SubRunAllocator::AllocateClassMemoryAndArena returns:
std::tuple<SubRunInitializer<T>, int, SubRunAllocator>
There is an early return causing memory issues:
Due to the destruction order of std::tuple:
SubRunInitializer(index 0) is destructed first. Its destructor calls::operator deleteon the backing memory.SubRunAllocator(index 2) is destructed second. Its destructor (~BagOfBytes) attempts to accessfEndByteto free any heap-allocated blocks. SincefEndBytepoints to a location inside the backing memory that was just freed by theSubRunInitializer, this results in a Use-After-Free.
ASAN Report
=================================================================
==2941250==ERROR: AddressSanitizer: heap-use-after-free on address 0x7d2d7c8476e0 at pc 0x55906e06b3a0 bp 0x7fffffff9730 sp 0x7fffffff9728
READ of size 8 at 0x7d2d7c8476e0 thread T0
#0 0x55906e06b39f in sktext::gpu::BagOfBytes::~BagOfBytes() ../../src/text/gpu/SubRunAllocator.cpp:39
#1 0x55906e06af73 in sktext::gpu::SubRunAllocator::~SubRunAllocator() ../../src/text/gpu/SubRunAllocator.h:211
#2 0x5590711e33b5 in std::_Head_base<2ul, sktext::gpu::SubRunAllocator, false>::~_Head_base() /usr/include/c++/15/tuple:200
#3 0x5590711e33d1 in std::_Tuple_impl<2ul, sktext::gpu::SubRunAllocator>::~_Tuple_impl() /usr/include/c++/15/tuple:546
#4 0x5590711e33ed in std::_Tuple_impl<1ul, int, sktext::gpu::SubRunAllocator>::~_Tuple_impl() /usr/include/c++/15/tuple:281
#5 0x5590711e3435 in std::_Tuple_impl<0ul, sktext::gpu::SubRunInitializer<sktext::gpu::SlugImpl>, int, sktext::gpu::SubRunAllocator>::~_Tuple_impl() /usr/include/c++/15/tuple:281
#6 0x5590711e3451 in std::tuple<sktext::gpu::SubRunInitializer<sktext::gpu::SlugImpl>, int, sktext::gpu::SubRunAllocator>::~tuple() /usr/include/c++/15/tuple:833
#7 0x5590711e2429 in sktext::gpu::SlugImpl::MakeFromBuffer(SkReadBuffer&, SkStrikeClient const*) ../../src/text/gpu/SlugImpl.cpp:67
0x7d2d7c8476e0 is located 480 bytes inside of 504-byte region [0x7d2d7c847500,0x7d2d7c8476f8)
freed by thread T0 here:
#0 0x7fdd7e11b653 in operator delete(void*) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:143
#1 0x5590711e3a07 in sktext::gpu::SubRunInitializer<sktext::gpu::SlugImpl>::~SubRunInitializer() ../../src/text/gpu/SubRunAllocator.h:193
#2 0x5590711e3409 in std::_Head_base<0ul, sktext::gpu::SubRunInitializer<sktext::gpu::SlugImpl>, false>::~_Head_base() /usr/include/c++/15/tuple:200
previously allocated by thread T0 here:
#0 0x7fdd7e11abbb in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:86
#1 0x5590711e38ca in std::tuple<sktext::gpu::SubRunInitializer<sktext::gpu::SlugImpl>, int, sktext::gpu::SubRunAllocator> sktext::gpu::SubRunAllocator::AllocateClassMemoryAndArena<sktext::gpu::SlugImpl>(int) ../../src/text/gpu/SubRunAllocator.h:247
References
On This Page