[libcxx-commits] [libcxx] [libc++] Assert non-negative counts in views::drop, views::take and views::counted (PR #234068)
Ilyas Mallah via libcxx-commits
libcxx-commits at lists.llvm.org
Tue Oct 20 20:48:52 PDT 2026
https://github.com/ilyas-mallah created https://github.com/llvm/llvm-project/pull/234068
Fast mode doesn't check the count of `views::drop`, `views::take` or `views::counted`, and a negative count makes the first read out of bounds. `views::drop(span, -1)` starts one element early, `views::take(subrange, -1)` ends before it starts, and `views::counted(ptr, -1)` returns a span of size `SIZE_MAX`. ASan reports a heap-buffer-overflow read on `main` for each.
`drop_view`'s constructor assertion was uncategorized, which fast mode compiles out, so it now uses `_LIBCPP_ASSERT_VALID_INPUT_RANGE`, as `span` and `iota_view` do. The overloads that return a `span`, `string_view` or `subrange` directly skip the views, so they get their own check. `take_view` and the `counted_iterator` form of `views::counted` are left as is. `take_view` clamps the size of a sized range, and anything else goes through `counted_iterator`, whose dereference already asserts in fast mode.
AI usage: Claude Code helped with the test runs and test scaffolding. I traced the issue, made the fix, and validated every change.
>From 9dc0fd8ce18f42d24061af939ff01e7639573ec3 Mon Sep 17 00:00:00 2001
From: Ilyas Mallah <github at ilyasm.dev>
Date: Wed, 14 Oct 2026 16:06:28 +0200
Subject: [PATCH] [libc++] Assert non-negative counts in views::drop,
views::take and views::counted
---
libcxx/include/__ranges/counted.h | 3 +
libcxx/include/__ranges/drop_view.h | 10 ++-
libcxx/include/__ranges/take_view.h | 4 +-
.../assert.negative_count.pass.cpp | 31 +++++++++
.../range.drop/assert.negative_count.pass.cpp | 64 +++++++++++++++++++
.../range.take/assert.negative_count.pass.cpp | 35 ++++++++++
6 files changed, 143 insertions(+), 4 deletions(-)
create mode 100644 libcxx/test/libcxx/ranges/range.adaptors/range.counted/assert.negative_count.pass.cpp
create mode 100644 libcxx/test/libcxx/ranges/range.adaptors/range.drop/assert.negative_count.pass.cpp
create mode 100644 libcxx/test/libcxx/ranges/range.adaptors/range.take/assert.negative_count.pass.cpp
diff --git a/libcxx/include/__ranges/counted.h b/libcxx/include/__ranges/counted.h
index 3b8dd5f05077eb..a0caabb4365854 100644
--- a/libcxx/include/__ranges/counted.h
+++ b/libcxx/include/__ranges/counted.h
@@ -10,6 +10,7 @@
#ifndef _LIBCPP___RANGES_COUNTED_H
#define _LIBCPP___RANGES_COUNTED_H
+#include <__assert>
#include <__concepts/convertible_to.h>
#include <__config>
#include <__cstddef/size_t.h>
@@ -47,12 +48,14 @@ struct __fn {
iter_difference_t<_It> __count) noexcept(noexcept(span(std::to_address(__it), static_cast<size_t>(__count))))
// Deliberately omit return-type SFINAE, because to_address is not SFINAE-friendly
{
+ _LIBCPP_ASSERT_VALID_INPUT_RANGE(__count >= 0, "views::counted(it, count) called with a negative count");
return span(std::to_address(__it), static_cast<size_t>(__count));
}
template <random_access_iterator _It>
_LIBCPP_HIDE_FROM_ABI static constexpr auto __go(_It __it, iter_difference_t<_It> __count) noexcept(
noexcept(subrange(__it, __it + __count))) -> decltype(subrange(__it, __it + __count)) {
+ _LIBCPP_ASSERT_VALID_INPUT_RANGE(__count >= 0, "views::counted(it, count) called with a negative count");
return subrange(__it, __it + __count);
}
diff --git a/libcxx/include/__ranges/drop_view.h b/libcxx/include/__ranges/drop_view.h
index c8b1f45b822d3b..759d24a20c2cb8 100644
--- a/libcxx/include/__ranges/drop_view.h
+++ b/libcxx/include/__ranges/drop_view.h
@@ -76,7 +76,7 @@ class drop_view : public view_interface<drop_view<_View>> {
_LIBCPP_HIDE_FROM_ABI constexpr explicit drop_view(_View __base, range_difference_t<_View> __count)
: __count_(__count), __base_(std::move(__base)) {
- _LIBCPP_ASSERT_UNCATEGORIZED(__count_ >= 0, "count must be greater than or equal to zero.");
+ _LIBCPP_ASSERT_VALID_INPUT_RANGE(__count_ >= 0, "count must be greater than or equal to zero.");
}
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI constexpr _View base() const&
@@ -229,8 +229,10 @@ struct __fn {
// Note: deliberately not forwarding `__rng` to guard against double moves.
ranges::begin(__rng) + std::min<_Dist>(ranges::distance(__rng), std::forward<_Np>(__n)),
ranges::end(__rng))) {
+ auto __count = static_cast<_Dist>(std::forward<_Np>(__n));
+ _LIBCPP_ASSERT_VALID_INPUT_RANGE(__count >= 0, "views::drop(range, count) called with a negative count");
return __passthrough_type_t<_RawRange>(
- ranges::begin(__rng) + std::min<_Dist>(ranges::distance(__rng), std::forward<_Np>(__n)), ranges::end(__rng));
+ ranges::begin(__rng) + std::min<_Dist>(ranges::distance(__rng), __count), ranges::end(__rng));
}
// [range.drop.overview]: the `subrange (StoreSize == true)` case.
@@ -253,8 +255,10 @@ struct __fn {
std::min<_Dist>(ranges::distance(__rng), std::forward<_Np>(__n))))) {
// Introducing local variables avoids calculating `min` and `distance` twice (at the cost of diverging from the
// expression used in the `noexcept` clause and the return statement).
+ auto __count = static_cast<_Dist>(std::forward<_Np>(__n));
+ _LIBCPP_ASSERT_VALID_INPUT_RANGE(__count >= 0, "views::drop(range, count) called with a negative count");
auto __dist = ranges::distance(__rng);
- auto __clamped = std::min<_Dist>(__dist, std::forward<_Np>(__n));
+ auto __clamped = std::min<_Dist>(__dist, __count);
return _RawRange(ranges::begin(__rng) + __clamped, ranges::end(__rng), std::__to_unsigned_like(__dist - __clamped));
}
// clang-format off
diff --git a/libcxx/include/__ranges/take_view.h b/libcxx/include/__ranges/take_view.h
index 23c07fc9932c43..d975fd0478fdab 100644
--- a/libcxx/include/__ranges/take_view.h
+++ b/libcxx/include/__ranges/take_view.h
@@ -271,8 +271,10 @@ struct __fn {
// Note: deliberately not forwarding `__rng` to guard against double moves.
ranges::begin(__rng),
ranges::begin(__rng) + std::min<_Dist>(ranges::distance(__rng), std::forward<_Np>(__n)))) {
+ auto __count = static_cast<_Dist>(std::forward<_Np>(__n));
+ _LIBCPP_ASSERT_VALID_INPUT_RANGE(__count >= 0, "views::take(range, count) called with a negative count");
return __passthrough_type_t<_RawRange>(
- ranges::begin(__rng), ranges::begin(__rng) + std::min<_Dist>(ranges::distance(__rng), std::forward<_Np>(__n)));
+ ranges::begin(__rng), ranges::begin(__rng) + std::min<_Dist>(ranges::distance(__rng), __count));
}
// [range.take.overview]: the `iota_view` case.
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.counted/assert.negative_count.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.counted/assert.negative_count.pass.cpp
new file mode 100644
index 00000000000000..0362283595e922
--- /dev/null
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.counted/assert.negative_count.pass.cpp
@@ -0,0 +1,31 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: can-test-hardening-assertions-fast
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-assertion-semantic={{ignore|observe}}
+
+// <ranges>
+
+// Make sure views::counted catches a negative count.
+
+#include <ranges>
+
+#include "check_assertion.h"
+#include "test_iterators.h"
+
+int main(int, char**) {
+ int a[] = {1, 2, 3, 4};
+
+ TEST_LIBCPP_ASSERT_FAILURE(
+ std::views::counted(static_cast<int*>(a), -1), "views::counted(it, count) called with a negative count");
+ TEST_LIBCPP_ASSERT_FAILURE(std::views::counted(random_access_iterator<int*>(a), -1),
+ "views::counted(it, count) called with a negative count");
+
+ return 0;
+}
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.drop/assert.negative_count.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.drop/assert.negative_count.pass.cpp
new file mode 100644
index 00000000000000..7241b163c6ba53
--- /dev/null
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.drop/assert.negative_count.pass.cpp
@@ -0,0 +1,64 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: can-test-hardening-assertions-fast
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-assertion-semantic={{ignore|observe}}
+
+// <ranges>
+
+// Make sure views::drop catches a negative count.
+
+#include <ranges>
+
+#include <span>
+#include <string_view>
+#include <vector>
+
+#include "check_assertion.h"
+#include "test_iterators.h"
+
+struct SizedViewWithUnsizedSentinel : std::ranges::view_base {
+ using iterator = random_access_iterator<int*>;
+ using sentinel = sentinel_wrapper<random_access_iterator<int*>>;
+
+ int* begin_ = nullptr;
+ int* end_ = nullptr;
+ SizedViewWithUnsizedSentinel(int* begin, int* end) : begin_(begin), end_(end) {}
+
+ iterator begin() const { return iterator(begin_); }
+ sentinel end() const { return sentinel(iterator(end_)); }
+ std::size_t size() const { return end_ - begin_; }
+};
+
+int main(int, char**) {
+ int a[] = {1, 2, 3, 4};
+
+ { // Goes through drop_view.
+ std::vector<int> v(a, a + 4);
+ TEST_LIBCPP_ASSERT_FAILURE(std::views::drop(v, -1), "count must be greater than or equal to zero.");
+ }
+
+ { // The overloads that return the same range type skip drop_view.
+ TEST_LIBCPP_ASSERT_FAILURE(
+ std::views::drop(std::span<int>(a), -1), "views::drop(range, count) called with a negative count");
+ TEST_LIBCPP_ASSERT_FAILURE(
+ std::views::drop(std::string_view("abcd"), -1), "views::drop(range, count) called with a negative count");
+ TEST_LIBCPP_ASSERT_FAILURE(std::views::drop(std::ranges::subrange(a, a + 4), -1),
+ "views::drop(range, count) called with a negative count");
+
+ using View = SizedViewWithUnsizedSentinel;
+ using Subrange = std::ranges::subrange<View::iterator, View::sentinel, std::ranges::subrange_kind::sized>;
+ View view(a, a + 4);
+ Subrange sized_subrange(view.begin(), view.end(), 4);
+ TEST_LIBCPP_ASSERT_FAILURE(
+ std::views::drop(sized_subrange, -1), "views::drop(range, count) called with a negative count");
+ }
+
+ return 0;
+}
diff --git a/libcxx/test/libcxx/ranges/range.adaptors/range.take/assert.negative_count.pass.cpp b/libcxx/test/libcxx/ranges/range.adaptors/range.take/assert.negative_count.pass.cpp
new file mode 100644
index 00000000000000..dd9ec5a4230dc4
--- /dev/null
+++ b/libcxx/test/libcxx/ranges/range.adaptors/range.take/assert.negative_count.pass.cpp
@@ -0,0 +1,35 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: can-test-hardening-assertions-fast
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// UNSUPPORTED: libcpp-assertion-semantic={{ignore|observe}}
+
+// <ranges>
+
+// Make sure views::take catches a negative count for the overloads that return the same range type.
+
+#include <ranges>
+
+#include <span>
+#include <string_view>
+
+#include "check_assertion.h"
+
+int main(int, char**) {
+ int a[] = {1, 2, 3, 4};
+
+ TEST_LIBCPP_ASSERT_FAILURE(
+ std::views::take(std::span<int>(a), -1), "views::take(range, count) called with a negative count");
+ TEST_LIBCPP_ASSERT_FAILURE(
+ std::views::take(std::string_view("abcd"), -1), "views::take(range, count) called with a negative count");
+ TEST_LIBCPP_ASSERT_FAILURE(
+ std::views::take(std::ranges::subrange(a, a + 4), -1), "views::take(range, count) called with a negative count");
+
+ return 0;
+}
More information about the libcxx-commits
mailing list