Skip to content

[libc++] Use correct size for deallocation of arrays in shared_ptr #68233

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 4 commits into from
Oct 5, 2023

Conversation

ilya-biryukov
Copy link
Contributor

Fixes #68051.

Current implementation passes the number of _AlignedStorage objects when it calls to allocate and the number of bytes on deallocate. This only applies to allocations that allocate control block and the storage together, i.e. make_shared and allocate_shared.

Found by ASan.

Fixes llvm#68051.

Current implementation passes the number of `_AlignedStorage` objects
when it calls to `allocate` and the number of **bytes** on deallocate.
This only applies to allocations that allocate control block and the
storage together, i.e. `make_shared` and `allocate_shared`.

Found by ASan.
@ilya-biryukov ilya-biryukov added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Oct 4, 2023
@ilya-biryukov ilya-biryukov requested a review from a team as a code owner October 4, 2023 16:45
@ilya-biryukov ilya-biryukov self-assigned this Oct 4, 2023
@llvmbot
Copy link
Member

llvmbot commented Oct 4, 2023

@llvm/pr-subscribers-libcxx

Changes

Fixes #68051.

Current implementation passes the number of _AlignedStorage objects when it calls to allocate and the number of bytes on deallocate. This only applies to allocations that allocate control block and the storage together, i.e. make_shared and allocate_shared.

Found by ASan.


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

2 Files Affected:

  • (modified) libcxx/include/__memory/shared_ptr.h (+3-2)
  • (added) libcxx/test/libcxx/memory/shared_ptr_array.pass.cpp (+25)
diff --git a/libcxx/include/__memory/shared_ptr.h b/libcxx/include/__memory/shared_ptr.h
index 33a1b95a31ddbd5..d9ddb8a17be273f 100644
--- a/libcxx/include/__memory/shared_ptr.h
+++ b/libcxx/include/__memory/shared_ptr.h
@@ -1137,7 +1137,8 @@ struct __unbounded_array_control_block<_Tp[], _Alloc> : __shared_weak_count
         __alloc_.~_Alloc();
         size_t __size = __unbounded_array_control_block::__bytes_for(__count_);
         _AlignedStorage* __storage = reinterpret_cast<_AlignedStorage*>(this);
-        allocator_traits<_StorageAlloc>::deallocate(__tmp, _PointerTraits::pointer_to(*__storage), __size);
+        allocator_traits<_StorageAlloc>::deallocate(
+            __tmp, _PointerTraits::pointer_to(*__storage), __size / sizeof(_AlignedStorage));
     }
 
     _LIBCPP_NO_UNIQUE_ADDRESS _Alloc __alloc_;
@@ -1220,7 +1221,7 @@ struct __bounded_array_control_block<_Tp[_Count], _Alloc>
 
         _ControlBlockAlloc __tmp(__alloc_);
         __alloc_.~_Alloc();
-        allocator_traits<_ControlBlockAlloc>::deallocate(__tmp, _PointerTraits::pointer_to(*this), sizeof(*this));
+        allocator_traits<_ControlBlockAlloc>::deallocate(__tmp, _PointerTraits::pointer_to(*this), 1);
     }
 
     _LIBCPP_NO_UNIQUE_ADDRESS _Alloc __alloc_;
diff --git a/libcxx/test/libcxx/memory/shared_ptr_array.pass.cpp b/libcxx/test/libcxx/memory/shared_ptr_array.pass.cpp
new file mode 100644
index 000000000000000..f885c7344e41b9b
--- /dev/null
+++ b/libcxx/test/libcxx/memory/shared_ptr_array.pass.cpp
@@ -0,0 +1,25 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// REQUIRES: -fsized-deallocation
+// ADDITIONAL_COMPILE_FLAGS: -fsized-deallocation
+
+// This test will fail with ASan if the implementation passes different sizes
+// to corresponding allocation and deallocation functions.
+
+#include <memory>
+
+int main() {
+    std::allocate_shared<int64_t[]>(std::allocator<int64_t>{}, 10);
+    std::make_shared<int64_t[]>(10);
+
+    std::allocate_shared<int64_t[10]>(std::allocator<int64_t>{});
+    std::make_shared<int64_t[10]>();
+}

@github-actions
Copy link

github-actions bot commented Oct 4, 2023

✅ With the latest revision this PR passed the C/C++ code formatter.

@EricWF
Copy link
Member

EricWF commented Oct 4, 2023

I wonder why ASAN wasn't flagging this externally. Any clues?

@EricWF
Copy link
Member

EricWF commented Oct 4, 2023

Could you fix the missing include in the failing test, then I think this is good to go!

Though it would be nice if we could figure out why ASAN wasn't firing here.

Copy link
Member

@EricWF EricWF left a comment

Choose a reason for hiding this comment

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

LGTM after getting the test passing (not your failure, but lets get the CI green).

@@ -1220,7 +1221,7 @@ struct __bounded_array_control_block<_Tp[_Count], _Alloc>

_ControlBlockAlloc __tmp(__alloc_);
__alloc_.~_Alloc();
allocator_traits<_ControlBlockAlloc>::deallocate(__tmp, _PointerTraits::pointer_to(*this), sizeof(*this));
allocator_traits<_ControlBlockAlloc>::deallocate(__tmp, _PointerTraits::pointer_to(*this), 1);
Copy link
Member

Choose a reason for hiding this comment

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

Oof, that's a nasty bug. Thanks for the fix.

Copy link
Member

@ldionne ldionne 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 fix! This is indeed a nasty bug, a bit surprising that it went unnoticed for so long.

@@ -1137,7 +1137,8 @@ struct __unbounded_array_control_block<_Tp[], _Alloc> : __shared_weak_count
__alloc_.~_Alloc();
size_t __size = __unbounded_array_control_block::__bytes_for(__count_);
_AlignedStorage* __storage = reinterpret_cast<_AlignedStorage*>(this);
allocator_traits<_StorageAlloc>::deallocate(__tmp, _PointerTraits::pointer_to(*__storage), __size);
allocator_traits<_StorageAlloc>::deallocate(
__tmp, _PointerTraits::pointer_to(*__storage), __size / sizeof(_AlignedStorage));
Copy link
Member

Choose a reason for hiding this comment

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

Is this not always __count_ + 1? i.e. the number of elements in the array + the size of the control block at the beginning? I guess I'm kinda confused with my own code now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It actually seems to be more complicated.

The aligned storage has the size equal to the alignment of the control block (which is 8 bytes on my machine), so, e.g., we allocate and deallocate 5 elements of AlignedStorage for int[] when __count_ is 0 (as the control block is 40 bytes on my machine).

In any case, I believe that reproducing exactly what gets passed to allocate is the right call here.

Choose a reason for hiding this comment

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

If alignof(size_t) < sizeof(size_t), it will not be __count_ + 1.

…_ptr

- use full signature in main
- return 0
…_ptr

use int instead of int64_t to avoid the need for an include
@ilya-biryukov
Copy link
Contributor Author

I wonder why ASAN wasn't flagging this externally. Any clues?

Thanks for the fix! This is indeed a nasty bug, a bit surprising that it went unnoticed for so long.

I think it's because it requires a rather rare combination of flags. First, we need to enable -fsized-deallocation[1] and build with ASan. In addition, one either needs to use std::boyer_moore_searcher in C++17 (which is a likely a rarely used API) or C++20's std::make_shared with array types (which should be more common, but C++20 is relatively rare itself).

[1]: it is surprising that -fsized-deallocation is not the default yet given that it was part of C++14. There was an attempt to make it the default in August 2023, but it got reverted.

@ilya-biryukov
Copy link
Contributor Author

I'm going to merge the change in as all comments are addressed and this already has LGTM from @EricWF.
The only pending build is for FreeBSD and it came out green last time (there were no significant changes afterwards), the change itself seems relatively straightforward.

I hope I don't break any policies or expectations here, but please let me know if that's the case.

@jyknight
Copy link
Member

Any objection to backporting this to 17.x branch?

@ldionne
Copy link
Member

ldionne commented Oct 13, 2023

No, we should totally do that. Do you want to create the cherry-pick issue?

@ilya-biryukov
Copy link
Contributor Author

Sorry, this slipped off my radar. I've filed #72255.

tru pushed a commit that referenced this pull request Nov 20, 2023
…68233)

Fixes #68051.

Current implementation passes the number of `_AlignedStorage` objects
when it calls to `allocate` and the number of **bytes** on `deallocate`.
This only applies to allocations that allocate control block and the
storage together, i.e. `make_shared` and `allocate_shared`.

Found by ASan under Clang combined with `-fsized-deallocation`.

(cherry picked from commit f722db0)
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.

std::allocate_shared & std::make_shared for arrays causes new-delete-size-mismatch under asan
6 participants