[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