[libcxx-commits] [libcxx] [libc++] Assert that the resize_and_overwrite operation returns a size in [0, n] (PR #230605)

Ilyas Mallah via libcxx-commits libcxx-commits at lists.llvm.org
Fri Oct 9 10:58:04 PDT 2026


https://github.com/ilyas-mallah created https://github.com/llvm/llvm-project/pull/230605

`basic_string::resize_and_overwrite(n, op)` makes the value `r` returned by `op` the new size of the string, and [string.capacity] requires `0 <= r <= n`. The only check in libc++ is an internal assertion that `r` is within `capacity()`, and it runs only in the debug mode. An operation that writes 4 bytes and returns 40 leaves 36 stale bytes in the string, and no sanitizer reports it. If `r` is above the capacity, the null terminator is written past the allocation.

This checks both bounds with `_LIBCPP_ASSERT_VALID_INPUT_RANGE`, the category `string_view` uses for invalid lengths. The operation can return any integer-like type, so the sign is only checked for signed types and the upper bound is compared in the result's unsigned type. That way a `signed char` -1 isn't read as 255, and a 128-bit result isn't truncated.

AI usage: Claude Code helped with the test runs and test scaffolding. I traced the issue, made the fix, and validated every change.


>From 60e9f009b948a9a258e7cbe123d2a7438a3157ff Mon Sep 17 00:00:00 2001
From: Ilyas Mallah <github at ilyasm.dev>
Date: Fri, 9 Oct 2026 16:03:41 +0200
Subject: [PATCH] [libc++] Assert that the resize_and_overwrite operation
 returns a size in [0, n]

---
 libcxx/include/string                         | 12 +++-
 .../assert.resize_and_overwrite.pass.cpp      | 71 +++++++++++++++++++
 2 files changed, 82 insertions(+), 1 deletion(-)
 create mode 100644 libcxx/test/libcxx/strings/basic.string/string.capacity/assert.resize_and_overwrite.pass.cpp

diff --git a/libcxx/include/string b/libcxx/include/string
index a27f9344916d1b..fcd27483e13964 100644
--- a/libcxx/include/string
+++ b/libcxx/include/string
@@ -639,9 +639,11 @@ basic_string<char32_t> operator""s( const char32_t *str, size_t len );
 #  include <__type_traits/is_nothrow_constructible.h>
 #  include <__type_traits/is_relocatable.h>
 #  include <__type_traits/is_same.h>
+#  include <__type_traits/is_signed.h>
 #  include <__type_traits/is_standard_layout.h>
 #  include <__type_traits/is_trivially_constructible.h>
 #  include <__type_traits/is_trivially_copyable.h>
+#  include <__type_traits/make_unsigned.h>
 #  include <__type_traits/remove_cv.h>
 #  include <__type_traits/remove_cvref.h>
 #  include <__utility/assume.h>
@@ -1250,7 +1252,15 @@ public:
     __annotate_delete();
     __set_size(__n);
     __annotate_new(__n);
-    __erase_to_end(std::move(__op)(data(), auto(__n)));
+    __result_type __r = std::move(__op)(data(), auto(__n));
+    if constexpr (__integer_like<__result_type>) { // Avoid extra errors after a failed static_assert
+      if constexpr (is_signed_v<__result_type>)
+        _LIBCPP_ASSERT_VALID_INPUT_RANGE(
+            __r >= 0, "string::resize_and_overwrite: the operation returned a negative size");
+      _LIBCPP_ASSERT_VALID_INPUT_RANGE(std::__to_unsigned_like(__r) <= __n,
+                                       "string::resize_and_overwrite: the operation returned a size larger than n");
+    }
+    __erase_to_end(__r);
   }
 #  endif
 
diff --git a/libcxx/test/libcxx/strings/basic.string/string.capacity/assert.resize_and_overwrite.pass.cpp b/libcxx/test/libcxx/strings/basic.string/string.capacity/assert.resize_and_overwrite.pass.cpp
new file mode 100644
index 00000000000000..35d901f8da3bd0
--- /dev/null
+++ b/libcxx/test/libcxx/strings/basic.string/string.capacity/assert.resize_and_overwrite.pass.cpp
@@ -0,0 +1,71 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// <string>
+
+// template<class Operation>
+// constexpr void resize_and_overwrite(size_type n, Operation op); // since C++23
+
+// The result r of the operation must satisfy 0 <= r <= n.
+
+// REQUIRES: std-at-least-c++23
+// REQUIRES: can-test-hardening-assertions-fast
+// Execution would continue into an out-of-bounds write.
+// UNSUPPORTED: libcpp-assertion-semantic={{ignore|observe}}
+
+#include <cstddef>
+#include <string>
+
+#include "check_assertion.h"
+#include "min_allocator.h"
+#include "test_macros.h"
+
+template <class S>
+void test() {
+  {
+    S s;
+    TEST_LIBCPP_ASSERT_FAILURE(s.resize_and_overwrite(5, [](auto*, std::size_t n) { return n + 1; }),
+                               "string::resize_and_overwrite: the operation returned a size larger than n");
+  }
+  {
+    S s(40, 'x');
+    TEST_LIBCPP_ASSERT_FAILURE(s.resize_and_overwrite(40, [](auto*, std::size_t n) { return n + 4096; }),
+                               "string::resize_and_overwrite: the operation returned a size larger than n");
+  }
+  {
+    S s(40, 'x');
+    TEST_LIBCPP_ASSERT_FAILURE(s.resize_and_overwrite(40, [](auto*, std::size_t) { return -1; }),
+                               "string::resize_and_overwrite: the operation returned a negative size");
+  }
+  {
+    // A narrow signed result must not be compared as its unsigned counterpart, where -1 would be 255.
+    S s;
+    TEST_LIBCPP_ASSERT_FAILURE(
+        s.resize_and_overwrite(300, [](auto*, std::size_t) { return static_cast<signed char>(-1); }),
+        "string::resize_and_overwrite: the operation returned a negative size");
+  }
+#ifndef TEST_HAS_NO_INT128
+  {
+    // A wide result must not be truncated to size_type before the comparison, where 2^64 + 1 would be 1.
+    S s;
+    TEST_LIBCPP_ASSERT_FAILURE(
+        s.resize_and_overwrite(5, [](auto*, std::size_t) { return (static_cast<__int128_t>(1) << 64) + 1; }),
+        "string::resize_and_overwrite: the operation returned a size larger than n");
+  }
+#endif
+}
+
+int main(int, char**) {
+  test<std::string>();
+  test<std::basic_string<char, std::char_traits<char>, min_allocator<char> > >();
+#ifndef TEST_HAS_NO_WIDE_CHARACTERS
+  test<std::wstring>();
+#endif
+
+  return 0;
+}



More information about the libcxx-commits mailing list