[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