Skip to content

[libc++][hardening] Use bounded iterators in std::vector and std::string #78929

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
set(LIBCXX_HARDENING_MODE "fast" CACHE STRING "")
set(LIBCXX_ABI_DEFINES "_LIBCPP_ABI_BOUNDED_ITERATORS" CACHE STRING "")
set(LIBCXX_ABI_DEFINES "_LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING" CACHE STRING "")
set(LIBCXX_ABI_DEFINES "_LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR" CACHE STRING "")
26 changes: 24 additions & 2 deletions libcxx/docs/Hardening.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,22 @@ Vendors can use the following ABI options to enable additional hardening checks:
- ``span``;
- ``string_view``.

- ``_LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING`` -- changes the iterator type of
``basic_string`` to a bounded iterator that keeps track of whether it's within
the bounds of the original container and asserts it on every dereference and
when performing iterator arithmetics.

ABI impact: changes the iterator type of ``basic_string`` and its
specializations, such as ``string`` and ``wstring``.

- ``_LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR`` -- changes the iterator type of
``vector`` to a bounded iterator that keeps track of whether it's within the
bounds of the original container and asserts it on every dereference and when
performing iterator arithmetics. Note: this doesn't yet affect
``vector<bool>``.

ABI impact: changes the iterator type of ``vector`` (except ``vector<bool>``).

ABI tags
--------

Expand Down Expand Up @@ -367,10 +383,10 @@ Hardened containers status
- ❌
* - ``vector``
- ✅
-
- ✅ (see note)
* - ``string``
- ✅
-
- ✅ (see note)
* - ``list``
- ✅
- ❌
Expand Down Expand Up @@ -429,6 +445,12 @@ Hardened containers status
- ❌
- N/A

Note: for ``vector`` and ``string``, the iterator does not check for
invalidation (accesses made via an invalidated iterator still lead to undefined
behavior)

Note: ``vector<bool>`` iterator is not currently hardened.

Testing
=======

Expand Down
14 changes: 14 additions & 0 deletions libcxx/docs/ReleaseNotes/19.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ Improvements and New Features
- ``std::ignore``\s ``const __ignore_t& operator=(_Tp&&) const`` was changed to
``const __ignore_type& operator=(const _Tp&) const noexcept`` for all language versions.

- Vendors can now configure the ABI so that ``string`` and ``vector`` will use bounded iterators when hardening is
enabled. Note that checks for iterator invalidation are currently not supported -- any accesses made through an
invalidated bounded iterator will still result in undefined behavior (bounded iterators follow the normal invalidation
rules of the associated container). ``string`` bounded iterators use the logical size of the container (``index
< str.size()``) whereas ``vector`` bounded iterators use the "physical" size of the container (``index
< vec.capacity()``) which is a less strict check; refer to the implementation for further details.

Bounded iterators can be enabled via the ``_LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING`` ABI macro for ``string`` and via
the ``_LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR`` ABI macro for ``vector``; note that checks will only be performed if
the hardening mode is set to ``fast`` or above (i.e., no checking is performed in the unchecked mode, even if bounded
iterators are enabled in the ABI configuration).

Note: bounded iterators currently are not supported for ``vector<bool>``.

Deprecations and Removals
-------------------------

Expand Down
13 changes: 13 additions & 0 deletions libcxx/include/__configuration/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,19 @@
// - `string_view`.
// #define _LIBCPP_ABI_BOUNDED_ITERATORS

// Changes the iterator type of `basic_string` to a bounded iterator that keeps track of whether it's within the bounds
// of the original container and asserts it on every dereference and when performing iterator arithmetics.
//
// ABI impact: changes the iterator type of `basic_string` and its specializations, such as `string` and `wstring`.
// #define _LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING

// Changes the iterator type of `vector` to a bounded iterator that keeps track of whether it's within the bounds of the
// original container and asserts it on every dereference and when performing iterator arithmetics. Note: this doesn't
// yet affect `vector<bool>`.
//
// ABI impact: changes the iterator type of `vector` (except `vector<bool>`).
// #define _LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR

#if defined(_LIBCPP_COMPILER_CLANG_BASED)
# if defined(__APPLE__)
# if defined(__i386__) || defined(__x86_64__)
Expand Down
2 changes: 2 additions & 0 deletions libcxx/include/__iterator/bounded_iter.h
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ struct __bounded_iter {
private:
template <class>
friend struct pointer_traits;
template <class, class>
friend struct __bounded_iter;
_Iterator __current_; // current iterator
_Iterator __begin_, __end_; // valid range represented as [begin, end]
};
Expand Down
35 changes: 33 additions & 2 deletions libcxx/include/string
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ basic_string<char32_t> operator""s( const char32_t *str, size_t len );
#include <__functional/unary_function.h>
#include <__fwd/string.h>
#include <__ios/fpos.h>
#include <__iterator/bounded_iter.h>
#include <__iterator/distance.h>
#include <__iterator/iterator_traits.h>
#include <__iterator/reverse_iterator.h>
Expand Down Expand Up @@ -822,9 +823,16 @@ public:
"Allocator::value_type must be same type as value_type");
static_assert(__check_valid_allocator<allocator_type>::value, "");

// TODO: Implement iterator bounds checking without requiring the global database.
#ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING
// Users might provide custom allocators, and prior to C++20 we have no existing way to detect whether the allocator's
// pointer type is contiguous (though it has to be by the Standard). Using the wrapper type ensures the iterator is
// considered contiguous.
typedef __bounded_iter<__wrap_iter<pointer>> iterator;
typedef __bounded_iter<__wrap_iter<const_pointer>> const_iterator;
#else
typedef __wrap_iter<pointer> iterator;
typedef __wrap_iter<const_pointer> const_iterator;
#endif
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

Expand Down Expand Up @@ -940,10 +948,33 @@ private:
__init_with_sentinel(std::move(__first), std::move(__last));
}

_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 iterator __make_iterator(pointer __p) { return iterator(__p); }
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 iterator __make_iterator(pointer __p) {
#ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING
// Bound the iterator according to the size (and not the capacity, unlike vector).
//
// By the Standard, string iterators are generally not guaranteed to stay valid when the container is modified,
// regardless of whether reallocation occurs. This allows us to check for out-of-bounds accesses using logical size,
// a stricter check, since correct code can never rely on being able to access newly-added elements via an existing
// iterator.
return std::__make_bounded_iter(
std::__wrap_iter<pointer>(__p),
std::__wrap_iter<pointer>(__get_pointer()),
std::__wrap_iter<pointer>(__get_pointer() + size()));
#else
return iterator(__p);
#endif // _LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING
}

_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 const_iterator __make_const_iterator(const_pointer __p) const {
#ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING
// Bound the iterator according to the size (and not the capacity, unlike vector).
return std::__make_bounded_iter(
std::__wrap_iter<const_pointer>(__p),
std::__wrap_iter<const_pointer>(__get_pointer()),
std::__wrap_iter<const_pointer>(__get_pointer() + size()));
#else
return const_iterator(__p);
#endif // _LIBCPP_ABI_BOUNDED_ITERATORS_IN_STRING
}

public:
Expand Down
37 changes: 36 additions & 1 deletion libcxx/include/vector
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ template<class T, class charT> requires is-vector-bool-reference<T> // Since C++
#include <__functional/unary_function.h>
#include <__fwd/vector.h>
#include <__iterator/advance.h>
#include <__iterator/bounded_iter.h>
#include <__iterator/distance.h>
#include <__iterator/iterator_traits.h>
#include <__iterator/reverse_iterator.h>
Expand Down Expand Up @@ -400,9 +401,16 @@ public:
typedef typename __alloc_traits::difference_type difference_type;
typedef typename __alloc_traits::pointer pointer;
typedef typename __alloc_traits::const_pointer const_pointer;
// TODO: Implement iterator bounds checking without requiring the global database.
#ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR
// Users might provide custom allocators, and prior to C++20 we have no existing way to detect whether the allocator's
// pointer type is contiguous (though it has to be by the Standard). Using the wrapper type ensures the iterator is
// considered contiguous.
typedef __bounded_iter<__wrap_iter<pointer>> iterator;
typedef __bounded_iter<__wrap_iter<const_pointer>> const_iterator;
#else
typedef __wrap_iter<pointer> iterator;
typedef __wrap_iter<const_pointer> const_iterator;
#endif
typedef std::reverse_iterator<iterator> reverse_iterator;
typedef std::reverse_iterator<const_iterator> const_reverse_iterator;

Expand Down Expand Up @@ -827,12 +835,39 @@ private:

_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI void __append(size_type __n);
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI void __append(size_type __n, const_reference __x);

_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI iterator __make_iter(pointer __p) _NOEXCEPT {
#ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR
// Bound the iterator according to the capacity, rather than the size.
//
// Vector guarantees that iterators stay valid as long as no reallocation occurs even if new elements are inserted
// into the container; for these cases, we need to make sure that the newly-inserted elements can be accessed
// through the bounded iterator without failing checks. The downside is that the bounded iterator won't catch
// access that is logically out-of-bounds, i.e., goes beyond the size, but is still within the capacity. With the
// current implementation, there is no connection between a bounded iterator and its associated container, so we
// don't have a way to update existing valid iterators when the container is resized and thus have to go with
// a laxer approach.
return std::__make_bounded_iter(
std::__wrap_iter<pointer>(__p),
std::__wrap_iter<pointer>(this->__begin_),
std::__wrap_iter<pointer>(this->__end_cap()));
#else
return iterator(__p);
#endif // _LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR
}

_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI const_iterator __make_iter(const_pointer __p) const _NOEXCEPT {
#ifdef _LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR
// Bound the iterator according to the capacity, rather than the size.
return std::__make_bounded_iter(
std::__wrap_iter<const_pointer>(__p),
std::__wrap_iter<const_pointer>(this->__begin_),
std::__wrap_iter<const_pointer>(this->__end_cap()));
#else
return const_iterator(__p);
#endif // _LIBCPP_ABI_BOUNDED_ITERATORS_IN_VECTOR
}

_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI void
__swap_out_circular_buffer(__split_buffer<value_type, allocator_type&>& __v);
_LIBCPP_CONSTEXPR_SINCE_CXX20 _LIBCPP_HIDE_FROM_ABI pointer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@

template <class T>
class small_pointer {
public:
using value_type = T;
using difference_type = std::int16_t;
using pointer = small_pointer;
using reference = T&;
using iterator_category = std::random_access_iterator_tag;

private:
std::uint16_t offset;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,39 @@

// Add to iterator out of bounds.

// REQUIRES: has-unix-headers
// UNSUPPORTED: !libcpp-has-legacy-debug-mode, c++03
// REQUIRES: has-unix-headers, libcpp-has-abi-bounded-iterators-in-vector
// UNSUPPORTED: libcpp-hardening-mode=none, c++03

#include <vector>
#include <cassert>

#include "check_assertion.h"
#include "fill_to_capacity.h"
#include "min_allocator.h"

int main(int, char**) {
{
typedef int T;
typedef std::vector<T> C;
C c(1);
fill_to_capacity(c);
C::iterator i = c.begin();
i += 1;
i += c.size();
assert(i == c.end());
i = c.begin();
TEST_LIBCPP_ASSERT_FAILURE(i + 2, "Attempted to add/subtract an iterator outside its valid range");
TEST_LIBCPP_ASSERT_FAILURE(i + 2, "__bounded_iter::operator+=: Attempt to advance an iterator past the end");
}

{
typedef int T;
typedef std::vector<T, min_allocator<T> > C;
C c(1);
fill_to_capacity(c);
C::iterator i = c.begin();
i += 1;
i += c.size();
assert(i == c.end());
i = c.begin();
TEST_LIBCPP_ASSERT_FAILURE(i + 2, "Attempted to add/subtract an iterator outside its valid range");
TEST_LIBCPP_ASSERT_FAILURE(i + 2, "__bounded_iter::operator+=: Attempt to advance an iterator past the end");
}

return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

// Decrement iterator prior to begin.

// REQUIRES: has-unix-headers
// UNSUPPORTED: !libcpp-has-legacy-debug-mode, c++03
// REQUIRES: has-unix-headers, libcpp-has-abi-bounded-iterators-in-vector
// UNSUPPORTED: libcpp-hardening-mode=none, c++03

#include <vector>
#include <cassert>
Expand All @@ -27,7 +27,7 @@ int main(int, char**) {
C::iterator i = c.end();
--i;
assert(i == c.begin());
TEST_LIBCPP_ASSERT_FAILURE(--i, "Attempted to decrement a non-decrementable iterator");
TEST_LIBCPP_ASSERT_FAILURE(--i, "__bounded_iter::operator--: Attempt to rewind an iterator past the start");
}

{
Expand All @@ -37,7 +37,7 @@ int main(int, char**) {
C::iterator i = c.end();
--i;
assert(i == c.begin());
TEST_LIBCPP_ASSERT_FAILURE(--i, "Attempted to decrement a non-decrementable iterator");
TEST_LIBCPP_ASSERT_FAILURE(--i, "__bounded_iter::operator--: Attempt to rewind an iterator past the start");
}

return 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,32 @@

// Dereference non-dereferenceable iterator.

// REQUIRES: has-unix-headers
// UNSUPPORTED: !libcpp-has-legacy-debug-mode, c++03
// REQUIRES: has-unix-headers, libcpp-has-abi-bounded-iterators-in-vector
// UNSUPPORTED: libcpp-hardening-mode=none, c++03

#include <vector>

#include "check_assertion.h"
#include "fill_to_capacity.h"
#include "min_allocator.h"

int main(int, char**) {
{
typedef int T;
typedef std::vector<T> C;
C c(1);
fill_to_capacity(c);
C::iterator i = c.end();
TEST_LIBCPP_ASSERT_FAILURE(*i, "Attempted to dereference a non-dereferenceable iterator");
TEST_LIBCPP_ASSERT_FAILURE(*i, "__bounded_iter::operator*: Attempt to dereference an iterator at the end");
}

{
typedef int T;
typedef std::vector<T, min_allocator<T> > C;
C c(1);
fill_to_capacity(c);
C::iterator i = c.end();
TEST_LIBCPP_ASSERT_FAILURE(*i, "Attempted to dereference a non-dereferenceable iterator");
TEST_LIBCPP_ASSERT_FAILURE(*i, "__bounded_iter::operator*: Attempt to dereference an iterator at the end");
}

return 0;
Expand Down
Loading
Loading