[libc-commits] [libc] [libc] Implement CheckedPtr utility (PR #234646)
via libc-commits
libc-commits at lists.llvm.org
Thu Oct 22 07:49:42 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-libc
Author: Pavel Labath (labath)
<details>
<summary>Changes</summary>
This adds CheckedPtr, a wrapper around cpp::unique_ptr (along with a make_unique helper) that asserts in !NDEBUG builds that the pointer was checked for nullness before it is dereferenced, destroyed or overwritten (e.g., move assignment). This way, a missing check is caught even if the allocation succeeds.
The idea is replace AllocChecker (which was meant to serve a similar purpose, but the checking part didn't materialize so far). See <https://discourse.llvm.org/t/rfc-error-checking-apis-for-memory-allocation-and-management/92511> for more details.
A few notable design choices:
- operator bool() is non-const so the checked flag does not need to be mutable (avoiding data races on concurrent const reads)
- moving a CheckedPtr disarms the source and leaves the destination unchecked (similar to llvm::Error), forcing the new owner to check it
- the class wraps unique_ptr rather than replacing it, as I expect we will use for a non-checked owning pointer (CheckedPtr isn't particularly suitable for long term storage in a member variable).
- the API is intentionally minimal for now (no default constructor, get, release, reset, swap, or nullptr comparisons).
Assisted-by: Gemini
---
Full diff: https://github.com/llvm/llvm-project/pull/234646.diff
4 Files Affected:
- (modified) libc/src/__support/CMakeLists.txt (+15)
- (added) libc/src/__support/checked_ptr.h (+200)
- (modified) libc/test/src/__support/CMakeLists.txt (+14)
- (added) libc/test/src/__support/checked_ptr_test.cpp (+297)
``````````diff
diff --git a/libc/src/__support/CMakeLists.txt b/libc/src/__support/CMakeLists.txt
index dfb1ed0e13ff3..2324af89ca479 100644
--- a/libc/src/__support/CMakeLists.txt
+++ b/libc/src/__support/CMakeLists.txt
@@ -171,6 +171,21 @@ add_header_library(
libc.src.__support.macros.config
)
+add_header_library(
+ checked_ptr
+ HDRS
+ checked_ptr.h
+ DEPENDS
+ .alloc_checker
+ .libc_assert
+ libc.hdr.types.size_t
+ libc.src.__support.CPP.type_traits
+ libc.src.__support.CPP.unique_ptr
+ libc.src.__support.CPP.utility
+ libc.src.__support.macros.attributes
+ libc.src.__support.macros.config
+)
+
add_header_library(
ctype_utils
HDRS
diff --git a/libc/src/__support/checked_ptr.h b/libc/src/__support/checked_ptr.h
new file mode 100644
index 0000000000000..3597ca7b05e3e
--- /dev/null
+++ b/libc/src/__support/checked_ptr.h
@@ -0,0 +1,200 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Smart pointer wrapper around cpp::unique_ptr enforcing null checks.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIBC_SRC___SUPPORT_CHECKED_PTR_H
+#define LLVM_LIBC_SRC___SUPPORT_CHECKED_PTR_H
+
+#include "hdr/types/size_t.h"
+#include "src/__support/CPP/type_traits.h"
+#include "src/__support/CPP/unique_ptr.h"
+#include "src/__support/CPP/utility.h"
+#include "src/__support/alloc-checker.h"
+#include "src/__support/libc_assert.h"
+#include "src/__support/macros/attributes.h"
+#include "src/__support/macros/config.h"
+
+namespace LIBC_NAMESPACE_DECL {
+
+namespace detail {
+
+class LIBC_TRIVIAL_ABI CheckedState {
+#ifndef NDEBUG
+ bool checked = false;
+
+public:
+ LIBC_INLINE constexpr CheckedState() = default;
+
+ // Moving disarms `other` and starts `this` unchecked so the new owner must
+ // explicitly check its value.
+ LIBC_INLINE constexpr CheckedState(CheckedState &&other) {
+ other.checked = true;
+ }
+
+ // Overwriting an existing CheckedState requires it to have been checked,
+ // disarms `other`, and resets `this` to unchecked.
+ LIBC_INLINE constexpr CheckedState &operator=(CheckedState &&other) {
+ assert_checked();
+ // Self-move-assignment leaves the (checked) state unchanged.
+ if (this == &other)
+ return *this;
+ other.checked = true;
+ checked = false;
+ return *this;
+ }
+
+ LIBC_INLINE ~CheckedState() { assert_checked(); }
+
+ LIBC_INLINE constexpr void mark_checked() { checked = true; }
+ LIBC_INLINE constexpr void assert_checked() const { LIBC_ASSERT(checked); }
+#else
+public:
+ LIBC_INLINE constexpr CheckedState() = default;
+ LIBC_INLINE constexpr CheckedState(CheckedState &&) = default;
+ LIBC_INLINE constexpr CheckedState &operator=(CheckedState &&) = default;
+
+ LIBC_INLINE constexpr void mark_checked() {}
+ LIBC_INLINE constexpr void assert_checked() const {}
+#endif
+};
+
+} // namespace detail
+
+/// A smart pointer wrapper around cpp::unique_ptr that enforces (in !NDEBUG
+/// builds) that callers check pointer nullness before accessing or destroying
+/// the pointer.
+///
+/// A pointer (including a null one) counts as checked once its operator bool
+/// has been called. In !NDEBUG builds, the following operations assert that the
+/// pointer has been checked:
+/// - dereferencing (operator*, operator->, operator[]),
+/// - extracting the pointer via take_unique(),
+/// - destruction,
+/// - being overwritten by a move assignment.
+///
+/// Moving from a CheckedPtr disarms the source and leaves the destination
+/// unchecked, forcing the new owner to check it.
+///
+/// In NDEBUG builds, nothing is tracked and CheckedPtr behaves like
+/// cpp::unique_ptr.
+template <typename T, typename Deleter = cpp::default_delete<T>>
+class [[nodiscard]] LIBC_TRIVIAL_ABI CheckedPtr {
+ using PtrType = cpp::unique_ptr<T, Deleter>;
+ LIBC_NO_UNIQUE_ADDRESS detail::CheckedState state;
+ PtrType ptr;
+
+ template <typename U, typename E> friend class CheckedPtr;
+
+public:
+ using deleter_type = typename PtrType::deleter_type;
+ using element_type = typename PtrType::element_type;
+ using pointer = typename PtrType::pointer;
+
+ LIBC_INLINE constexpr explicit CheckedPtr(pointer p) : ptr(p) {}
+ LIBC_INLINE constexpr CheckedPtr(pointer p, const deleter_type &d)
+ : ptr(p, d) {}
+ LIBC_INLINE constexpr CheckedPtr(pointer p, deleter_type &&d)
+ : ptr(p, cpp::move(d)) {}
+
+ LIBC_INLINE constexpr CheckedPtr(CheckedPtr &&other) = default;
+ LIBC_INLINE constexpr CheckedPtr &operator=(CheckedPtr &&other) = default;
+
+ // Conversion move constructor (available iff the underlying cpp::unique_ptr
+ // conversion is)
+ template <typename U, typename E,
+ cpp::enable_if_t<
+ cpp::is_convertible_v<cpp::unique_ptr<U, E>, PtrType>, int> = 0>
+ LIBC_INLINE constexpr CheckedPtr(CheckedPtr<U, E> &&other)
+ : state(cpp::move(other.state)), ptr(cpp::move(other.ptr)) {}
+
+ // Conversion move assignment (available iff the underlying cpp::unique_ptr
+ // assignment is)
+ template <
+ typename U, typename E,
+ cpp::enable_if_t<
+ cpp::is_assignable_v<PtrType &, cpp::unique_ptr<U, E> &&>, int> = 0>
+ LIBC_INLINE constexpr CheckedPtr &operator=(CheckedPtr<U, E> &&other) {
+ state = cpp::move(other.state);
+ ptr = cpp::move(other.ptr);
+ return *this;
+ }
+
+ /// Returns true if the pointer is non-null, and marks it as checked.
+ ///
+ /// This is intentionally non-const: recording the check in a const member
+ /// function would require a mutable flag, and concurrent checks through const
+ /// references would then race on it. As a consequence, a CheckedPtr cannot be
+ /// checked through a const reference (but it can be used through one once it
+ /// has been checked).
+ LIBC_INLINE constexpr explicit operator bool() {
+ state.mark_checked();
+ return static_cast<bool>(ptr);
+ }
+
+ template <typename U = T, cpp::enable_if_t<!cpp::is_array_v<U>, int> = 0>
+ LIBC_INLINE constexpr cpp::add_lvalue_reference_t<U> operator*() const {
+ state.assert_checked();
+ return *ptr;
+ }
+
+ template <typename U = T, cpp::enable_if_t<!cpp::is_array_v<U>, int> = 0>
+ LIBC_INLINE constexpr pointer operator->() const {
+ state.assert_checked();
+ return ptr.get();
+ }
+
+ // The return type depends on U (rather than using element_type) so that it is
+ // not formed for non-array types, e.g. CheckedPtr<void, D>.
+ template <typename U = T, cpp::enable_if_t<cpp::is_array_v<U>, int> = 0>
+ LIBC_INLINE constexpr cpp::remove_extent_t<U> &operator[](size_t i) const {
+ state.assert_checked();
+ return ptr[i];
+ }
+
+ /// Moves the owned pointer into a plain cpp::unique_ptr, leaving this
+ /// CheckedPtr null. Asserts that the pointer has been checked.
+ [[nodiscard]] LIBC_INLINE constexpr PtrType take_unique() {
+ state.assert_checked();
+ return cpp::move(ptr);
+ }
+};
+
+/// Allocates and constructs an object of type T from `args`. Returns a null
+/// CheckedPtr if the allocation fails.
+template <typename T, typename... Args,
+ cpp::enable_if_t<!cpp::is_array_v<T>, int> = 0>
+LIBC_INLINE CheckedPtr<T> make_unique(Args &&...args) {
+ // There is no need to inspect `ac`: the AllocChecker operator new is
+ // noexcept, so on failure no object is constructed and the new-expression
+ // evaluates to nullptr, which the caller is then forced to check for.
+ AllocChecker ac;
+ return CheckedPtr<T>(new (ac) T(cpp::forward<Args>(args)...));
+}
+
+/// Allocates an array of `n` value-initialized elements. T must be an array of
+/// unknown bound (e.g. `make_unique<int[]>(n)`). Returns a null CheckedPtr if
+/// the allocation fails. Note some allocators return a nullptr for zero-sized
+/// allocations (n=0), which makes it impossible to identify an allocation
+/// failure in this case.
+template <
+ typename T,
+ cpp::enable_if_t<cpp::is_same_v<T, cpp::remove_extent_t<T>[]>, int> = 0>
+LIBC_INLINE CheckedPtr<T> make_unique(size_t n) {
+ using Elem = cpp::remove_extent_t<T>;
+ // See the non-array overload for why `ac` is not inspected.
+ AllocChecker ac;
+ return CheckedPtr<T>(new (ac) Elem[n]());
+}
+
+} // namespace LIBC_NAMESPACE_DECL
+
+#endif // LLVM_LIBC_SRC___SUPPORT_CHECKED_PTR_H
diff --git a/libc/test/src/__support/CMakeLists.txt b/libc/test/src/__support/CMakeLists.txt
index 765eec6b42178..947504ea43987 100644
--- a/libc/test/src/__support/CMakeLists.txt
+++ b/libc/test/src/__support/CMakeLists.txt
@@ -122,6 +122,20 @@ add_libc_test(
libc.src.__support.blockstore
)
+add_libc_test(
+ checked_ptr_test
+ SUITE
+ libc-support-tests
+ SRCS
+ checked_ptr_test.cpp
+ DEPENDS
+ libc.src.__support.alloc_checker
+ libc.src.__support.checked_ptr
+ libc.src.__support.CPP.type_traits
+ libc.src.__support.CPP.unique_ptr
+ libc.src.__support.CPP.utility
+)
+
add_libc_test(
endian_internal_test
SUITE
diff --git a/libc/test/src/__support/checked_ptr_test.cpp b/libc/test/src/__support/checked_ptr_test.cpp
new file mode 100644
index 0000000000000..80bb31adbfcd3
--- /dev/null
+++ b/libc/test/src/__support/checked_ptr_test.cpp
@@ -0,0 +1,297 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Unittests for CheckedPtr and make_unique.
+///
+//===----------------------------------------------------------------------===//
+
+#include "src/__support/CPP/type_traits.h"
+#include "src/__support/CPP/unique_ptr.h"
+#include "src/__support/CPP/utility.h"
+#include "src/__support/alloc-checker.h"
+#include "src/__support/checked_ptr.h"
+#include "test/UnitTest/Test.h"
+
+using LIBC_NAMESPACE::AllocChecker;
+using LIBC_NAMESPACE::CheckedPtr;
+using LIBC_NAMESPACE::make_unique;
+using LIBC_NAMESPACE::cpp::is_constructible_v;
+using LIBC_NAMESPACE::cpp::move;
+using LIBC_NAMESPACE::cpp::unique_ptr;
+
+// Verify compile-time properties:
+// - Non-copyable and non-default-constructible.
+// - Explicit conversion to bool is only allowed on non-const lvalues/rvalues.
+static_assert(!is_constructible_v<CheckedPtr<int>>);
+static_assert(!is_constructible_v<CheckedPtr<int>, const CheckedPtr<int> &>);
+static_assert(is_constructible_v<bool, CheckedPtr<int> &>);
+static_assert(!is_constructible_v<bool, const CheckedPtr<int> &>);
+static_assert(is_constructible_v<bool, CheckedPtr<int[]> &>);
+static_assert(!is_constructible_v<bool, const CheckedPtr<int[]> &>);
+
+struct DestructTracker {
+ int *destruct_count = nullptr;
+ DestructTracker() = default;
+ DestructTracker(int *count) : destruct_count(count) {}
+ ~DestructTracker() {
+ if (destruct_count)
+ (*destruct_count)++;
+ }
+};
+
+struct Foo {
+ int val;
+ Foo(int v) : val(v) {}
+};
+
+struct Base {
+ int val;
+ virtual ~Base() = default;
+ Base(int v) : val(v) {}
+};
+
+struct Derived : public Base {
+ Derived(int v) : Base(v) {}
+};
+
+struct CustomDeleter {
+ int *count;
+ void operator()(int *p) const {
+ (*count)++;
+ delete p;
+ }
+};
+
+struct CustomArrayDeleter {
+ int *count;
+ void operator()(int *p) const {
+ if (count)
+ (*count)++;
+ delete[] p;
+ }
+};
+
+TEST(LlvmLibcCheckedPtrTest, BasicMakeUniqueAndAccess) {
+ int destruct_count = 0;
+ {
+ auto ptr = make_unique<DestructTracker>(&destruct_count);
+ ASSERT_TRUE(static_cast<bool>(ptr));
+
+ // Once checked, access via const reference is valid and safe.
+ const auto &cref = ptr;
+ ASSERT_EQ(*cref->destruct_count, 0);
+ ASSERT_EQ(*(*cref).destruct_count, 0);
+ }
+ ASSERT_EQ(destruct_count, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, NullptrCheck) {
+ CheckedPtr<int> ptr(nullptr);
+ ASSERT_FALSE(static_cast<bool>(ptr));
+}
+
+TEST(LlvmLibcCheckedPtrTest, MoveRequiresRecheck) {
+ int destruct_count = 0;
+ {
+ auto ptr1 = make_unique<DestructTracker>(&destruct_count);
+ ASSERT_TRUE(static_cast<bool>(ptr1));
+
+ // Even though ptr1 was checked, moving into ptr2 requires checking ptr2.
+ CheckedPtr<DestructTracker> ptr2(move(ptr1));
+ ASSERT_TRUE(static_cast<bool>(ptr2));
+ ASSERT_EQ(*ptr2->destruct_count, 0);
+ }
+ ASSERT_EQ(destruct_count, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, MoveUncheckedThenCheck) {
+ int destruct_count = 0;
+ {
+ auto ptr1 = make_unique<DestructTracker>(&destruct_count);
+ // Move before checking: ptr1 is disarmed; ptr2 must be checked.
+ CheckedPtr<DestructTracker> ptr2(move(ptr1));
+ ASSERT_TRUE(static_cast<bool>(ptr2));
+ ASSERT_EQ(*ptr2->destruct_count, 0);
+ }
+ ASSERT_EQ(destruct_count, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, MoveAssignment) {
+ int destruct_count1 = 0;
+ int destruct_count2 = 0;
+ {
+ auto ptr1 = make_unique<DestructTracker>(&destruct_count1);
+ auto ptr2 = make_unique<DestructTracker>(&destruct_count2);
+ ASSERT_TRUE(static_cast<bool>(ptr1));
+ ASSERT_TRUE(static_cast<bool>(ptr2));
+
+ ptr2 = move(ptr1);
+ ASSERT_EQ(destruct_count1, 0);
+ ASSERT_EQ(destruct_count2, 1);
+ ASSERT_TRUE(static_cast<bool>(ptr2));
+ }
+ ASSERT_EQ(destruct_count1, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, Conversions) {
+ auto derived = make_unique<Derived>(100);
+ CheckedPtr<Base> base(move(derived));
+ ASSERT_TRUE(static_cast<bool>(base));
+ ASSERT_EQ(base->val, 100);
+
+ auto mut_int = make_unique<int>(42);
+ CheckedPtr<const int> const_int(move(mut_int));
+ ASSERT_TRUE(static_cast<bool>(const_int));
+ ASSERT_EQ(*const_int, 42);
+}
+
+TEST(LlvmLibcCheckedPtrTest, Array) {
+ int destruct_count = 0;
+ {
+ auto ptr = make_unique<DestructTracker[]>(3);
+ ASSERT_TRUE(static_cast<bool>(ptr));
+ ptr[0].destruct_count = &destruct_count;
+ ptr[1].destruct_count = &destruct_count;
+ ptr[2].destruct_count = &destruct_count;
+ ASSERT_EQ(destruct_count, 0);
+ }
+ ASSERT_EQ(destruct_count, 3);
+}
+
+TEST(LlvmLibcCheckedPtrTest, CustomDeleters) {
+ int obj_deletes = 0;
+ {
+ AllocChecker ac;
+ CheckedPtr<int, CustomDeleter> ptr(new (ac) int(7),
+ CustomDeleter{&obj_deletes});
+ ASSERT_TRUE(static_cast<bool>(ptr));
+ ASSERT_EQ(*ptr, 7);
+ ASSERT_EQ(obj_deletes, 0);
+ }
+ ASSERT_EQ(obj_deletes, 1);
+
+ int arr_deletes = 0;
+ {
+ AllocChecker ac;
+ CheckedPtr<int[], CustomArrayDeleter> arr(new (ac) int[2],
+ CustomArrayDeleter{&arr_deletes});
+ ASSERT_TRUE(static_cast<bool>(arr));
+ arr[0] = 10;
+ arr[1] = 20;
+ ASSERT_EQ(arr[0] + arr[1], 30);
+ ASSERT_EQ(arr_deletes, 0);
+ }
+ ASSERT_EQ(arr_deletes, 1);
+}
+
+TEST(LlvmLibcCheckedPtrTest, SelfSwap) {
+ auto ptr = make_unique<int>(1);
+ ASSERT_TRUE(static_cast<bool>(ptr));
+ // Self-move-assignment (which happens as a part of self-swap) must be a
+ // no-op.
+ LIBC_NAMESPACE::cpp::swap(ptr, ptr);
+ ASSERT_TRUE(static_cast<bool>(ptr));
+ ASSERT_EQ(*ptr, 1);
+}
+
+struct VoidDeleter {
+ void operator()(void *) const {}
+};
+
+TEST(LlvmLibcCheckedPtrTest, Void) {
+ CheckedPtr<void, VoidDeleter> ptr(nullptr);
+ ASSERT_FALSE(static_cast<bool>(ptr));
+}
+
+TEST(LlvmLibcCheckedPtrTest, TakeUnique) {
+ int destruct_count = 0;
+ unique_ptr<DestructTracker> owner;
+ {
+ auto ptr = make_unique<DestructTracker>(&destruct_count);
+ ASSERT_TRUE(static_cast<bool>(ptr));
+ owner = ptr.take_unique();
+ // `ptr` is now null and still checked, so destroying it must neither delete
+ // the object nor assert.
+ }
+ ASSERT_EQ(destruct_count, 0);
+ ASSERT_TRUE(static_cast<bool>(owner));
+ owner.reset();
+ ASSERT_EQ(destruct_count, 1);
+}
+
+#if !defined(NDEBUG) && LIBC_TEST_SUBPROCESS_TESTS
+
+#if defined(LIBC_FULL_BUILD) && !defined(LIBC_COPT_USE_C_ASSERT)
+#define EXPECT_ASSERT_FAILS(stmt) EXPECT_EXITS(stmt, 255)
+#else
+#define EXPECT_ASSERT_FAILS(stmt) EXPECT_DEATH(stmt, WITH_SIGNAL(-1))
+#endif
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedDestructionDies) {
+ EXPECT_ASSERT_FAILS([] {
+ auto ptr = make_unique<int>(42);
+ (void)ptr;
+ });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, MoveFromCheckedRequiresRecheckOrDies) {
+ EXPECT_ASSERT_FAILS([] {
+ auto ptr1 = make_unique<int>(42);
+ (void)static_cast<bool>(ptr1);
+ CheckedPtr<int> ptr2(move(ptr1));
+ (void)*ptr2;
+ });
+}
+
+// In the tests below, the pointer is checked after the operation under test, so
+// that its destruction cannot be the thing that triggers the assertion.
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedDereferenceDies) {
+ EXPECT_ASSERT_FAILS([] {
+ auto ptr = make_unique<int>(42);
+ (void)*ptr;
+ (void)static_cast<bool>(ptr);
+ });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedArrowDies) {
+ EXPECT_ASSERT_FAILS([] {
+ auto ptr = make_unique<Foo>(42);
+ (void)ptr->val;
+ (void)static_cast<bool>(ptr);
+ });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedArraySubscriptDies) {
+ EXPECT_ASSERT_FAILS([] {
+ auto ptr = make_unique<int[]>(3);
+ (void)ptr[0];
+ (void)static_cast<bool>(ptr);
+ });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, OverwritingUncheckedViaMoveAssignmentDies) {
+ EXPECT_ASSERT_FAILS([] {
+ auto ptr1 = make_unique<int>(1);
+ auto ptr2 = make_unique<int>(2);
+ (void)static_cast<bool>(ptr2);
+ ptr1 = move(ptr2);
+ (void)static_cast<bool>(ptr1);
+ });
+}
+
+TEST(LlvmLibcCheckedPtrDeathTest, UncheckedTakeUniqueDies) {
+ EXPECT_ASSERT_FAILS([] {
+ auto ptr = make_unique<int>(42);
+ (void)ptr.take_unique();
+ (void)static_cast<bool>(ptr);
+ });
+}
+
+#endif // !defined(NDEBUG) && LIBC_TEST_SUBPROCESS_TESTS
``````````
</details>
https://github.com/llvm/llvm-project/pull/234646
More information about the libc-commits
mailing list