Skip to content

Commit 848b20d

Browse files
authored
[libc++] Correctly handle custom deleters in hardened unique_ptr<T[]> (#110685)
It turns out that we can never do bounds-checking for unique_ptrs with custom deleters, except when converting from a unique_ptr with a default deleter to one with a custom deleter. If we had an API like `std::make_unique` that allowed passing a custom deleter, we could at least get bounds checking when the unique_ptr is created through those APIs, but for now that is not possible. Fixes #110683
1 parent 313ad85 commit 848b20d

File tree

4 files changed

+72
-38
lines changed

4 files changed

+72
-38
lines changed

libcxx/include/__memory/array_cookie.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
_LIBCPP_BEGIN_NAMESPACE_STD
2525

2626
// Trait representing whether a type requires an array cookie at the start of its allocation when
27-
// allocated as `new T[n]` and deallocated as `delete array`.
27+
// allocated as `new T[n]` and deallocated as `delete[] array`.
2828
//
2929
// Under the Itanium C++ ABI [1], we know that an array cookie is available unless `T` is trivially
3030
// destructible and the call to `operator delete[]` is not a sized operator delete. Under ABIs other

libcxx/include/__memory/unique_ptr.h

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ struct _LIBCPP_TEMPLATE_VIS default_delete<_Tp[]> {
101101
}
102102
};
103103

104+
template <class _Deleter>
105+
struct __is_default_deleter : false_type {};
106+
107+
template <class _Tp>
108+
struct __is_default_deleter<default_delete<_Tp> > : true_type {};
109+
104110
template <class _Deleter>
105111
struct __unique_ptr_deleter_sfinae {
106112
static_assert(!is_reference<_Deleter>::value, "incorrect specialization");
@@ -307,11 +313,16 @@ class _LIBCPP_UNIQUE_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS unique_ptr {
307313
// 1. When an array cookie (see [1]) exists at the beginning of the array allocation, we are
308314
// able to reuse that cookie to extract the size of the array and perform bounds checking.
309315
// An array cookie is a size inserted at the beginning of the allocation by the compiler.
310-
// That size is inserted implicitly when doing `new T[n]` in some cases, and its purpose
311-
// is to allow the runtime to destroy the `n` array elements when doing `delete array`.
316+
// That size is inserted implicitly when doing `new T[n]` in some cases (as of writing this
317+
// exactly when the array elements are not trivially destructible), and its main purpose is
318+
// to allow the runtime to destroy the `n` array elements when doing `delete[] array`.
312319
// When we are able to use array cookies, we reuse information already available in the
313320
// current runtime, so bounds checking does not require changing libc++'s ABI.
314321
//
322+
// However, note that we cannot assume the presence of an array cookie when a custom deleter
323+
// is used, because the unique_ptr could have been created from an allocation that wasn't
324+
// obtained via `new T[n]` (since it may not be deleted with `delete[] arr`).
325+
//
315326
// 2. When the "bounded unique_ptr" ABI configuration (controlled by `_LIBCPP_ABI_BOUNDED_UNIQUE_PTR`)
316327
// is enabled, we store the size of the allocation (when it is known) so we can check it when
317328
// indexing into the `unique_ptr`. That changes the layout of `std::unique_ptr<T[]>`, which is
@@ -328,7 +339,7 @@ class _LIBCPP_UNIQUE_PTR_TRIVIAL_ABI _LIBCPP_TEMPLATE_VIS unique_ptr {
328339
// try to fall back to using an array cookie when available.
329340
//
330341
// Finally, note that when this ABI configuration is enabled, we have no choice but to always
331-
// make space for a size to be stored in the unique_ptr. Indeed, while we might want to avoid
342+
// make space for the size to be stored in the unique_ptr. Indeed, while we might want to avoid
332343
// storing the size when an array cookie is available, knowing whether an array cookie is available
333344
// requires the type stored in the unique_ptr to be complete, while unique_ptr can normally
334345
// accommodate incomplete types.
@@ -339,7 +350,9 @@ struct __unique_ptr_array_bounds_stateless {
339350
__unique_ptr_array_bounds_stateless() = default;
340351
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR explicit __unique_ptr_array_bounds_stateless(size_t) {}
341352

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

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

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

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

564583
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX23 __add_lvalue_reference_t<_Tp> operator[](size_t __i) const {
565-
_LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(__checker_.__in_bounds(std::__to_address(__ptr_), __i),
584+
_LIBCPP_ASSERT_VALID_ELEMENT_ACCESS(__checker_.__in_bounds<deleter_type>(std::__to_address(__ptr_), __i),
566585
"unique_ptr<T[]>::operator[](index): index out of range");
567586
return __ptr_[__i];
568587
}

libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/assert.subscript.pass.cpp

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -48,31 +48,24 @@ struct MyDeleter {
4848

4949
template <class WithCookie, class NoCookie>
5050
void test() {
51-
// For types with an array cookie, we can always detect OOB accesses.
51+
// For types with an array cookie, we can always detect OOB accesses. Note that reliance on an array
52+
// cookie is limited to the default deleter, since a unique_ptr with a custom deleter may not have
53+
// been allocated with `new T[n]`.
5254
{
53-
// Check with the default deleter
5455
{
55-
{
56-
std::unique_ptr<WithCookie[]> ptr(new WithCookie[5]);
57-
TEST_LIBCPP_ASSERT_FAILURE(ptr[6], "unique_ptr<T[]>::operator[](index): index out of range");
58-
}
59-
{
60-
std::unique_ptr<WithCookie[]> ptr = std::make_unique<WithCookie[]>(5);
61-
TEST_LIBCPP_ASSERT_FAILURE(ptr[6], "unique_ptr<T[]>::operator[](index): index out of range");
62-
}
63-
#if TEST_STD_VER >= 20
64-
{
65-
std::unique_ptr<WithCookie[]> ptr = std::make_unique_for_overwrite<WithCookie[]>(5);
66-
TEST_LIBCPP_ASSERT_FAILURE(ptr[6] = WithCookie(), "unique_ptr<T[]>::operator[](index): index out of range");
67-
}
68-
#endif
56+
std::unique_ptr<WithCookie[]> ptr(new WithCookie[5]);
57+
TEST_LIBCPP_ASSERT_FAILURE(ptr[6], "unique_ptr<T[]>::operator[](index): index out of range");
6958
}
70-
71-
// Check with a custom deleter
7259
{
73-
std::unique_ptr<WithCookie[], MyDeleter> ptr(new WithCookie[5]);
60+
std::unique_ptr<WithCookie[]> ptr = std::make_unique<WithCookie[]>(5);
7461
TEST_LIBCPP_ASSERT_FAILURE(ptr[6], "unique_ptr<T[]>::operator[](index): index out of range");
7562
}
63+
#if TEST_STD_VER >= 20
64+
{
65+
std::unique_ptr<WithCookie[]> ptr = std::make_unique_for_overwrite<WithCookie[]>(5);
66+
TEST_LIBCPP_ASSERT_FAILURE(ptr[6] = WithCookie(), "unique_ptr<T[]>::operator[](index): index out of range");
67+
}
68+
#endif
7669
}
7770

7871
// For types that don't have an array cookie, things are a bit more complicated. We can detect OOB accesses
@@ -97,14 +90,9 @@ void test() {
9790
#endif
9891

9992
// Make sure that we carry the bounds information properly through conversions, assignments, etc.
100-
// These tests are mostly relevant when the ABI setting is enabled (with a stateful bounds-checker),
101-
// but we still run them for types with an array cookie either way.
93+
// These tests are only relevant when the ABI setting is enabled (with a stateful bounds-checker).
10294
#if defined(_LIBCPP_ABI_BOUNDED_UNIQUE_PTR)
103-
using Types = types::type_list<NoCookie, WithCookie>;
104-
#else
105-
using Types = types::type_list<WithCookie>;
106-
#endif
107-
types::for_each(Types(), []<class T> {
95+
types::for_each(types::type_list<NoCookie, WithCookie>(), []<class T> {
10896
// Bounds carried through move construction
10997
{
11098
std::unique_ptr<T[]> ptr = std::make_unique<T[]>(5);
@@ -135,6 +123,7 @@ void test() {
135123
TEST_LIBCPP_ASSERT_FAILURE(other[6], "unique_ptr<T[]>::operator[](index): index out of range");
136124
}
137125
});
126+
#endif
138127
}
139128

140129
template <std::size_t Size>

libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/op_subscript.runtime.pass.cpp

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ struct WithNonTrivialDtor {
4646
template <class T>
4747
struct CustomDeleter : std::default_delete<T> {};
4848

49+
struct NoopDeleter {
50+
template <class T>
51+
TEST_CONSTEXPR_CXX23 void operator()(T*) const {}
52+
};
53+
4954
TEST_CONSTEXPR_CXX23 bool test() {
5055
// Basic test
5156
{
@@ -112,12 +117,33 @@ TEST_CONSTEXPR_CXX23 bool test() {
112117
WithNonTrivialDtor<16>,
113118
WithNonTrivialDtor<256>>;
114119
types::for_each(TrickyCookieTypes(), []<class T> {
115-
types::for_each(types::type_list<std::default_delete<T[]>, CustomDeleter<T[]>>(), []<class Deleter> {
116-
std::unique_ptr<T[], Deleter> p(new T[3]);
120+
// Array allocated with `new T[n]`, default deleter
121+
{
122+
std::unique_ptr<T[], std::default_delete<T[]>> p(new T[3]);
123+
assert(p[0] == T());
124+
assert(p[1] == T());
125+
assert(p[2] == T());
126+
}
127+
128+
// Array allocated with `new T[n]`, custom deleter
129+
{
130+
std::unique_ptr<T[], CustomDeleter<T[]>> p(new T[3]);
131+
assert(p[0] == T());
132+
assert(p[1] == T());
133+
assert(p[2] == T());
134+
}
135+
136+
// Array not allocated with `new T[n]`, custom deleter
137+
//
138+
// This test aims to ensure that the implementation doesn't try to use an array cookie
139+
// when there is none.
140+
{
141+
T array[50] = {};
142+
std::unique_ptr<T[], NoopDeleter> p(&array[0]);
117143
assert(p[0] == T());
118144
assert(p[1] == T());
119145
assert(p[2] == T());
120-
});
146+
}
121147
});
122148
}
123149
#endif // C++20

0 commit comments

Comments
 (0)