[libcxx-commits] [libcxx] eec054b - [libc++] Fix use-after-free during self-assignment (#225296)

via libcxx-commits libcxx-commits at lists.llvm.org
Sat Oct 17 21:27:05 PDT 2026


Author: Chris Kennelly
Date: 2026-10-17T21:26:58-07:00
New Revision: eec054bb4a74ed8605efc4c25ab7f046f665d0b3

URL: https://github.com/llvm/llvm-project/commit/eec054bb4a74ed8605efc4c25ab7f046f665d0b3
DIFF: https://github.com/llvm/llvm-project/commit/eec054bb4a74ed8605efc4c25ab7f046f665d0b3.diff

LOG: [libc++] Fix use-after-free during self-assignment (#225296)

The range [data(), data() + size() + 1) points into the string's own
buffer and is one element longer than the capacity when the buffer is
full, so assigning it reallocates:

  std::string str;
str.resize(str.capacity(), 'a'); // size() == capacity()
  str.assign(str.data(), str.data() + str.size() + 1);  // reallocates

__assign_trivial called __grow_by_without_replace up front and read the
range afterwards, by which point the buffer holding it had been
released: that is a heap-use-after-free for a long string, and for a
short string the long representation has been written over the small
buffer the range points into, so the assignment silently stores garbage.

Allocate the new buffer and read the range into it before releasing the
buffer the string holds. That also keeps the range valid in the exotic
case where it is the byte representation of the string object itself,
which the comment this replaces called out. The capacity the assignment
grows to is unchanged.

This regressed in baf6f91851ed ("[libc++][ranges] Implement the changes
to `basic_string` from P1206 (`ranges::to`)"), which moved
assign(_ForwardIterator, _ForwardIterator) into __assign_trivial and
dropped the __addr_in_range check that had routed self-referencing
ranges through a temporary since e87479b00fcc; the bug is present in
every release since LLVM 17. See https://godbolt.org/z/nWPvK5hqP

Constant evaluation diagnoses the read of the released buffer, so the
static_assert covering the new case rejects the unpatched header
outright in C++20 and later.

Assisted-by: Antigravity

Added: 
    

Modified: 
    libcxx/include/string
    libcxx/test/std/strings/basic.string/string.modifiers/string_assign/iterator.pass.cpp

Removed: 
    


################################################################################
diff  --git a/libcxx/include/string b/libcxx/include/string
index 28c32cc60fe714..33175c115fd10c 100644
--- a/libcxx/include/string
+++ b/libcxx/include/string
@@ -2936,16 +2936,19 @@ basic_string<_CharT, _Traits, _Allocator>::__assign_trivial(_Iterator __first, _
   size_type __old_size = size();
   size_type __cap      = capacity();
   if (__cap < __n) {
-    // Unlike `append` functions, if the input range points into the string itself, there is no case that the input
-    // range could get invalidated by reallocation:
-    // 1. If the input range is a subset of the string itself, its size cannot exceed the capacity of the string,
-    //    thus no reallocation would happen.
-    // 2. In the exotic case where the input range is the byte representation of the string itself, the string
-    //    object itself stays valid even if reallocation happens.
-    size_type __sz = size();
-    __grow_by_without_replace(__cap, __n - __cap, __sz, 0, __sz);
-    __annotate_increase(__n);
-  } else if (__n > __old_size)
+    // __cap < __n does not mean the range is outside the string: it can be [data(), data() + size() + 1) of a full
+    // buffer, or the bytes of the object itself. Copy into the new buffer before releasing the old one.
+    __long __buffer = __allocate_long_buffer(__alloc_, __n, __get_amortized_growth_capacity(__n));
+    auto __end      = __copy_non_overlapping_range(__first, __last, std::__to_address(__buffer.__data_));
+    traits_type::assign(*__end, value_type());
+
+    __annotate_delete();
+    auto __guard = std::__make_scope_guard(__annotate_new_size(*this));
+    __reset_internal_buffer(__buffer);
+    return;
+  }
+
+  if (__n > __old_size)
     __annotate_increase(__n - __old_size);
   pointer __p = __get_pointer();
   for (; __first != __last; ++__p, (void)++__first)

diff  --git a/libcxx/test/std/strings/basic.string/string.modifiers/string_assign/iterator.pass.cpp b/libcxx/test/std/strings/basic.string/string.modifiers/string_assign/iterator.pass.cpp
index b2306807fcf7b9..3c50164f01f367 100644
--- a/libcxx/test/std/strings/basic.string/string.modifiers/string_assign/iterator.pass.cpp
+++ b/libcxx/test/std/strings/basic.string/string.modifiers/string_assign/iterator.pass.cpp
@@ -11,6 +11,8 @@
 // template<class InputIterator>
 //   basic_string& assign(InputIterator first, InputIterator last); // constexpr since C++20
 
+// XFAIL: FROZEN-CXX03-HEADERS-FIXME
+
 #include <cassert>
 #include <cstdint>
 #include <string>
@@ -28,6 +30,22 @@ TEST_CONSTEXPR_CXX20 void test(S s, It first, It last, S expected) {
   LIBCPP_ASSERT(is_string_asan_correct(s));
 }
 
+// [data(), data() + size() + 1) includes the null terminator, so it is one element longer than capacity() when the
+// buffer is full: assigning it reallocates while the range still points into the old buffer.
+template <class S>
+TEST_CONSTEXPR_CXX20 void test_self_referencing_range(typename S::size_type minimum_capacity) {
+  S s(minimum_capacity, 'a');
+  s.resize(s.capacity(), 'a');
+  assert(s.size() == s.capacity());
+  S expected = s + S(1, '\0');
+
+  // Note that the assignment must happen in place: copying the string first would make the range point elsewhere.
+  s.assign(s.data(), s.data() + s.size() + 1);
+  LIBCPP_ASSERT(s.__invariants());
+  assert(s == expected);
+  LIBCPP_ASSERT(is_string_asan_correct(s));
+}
+
 #ifndef TEST_HAS_NO_EXCEPTIONS
 struct Widget {
   operator char() const { throw 42; }
@@ -163,10 +181,8 @@ TEST_CONSTEXPR_CXX20 void test_string() {
   }
 
   { // regression-test assigning to self in sneaky ways
-    S sneaky = "hello";
-    sneaky.resize(sneaky.capacity(), 'x');
-    S expected = sneaky + S(1, '\0');
-    test(sneaky, sneaky.data(), sneaky.data() + sneaky.size() + 1, expected);
+    test_self_referencing_range<S>(0);  // the small buffer, which the long representation is written over
+    test_self_referencing_range<S>(64); // a heap buffer, which is released by the assignment
   }
 }
 


        


More information about the libcxx-commits mailing list