Skip to content
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

[libc++] Correctly handle custom deleters in hardened unique_ptr<T[]> #110685

Merged
merged 3 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion libcxx/include/__memory/array_cookie.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
_LIBCPP_BEGIN_NAMESPACE_STD

// Trait representing whether a type requires an array cookie at the start of its allocation when
// allocated as `new T[n]` and deallocated as `delete array`.
// allocated as `new T[n]` and deallocated as `delete[] array`.
//
// Under the Itanium C++ ABI [1], we know that an array cookie is available unless `T` is trivially
// destructible and the call to `operator delete[]` is not a sized operator delete. Under ABIs other
Expand Down
35 changes: 27 additions & 8 deletions libcxx/include/__memory/unique_ptr.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ struct _LIBCPP_TEMPLATE_VIS default_delete<_Tp[]> {
}
};

template <class _Deleter>
struct __is_default_deleter : false_type {};

template <class _Tp>
struct __is_default_deleter<default_delete<_Tp> > : true_type {};

template <class _Deleter>
struct __unique_ptr_deleter_sfinae {
static_assert(!is_reference<_Deleter>::value, "incorrect specialization");
Expand Down Expand Up @@ -307,11 +313,16 @@ class _LIBCPP_UNIQUE_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS unique_ptr {
// 1. When an array cookie (see [1]) exists at the beginning of the array allocation, we are
// able to reuse that cookie to extract the size of the array and perform bounds checking.
// An array cookie is a size inserted at the beginning of the allocation by the compiler.
// That size is inserted implicitly when doing `new T[n]` in some cases, and its purpose
// is to allow the runtime to destroy the `n` array elements when doing `delete array`.
// That size is inserted implicitly when doing `new T[n]` in some cases (as of writing this
// exactly when the array elements are not trivially destructible), and its main purpose is
// to allow the runtime to destroy the `n` array elements when doing `delete[] array`.
// When we are able to use array cookies, we reuse information already available in the
// current runtime, so bounds checking does not require changing libc++'s ABI.
//
// However, note that we cannot assume the presence of an array cookie when a custom deleter
// is used, because the unique_ptr could have been created from an allocation that wasn't
// obtained via `new T[n]` (since it may not be deleted with `delete[] arr`).
//
// 2. When the "bounded unique_ptr" ABI configuration (controlled by `_LIBCPP_ABI_BOUNDED_UNIQUE_PTR`)
// is enabled, we store the size of the allocation (when it is known) so we can check it when
// indexing into the `unique_ptr`. That changes the layout of `std::unique_ptr<T[]>`, which is
Expand All @@ -328,7 +339,7 @@ class _LIBCPP_UNIQUE_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS unique_ptr {
// try to fall back to using an array cookie when available.
//
// Finally, note that when this ABI configuration is enabled, we have no choice but to always
// make space for a size to be stored in the unique_ptr. Indeed, while we might want to avoid
// make space for the size to be stored in the unique_ptr. Indeed, while we might want to avoid
// storing the size when an array cookie is available, knowing whether an array cookie is available
// requires the type stored in the unique_ptr to be complete, while unique_ptr can normally
// accommodate incomplete types.
Expand All @@ -339,7 +350,9 @@ struct __unique_ptr_array_bounds_stateless {
__unique_ptr_array_bounds_stateless() = default;
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __unique_ptr_array_bounds_stateless(size_t) {}

template <class _Tp, __enable_if_t<__has_array_cookie<_Tp>::value, int> = 0>
template <class _Deleter,
class _Tp,
__enable_if_t<__is_default_deleter<_Deleter>::value && __has_array_cookie<_Tp>::value, int> = 0>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR bool __in_bounds(_Tp* __ptr, size_t __index) const {
// In constant expressions, we can't check the array cookie so we just pretend that the index
// is in-bounds. The compiler catches invalid accesses anyway.
Expand All @@ -349,7 +362,9 @@ struct __unique_ptr_array_bounds_stateless {
return __index < __cookie;
}

template <class _Tp, __enable_if_t<!__has_array_cookie<_Tp>::value, int> = 0>
template <class _Deleter,
class _Tp,
__enable_if_t<!__is_default_deleter<_Deleter>::value || !__has_array_cookie<_Tp>::value, int> = 0>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR bool __in_bounds(_Tp*, size_t) const {
return true; // If we don't have an array cookie, we assume the access is in-bounds
}
Expand All @@ -365,7 +380,9 @@ struct __unique_ptr_array_bounds_stored {
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __unique_ptr_array_bounds_stored(size_t __size) : __size_(__size) {}

// Use the array cookie if there's one
template <class _Tp, __enable_if_t<__has_array_cookie<_Tp>::value, int> = 0>
template <class _Deleter,
class _Tp,
__enable_if_t<__is_default_deleter<_Deleter>::value && __has_array_cookie<_Tp>::value, int> = 0>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR bool __in_bounds(_Tp* __ptr, size_t __index) const {
if (__libcpp_is_constant_evaluated())
return true;
Expand All @@ -374,7 +391,9 @@ struct __unique_ptr_array_bounds_stored {
}

// Otherwise, fall back on the stored size (if any)
template <class _Tp, __enable_if_t<!__has_array_cookie<_Tp>::value, int> = 0>
template <class _Deleter,
class _Tp,
__enable_if_t<!__is_default_deleter<_Deleter>::value || !__has_array_cookie<_Tp>::value, int> = 0>
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR bool __in_bounds(_Tp*, size_t __index) const {
return __index < __size_;
}
Expand Down Expand Up @@ -562,7 +581,7 @@ class _LIBCPP_UNIQUE_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS unique_ptr<_Tp[], _Dp>
}

_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX23 __add_lvalue_reference_t<_Tp> operator[](size_t __i) const {
_LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(__checker_.__in_bounds(std::__to_address(__ptr_), __i),
_LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(__checker_.__in_bounds<deleter_type>(std::__to_address(__ptr_), __i),
"unique_ptr<T[]>::operator[](index): index out of range");
return __ptr_[__i];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,31 +48,24 @@ struct MyDeleter {

template <class WithCookie, class NoCookie>
void test() {
// For types with an array cookie, we can always detect OOB accesses.
// For types with an array cookie, we can always detect OOB accesses. Note that reliance on an array
// cookie is limited to the default deleter, since a unique_ptr with a custom deleter may not have
// been allocated with `new T[n]`.
{
// Check with the default deleter
{
{
std::unique_ptr<WithCookie[]> ptr(new WithCookie[5]);
TEST_LIBCPP_ASSERT_FAILURE(ptr[6], "unique_ptr<T[]>::operator[](index): index out of range");
}
{
std::unique_ptr<WithCookie[]> ptr = std::make_unique<WithCookie[]>(5);
TEST_LIBCPP_ASSERT_FAILURE(ptr[6], "unique_ptr<T[]>::operator[](index): index out of range");
}
#if TEST_STD_VER >= 20
{
std::unique_ptr<WithCookie[]> ptr = std::make_unique_for_overwrite<WithCookie[]>(5);
TEST_LIBCPP_ASSERT_FAILURE(ptr[6] = WithCookie(), "unique_ptr<T[]>::operator[](index): index out of range");
}
#endif
std::unique_ptr<WithCookie[]> ptr(new WithCookie[5]);
TEST_LIBCPP_ASSERT_FAILURE(ptr[6], "unique_ptr<T[]>::operator[](index): index out of range");
}

// Check with a custom deleter
{
std::unique_ptr<WithCookie[], MyDeleter> ptr(new WithCookie[5]);
std::unique_ptr<WithCookie[]> ptr = std::make_unique<WithCookie[]>(5);
TEST_LIBCPP_ASSERT_FAILURE(ptr[6], "unique_ptr<T[]>::operator[](index): index out of range");
}
#if TEST_STD_VER >= 20
{
std::unique_ptr<WithCookie[]> ptr = std::make_unique_for_overwrite<WithCookie[]>(5);
TEST_LIBCPP_ASSERT_FAILURE(ptr[6] = WithCookie(), "unique_ptr<T[]>::operator[](index): index out of range");
}
#endif
}

// For types that don't have an array cookie, things are a bit more complicated. We can detect OOB accesses
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ struct WithNonTrivialDtor {
template <class T>
struct CustomDeleter : std::default_delete<T> {};

struct NoopDeleter {
template <class T>
TEST_CONSTEXPR void operator()(T*) const {}
};

TEST_CONSTEXPR_CXX23 bool test() {
// Basic test
{
Expand Down Expand Up @@ -112,12 +117,33 @@ TEST_CONSTEXPR_CXX23 bool test() {
WithNonTrivialDtor<16>,
WithNonTrivialDtor<256>>;
types::for_each(TrickyCookieTypes(), []<class T> {
types::for_each(types::type_list<std::default_delete<T[]>, CustomDeleter<T[]>>(), []<class Deleter> {
std::unique_ptr<T[], Deleter> p(new T[3]);
// Array allocated with `new T[n]`, default deleter
{
std::unique_ptr<T[], std::default_delete<T[]>> p(new T[3]);
assert(p[0] == T());
assert(p[1] == T());
assert(p[2] == T());
}

// Array allocated with `new T[n]`, custom deleter
{
std::unique_ptr<T[], CustomDeleter<T[]>> p(new T[3]);
assert(p[0] == T());
assert(p[1] == T());
assert(p[2] == T());
}

// Array not allocated with `new T[n]`, custom deleter
//
// This test aims to ensure that the implementation doesn't try to use an array cookie
// when there is none.
{
T array[50] = {};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nico I verified locally and this test breaks down without my patch. That's a smaller version of your reproducer.

std::unique_ptr<T[], NoopDeleter> p(&array[0]);
assert(p[0] == T());
assert(p[1] == T());
assert(p[2] == T());
});
}
});
}
#endif // C++20
Expand Down
Loading