From cd25753a73de8f801fbec9deb2aa658b973face7 Mon Sep 17 00:00:00 2001
From: Keishi Hattori <keishi.hattori@gmail.com>
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<Allocator> 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) <noreply@anthropic.com>
---
 .../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 <typename Allocator>
+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<Allocator::kIsGarbageCollected &&
-                         kEnableHeapVectorActiveIteratorChecks,
+  std::conditional_t<kEnableActiveIteratorChecks<Allocator>,
                      ActiveIteratorCounter,
                      NoActiveIteratorCounter>
       active_iterator_state_;
@@ -653,10 +668,9 @@ class VectorBuffer<T, 0, Allocator> : protected VectorBufferBase<T, Allocator> {
   }
 
   void DeallocateBuffer(T* buffer_to_deallocate) {
-    if constexpr (Allocator::kIsGarbageCollected &&
-                  kEnableHeapVectorActiveIteratorChecks) {
+    if constexpr (kEnableActiveIteratorChecks<Allocator>) {
       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<T, Allocator> {
   }
 
   NOINLINE void ReallyDeallocateBuffer(T* buffer_to_deallocate) {
-    if constexpr (Allocator::kIsGarbageCollected &&
-                  kEnableHeapVectorActiveIteratorChecks) {
+    if constexpr (kEnableActiveIteratorChecks<Allocator>) {
       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<T>& 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<T, INLINE_CAPACITY, Allocator> {
 #if DCHECK_IS_ON()
     return iterator(ptr, &this->modifications_);
 #else
-    if constexpr (Allocator::kIsGarbageCollected &&
-                  kEnableHeapVectorActiveIteratorChecks) {
+    if constexpr (kEnableActiveIteratorChecks<Allocator>) {
       return iterator(ptr, &this->active_iterator_state_.count);
     } else {
       return iterator(ptr);
@@ -1871,8 +1888,7 @@ class Vector : private VectorBuffer<T, INLINE_CAPACITY, Allocator> {
 #if DCHECK_IS_ON()
     return const_iterator(ptr, &this->modifications_);
 #else
-    if constexpr (Allocator::kIsGarbageCollected &&
-                  kEnableHeapVectorActiveIteratorChecks) {
+    if constexpr (kEnableActiveIteratorChecks<Allocator>) {
       return const_iterator(
           ptr, const_cast<wtf_size_t*>(&this->active_iterator_state_.count));
     } else {
-- 
2.43.0


From 7784454a0c9547d9dbb9c441e925c357b36737b4 Mon Sep 17 00:00:00 2001
From: Keishi Hattori <keishi.hattori@gmail.com>
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<T, Allocator>
  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) <noreply@anthropic.com>
---
 .../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 <atomic>
+
+#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<bool> 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<T, Allocator> 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<T, 0, Allocator> : protected VectorBufferBase<T, Allocator> {
 
   void DeallocateBuffer(T* buffer_to_deallocate) {
     if constexpr (kEnableActiveIteratorChecks<Allocator>) {
-      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<T, Allocator> {
 
   NOINLINE void ReallyDeallocateBuffer(T* buffer_to_deallocate) {
     if constexpr (kEnableActiveIteratorChecks<Allocator>) {
-      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

