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

Conversation

ldionne
Copy link
Member

@ldionne ldionne commented Oct 1, 2024

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

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 llvm#110683
@ldionne ldionne requested a review from nico October 1, 2024 15:19
@ldionne ldionne requested a review from a team as a code owner October 1, 2024 15:19
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Oct 1, 2024
@llvmbot
Copy link
Member

llvmbot commented Oct 1, 2024

@llvm/pr-subscribers-libcxx

Author: Louis Dionne (ldionne)

Changes

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


Full diff: https://github.com/llvm/llvm-project/pull/110685.diff

4 Files Affected:

  • (modified) libcxx/include/__memory/array_cookie.h (+1-1)
  • (modified) libcxx/include/__memory/unique_ptr.h (+27-8)
  • (modified) libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/assert.subscript.pass.cpp (+12-19)
  • (modified) libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/op_subscript.runtime.pass.cpp (+29-3)
diff --git a/libcxx/include/__memory/array_cookie.h b/libcxx/include/__memory/array_cookie.h
index 34eec643206103..10b29c9dcc78e3 100644
--- a/libcxx/include/__memory/array_cookie.h
+++ b/libcxx/include/__memory/array_cookie.h
@@ -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
diff --git a/libcxx/include/__memory/unique_ptr.h b/libcxx/include/__memory/unique_ptr.h
index 11215dc111e36a..6e42ef1eaa1a3c 100644
--- a/libcxx/include/__memory/unique_ptr.h
+++ b/libcxx/include/__memory/unique_ptr.h
@@ -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");
@@ -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
@@ -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.
@@ -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.
@@ -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
   }
@@ -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;
@@ -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_;
   }
@@ -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];
   }
diff --git a/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/assert.subscript.pass.cpp b/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/assert.subscript.pass.cpp
index 1eaf2d5900356b..66a7882c0ad34e 100644
--- a/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/assert.subscript.pass.cpp
+++ b/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/assert.subscript.pass.cpp
@@ -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
diff --git a/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/op_subscript.runtime.pass.cpp b/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/op_subscript.runtime.pass.cpp
index ebfad8ec724e51..ab33d38cd15a6e 100644
--- a/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/op_subscript.runtime.pass.cpp
+++ b/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/op_subscript.runtime.pass.cpp
@@ -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
   {
@@ -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] = {};
+        std::unique_ptr<T[], NoopDeleter> p(&array[0]);
         assert(p[0] == T());
         assert(p[1] == T());
         assert(p[2] == T());
-      });
+      }
     });
   }
 #endif // C++20

// 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.

Copy link
Contributor

@nico nico left a comment

Choose a reason for hiding this comment

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

Thanks for the quick fix!

Might be nice to give custom deleters some way to opt in to this, but that can be for the future :)

@ldionne
Copy link
Member Author

ldionne commented Oct 1, 2024

Thanks for the quick fix!

Might be nice to give custom deleters some way to opt in to this, but that can be for the future :)

I am iffy about exposing a vendor extension for this. Instead I think it would make more sense to pursue standardization of make_unique-like APIs that support a custom deleter.

@nico
Copy link
Contributor

nico commented Oct 1, 2024

I am iffy about exposing a vendor extension for this. Instead I think it would make more sense to pursue standardization of make_unique-like APIs that support a custom deleter.

How does would that API help? Would it call new [] internally for T[]s and require the custom deleter to eventually call delete[]?

(To be clear I agree about the iffy and don't want to argue for anything here, just asking for my understanding.)

@ldionne
Copy link
Member Author

ldionne commented Oct 1, 2024

How does would that API help? Would it call new [] internally for T[]s and require the custom deleter to eventually call delete[]?

std::make_unique currently looks like

auto ptr = std::make_unique<T[]>(10); // allocated using new T[10]

We know the size of the allocation because it gets passed to the function. The same would be true with a custom deleter, e.g.

auto ptr = std::make_unique<T[]>(10, mydeleter);

If that were the case, we could stash the size passed to make_unique inside the unique_ptr when we have the right ABI configuration. However, this doesn't help the case where we don't have the "fat" ABI configuration at all.

@nico
Copy link
Contributor

nico commented Oct 3, 2024

Looks like the two failing bots are true failures:

stage2 (generic-cxx11, clang-19, clang++-19):

# executed command: /usr/bin/clang++-19 /home/runner/_work/llvm-project/llvm-project/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/op_subscript.runtime.pass.cpp -pthread --target=x86_64-unknown-linux-gnu -nostdinc++ -I /home/runner/_work/llvm-project/llvm-project/build/generic-cxx11/libcxx/test-suite-install/include/c++/v1 -I /home/runner/_work/llvm-project/llvm-project/build/generic-cxx11/libcxx/test-suite-install/include/c++/v1 -I /home/runner/_work/llvm-project/llvm-project/libcxx/test/support -std=c++11 -Werror -Wall -Wctad-maybe-unsupported -Wextra -Wshadow -Wundef -Wunused-template -Wno-unused-command-line-argument -Wno-attributes -Wno-pessimizing-move -Wno-noexcept-type -Wno-atomic-alignment -Wno-reserved-module-identifier -Wdeprecated-copy -Wdeprecated-copy-dtor -Wno-user-defined-literals -Wno-tautological-compare -Wsign-compare -Wunused-variable -Wunused-parameter -Wunreachable-code -Wno-unused-local-typedef -Wno-local-type-template-args -Wno-c++11-extensions -Wno-unknown-pragmas -Wno-pass-failed -Wno-mismatched-new-delete -Wno-redundant-move -Wno-self-move -D_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER -D_LIBCPP_ENABLE_EXPERIMENTAL -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_NONE -Werror=thread-safety -Wuser-defined-warnings -lc++experimental -nostdlib++ -L /home/runner/_work/llvm-project/llvm-project/build/generic-cxx11/libcxx/test-suite-install/lib -Wl,-rpath,/home/runner/_work/llvm-project/llvm-project/build/generic-cxx11/libcxx/test-suite-install/lib -lc++ -latomic -o /home/runner/_work/llvm-project/llvm-project/build/generic-cxx11/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/Output/op_subscript.runtime.pass.cpp.dir/t.tmp.exe
# .---command stderr------------
# | /home/runner/_work/llvm-project/llvm-project/libcxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/op_subscript.runtime.pass.cpp:51:23: error: constexpr function's return type 'void' is not a literal type
# |    51 |   TEST_CONSTEXPR void operator()(T*) const {}
# |       |                       ^
# | 1 error generated.
# `-----------------------------
# error: command failed with exit status: 1

macos (apple-system-hardened, macos-13):

# executed command: /Users/runner/work/llvm-project/llvm-project/.venv/bin/python3.12 /Users/runner/work/llvm-project/llvm-project/libcxx/utils/run.py --execdir /Users/runner/work/llvm-project/llvm-project/build/apple-system-hardened/cxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/Output/assert.subscript.pass.cpp.dir -- /Users/runner/work/llvm-project/llvm-project/build/apple-system-hardened/cxx/test/std/utilities/smartptr/unique.ptr/unique.ptr.class/unique.ptr.observers/Output/assert.subscript.pass.cpp.dir/t.tmp.exe
# .---command stderr------------
# | Failure: EXPECT_DEATH( other[6] ) failed!
# | (reason: <invalid cause (child did not die)>)
# | 
# | child exit code: 5
# | ---------- standard err ----------
# | 
# | ----------------------------------
# | ---------- standard out ----------
# | 
# | ----------------------------------
# | Assertion failed: (( ExpectDeath(DeathCause::Trap, "other[6]", [&]() { (void)(other[6]); }) )), function operator(), file assert.subscript.pass.cpp, line 120.
# `-----------------------------
# error: command failed with exit status: 2

@ldionne
Copy link
Member Author

ldionne commented Oct 3, 2024

Sorry for the delay, I thought I had merged this already. The tests should be good this time around.

@ldionne ldionne merged commit 848b20d into llvm:main Oct 3, 2024
64 checks passed
@ldionne ldionne deleted the review/unique_ptr-harden-custom-deleter branch October 3, 2024 15:46
xgupta pushed a commit to xgupta/llvm-project that referenced this pull request Oct 4, 2024
…llvm#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 llvm#110683
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[libc++] Hardened unique_ptr<T[]> incorrectly handles custom deleters
3 participants