From cd25753a73de8f801fbec9deb2aa658b973face7 Mon Sep 17 00:00:00 2001 From: Keishi Hattori Date: Wed, 17 Jun 2026 13:17:00 +0900 Subject: [PATCH 1/2] [wtf] Add separate build flag to enable active iterator checks for Vector Active-iterator counting (used to detect freeing a vector backing while iterators into it are still alive) was previously available only for the garbage-collected blink::HeapVector, gated on enable_heap_vector_active_iterator_checks. This extends the same mechanism to non-garbage-collected, PartitionAllocator-backed blink::Vector, controlled by a new, independent build arg enable_vector_active_iterator_checks (ENABLE_VECTOR_ACTIVE_- ITERATOR_CHECKS). The two flags can be toggled separately so HeapVector and Vector checks can be enabled independently. Implementation: - New build arg + buildflag and kEnableVectorActiveIteratorChecks constant. - New variable template kEnableActiveIteratorChecks that selects the right flag based on Allocator::kIsGarbageCollected, replacing the open-coded "kIsGarbageCollected && kEnableHeapVectorActiveIteratorChecks" conditions at the counter member, the deallocation CHECKs, and MakeIterator/MakeConstIterator. - UncheckedIterator's counter plumbing (member, counting ctors/dtor/ assignment) is now compiled when either flag is enabled, since the iterator type is shared between heap and non-heap vectors. No behavior change when both flags are off (the default). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../blink/renderer/platform/wtf/BUILD.gn | 9 +++- .../blink/renderer/platform/wtf/vector.h | 48 ++++++++++++------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/third_party/blink/renderer/platform/wtf/BUILD.gn b/third_party/blink/renderer/platform/wtf/BUILD.gn index d8b3af10bca0a..ee0df0025fe36 100644 --- a/third_party/blink/renderer/platform/wtf/BUILD.gn +++ b/third_party/blink/renderer/platform/wtf/BUILD.gn @@ -16,13 +16,20 @@ declare_args() { # Enable active iterator counting in blink::HeapVector to detect # use-after-free. enable_heap_vector_active_iterator_checks = false + + # Enable active iterator counting in non-garbage-collected blink::Vector + # (i.e. PartitionAllocator-backed) to detect use-after-free. + enable_vector_active_iterator_checks = false } buildflag_header("wtf_buildflags") { header = "wtf_buildflags.h" header_dir = "third_party/blink/renderer/platform/wtf" - flags = [ "ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS=$enable_heap_vector_active_iterator_checks" ] + flags = [ + "ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS=$enable_heap_vector_active_iterator_checks", + "ENABLE_VECTOR_ACTIVE_ITERATOR_CHECKS=$enable_vector_active_iterator_checks", + ] } visibility = [ diff --git a/third_party/blink/renderer/platform/wtf/vector.h b/third_party/blink/renderer/platform/wtf/vector.h index 2006e2575fee5..b53b57a382e5a 100644 --- a/third_party/blink/renderer/platform/wtf/vector.h +++ b/third_party/blink/renderer/platform/wtf/vector.h @@ -71,6 +71,22 @@ inline constexpr bool kEnableHeapVectorActiveIteratorChecks = true; inline constexpr bool kEnableHeapVectorActiveIteratorChecks = false; #endif +#if BUILDFLAG(ENABLE_VECTOR_ACTIVE_ITERATOR_CHECKS) +inline constexpr bool kEnableVectorActiveIteratorChecks = true; +#else +inline constexpr bool kEnableVectorActiveIteratorChecks = false; +#endif + +// Whether active-iterator checks are enabled for a Vector using the given +// `Allocator`. Garbage-collected vectors (HeapVector) are controlled by +// kEnableHeapVectorActiveIteratorChecks, while non-garbage-collected vectors +// (PartitionAllocator-backed Vector) are controlled by the independent +// kEnableVectorActiveIteratorChecks. This lets the two be turned on separately. +template +inline constexpr bool kEnableActiveIteratorChecks = + (Allocator::kIsGarbageCollected && kEnableHeapVectorActiveIteratorChecks) || + (!Allocator::kIsGarbageCollected && kEnableVectorActiveIteratorChecks); + #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) // The allocation pool for nodes is one big chunk that ASAN has no insight // into, so it can cloak errors. Make it as small as possible to force nodes @@ -596,8 +612,7 @@ class GC_PLUGIN_IGNORE("crbug.com/428987863") VectorBufferBase { }; struct NoActiveIteratorCounter {}; NO_UNIQUE_ADDRESS - std::conditional_t, ActiveIteratorCounter, NoActiveIteratorCounter> active_iterator_state_; @@ -653,10 +668,9 @@ class VectorBuffer : protected VectorBufferBase { } void DeallocateBuffer(T* buffer_to_deallocate) { - if constexpr (Allocator::kIsGarbageCollected && - kEnableHeapVectorActiveIteratorChecks) { + if constexpr (kEnableActiveIteratorChecks) { CHECK_EQ(this->active_iterator_state_.count, 0u) - << "HeapVector backing freed while iterators are still alive."; + << "Vector backing freed while iterators are still alive."; } Allocator::FreeVectorBacking(buffer_to_deallocate); } @@ -775,10 +789,9 @@ class VectorBuffer : protected VectorBufferBase { } NOINLINE void ReallyDeallocateBuffer(T* buffer_to_deallocate) { - if constexpr (Allocator::kIsGarbageCollected && - kEnableHeapVectorActiveIteratorChecks) { + if constexpr (kEnableActiveIteratorChecks) { CHECK_EQ(this->active_iterator_state_.count, 0u) - << "HeapVector backing freed while iterators are still alive."; + << "Vector backing freed while iterators are still alive."; } Allocator::FreeVectorBacking(buffer_to_deallocate); } @@ -1103,7 +1116,8 @@ class GC_PLUGIN_IGNORE("crbug.com/428987863") UncheckedIterator { modifications_ptr_(modifications_ptr), captured_modifications_(modifications_ptr ? *modifications_ptr : 0) {} UncheckedIterator(const UncheckedIterator& other) = default; -#elif BUILDFLAG(ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS) +#elif BUILDFLAG(ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS) || \ + BUILDFLAG(ENABLE_VECTOR_ACTIVE_ITERATOR_CHECKS) // Constructor that registers this iterator with the owning Vector's // active_iterators_ count. Used to detect freeing a backing while // iterators are alive. @@ -1133,7 +1147,9 @@ class GC_PLUGIN_IGNORE("crbug.com/428987863") UncheckedIterator { UncheckedIterator(const base::CheckedContiguousIterator& other) : current_(base::to_address(other)) {} ~UncheckedIterator() { -#if !DCHECK_IS_ON() && BUILDFLAG(ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS) +#if !DCHECK_IS_ON() && \ + (BUILDFLAG(ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS) || \ + BUILDFLAG(ENABLE_VECTOR_ACTIVE_ITERATOR_CHECKS)) if (active_iterator_count_) { --(*active_iterator_count_); } @@ -1159,7 +1175,8 @@ class GC_PLUGIN_IGNORE("crbug.com/428987863") UncheckedIterator { } return *this; } -#elif BUILDFLAG(ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS) +#elif BUILDFLAG(ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS) || \ + BUILDFLAG(ENABLE_VECTOR_ACTIVE_ITERATOR_CHECKS) UncheckedIterator& operator=(const UncheckedIterator& other) { if (this != &other) { if (active_iterator_count_) { @@ -1286,7 +1303,8 @@ class GC_PLUGIN_IGNORE("crbug.com/428987863") UncheckedIterator { #if DCHECK_IS_ON() const int64_t* modifications_ptr_ = nullptr; int64_t captured_modifications_ = 0; -#elif BUILDFLAG(ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS) +#elif BUILDFLAG(ENABLE_HEAP_VECTOR_ACTIVE_ITERATOR_CHECKS) || \ + BUILDFLAG(ENABLE_VECTOR_ACTIVE_ITERATOR_CHECKS) wtf_size_t* active_iterator_count_ = nullptr; #endif }; @@ -1859,8 +1877,7 @@ class Vector : private VectorBuffer { #if DCHECK_IS_ON() return iterator(ptr, &this->modifications_); #else - if constexpr (Allocator::kIsGarbageCollected && - kEnableHeapVectorActiveIteratorChecks) { + if constexpr (kEnableActiveIteratorChecks) { return iterator(ptr, &this->active_iterator_state_.count); } else { return iterator(ptr); @@ -1871,8 +1888,7 @@ class Vector : private VectorBuffer { #if DCHECK_IS_ON() return const_iterator(ptr, &this->modifications_); #else - if constexpr (Allocator::kIsGarbageCollected && - kEnableHeapVectorActiveIteratorChecks) { + if constexpr (kEnableActiveIteratorChecks) { return const_iterator( ptr, const_cast(&this->active_iterator_state_.count)); } else { -- 2.43.0 From 7784454a0c9547d9dbb9c441e925c357b36737b4 Mon Sep 17 00:00:00 2001 From: Keishi Hattori Date: Wed, 17 Jun 2026 13:25:17 +0900 Subject: [PATCH 2/2] [wtf] Report (don't crash) when Vector backing is freed while iterating The active-iterator checks previously CHECK-crashed when a Vector backing was freed (e.g. reallocated) while iterators into it were still alive. Convert that to a non-fatal base::debug::DumpWithoutCrashing so the bad call stack is reported without taking down the process. To keep this off the hot path and out of every translation unit: - The count comparison stays inline at the two deallocation sites, guarded with [[unlikely]] so a normal free is just a load and a predicted-not- taken branch. - The dump itself lives in a new, non-templated, NOINLINE free function (ReportVectorBackingFreedWhileIteratorsAlive) defined in a new vector.cc. This keeps base/debug/dump_without_crashing.h out of vector.h, avoids duplicating the reporting logic into every Vector instantiation, and is actually smaller than the previous per-instantiation CHECK_EQ + message string. - Reports at most once per process (atomic flag), on top of the per-location throttling DumpWithoutCrashing already applies, to avoid flooding crash reporting when a bad pattern is hit in a hot loop. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../blink/renderer/platform/wtf/BUILD.gn | 1 + .../blink/renderer/platform/wtf/vector.cc | 26 +++++++++++++++++++ .../blink/renderer/platform/wtf/vector.h | 21 ++++++++++++--- 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 third_party/blink/renderer/platform/wtf/vector.cc diff --git a/third_party/blink/renderer/platform/wtf/BUILD.gn b/third_party/blink/renderer/platform/wtf/BUILD.gn index ee0df0025fe36..2628500fded24 100644 --- a/third_party/blink/renderer/platform/wtf/BUILD.gn +++ b/third_party/blink/renderer/platform/wtf/BUILD.gn @@ -226,6 +226,7 @@ component("wtf") { "type_traits.h", "uuid.cc", "uuid.h", + "vector.cc", "vector.h", "vector_backed_linked_list.h", "vector_traits.h", diff --git a/third_party/blink/renderer/platform/wtf/vector.cc b/third_party/blink/renderer/platform/wtf/vector.cc new file mode 100644 index 0000000000000..a7a51d6d38398 --- /dev/null +++ b/third_party/blink/renderer/platform/wtf/vector.cc @@ -0,0 +1,26 @@ +// 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. + +#include "third_party/blink/renderer/platform/wtf/vector.h" + +#include + +#include "base/compiler_specific.h" +#include "base/debug/dump_without_crashing.h" + +namespace blink { + +NOINLINE void ReportVectorBackingFreedWhileIteratorsAlive() { + // This indicates a use-after-free hazard: a Vector backing was freed (e.g. + // reallocated) while iterators into it were still alive. Rather than crashing + // we capture a single dump so the offending call stack is reported without + // taking down the process. Report at most once per process to avoid flooding + // crash reporting when the same bad pattern is hit repeatedly in a hot loop. + static std::atomic has_dumped{false}; + if (!has_dumped.exchange(true, std::memory_order_relaxed)) { + base::debug::DumpWithoutCrashing(); + } +} + +} // namespace blink diff --git a/third_party/blink/renderer/platform/wtf/vector.h b/third_party/blink/renderer/platform/wtf/vector.h index b53b57a382e5a..01c07ea6bd0e4 100644 --- a/third_party/blink/renderer/platform/wtf/vector.h +++ b/third_party/blink/renderer/platform/wtf/vector.h @@ -87,6 +87,17 @@ inline constexpr bool kEnableActiveIteratorChecks = (Allocator::kIsGarbageCollected && kEnableHeapVectorActiveIteratorChecks) || (!Allocator::kIsGarbageCollected && kEnableVectorActiveIteratorChecks); +// Cold, out-of-line reporting helper invoked when a Vector backing is freed +// (e.g. via reallocation) while iterators into it are still alive. Defined in +// vector.cc so that the heavy base/debug/dump_without_crashing.h dependency and +// the (rarely taken) reporting logic stay out of this header: they are not +// duplicated into every Vector instantiation, and they don't leak +// into the many translation units that include vector.h. The actual count +// comparison is kept inline at the call sites and guarded with [[unlikely]] so +// the normal deallocation path is just a load and a predicted-not-taken branch. +// Marked NOINLINE so it is never pulled back into the hot path under LTO. +WTF_EXPORT NOINLINE void ReportVectorBackingFreedWhileIteratorsAlive(); + #if defined(MEMORY_TOOL_REPLACES_ALLOCATOR) // The allocation pool for nodes is one big chunk that ASAN has no insight // into, so it can cloak errors. Make it as small as possible to force nodes @@ -669,8 +680,9 @@ class VectorBuffer : protected VectorBufferBase { void DeallocateBuffer(T* buffer_to_deallocate) { if constexpr (kEnableActiveIteratorChecks) { - CHECK_EQ(this->active_iterator_state_.count, 0u) - << "Vector backing freed while iterators are still alive."; + if (this->active_iterator_state_.count != 0u) [[unlikely]] { + ReportVectorBackingFreedWhileIteratorsAlive(); + } } Allocator::FreeVectorBacking(buffer_to_deallocate); } @@ -790,8 +802,9 @@ class VectorBuffer : protected VectorBufferBase { NOINLINE void ReallyDeallocateBuffer(T* buffer_to_deallocate) { if constexpr (kEnableActiveIteratorChecks) { - CHECK_EQ(this->active_iterator_state_.count, 0u) - << "Vector backing freed while iterators are still alive."; + if (this->active_iterator_state_.count != 0u) [[unlikely]] { + ReportVectorBackingFreedWhileIteratorsAlive(); + } } Allocator::FreeVectorBacking(buffer_to_deallocate); } -- 2.43.0