[libcxx-commits] [libcxx] [libcxx] Implementing hazard ptrs P2530 (PR #218218)

Nikita Grivin via libcxx-commits libcxx-commits at lists.llvm.org
Sun Aug 23 01:26:05 PDT 2026


https://github.com/MindSpectre updated https://github.com/llvm/llvm-project/pull/218218

>From ff4e96ee6e2ab4a22b93518cf8563271e2fc684e Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 02:05:23 +0300
Subject: [PATCH 01/17] [libc++] hazard_pointer: add <hazard_pointer> headers
 and the dylib ABI seam

---
 libcxx/include/CMakeLists.txt                 |   4 +
 libcxx/include/__configuration/availability.h |   5 +
 libcxx/include/__hazard_pointer/domain.h      |  93 +++++++++++++
 .../include/__hazard_pointer/hazard_pointer.h | 129 ++++++++++++++++++
 .../hazard_pointer_obj_base.h                 |  95 +++++++++++++
 libcxx/include/hazard_pointer                 |  75 ++++++++++
 libcxx/include/module.modulemap.in            |   9 ++
 .../saferecl/saferecl.hp/abi.compile.pass.cpp |  44 ++++++
 .../saferecl.hp.base/types.compile.pass.cpp   |  75 ++++++++++
 libcxx/utils/libcxx/header_information.py     |   1 -
 10 files changed, 529 insertions(+), 1 deletion(-)
 create mode 100644 libcxx/include/__hazard_pointer/domain.h
 create mode 100644 libcxx/include/__hazard_pointer/hazard_pointer.h
 create mode 100644 libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h
 create mode 100644 libcxx/include/hazard_pointer
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp

diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
index cb7ae820bc012..b01d83ed894f9 100644
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -481,6 +481,9 @@ set(files
   __fwd/variant.h
   __fwd/vector.h
   __hash_table
+  __hazard_pointer/domain.h
+  __hazard_pointer/hazard_pointer.h
+  __hazard_pointer/hazard_pointer_obj_base.h
   __ios/fpos.h
   __iterator/access.h
   __iterator/advance.h
@@ -1041,6 +1044,7 @@ set(files
   fstream
   functional
   future
+  hazard_pointer
   initializer_list
   iomanip
   ios
diff --git a/libcxx/include/__configuration/availability.h b/libcxx/include/__configuration/availability.h
index d0788083c0128..5301d430f1c3b 100644
--- a/libcxx/include/__configuration/availability.h
+++ b/libcxx/include/__configuration/availability.h
@@ -264,6 +264,11 @@
 #define _LIBCPP_AVAILABILITY_HAS_BAD_VARIANT_ACCESS_WITH_MSG_KEY_FUNCTION _LIBCPP_INTRODUCED_IN_LLVM_23
 // No attribute, since we've had bad_variant_access in the headers before
 
+// This controls the availability of the C++26 <hazard_pointer> facility. The hazard pointer
+// domain (records, retired lists, reclamation) lives in the built library.
+#define _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER _LIBCPP_INTRODUCED_IN_LLVM_23
+#define _LIBCPP_AVAILABILITY_HAZARD_POINTER _LIBCPP_INTRODUCED_IN_LLVM_23_ATTRIBUTE
+
 // This controls the availability of floating-point std::from_chars functions.
 // These overloads were added later than the integer overloads.
 #define _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT _LIBCPP_INTRODUCED_IN_LLVM_20
diff --git a/libcxx/include/__hazard_pointer/domain.h b/libcxx/include/__hazard_pointer/domain.h
new file mode 100644
index 0000000000000..fff6e6b9c428a
--- /dev/null
+++ b/libcxx/include/__hazard_pointer/domain.h
@@ -0,0 +1,93 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef _LIBCPP___HAZARD_POINTER_DOMAIN_H
+#define _LIBCPP___HAZARD_POINTER_DOMAIN_H
+
+#include <__atomic/atomic.h>
+#include <__atomic/fence.h>
+#include <__atomic/memory_order.h>
+#include <__config>
+
+#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#  pragma GCC system_header
+#endif
+
+#if _LIBCPP_STD_VER >= 26 && _LIBCPP_HAS_THREADS
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+// ABI contract between <hazard_pointer> and the built library (frozen since LLVM 23)
+// ---------------------------------------------------------------------------------
+// - __hazard_pointer_slot is the first base of every hazard-pointer record owned by the library. It is
+//   the only part of a record the headers know: try_protect() and reset_protection() store into
+//   __value_ inline. sizeof(__hazard_pointer_slot) == sizeof(void*).
+// - __hazard_pointer_obj_node is the private base of hazard_pointer_obj_base<T, D>. Retired lists are
+//   intrusive, so the node lives inside every hazard-protectable object and its layout is part of the
+//   user's object layout: {__next_, __reclaim_}, 2 * sizeof(void*). __next_ == this means "not retired".
+// - __hazard_pointer_acquire(), __hazard_pointer_release() and __hazard_pointer_retire() are the only
+//   entry points. acquire() returns an unassociated slot (__value_ == nullptr) which stays valid for the
+//   whole process lifetime (records are never freed). release() ends the epoch itself (release-store of
+//   nullptr) and returns the slot to the pool; the headers never touch a slot after handing it back.
+//   retire() takes a node whose __reclaim_ is set, issues the retire-side seq_cst fence, publishes the
+//   node and may run reclamation (and thus deleters) synchronously on the calling thread. Deleters are
+//   never invoked from acquire()/release() and never on a background thread. All three entry points are
+//   callable during static destruction and thread teardown.
+// - Reader protocol (v1, inline in the headers): reset_protection(p) is a release-store of the address
+//   of p's node into __value_; try_protect() is: store, __hazard_pointer_reader_fence() (seq_cst),
+//   acquire-reload of src, compare. The library issues a seq_cst fence before publishing retired objects
+//   and before scanning slots, and loads slots with acquire ordering (or relaxed followed by an acquire
+//   fence). A future asymmetric-fence scheme replaces __hazard_pointer_reader_fence() under a new
+//   _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER_ASYMMETRIC_FENCE macro; the v1 protocol stays correct
+//   against any library that keeps the obligations above.
+
+struct __hazard_pointer_slot {
+  atomic<const void*> __value_;
+};
+
+struct __hazard_pointer_obj_node {
+  using __reclaim_fn _LIBCPP_NODEBUG = void (*)(__hazard_pointer_obj_node*) noexcept;
+
+  __hazard_pointer_obj_node* __next_;
+  __reclaim_fn __reclaim_;
+
+  // Every construction yields a fresh, not-retired node and assignment never touches the node: a copy
+  // of a retired-but-still-protected object must itself be a fresh object.
+  _LIBCPP_HIDE_FROM_ABI constexpr __hazard_pointer_obj_node() noexcept : __next_(this), __reclaim_(nullptr) {}
+  _LIBCPP_HIDE_FROM_ABI constexpr __hazard_pointer_obj_node(const __hazard_pointer_obj_node&) noexcept
+      : __next_(this), __reclaim_(nullptr) {}
+  _LIBCPP_HIDE_FROM_ABI constexpr __hazard_pointer_obj_node& operator=(const __hazard_pointer_obj_node&) noexcept {
+    return *this;
+  }
+  _LIBCPP_HIDE_FROM_ABI ~__hazard_pointer_obj_node() = default;
+};
+
+// The reader-side fence of try_protect(): the one place to change when asymmetric fences arrive.
+_LIBCPP_HIDE_FROM_ABI inline void __hazard_pointer_reader_fence() noexcept {
+  std::atomic_thread_fence(memory_order_seq_cst);
+}
+
+_LIBCPP_BEGIN_EXPLICIT_ABI_ANNOTATIONS
+
+[[__gnu__::__returns_nonnull__]] _LIBCPP_AVAILABILITY_HAZARD_POINTER _LIBCPP_EXPORTED_FROM_ABI __hazard_pointer_slot*
+__hazard_pointer_acquire();
+
+_LIBCPP_AVAILABILITY_HAZARD_POINTER _LIBCPP_EXPORTED_FROM_ABI void
+__hazard_pointer_release([[__gnu__::__nonnull__]] __hazard_pointer_slot* __slot) noexcept;
+
+_LIBCPP_AVAILABILITY_HAZARD_POINTER _LIBCPP_EXPORTED_FROM_ABI void
+__hazard_pointer_retire([[__gnu__::__nonnull__]] __hazard_pointer_obj_node* __node) noexcept;
+
+_LIBCPP_END_EXPLICIT_ABI_ANNOTATIONS
+
+_LIBCPP_END_NAMESPACE_STD
+
+#endif // _LIBCPP_STD_VER >= 26 && _LIBCPP_HAS_THREADS
+
+#endif // _LIBCPP___HAZARD_POINTER_DOMAIN_H
diff --git a/libcxx/include/__hazard_pointer/hazard_pointer.h b/libcxx/include/__hazard_pointer/hazard_pointer.h
new file mode 100644
index 0000000000000..136569f6b922e
--- /dev/null
+++ b/libcxx/include/__hazard_pointer/hazard_pointer.h
@@ -0,0 +1,129 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef _LIBCPP___HAZARD_POINTER_HAZARD_POINTER_H
+#define _LIBCPP___HAZARD_POINTER_HAZARD_POINTER_H
+
+#include <__assert>
+#include <__atomic/atomic.h>
+#include <__atomic/memory_order.h>
+#include <__config>
+#include <__cstddef/nullptr_t.h>
+#include <__hazard_pointer/domain.h>
+#include <__hazard_pointer/hazard_pointer_obj_base.h>
+#include <__memory/addressof.h>
+#include <__utility/exchange.h>
+#include <__utility/swap.h>
+
+#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#  pragma GCC system_header
+#endif
+
+_LIBCPP_PUSH_MACROS
+#include <__undef_macros>
+
+#if _LIBCPP_STD_VER >= 26 && _LIBCPP_HAS_THREADS
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+class _LIBCPP_AVAILABILITY_HAZARD_POINTER hazard_pointer {
+  __hazard_pointer_slot* __slot_ = nullptr; // empty <=> nullptr
+
+  friend _LIBCPP_HIDE_FROM_ABI hazard_pointer make_hazard_pointer();
+
+  _LIBCPP_HIDE_FROM_ABI explicit hazard_pointer(__hazard_pointer_slot* __slot) noexcept : __slot_(__slot) {}
+
+  // The address a hazard pointer holds for *__ptr: that of its (private) node base. Deduces D through the
+  // derived-to-base conversion; hazard_pointer is a friend of hazard_pointer_obj_base, so the conversion to
+  // the private base is accessible here. A null T* yields a null node.
+  template <class _Tp, class _Dp>
+  _LIBCPP_HIDE_FROM_ABI static const __hazard_pointer_obj_node*
+  __node_of(const hazard_pointer_obj_base<_Tp, _Dp>* __ptr) noexcept {
+    return __ptr;
+  }
+
+public:
+  _LIBCPP_HIDE_FROM_ABI hazard_pointer() noexcept = default;
+
+  _LIBCPP_HIDE_FROM_ABI hazard_pointer(hazard_pointer&& __other) noexcept
+      : __slot_(std::exchange(__other.__slot_, nullptr)) {}
+
+  _LIBCPP_HIDE_FROM_ABI hazard_pointer& operator=(hazard_pointer&& __other) noexcept {
+    if (this != std::addressof(__other)) {
+      if (__slot_ != nullptr)
+        std::__hazard_pointer_release(__slot_);
+      __slot_ = std::exchange(__other.__slot_, nullptr);
+    }
+    return *this;
+  }
+
+  _LIBCPP_HIDE_FROM_ABI ~hazard_pointer() {
+    if (__slot_ != nullptr)
+      std::__hazard_pointer_release(__slot_);
+  }
+
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI bool empty() const noexcept { return __slot_ == nullptr; }
+
+  template <class _Tp>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI _Tp* protect(const atomic<_Tp*>& __src) noexcept {
+    _Tp* __ptr = __src.load(memory_order_relaxed);
+    while (!try_protect(__ptr, __src)) {
+    }
+    return __ptr;
+  }
+
+  template <class _Tp>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI bool try_protect(_Tp*& __ptr, const atomic<_Tp*>& __src) noexcept {
+    static_assert(
+        __hazard_protectable<_Tp>,
+        "hazard_pointer::try_protect(): T must be a hazard-protectable type (a class with exactly one public, "
+        "non-virtual hazard_pointer_obj_base<T, D> base)");
+    _Tp* __old = __ptr;
+    reset_protection(__old);
+    std::__hazard_pointer_reader_fence();
+    __ptr = __src.load(memory_order_acquire);
+    if (__old != __ptr) {
+      reset_protection();
+      return false;
+    }
+    return true;
+  }
+
+  template <class _Tp>
+  _LIBCPP_HIDE_FROM_ABI void reset_protection(const _Tp* __ptr) noexcept {
+    static_assert(__hazard_protectable<_Tp>,
+                  "hazard_pointer: T must be a hazard-protectable type (a class with exactly one public, "
+                  "non-virtual hazard_pointer_obj_base<T, D> base)");
+    if constexpr (__hazard_protectable<_Tp>) {
+      _LIBCPP_ASSERT_NON_NULL(__slot_ != nullptr, "hazard_pointer::reset_protection(): hazard_pointer is empty");
+      __slot_->__value_.store(__node_of(__ptr), memory_order_release);
+    }
+  }
+
+  _LIBCPP_HIDE_FROM_ABI void reset_protection(nullptr_t = nullptr) noexcept {
+    _LIBCPP_ASSERT_NON_NULL(__slot_ != nullptr, "hazard_pointer::reset_protection(): hazard_pointer is empty");
+    __slot_->__value_.store(nullptr, memory_order_release);
+  }
+
+  _LIBCPP_HIDE_FROM_ABI void swap(hazard_pointer& __other) noexcept { std::swap(__slot_, __other.__slot_); }
+};
+
+[[nodiscard]] _LIBCPP_HIDE_FROM_ABI _LIBCPP_AVAILABILITY_HAZARD_POINTER inline hazard_pointer make_hazard_pointer() {
+  return hazard_pointer(std::__hazard_pointer_acquire());
+}
+
+_LIBCPP_HIDE_FROM_ABI inline void swap(hazard_pointer& __a, hazard_pointer& __b) noexcept { __a.swap(__b); }
+
+_LIBCPP_END_NAMESPACE_STD
+
+#endif // _LIBCPP_STD_VER >= 26 && _LIBCPP_HAS_THREADS
+
+_LIBCPP_POP_MACROS
+
+#endif // _LIBCPP___HAZARD_POINTER_HAZARD_POINTER_H
diff --git a/libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h b/libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h
new file mode 100644
index 0000000000000..25f81f82fc186
--- /dev/null
+++ b/libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h
@@ -0,0 +1,95 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef _LIBCPP___HAZARD_POINTER_HAZARD_POINTER_OBJ_BASE_H
+#define _LIBCPP___HAZARD_POINTER_HAZARD_POINTER_OBJ_BASE_H
+
+#include <__assert>
+#include <__concepts/same_as.h>
+#include <__config>
+#include <__hazard_pointer/domain.h>
+#include <__memory/unique_ptr.h>
+#include <__type_traits/is_assignable.h>
+#include <__type_traits/is_class.h>
+#include <__type_traits/is_constructible.h>
+#include <__utility/move.h>
+
+#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#  pragma GCC system_header
+#endif
+
+_LIBCPP_PUSH_MACROS
+#include <__undef_macros>
+
+#if _LIBCPP_STD_VER >= 26 && _LIBCPP_HAS_THREADS
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+class hazard_pointer;
+
+template <class _Tp, class _Dp = default_delete<_Tp>>
+class hazard_pointer_obj_base;
+
+// Deduction-only helper (declared, never defined): given a T*, find *the* hazard_pointer_obj_base base
+// through a derived-to-base conversion. Deduction fails when there is no such base or more than one
+// (ambiguous), the return type is ill-formed when the base is virtual, and the conversion is inaccessible
+// when the base is not public.
+template <class _Tp, class _Dp>
+auto __hazard_pointer_protectable_check(hazard_pointer_obj_base<_Tp, _Dp>* __base)
+    -> decltype(static_cast<_Tp*>(__base));
+
+// [saferecl.hp.general]/2: a class type T is hazard-protectable if it has exactly one base class of type
+// hazard_pointer_obj_base<T, D> for some D, that base is public and non-virtual, and it has no base classes
+// of type hazard_pointer_obj_base<T2, D2> for any other combination T2, D2.
+template <class _Tp>
+concept __hazard_protectable = is_class_v<_Tp> && requires(_Tp* __p) {
+  { std::__hazard_pointer_protectable_check(__p) } -> same_as<_Tp*>;
+};
+
+template <class _Tp, class _Dp>
+class _LIBCPP_AVAILABILITY_HAZARD_POINTER hazard_pointer_obj_base : private __hazard_pointer_obj_node {
+  static_assert(is_default_constructible_v<_Dp> && is_move_assignable_v<_Dp>,
+                "hazard_pointer_obj_base<T, D>: D must be Cpp17DefaultConstructible and Cpp17MoveAssignable");
+
+  // hazard_pointer::__node_of performs the derived-to-private-base conversion.
+  friend class hazard_pointer;
+
+  _LIBCPP_NO_UNIQUE_ADDRESS _Dp __deleter_ = _Dp();
+
+public:
+  _LIBCPP_HIDE_FROM_ABI void retire(_Dp __d = _Dp()) noexcept {
+    static_assert(__hazard_protectable<_Tp>,
+                  "hazard_pointer_obj_base<T, D>::retire(): T must be a hazard-protectable type (a class with exactly "
+                  "one public, non-virtual hazard_pointer_obj_base<T, D> base)");
+    _LIBCPP_ASSERT_VALID_DEALLOCATION(
+        this->__next_ == this, "hazard_pointer_obj_base::retire(): object has already been retired");
+    __deleter_       = std::move(__d);
+    this->__reclaim_ = [](__hazard_pointer_obj_node* __node) noexcept {
+      hazard_pointer_obj_base* __base = static_cast<hazard_pointer_obj_base*>(__node);
+      __base->__deleter_(static_cast<_Tp*>(__base));
+    };
+    std::__hazard_pointer_retire(this);
+  }
+
+protected:
+  _LIBCPP_HIDE_FROM_ABI hazard_pointer_obj_base()                                          = default;
+  _LIBCPP_HIDE_FROM_ABI hazard_pointer_obj_base(const hazard_pointer_obj_base&)            = default;
+  _LIBCPP_HIDE_FROM_ABI hazard_pointer_obj_base(hazard_pointer_obj_base&&)                 = default;
+  _LIBCPP_HIDE_FROM_ABI hazard_pointer_obj_base& operator=(const hazard_pointer_obj_base&) = default;
+  _LIBCPP_HIDE_FROM_ABI hazard_pointer_obj_base& operator=(hazard_pointer_obj_base&&)      = default;
+  _LIBCPP_HIDE_FROM_ABI ~hazard_pointer_obj_base()                                         = default;
+};
+
+_LIBCPP_END_NAMESPACE_STD
+
+#endif // _LIBCPP_STD_VER >= 26 && _LIBCPP_HAS_THREADS
+
+_LIBCPP_POP_MACROS
+
+#endif // _LIBCPP___HAZARD_POINTER_HAZARD_POINTER_OBJ_BASE_H
diff --git a/libcxx/include/hazard_pointer b/libcxx/include/hazard_pointer
new file mode 100644
index 0000000000000..db3ccdbd2be85
--- /dev/null
+++ b/libcxx/include/hazard_pointer
@@ -0,0 +1,75 @@
+// -*- C++ -*-
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef _LIBCPP_HAZARD_POINTER
+#define _LIBCPP_HAZARD_POINTER
+
+/*
+    hazard_pointer synopsis
+
+namespace std {
+  // [saferecl.hp.base], class template hazard_pointer_obj_base
+  template<class T, class D = default_delete<T>>
+  class hazard_pointer_obj_base {                          // since C++26
+  public:
+    void retire(D d = D()) noexcept;
+  protected:
+    hazard_pointer_obj_base() = default;
+    hazard_pointer_obj_base(const hazard_pointer_obj_base&) = default;
+    hazard_pointer_obj_base(hazard_pointer_obj_base&&) = default;
+    hazard_pointer_obj_base& operator=(const hazard_pointer_obj_base&) = default;
+    hazard_pointer_obj_base& operator=(hazard_pointer_obj_base&&) = default;
+    ~hazard_pointer_obj_base() = default;
+  private:
+    D deleter;      // exposition only
+  };
+
+  // [saferecl.hp.holder], class hazard_pointer
+  class hazard_pointer {                                   // since C++26
+  public:
+    hazard_pointer() noexcept;
+    hazard_pointer(hazard_pointer&&) noexcept;
+    hazard_pointer& operator=(hazard_pointer&&) noexcept;
+    ~hazard_pointer();
+
+    bool empty() const noexcept;
+    template<class T> T* protect(const atomic<T*>& src) noexcept;
+    template<class T> bool try_protect(T*& ptr, const atomic<T*>& src) noexcept;
+    template<class T> void reset_protection(const T* ptr) noexcept;
+    void reset_protection(nullptr_t = nullptr) noexcept;
+    void swap(hazard_pointer&) noexcept;
+  };
+
+  // [saferecl.hp.holder.nonmem], non-member functions
+  hazard_pointer make_hazard_pointer();                    // since C++26
+  void swap(hazard_pointer&, hazard_pointer&) noexcept;    // since C++26
+}
+
+*/
+
+#if __cplusplus < 201103L && defined(_LIBCPP_USE_FROZEN_CXX03_HEADERS)
+#  include <__cxx03/__config>
+#else
+#  include <__config>
+
+#  if _LIBCPP_HAS_THREADS
+
+#    include <__hazard_pointer/domain.h>
+#    include <__hazard_pointer/hazard_pointer.h>
+#    include <__hazard_pointer/hazard_pointer_obj_base.h>
+#    include <version>
+
+#    if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#      pragma GCC system_header
+#    endif
+
+#  endif // _LIBCPP_HAS_THREADS
+#endif // __cplusplus < 201103L && defined(_LIBCPP_USE_FROZEN_CXX03_HEADERS)
+
+#endif // _LIBCPP_HAZARD_POINTER
diff --git a/libcxx/include/module.modulemap.in b/libcxx/include/module.modulemap.in
index b11055940a82c..5b27661abed4d 100644
--- a/libcxx/include/module.modulemap.in
+++ b/libcxx/include/module.modulemap.in
@@ -1469,6 +1469,15 @@ module std [system] {
     export *
   }
 
+  module hazard_pointer {
+    module domain                         { header "__hazard_pointer/domain.h" }
+    module hazard_pointer                 { header "__hazard_pointer/hazard_pointer.h" }
+    module hazard_pointer_obj_base        { header "__hazard_pointer/hazard_pointer_obj_base.h" }
+
+    header "hazard_pointer"
+    export *
+  }
+
   module initializer_list {
     header "initializer_list"
     export *
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
new file mode 100644
index 0000000000000..3e38e4d09b63f
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
@@ -0,0 +1,44 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+
+// <hazard_pointer>
+
+// Ensure that we never change the size or alignment of the ABI-visible hazard pointer types.
+// hazard_pointer is one pointer to a slot; the per-object node is {next, reclaim}; the deleter of
+// hazard_pointer_obj_base uses [[no_unique_address]] so an empty deleter costs nothing.
+
+#include <hazard_pointer>
+#include <memory>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+struct StatefulDeleter {
+  void* state;
+  void operator()(struct Big*) const noexcept {}
+};
+struct Big : std::hazard_pointer_obj_base<Big, StatefulDeleter> {};
+
+static_assert(sizeof(std::hazard_pointer) == sizeof(void*));
+static_assert(alignof(std::hazard_pointer) == alignof(void*));
+
+static_assert(sizeof(std::__hazard_pointer_slot) == sizeof(void*));
+static_assert(alignof(std::__hazard_pointer_slot) == alignof(void*));
+
+static_assert(sizeof(std::__hazard_pointer_obj_node) == 2 * sizeof(void*));
+static_assert(alignof(std::__hazard_pointer_obj_node) == alignof(void*));
+
+static_assert(sizeof(std::hazard_pointer_obj_base<Node>) == 2 * sizeof(void*));
+static_assert(alignof(std::hazard_pointer_obj_base<Node>) == alignof(void*));
+static_assert(sizeof(Node) == 2 * sizeof(void*));
+
+static_assert(sizeof(std::hazard_pointer_obj_base<Big, StatefulDeleter>) == 3 * sizeof(void*));
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp
new file mode 100644
index 0000000000000..7f40ca7a69636
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp
@@ -0,0 +1,75 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+
+// <hazard_pointer>
+
+// template<class T, class D = default_delete<T>> class hazard_pointer_obj_base;
+//   void retire(D d = D()) noexcept;
+//   protected: defaulted special members
+
+#include <hazard_pointer>
+#include <memory>
+#include <type_traits>
+#include <utility>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+struct Deleter {
+  void operator()(struct WithDeleter*) const noexcept {}
+};
+struct WithDeleter : std::hazard_pointer_obj_base<WithDeleter, Deleter> {};
+
+// The default deleter is default_delete<T>.
+static_assert(std::is_base_of_v<std::hazard_pointer_obj_base<Node, std::default_delete<Node>>, Node>);
+
+// retire is noexcept and takes D by value with a default argument.
+static_assert(noexcept(std::declval<Node&>().retire()));
+static_assert(noexcept(std::declval<Node&>().retire(std::default_delete<Node>())));
+static_assert(noexcept(std::declval<WithDeleter&>().retire(Deleter())));
+static_assert(std::is_same_v<decltype(std::declval<Node&>().retire()), void>);
+
+// The special members are protected: not usable from outside, but usable by the derived class.
+static_assert(!std::is_default_constructible_v<std::hazard_pointer_obj_base<Node>>);
+static_assert(!std::is_copy_constructible_v<std::hazard_pointer_obj_base<Node>>);
+static_assert(!std::is_destructible_v<std::hazard_pointer_obj_base<Node>>);
+static_assert(std::is_default_constructible_v<Node>);
+static_assert(std::is_copy_constructible_v<Node>);
+static_assert(std::is_move_constructible_v<Node>);
+static_assert(std::is_copy_assignable_v<Node>);
+static_assert(std::is_move_assignable_v<Node>);
+static_assert(std::is_destructible_v<Node>);
+
+// class hazard_pointer: move-only, all documented members present with the right noexcept-ness.
+static_assert(std::is_nothrow_default_constructible_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_move_constructible_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_move_assignable_v<std::hazard_pointer>);
+static_assert(!std::is_copy_constructible_v<std::hazard_pointer>);
+static_assert(!std::is_copy_assignable_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_swappable_v<std::hazard_pointer>);
+static_assert(noexcept(std::declval<const std::hazard_pointer&>().empty()));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().protect(std::declval<const std::atomic<Node*>&>())));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().try_protect(
+    std::declval<Node*&>(), std::declval<const std::atomic<Node*>&>())));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection(std::declval<const Node*>())));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection()));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection(nullptr)));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().swap(std::declval<std::hazard_pointer&>())));
+static_assert(noexcept(std::swap(std::declval<std::hazard_pointer&>(), std::declval<std::hazard_pointer&>())));
+static_assert(!noexcept(std::make_hazard_pointer()));
+static_assert(std::is_same_v<decltype(std::make_hazard_pointer()), std::hazard_pointer>);
+static_assert(
+    std::is_same_v<decltype(std::declval<std::hazard_pointer&>().protect(std::declval<const std::atomic<Node*>&>())),
+                   Node*>);
+static_assert(std::is_same_v<decltype(std::declval<std::hazard_pointer&>().try_protect(
+                                 std::declval<Node*&>(), std::declval<const std::atomic<Node*>&>())),
+                             bool>);
diff --git a/libcxx/utils/libcxx/header_information.py b/libcxx/utils/libcxx/header_information.py
index 5e4c98675e41b..4408b3b121e15 100644
--- a/libcxx/utils/libcxx/header_information.py
+++ b/libcxx/utils/libcxx/header_information.py
@@ -165,7 +165,6 @@ def __hash__(self) -> int:
 headers_not_available = list(map(Header, [
     "debugging",
     "generator",
-    "hazard_pointer",
     "inplace_vector",
     "linalg",
     "rcu",

>From 2396834b0b7970bfc9c77a81bd01b290b59805d4 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 02:20:34 +0300
Subject: [PATCH 02/17] [libc++] hazard_pointer: engine part 1 (records, grow
 array, available list, thread cache, acquire/release)

---
 libcxx/src/CMakeLists.txt                     |   1 +
 libcxx/src/hazard_pointer.cpp                 | 436 ++++++++++++++++++
 .../assign.move.pass.cpp                      |  78 ++++
 .../saferecl.hp.holder.ctor/default.pass.cpp  |  28 ++
 .../saferecl.hp.holder.ctor/dtor.pass.cpp     |  50 ++
 .../saferecl.hp.holder.ctor/move.pass.cpp     |  51 ++
 .../saferecl.hp.holder.mem/empty.pass.cpp     |  32 ++
 .../saferecl.hp.holder.mem/protect.pass.cpp   |  61 +++
 .../reset_protection.pass.cpp                 |  42 ++
 .../swap.member.pass.cpp                      |  60 +++
 .../try_protect.pass.cpp                      |  66 +++
 .../make_hazard_pointer.pass.cpp              |  44 ++
 .../swap.free.pass.cpp                        |  37 ++
 13 files changed, 986 insertions(+)
 create mode 100644 libcxx/src/hazard_pointer.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/default.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/empty.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/reset_protection.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/swap.member.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/try_protect.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.nonmem/make_hazard_pointer.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.nonmem/swap.free.pass.cpp

diff --git a/libcxx/src/CMakeLists.txt b/libcxx/src/CMakeLists.txt
index de7817ad69f26..d609f881eada6 100644
--- a/libcxx/src/CMakeLists.txt
+++ b/libcxx/src/CMakeLists.txt
@@ -67,6 +67,7 @@ if (LIBCXX_ENABLE_THREADS)
     condition_variable_destructor.cpp
     condition_variable.cpp
     future.cpp
+    hazard_pointer.cpp
     mutex_destructor.cpp
     mutex.cpp
     shared_mutex.cpp
diff --git a/libcxx/src/hazard_pointer.cpp b/libcxx/src/hazard_pointer.cpp
new file mode 100644
index 0000000000000..3e93c3b206a1f
--- /dev/null
+++ b/libcxx/src/hazard_pointer.cpp
@@ -0,0 +1,436 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include <__hazard_pointer/domain.h>
+
+#include <__assert>
+#include <__atomic/atomic.h>
+#include <__atomic/fence.h>
+#include <__atomic/memory_order.h>
+#include <__bit/bit_ceil.h>
+#include <__chrono/duration.h>
+#include <__chrono/steady_clock.h>
+#include <__mutex/once_flag.h>
+#include <__thread/support.h>
+#include <cstddef>
+#include <cstdint>
+#include <new>
+
+// The engine behind <hazard_pointer>: a port of Menagerie's Folly-baseline hazard pointer domain
+// (itself a port of folly::hazptr, untagged path only), reduced to the single default domain the
+// Standard specifies. Nothing in this file is ABI: only the three functions at the bottom, and the
+// two structs in <__hazard_pointer/domain.h> they take/return, are.
+//
+// Terminology: a *record* is a hazard pointer (its slot holds the protected node address); a *node* is
+// the header embedded in every hazard-protectable object; the *available list* holds free records; each
+// thread keeps a small cache of free records; retired nodes wait in eight sharded lists until a
+// reclamation pass -- run inline by whichever thread's retire() crosses the threshold -- deletes the
+// unprotected ones.
+
+#if defined(__SANITIZE_THREAD__)
+#  define _LIBCPP_HAZARD_POINTER_TSAN 1
+#elif defined(__has_feature)
+#  if __has_feature(thread_sanitizer)
+#    define _LIBCPP_HAZARD_POINTER_TSAN 1
+#  endif
+#endif
+#ifndef _LIBCPP_HAZARD_POINTER_TSAN
+#  define _LIBCPP_HAZARD_POINTER_TSAN 0
+#endif
+
+_LIBCPP_BEGIN_NAMESPACE_STD
+
+namespace {
+
+// ---- configuration --------------------------------------------------------------------------------
+
+[[maybe_unused]] constexpr int __retired_count_threshold  = 1000; // a pass runs once this many objects are retired ...
+[[maybe_unused]] constexpr int __threshold_multiplier     = 2; // ... or twice the record count, whichever is larger ...
+[[maybe_unused]] constexpr uint64_t __sync_time_period_ns = 2'000'000'000; // ... or two seconds after the previous pass
+[[maybe_unused]] constexpr int __shard_count              = 8;
+[[maybe_unused]] constexpr int __shard_mask               = __shard_count - 1;
+[[maybe_unused]] constexpr int __ignored_low_bits         = 8;
+constexpr uintptr_t __lock_bit                            = 1;
+constexpr uint8_t __thread_cache_capacity                 = 9;
+#if defined(__GCC_DESTRUCTIVE_SIZE)
+constexpr size_t __cache_line_size = __GCC_DESTRUCTIVE_SIZE;
+#else
+constexpr size_t __cache_line_size = 64;
+#endif
+
+// 64-bit mixer shared by the shard function and the protected set.
+[[maybe_unused]] inline size_t __mix_pointer(uintptr_t __x) noexcept {
+  if constexpr (sizeof(uintptr_t) == 8) {
+    __x ^= __x >> 32;
+    __x *= 0x9E3779B97F4A7C15ULL;
+    __x ^= __x >> 29;
+  } else {
+    __x ^= __x >> 16;
+    __x *= 0x9E3779B1U;
+    __x ^= __x >> 15;
+  }
+  return static_cast<size_t>(__x);
+}
+
+// ---- record ----------------------------------------------------------------------------------------
+
+// One cache line per record: records of different threads must not share a line (every protect()
+// writes to it). Records are created by the domain's grow array and never destroyed.
+struct alignas(__cache_line_size) __hazard_pointer_record : __hazard_pointer_slot {
+  __hazard_pointer_record* __next_available_ = nullptr; // available-list link; meaningful only while free
+  __hazard_pointer_record() noexcept : __hazard_pointer_slot{nullptr} {}
+};
+
+// ---- grow-only array (port of Menagerie's AtomicGrowArray / folly::atomic_grow_array) ---------------
+
+// Concurrent grow-only array with wait-free indexed access and reference stability. operator[](i) is
+// valid for every i: if i >= __size() the array grows to bit_ceil(i + 1), default-constructing every
+// element up to the new capacity. Growth is a race decided by one CAS; losers destroy what they built.
+// Each growth round is one allocation holding {header}{_Tp* list[capacity]}{_Tp slab[capacity - base]};
+// older rounds stay alive on a chain, so element addresses never change. __size() is monotonic and always
+// a lower bound. Never destroyed (the domain is immortal). _Tp must be nothrow default constructible.
+template <class _Tp>
+class __grow_array {
+  struct __round {
+    __round* __next_ = nullptr; // the previous (smaller) round; owns elements [0, __next_->__size_)
+    size_t __size_   = 0;       // > __next_->__size_
+    _Tp** __list() noexcept { return reinterpret_cast<_Tp**>(reinterpret_cast<byte*>(this) + sizeof(__round)); }
+  };
+
+  atomic<size_t> __size_{0};
+  atomic<__round*> __array_{nullptr};
+
+  static constexpr size_t __align() noexcept {
+    return alignof(_Tp) > alignof(max_align_t) ? alignof(_Tp) : alignof(max_align_t);
+  }
+  static constexpr size_t __ceil_to(size_t __value, size_t __alignment) noexcept {
+    return (__value + __alignment - 1) / __alignment * __alignment;
+  }
+  static constexpr size_t __bytes(size_t __capacity, size_t __base) noexcept {
+    return __ceil_to(sizeof(__round) + __capacity * sizeof(_Tp*), __align()) +
+           __ceil_to((__capacity - __base) * sizeof(_Tp), __align());
+  }
+  static _Tp* __slab(__round* __curr) noexcept {
+    uintptr_t __list_end = reinterpret_cast<uintptr_t>(__curr->__list() + __curr->__size_);
+    return reinterpret_cast<_Tp*>(__ceil_to(static_cast<size_t>(__list_end), __align()));
+  }
+
+  // Builds a round of __capacity elements on top of __next; returns nullptr (and updates __next) when
+  // another thread published a round meanwhile. May throw bad_alloc.
+  __round* __new_round(size_t __capacity, __round*& __next) {
+    size_t __base   = __next ? __next->__size_ : 0;
+    void* __raw     = ::operator new(__bytes(__capacity, __base), align_val_t{__align()});
+    __round* __curr = ::new (__raw) __round{};
+    __curr->__next_ = __next;
+    __curr->__size_ = __capacity;
+    _Tp* __slab_ptr = __slab(__curr);
+    _Tp** __list    = __curr->__list();
+    for (size_t __i = 0; __i < __base; ++__i)
+      __list[__i] = __next->__list()[__i];
+    for (size_t __i = __base; __i < __capacity; ++__i)
+      __list[__i] = nullptr;
+    for (size_t __i = __base; __i < __capacity; ++__i) {
+      __round* __seen = __array_.load(memory_order_acquire);
+      if (__seen != __next) { // race lost early: stop building
+        __next = __seen;
+        __del_round(__curr);
+        return nullptr;
+      }
+      __list[__i] = ::new (static_cast<void*>(&__slab_ptr[__i - __base])) _Tp();
+    }
+    return __curr;
+  }
+
+  void __del_round(__round* __curr) noexcept {
+    size_t __built  = __curr->__size_;
+    __round* __next = __curr->__next_;
+    size_t __base   = __next ? __next->__size_ : 0;
+    size_t __size   = __bytes(__curr->__size_, __base);
+    _Tp** __list    = __curr->__list();
+    while (__built > __base && __list[__built - 1] == nullptr) // never-constructed tail
+      --__built;
+    for (size_t __i = 0; __i < __built - __base; ++__i)
+      __list[__built - 1 - __i]->~_Tp();
+    ::operator delete(static_cast<void*>(__curr), __size, align_val_t{__align()});
+  }
+
+  __round* __at_slow(size_t __index) {
+    // Optimistic concurrency: no mutex, so element constructors may themselves use the array.
+    __round* __p      = __array_.load(memory_order_acquire);
+    size_t __capacity = std::__bit_ceil(__index + 1);
+    while (true) {
+      if (__p && __index < __p->__size_)
+        return __p;
+      __round* __q = __new_round(__capacity, __p); // __p is updated when the race is lost early
+      if (__q == nullptr)
+        continue;
+      if (__array_.compare_exchange_strong(__p, __q, memory_order_acq_rel, memory_order_acquire)) {
+        __size_.store(__capacity, memory_order_release);
+        return __q;
+      }
+      __del_round(__q);
+    }
+  }
+
+public:
+  constexpr __grow_array() noexcept            = default;
+  __grow_array(const __grow_array&)            = delete;
+  __grow_array& operator=(const __grow_array&) = delete;
+
+  size_t __size() const noexcept { return __size_.load(memory_order_acquire); }
+
+  // Reference to element __index, growing the array if needed. May throw bad_alloc.
+  _Tp& operator[](size_t __index) {
+    bool __fast  = __index < __size_.load(memory_order_acquire);
+    __round* __p = __fast ? __array_.load(memory_order_acquire) : __at_slow(__index);
+    return *__p->__list()[__index];
+  }
+
+  // Pointers to the first min(__count, size) elements constructed so far, in index order; __count is
+  // clamped. Returns nullptr when the array is still empty.
+  _Tp* const* __pointers(size_t& __count) const noexcept {
+    __round* __p = __array_.load(memory_order_acquire);
+    if (__p == nullptr) {
+      __count = 0;
+      return nullptr;
+    }
+    if (__count > __p->__size_)
+      __count = __p->__size_;
+    return __p->__list();
+  }
+};
+
+// ---- per-thread cache of free records ---------------------------------------------------------------
+
+// A per-thread stack of up to 9 free records. It sits in front of the domain's available list:
+// __hazard_pointer_acquire() pops from it and __hazard_pointer_release() pushes to it, so the steady
+// state costs no atomics at all. Reached through a pthread key (see __hazard_pointer_domain::__thread_cache).
+struct __hazard_pointer_thread_cache {
+  __hazard_pointer_record* __entries_[__thread_cache_capacity] = {};
+  uint8_t __count_                                             = 0;
+
+  __hazard_pointer_record* __try_get() noexcept { return __count_ > 0 ? __entries_[--__count_] : nullptr; }
+
+  bool __try_put(__hazard_pointer_record* __record) noexcept {
+    if (__count_ < __thread_cache_capacity) {
+      __entries_[__count_++] = __record;
+      return true;
+    }
+    return false;
+  }
+};
+
+// Sentinel stored in the TLS key once a thread's cache has been torn down (see __at_thread_exit): a
+// hazard_pointer destroyed later during that thread's teardown bypasses the cache.
+__hazard_pointer_thread_cache __dead_thread_cache;
+
+// ---- the domain --------------------------------------------------------------------------------------
+
+class __hazard_pointer_domain {
+  using __record = __hazard_pointer_record;
+
+  // ---- records: every record ever created (never destroyed) and the free ones ---------------------
+  __grow_array<__record> __records_;
+  atomic<int> __record_count_{0};    // records handed out so far = the prefix of __records_ the scan reads
+  atomic<uintptr_t> __available_{0}; // head of the free list; low bit = lock
+
+  // ---- per-thread caches ----------------------------------------------------------------------------
+  once_flag __tls_once_;
+  __libcpp_tls_key __tls_key_ = {};
+  bool __tls_ok_              = false;
+
+  // [Task 3: retired-list members]
+
+public:
+  constexpr __hazard_pointer_domain() noexcept                       = default;
+  __hazard_pointer_domain(const __hazard_pointer_domain&)            = delete;
+  __hazard_pointer_domain& operator=(const __hazard_pointer_domain&) = delete;
+
+  // ---- entry points -------------------------------------------------------------------------------
+
+  // A free, unassociated record for the calling thread. May throw bad_alloc.
+  __hazard_pointer_slot* __acquire() {
+    if (__hazard_pointer_thread_cache* __cache = __thread_cache(/*__create=*/true))
+      if (__record* __rec = __cache->__try_get())
+        return __rec;
+    return __acquire_records(1);
+  }
+
+  // Ends the epoch of __slot and returns its record to the calling thread's cache or the domain.
+  void __release(__hazard_pointer_slot* __slot) noexcept {
+    __record* __rec = static_cast<__record*>(__slot);
+    __rec->__value_.store(nullptr, memory_order_release);
+    if (__hazard_pointer_thread_cache* __cache = __thread_cache(/*__create=*/true))
+      if (__cache->__try_put(__rec))
+        return;
+    __push_available(__rec, __rec);
+  }
+
+  // [Task 3: retire + reclamation]
+
+  static void _LIBCPP_TLS_DESTRUCTOR_CC __at_thread_exit(void* __p);
+
+private:
+  // ---- available list (lock-bit stack of free records) ------------------------------------------
+
+  uintptr_t __load_available() const noexcept { return __available_.load(memory_order_acquire); }
+  void __store_available(uintptr_t __value) noexcept { __available_.store(__value, memory_order_release); }
+  bool __cas_available(uintptr_t& __expected, uintptr_t __desired) noexcept {
+    return __available_.compare_exchange_weak(__expected, __desired, memory_order_acq_rel, memory_order_acquire);
+  }
+
+  // Pops up to __count records; returns the head of a __next_available_-linked chain and sets __popped.
+  __record* __try_pop_available(uint8_t __count, uint8_t& __popped) noexcept {
+    while (true) {
+      uintptr_t __available = __load_available();
+      if (__available == 0) {
+        __popped = 0;
+        return nullptr;
+      }
+      if ((__available & __lock_bit) == 0) {
+        if (__cas_available(__available, __available | __lock_bit)) { // locked
+          __record* __head = reinterpret_cast<__record*>(__available);
+          __popped         = __pop_available_release_lock(__count, __head);
+          return __head;
+        }
+      } else {
+        std::__libcpp_thread_yield();
+      }
+    }
+  }
+
+  // Lock already held: detaches up to __count records starting at __head, then stores the new head
+  // (which releases the lock).
+  uint8_t __pop_available_release_lock(uint8_t __count, __record* __head) noexcept {
+    __record* __tail = __head;
+    uint8_t __taken  = 1;
+    __record* __next = __tail->__next_available_;
+    while (__next != nullptr && __taken < __count) {
+      __tail = __next;
+      __next = __tail->__next_available_;
+      ++__taken;
+    }
+    _LIBCPP_ASSERT_INTERNAL(
+        (reinterpret_cast<uintptr_t>(__next) & __lock_bit) == 0, "hazard pointer record misaligned");
+    __store_available(reinterpret_cast<uintptr_t>(__next)); // releases the lock
+    __tail->__next_available_ = nullptr;
+    return __taken;
+  }
+
+  // Pushes the chain [__head, __tail] (tail's link is overwritten) onto the available list.
+  void __push_available(__record* __head, __record* __tail) noexcept {
+    uintptr_t __new_head = reinterpret_cast<uintptr_t>(__head);
+    _LIBCPP_ASSERT_INTERNAL((__new_head & __lock_bit) == 0, "hazard pointer record misaligned");
+    while (true) {
+      uintptr_t __available = __load_available();
+      if ((__available & __lock_bit) == 0) {
+        __tail->__next_available_ = reinterpret_cast<__record*>(__available);
+        if (__cas_available(__available, __new_head))
+          return;
+      } else {
+        std::__libcpp_thread_yield();
+      }
+    }
+  }
+
+  // ---- record creation ------------------------------------------------------------------------------
+
+  __record* __create_record() { // may throw bad_alloc
+    // This increment happens-before the loads of any scan that must see the new record: acquiring a
+    // hazard pointer precedes the reader's seq_cst fence in try_protect(), which synchronizes with the
+    // reclaimer's seq_cst fence before the scan.
+    int __index = __record_count_.fetch_add(1, memory_order_relaxed);
+    return &__records_[static_cast<size_t>(__index)];
+  }
+
+  // Pops __count records from the available list, creating the shortfall. Returns the head of a
+  // __next_available_-linked chain of exactly __count records.
+  __record* __acquire_records(uint8_t __count) {
+    uint8_t __popped = 0;
+    __record* __head = __try_pop_available(__count, __popped);
+    for (; __popped < __count; ++__popped) {
+      __record* __created          = __create_record();
+      __created->__next_available_ = __head;
+      __head                       = __created;
+    }
+    return __head;
+  }
+
+  // ---- thread cache plumbing --------------------------------------------------------------------------
+
+  void __init_tls() noexcept {
+    std::call_once(__tls_once_, [this] {
+      __tls_ok_ = std::__libcpp_tls_create(&__tls_key_, &__hazard_pointer_domain::__at_thread_exit) == 0;
+    });
+  }
+
+  // The calling thread's cache, or nullptr when the thread has none (TLS unavailable, allocation failed,
+  // or its cache was already torn down). __create: allocate one on first use.
+  __hazard_pointer_thread_cache* __thread_cache(bool __create) noexcept {
+    __init_tls();
+    if (!__tls_ok_)
+      return nullptr;
+    void* __p = std::__libcpp_tls_get(__tls_key_);
+    if (__p == static_cast<void*>(&__dead_thread_cache))
+      return nullptr;
+    if (__p == nullptr && __create) {
+      __p = ::new (nothrow) __hazard_pointer_thread_cache();
+      if (__p != nullptr && std::__libcpp_tls_set(__tls_key_, __p) != 0) {
+        delete static_cast<__hazard_pointer_thread_cache*>(__p);
+        __p = nullptr;
+      }
+    }
+    return static_cast<__hazard_pointer_thread_cache*>(__p);
+  }
+
+  // Returns every cached record to the available list.
+  void __evict(__hazard_pointer_thread_cache& __cache) noexcept {
+    if (__cache.__count_ == 0)
+      return;
+    __record* __head = nullptr;
+    __record* __tail = nullptr;
+    while (__cache.__count_ > 0) {
+      __record* __rec          = __cache.__entries_[--__cache.__count_];
+      __rec->__next_available_ = __head;
+      __head                   = __rec;
+      if (__tail == nullptr)
+        __tail = __rec;
+    }
+    __push_available(__head, __tail);
+  }
+};
+
+constinit __hazard_pointer_domain __domain;
+
+// Runs at thread exit with the thread's cache: return its records to the domain and mark the thread as
+// past its cache. Storing the sentinel schedules one more destructor round (POSIX semantics), which
+// returns immediately below.
+void _LIBCPP_TLS_DESTRUCTOR_CC __hazard_pointer_domain::__at_thread_exit(void* __p) {
+  if (__p == nullptr || __p == static_cast<void*>(&__dead_thread_cache))
+    return;
+  __hazard_pointer_thread_cache* __cache = static_cast<__hazard_pointer_thread_cache*>(__p);
+  __domain.__evict(*__cache);
+  delete __cache;
+  std::__libcpp_tls_set(__domain.__tls_key_, &__dead_thread_cache);
+}
+
+} // namespace
+
+_LIBCPP_BEGIN_EXPLICIT_ABI_ANNOTATIONS
+
+_LIBCPP_EXPORTED_FROM_ABI __hazard_pointer_slot* __hazard_pointer_acquire() { return __domain.__acquire(); }
+
+_LIBCPP_EXPORTED_FROM_ABI void __hazard_pointer_release(__hazard_pointer_slot* __slot) noexcept {
+  __domain.__release(__slot);
+}
+
+// [Task 3: __hazard_pointer_retire definition]
+
+_LIBCPP_END_EXPLICIT_ABI_ANNOTATIONS
+
+_LIBCPP_END_NAMESPACE_STD
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.pass.cpp
new file mode 100644
index 0000000000000..cc922aa958ac9
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.pass.cpp
@@ -0,0 +1,78 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// hazard_pointer& operator=(hazard_pointer&& other) noexcept;
+//   If this == &other: no effect. Otherwise, if *this is not empty, destroys the hazard pointer owned
+//   by *this. Postconditions: *this owns other's hazard pointer (if any); other is empty.
+
+#include <hazard_pointer>
+#include <cassert>
+#include <utility>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+int main(int, char**) {
+  {
+    // empty = empty
+    std::hazard_pointer a, b;
+    a = std::move(b);
+    assert(a.empty());
+    assert(b.empty());
+  }
+  {
+    // nonempty = empty
+    std::hazard_pointer a = std::make_hazard_pointer(), b;
+    a                     = std::move(b);
+    assert(a.empty());
+    assert(b.empty());
+  }
+  {
+    // empty = nonempty
+    std::hazard_pointer a, b = std::make_hazard_pointer();
+    a = std::move(b);
+    assert(!a.empty());
+    assert(b.empty());
+  }
+  {
+    // nonempty = nonempty
+    Node node;
+    std::atomic<Node*> src{&node};
+    std::hazard_pointer a = std::make_hazard_pointer(), b = std::make_hazard_pointer();
+    (void)a.protect(src);
+    a = std::move(b);
+    assert(!a.empty());
+    assert(b.empty());
+    assert(a.protect(src) == &node);
+  }
+  {
+    // self-move: no effect
+    std::hazard_pointer a    = std::make_hazard_pointer();
+    std::hazard_pointer& ref = a;
+    a                        = std::move(ref);
+    assert(!a.empty());
+    std::hazard_pointer e;
+    std::hazard_pointer& eref = e;
+    e                         = std::move(eref);
+    assert(e.empty());
+  }
+  {
+    // returns *this
+    std::hazard_pointer a, b = std::make_hazard_pointer();
+    std::hazard_pointer& r = (a = std::move(b));
+    assert(&r == &a);
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/default.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/default.pass.cpp
new file mode 100644
index 0000000000000..78cd09c7b0ebe
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/default.pass.cpp
@@ -0,0 +1,28 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// hazard_pointer() noexcept;   Postconditions: *this is empty.
+
+#include <hazard_pointer>
+#include <cassert>
+
+#include "test_macros.h"
+
+int main(int, char**) {
+  std::hazard_pointer h;
+  assert(h.empty());
+  const std::hazard_pointer ch;
+  assert(ch.empty());
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.pass.cpp
new file mode 100644
index 0000000000000..a02dcb507781e
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.pass.cpp
@@ -0,0 +1,50 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// ~hazard_pointer();
+//   If *this is not empty, destroys the hazard pointer owned by *this, thereby ending its current
+//   protection epoch. (Reclaimability after the epoch ends is tested in ../../protection.pass.cpp.)
+
+#include <hazard_pointer>
+#include <cassert>
+#include <utility>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+int main(int, char**) {
+  {
+    std::hazard_pointer empty; // destroying an empty hazard_pointer is a no-op
+  }
+  {
+    Node node;
+    std::atomic<Node*> src{&node};
+    std::hazard_pointer h = std::make_hazard_pointer();
+    (void)h.protect(src); // destroyed while associated
+  }
+  {
+    std::hazard_pointer a = std::make_hazard_pointer();
+    std::hazard_pointer b(std::move(a)); // a is empty: only b destroys the hazard pointer
+  }
+  // Destroy many, then acquire many again: the pool must be reusable.
+  for (int round = 0; round < 3; ++round) {
+    std::hazard_pointer hps[64];
+    for (std::hazard_pointer& h : hps)
+      h = std::make_hazard_pointer();
+    for (std::hazard_pointer& h : hps)
+      assert(!h.empty());
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp
new file mode 100644
index 0000000000000..a66cb1627d566
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp
@@ -0,0 +1,51 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// hazard_pointer(hazard_pointer&& other) noexcept;
+//   Postconditions: If other is empty, *this is empty. Otherwise *this owns the hazard pointer
+//   originally owned by other; other is empty.
+
+#include <hazard_pointer>
+#include <cassert>
+#include <utility>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {
+  int value = 0;
+};
+
+int main(int, char**) {
+  {
+    std::hazard_pointer empty;
+    std::hazard_pointer h(std::move(empty));
+    assert(h.empty());
+    assert(empty.empty());
+  }
+  {
+    Node node;
+    std::atomic<Node*> src{&node};
+    std::hazard_pointer a = std::make_hazard_pointer();
+    Node* p               = a.protect(src);
+    assert(p == &node);
+    std::hazard_pointer b(std::move(a));
+    assert(!b.empty());
+    assert(a.empty());
+    // b owns the hazard pointer, still associated with node; b can keep using it.
+    b.reset_protection();
+    Node* q = b.protect(src);
+    assert(q == &node);
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/empty.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/empty.pass.cpp
new file mode 100644
index 0000000000000..51456bd86646a
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/empty.pass.cpp
@@ -0,0 +1,32 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// bool empty() const noexcept;
+
+#include <hazard_pointer>
+#include <cassert>
+#include <utility>
+
+#include "test_macros.h"
+
+int main(int, char**) {
+  const std::hazard_pointer e;
+  assert(e.empty());
+  std::hazard_pointer h = std::make_hazard_pointer();
+  assert(!h.empty());
+  std::hazard_pointer m = std::move(h);
+  assert(h.empty());
+  assert(!m.empty());
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp
new file mode 100644
index 0000000000000..692eecdf29e24
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp
@@ -0,0 +1,61 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// template<class T> T* protect(const atomic<T*>& src) noexcept;
+//   Equivalent to: T* ptr = src.load(relaxed); while (!try_protect(ptr, src)) {} return ptr;
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {
+  int value;
+  explicit Node(int v) : value(v) {}
+};
+
+int main(int, char**) {
+  std::hazard_pointer h = std::make_hazard_pointer();
+  {
+    Node a(1);
+    std::atomic<Node*> src{&a};
+    Node* p = h.protect(src);
+    assert(p == &a);
+    assert(p->value == 1);
+    // Protecting again (a new epoch) works and returns the current value.
+    Node b(2);
+    src.store(&b);
+    Node* q = h.protect(src);
+    assert(q == &b);
+    assert(q->value == 2);
+  }
+  {
+    // A null source: protect returns nullptr and the hazard pointer ends up unassociated.
+    std::atomic<Node*> src{nullptr};
+    Node* p = h.protect(src);
+    assert(p == nullptr);
+  }
+  {
+    // Works with a pointer to a derived-from-Node? No: T must be exactly the hazard-protectable type.
+    // (Diagnostics are covered by libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp)
+    // Works with two hazard pointers protecting the same object.
+    Node a(3);
+    std::atomic<Node*> src{&a};
+    std::hazard_pointer h2 = std::make_hazard_pointer();
+    assert(h.protect(src) == &a);
+    assert(h2.protect(src) == &a);
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/reset_protection.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/reset_protection.pass.cpp
new file mode 100644
index 0000000000000..8a9f9b5a4fda0
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/reset_protection.pass.cpp
@@ -0,0 +1,42 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// template<class T> void reset_protection(const T* ptr) noexcept;
+// void reset_protection(nullptr_t = nullptr) noexcept;
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+int main(int, char**) {
+  Node a, b;
+  std::hazard_pointer h = std::make_hazard_pointer();
+  h.reset_protection();        // unassociated -> unassociated
+  h.reset_protection(nullptr); // same
+  h.reset_protection(&a);      // associate with a
+  h.reset_protection(&b);      // re-associate
+  const Node* cb = &b;
+  h.reset_protection(cb); // pointer-to-const is fine: T is deduced as Node
+  Node* null = nullptr;
+  h.reset_protection(null); // a null T*: equivalent to reset_protection()
+  h.reset_protection();
+  // Still usable afterwards.
+  std::atomic<Node*> src{&a};
+  assert(h.protect(src) == &a);
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/swap.member.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/swap.member.pass.cpp
new file mode 100644
index 0000000000000..277364c512135
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/swap.member.pass.cpp
@@ -0,0 +1,60 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// void swap(hazard_pointer& other) noexcept;
+//   Swaps ownership; the hazard pointers themselves stay associated with what they protected.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+template <class T>
+concept IsNoThrowMemberSwappable = requires(T& t) {
+  { t.swap(t) } noexcept;
+};
+static_assert(IsNoThrowMemberSwappable<std::hazard_pointer>);
+
+int main(int, char**) {
+  {
+    std::hazard_pointer a, b;
+    a.swap(b);
+    assert(a.empty() && b.empty());
+  }
+  {
+    std::hazard_pointer a = std::make_hazard_pointer(), b;
+    a.swap(b);
+    assert(a.empty());
+    assert(!b.empty());
+    b.swap(a);
+    assert(!a.empty());
+    assert(b.empty());
+  }
+  {
+    Node node;
+    std::atomic<Node*> src{&node};
+    std::hazard_pointer a = std::make_hazard_pointer(), b = std::make_hazard_pointer();
+    (void)a.protect(src);
+    a.swap(b);
+    assert(!a.empty() && !b.empty());
+    // No epoch ended or started: b now owns the pointer associated with node and may keep using it.
+    assert(b.protect(src) == &node);
+    a.swap(a); // self-swap: no effect
+    assert(!a.empty());
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/try_protect.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/try_protect.pass.cpp
new file mode 100644
index 0000000000000..c5e27fcb4e3c2
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/try_protect.pass.cpp
@@ -0,0 +1,66 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// template<class T> bool try_protect(T*& ptr, const atomic<T*>& src) noexcept;
+//   old = ptr; reset_protection(old); ptr = src.load(acquire); if (old != ptr) reset_protection();
+//   Returns old == ptr.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+int main(int, char**) {
+  Node a, b;
+  std::hazard_pointer h = std::make_hazard_pointer();
+  {
+    // src unchanged: succeeds, ptr unchanged.
+    std::atomic<Node*> src{&a};
+    Node* ptr = src.load();
+    assert(h.try_protect(ptr, src));
+    assert(ptr == &a);
+  }
+  {
+    // src changed between the caller's load and try_protect: fails and reports the new value.
+    std::atomic<Node*> src{&a};
+    Node* ptr = src.load();
+    src.store(&b);
+    assert(!h.try_protect(ptr, src));
+    assert(ptr == &b);
+    // The reported value is now current: the retry succeeds.
+    assert(h.try_protect(ptr, src));
+    assert(ptr == &b);
+  }
+  {
+    // A stale null pointer.
+    std::atomic<Node*> src{&a};
+    Node* ptr = nullptr;
+    assert(!h.try_protect(ptr, src));
+    assert(ptr == &a);
+    assert(h.try_protect(ptr, src));
+  }
+  {
+    // A stale non-null pointer against a now-null source.
+    std::atomic<Node*> src{nullptr};
+    Node* ptr = &a;
+    assert(!h.try_protect(ptr, src));
+    assert(ptr == nullptr);
+    assert(h.try_protect(ptr, src));
+    assert(ptr == nullptr);
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.nonmem/make_hazard_pointer.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.nonmem/make_hazard_pointer.pass.cpp
new file mode 100644
index 0000000000000..3fcc11c303bcc
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.nonmem/make_hazard_pointer.pass.cpp
@@ -0,0 +1,44 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// hazard_pointer make_hazard_pointer();
+
+#include <hazard_pointer>
+#include <cassert>
+#include <vector>
+
+#include "test_macros.h"
+
+int main(int, char**) {
+  {
+    std::hazard_pointer h = std::make_hazard_pointer();
+    assert(!h.empty());
+  }
+  {
+    // Many nonempty hazard pointers alive at once.
+    std::vector<std::hazard_pointer> hps;
+    for (int i = 0; i < 100; ++i)
+      hps.push_back(std::make_hazard_pointer());
+    for (const std::hazard_pointer& h : hps)
+      assert(!h.empty());
+  }
+  {
+    // Repeated construction/destruction keeps working (hazard pointers are pooled and reused).
+    for (int i = 0; i < 10000; ++i) {
+      std::hazard_pointer h = std::make_hazard_pointer();
+      assert(!h.empty());
+    }
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.nonmem/swap.free.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.nonmem/swap.free.pass.cpp
new file mode 100644
index 0000000000000..b7db7fd84ce0c
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.nonmem/swap.free.pass.cpp
@@ -0,0 +1,37 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// void swap(hazard_pointer& a, hazard_pointer& b) noexcept;   Equivalent to a.swap(b).
+
+#include <hazard_pointer>
+#include <cassert>
+
+#include "test_macros.h"
+
+template <class T>
+concept IsNoThrowFreeSwappable = requires(T& t) {
+  { swap(t, t) } noexcept;
+};
+static_assert(IsNoThrowFreeSwappable<std::hazard_pointer>);
+
+int main(int, char**) {
+  std::hazard_pointer a = std::make_hazard_pointer(), b;
+  swap(a, b); // ADL
+  assert(a.empty());
+  assert(!b.empty());
+  std::swap(a, b);
+  assert(!a.empty());
+  assert(b.empty());
+  return 0;
+}

>From a6d7587e6fe91c3431ccdffe8def6c9b02c626f2 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 02:36:17 +0300
Subject: [PATCH 03/17] [libc++] hazard_pointer: never allocate a thread cache
 on the release path

---
 libcxx/src/hazard_pointer.cpp | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/libcxx/src/hazard_pointer.cpp b/libcxx/src/hazard_pointer.cpp
index 3e93c3b206a1f..4a50d026d791e 100644
--- a/libcxx/src/hazard_pointer.cpp
+++ b/libcxx/src/hazard_pointer.cpp
@@ -225,8 +225,10 @@ struct __hazard_pointer_thread_cache {
   }
 };
 
-// Sentinel stored in the TLS key once a thread's cache has been torn down (see __at_thread_exit): a
-// hazard_pointer destroyed later during that thread's teardown bypasses the cache.
+// Sentinel stored in the TLS key by __at_thread_exit once a thread's cache has been torn down. It
+// keeps a hazard_pointer destroyed later in the same destructor round (or acquired then) from
+// touching the freed cache; TSD implementations reset the value to null before each further
+// destructor round, which is why __release never creates a cache.
 __hazard_pointer_thread_cache __dead_thread_cache;
 
 // ---- the domain --------------------------------------------------------------------------------------
@@ -261,11 +263,12 @@ class __hazard_pointer_domain {
     return __acquire_records(1);
   }
 
-  // Ends the epoch of __slot and returns its record to the calling thread's cache or the domain.
+  // Ends the epoch of __slot and returns its record to the calling thread's cache, if it has one,
+  // else to the domain. Never allocates a cache: see the note above __dead_thread_cache.
   void __release(__hazard_pointer_slot* __slot) noexcept {
     __record* __rec = static_cast<__record*>(__slot);
     __rec->__value_.store(nullptr, memory_order_release);
-    if (__hazard_pointer_thread_cache* __cache = __thread_cache(/*__create=*/true))
+    if (__hazard_pointer_thread_cache* __cache = __thread_cache(/*__create=*/false))
       if (__cache->__try_put(__rec))
         return;
     __push_available(__rec, __rec);

>From 5595f2dadc9f4c6e61cd62a0718f466ffd484d98 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 02:43:30 +0300
Subject: [PATCH 04/17] [libc++] hazard_pointer: engine part 2 (retire, sharded
 retired lists, reclamation pass)

---
 libcxx/src/hazard_pointer.cpp                 | 328 +++++++++++++++++-
 .../saferecl/saferecl.hp/reclamation.pass.cpp | 134 +++++++
 .../saferecl/saferecl.hp/protection.pass.cpp  |  95 +++++
 .../saferecl.hp/readers_writers.pass.cpp      |  88 +++++
 .../saferecl.hp.base/retire.pass.cpp          |  83 +++++
 5 files changed, 716 insertions(+), 12 deletions(-)
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/readers_writers.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp

diff --git a/libcxx/src/hazard_pointer.cpp b/libcxx/src/hazard_pointer.cpp
index 4a50d026d791e..8fdd121c8f7f2 100644
--- a/libcxx/src/hazard_pointer.cpp
+++ b/libcxx/src/hazard_pointer.cpp
@@ -49,14 +49,14 @@ namespace {
 
 // ---- configuration --------------------------------------------------------------------------------
 
-[[maybe_unused]] constexpr int __retired_count_threshold  = 1000; // a pass runs once this many objects are retired ...
-[[maybe_unused]] constexpr int __threshold_multiplier     = 2; // ... or twice the record count, whichever is larger ...
-[[maybe_unused]] constexpr uint64_t __sync_time_period_ns = 2'000'000'000; // ... or two seconds after the previous pass
-[[maybe_unused]] constexpr int __shard_count              = 8;
-[[maybe_unused]] constexpr int __shard_mask               = __shard_count - 1;
-[[maybe_unused]] constexpr int __ignored_low_bits         = 8;
-constexpr uintptr_t __lock_bit                            = 1;
-constexpr uint8_t __thread_cache_capacity                 = 9;
+constexpr int __retired_count_threshold   = 1000;          // a pass runs once this many objects are retired ...
+constexpr int __threshold_multiplier      = 2;             // ... or twice the record count, whichever is larger ...
+constexpr uint64_t __sync_time_period_ns  = 2'000'000'000; // ... or two seconds after the previous pass
+constexpr int __shard_count               = 8;
+constexpr int __shard_mask                = __shard_count - 1;
+constexpr int __ignored_low_bits          = 8;
+constexpr uintptr_t __lock_bit            = 1;
+constexpr uint8_t __thread_cache_capacity = 9;
 #if defined(__GCC_DESTRUCTIVE_SIZE)
 constexpr size_t __cache_line_size = __GCC_DESTRUCTIVE_SIZE;
 #else
@@ -64,7 +64,7 @@ constexpr size_t __cache_line_size = 64;
 #endif
 
 // 64-bit mixer shared by the shard function and the protected set.
-[[maybe_unused]] inline size_t __mix_pointer(uintptr_t __x) noexcept {
+inline size_t __mix_pointer(uintptr_t __x) noexcept {
   if constexpr (sizeof(uintptr_t) == 8) {
     __x ^= __x >> 32;
     __x *= 0x9E3779B97F4A7C15ULL;
@@ -231,6 +231,121 @@ struct __hazard_pointer_thread_cache {
 // destructor round, which is why __release never creates a cache.
 __hazard_pointer_thread_cache __dead_thread_cache;
 
+// ---- intrusive lists of retired nodes -----------------------------------------------------------------
+
+using __node = __hazard_pointer_obj_node;
+
+// A (head, tail, count) list of nodes; single-threaded.
+struct __node_list {
+  __node* __head_ = nullptr;
+  __node* __tail_ = nullptr;
+  int __count_    = 0;
+
+  bool __empty() const noexcept { return __head_ == nullptr; }
+
+  void __push(__node* __n) noexcept {
+    __n->__next_ = nullptr;
+    if (__tail_ != nullptr)
+      __tail_->__next_ = __n;
+    else
+      __head_ = __n;
+    __tail_ = __n;
+    ++__count_;
+  }
+
+  void __clear() noexcept {
+    __head_  = nullptr;
+    __tail_  = nullptr;
+    __count_ = 0;
+  }
+};
+
+// A head-only list shared between threads: lock-free push of a whole __node_list, wait-free pop-all.
+struct __shared_node_list {
+  atomic<__node*> __head_{nullptr};
+
+  void __push(__node_list& __list) noexcept { // prepends the whole list, kept in order
+    if (__list.__empty())
+      return;
+    __node* __old_head = __head_.load(memory_order_acquire);
+    do {
+      __list.__tail_->__next_ = __old_head;
+    } while (!__head_.compare_exchange_weak(__old_head, __list.__head_, memory_order_acq_rel, memory_order_acquire));
+    __list.__clear();
+  }
+
+  __node* __pop_all() noexcept { return __head_.exchange(nullptr, memory_order_acq_rel); }
+
+  bool __empty() const noexcept { return __head_.load(memory_order_acquire) == nullptr; }
+};
+
+// ---- the set of protected addresses built by every reclamation pass ------------------------------------
+
+// Open addressing, power-of-two table, __mix_pointer as the hash. Storage comes from nothrow new;
+// __insert() returns false when the table cannot grow, in which case the pass falls back to matching
+// each retired node against the records directly (see __hazard_pointer_domain::__match_reclaim).
+class __protected_set {
+  uintptr_t* __slots_ = nullptr; // 0 = empty; only non-null addresses are inserted
+  size_t __capacity_  = 0;
+  size_t __mask_      = 0;
+  size_t __size_      = 0;
+
+  void __insert_no_grow(uintptr_t __key) noexcept {
+    size_t __i = __mix_pointer(__key) & __mask_;
+    while (__slots_[__i] != 0) {
+      if (__slots_[__i] == __key)
+        return;
+      __i = (__i + 1) & __mask_;
+    }
+    __slots_[__i] = __key;
+    ++__size_;
+  }
+
+  bool __grow() noexcept {
+    size_t __new_capacity  = __capacity_ == 0 ? 16 : __capacity_ * 2;
+    uintptr_t* __new_slots = ::new (nothrow) uintptr_t[__new_capacity](); // zero-initialised
+    if (__new_slots == nullptr)
+      return false;
+    uintptr_t* __old      = __slots_;
+    size_t __old_capacity = __capacity_;
+    __slots_              = __new_slots;
+    __capacity_           = __new_capacity;
+    __mask_               = __new_capacity - 1;
+    __size_               = 0;
+    for (size_t __i = 0; __i < __old_capacity; ++__i)
+      if (__old[__i] != 0)
+        __insert_no_grow(__old[__i]);
+    delete[] __old;
+    return true;
+  }
+
+public:
+  __protected_set()                                  = default;
+  __protected_set(const __protected_set&)            = delete;
+  __protected_set& operator=(const __protected_set&) = delete;
+  ~__protected_set() { delete[] __slots_; }
+
+  bool __insert(const void* __p) noexcept {
+    if ((__size_ + 1) * 2 > __capacity_ && !__grow())
+      return false;
+    __insert_no_grow(reinterpret_cast<uintptr_t>(__p));
+    return true;
+  }
+
+  bool __contains(const void* __p) const noexcept {
+    if (__size_ == 0)
+      return false;
+    uintptr_t __key = reinterpret_cast<uintptr_t>(__p);
+    size_t __i      = __mix_pointer(__key) & __mask_;
+    while (__slots_[__i] != 0) {
+      if (__slots_[__i] == __key)
+        return true;
+      __i = (__i + 1) & __mask_;
+    }
+    return false;
+  }
+};
+
 // ---- the domain --------------------------------------------------------------------------------------
 
 class __hazard_pointer_domain {
@@ -246,7 +361,10 @@ class __hazard_pointer_domain {
   __libcpp_tls_key __tls_key_ = {};
   bool __tls_ok_              = false;
 
-  // [Task 3: retired-list members]
+  // ---- retired objects ------------------------------------------------------------------------------
+  __shared_node_list __retired_[__shard_count]; // sharded by node address to spread contention
+  atomic<int> __retired_count_{0};              // signed: transiently negative during a pass (see below)
+  atomic<uint64_t> __due_time_{0};              // steady_clock nanoseconds; a pass also runs when now >= due
 
 public:
   constexpr __hazard_pointer_domain() noexcept                       = default;
@@ -274,8 +392,192 @@ class __hazard_pointer_domain {
     __push_available(__rec, __rec);
   }
 
-  // [Task 3: retire + reclamation]
+  // Retires __node: the tail of hazard_pointer_obj_base::retire(). May run a reclamation pass (and thus
+  // deleters) synchronously.
+  void __retire(__node* __n) noexcept {
+    // Retire-side fence: orders the user's store to `src` (sequenced before this call) before the
+    // reclaimer's scan, even when the reclaimer is another thread. See <__hazard_pointer/domain.h>.
+    std::atomic_thread_fence(memory_order_seq_cst);
+    __node_list __list;
+    __list.__push(__n);
+    __retired_[__shard_of(__n)].__push(__list);
+    __retired_count_.fetch_add(1, memory_order_release);
+    __check_threshold_and_reclaim();
+  }
+
+private:
+  // ---- trigger ----------------------------------------------------------------------------------------
+
+  static uint64_t __now_ns() noexcept {
+    return static_cast<uint64_t>(
+        chrono::duration_cast<chrono::nanoseconds>(chrono::steady_clock::now().time_since_epoch()).count());
+  }
+
+  static size_t __shard_of(const __node* __n) noexcept {
+    return (__mix_pointer(reinterpret_cast<uintptr_t>(__n)) >> __ignored_low_bits) & __shard_mask;
+  }
+
+  int __threshold() const noexcept {
+    int __records = __record_count_.load(memory_order_relaxed);
+    int __scaled  = __threshold_multiplier * __records;
+    return __scaled > __retired_count_threshold ? __scaled : __retired_count_threshold;
+  }
 
+  // If the retired count reached the threshold, claims it (resets it to 0) and returns it; else 0.
+  int __check_count_threshold() noexcept {
+    int __count = __retired_count_.load(memory_order_acquire);
+    while (__count >= __threshold()) {
+      if (__retired_count_.compare_exchange_weak(__count, 0, memory_order_acq_rel, memory_order_relaxed)) {
+        __due_time_.store(__now_ns() + __sync_time_period_ns, memory_order_release);
+        return __count;
+      }
+    }
+    return 0;
+  }
+
+  // If the due time has passed, claims the current count (may be small) and returns it; else 0.
+  int __check_due_time() noexcept {
+    uint64_t __time = __now_ns();
+    uint64_t __due  = __due_time_.load(memory_order_acquire);
+    if (__time < __due || !__due_time_.compare_exchange_strong(
+                              __due, __time + __sync_time_period_ns, memory_order_acq_rel, memory_order_relaxed))
+      return 0;
+    int __count = __retired_count_.exchange(0, memory_order_acq_rel);
+    if (__count < 0) {
+      __retired_count_.fetch_add(__count, memory_order_release);
+      return 0;
+    }
+    return __count;
+  }
+
+  void __check_threshold_and_reclaim() noexcept {
+    int __count = __check_count_threshold();
+    if (__count == 0) {
+      __count = __check_due_time();
+      if (__count == 0)
+        return;
+    }
+    __do_reclamation(__count);
+  }
+
+  // ---- reclamation pass ---------------------------------------------------------------------------------
+
+  bool __retired_empty() const noexcept {
+    for (const __shared_node_list& __shard : __retired_)
+      if (!__shard.__empty())
+        return false;
+    return true;
+  }
+
+  bool __extract_retired(__node* __out[__shard_count]) noexcept {
+    bool __any = false;
+    for (int __s = 0; __s < __shard_count; ++__s) {
+      __out[__s] = __retired_[__s].__pop_all();
+      if (__out[__s] != nullptr)
+        __any = true;
+    }
+    return __any;
+  }
+
+  // Loads every non-null slot value into __set. Returns false when the set could not be built (allocation
+  // failure); the caller then matches linearly.
+  bool __load_protected(__protected_set& __set) noexcept {
+    int __handed_out        = __record_count_.load(memory_order_relaxed);
+    size_t __count          = __handed_out > 0 ? static_cast<size_t>(__handed_out) : 0;
+    __record* const* __recs = __records_.__pointers(__count); // clamps __count to what exists
+#if _LIBCPP_HAZARD_POINTER_TSAN
+    constexpr memory_order __order = memory_order_acquire; // TSan does not model fences
+#else
+    constexpr memory_order __order = memory_order_relaxed; // the fence below provides the acquire ordering
+#endif
+    // Load a small batch up front so the branches below do not form one long dependency chain.
+    constexpr size_t __chunk = 8;
+    size_t __i               = 0;
+    for (; __i + __chunk <= __count; __i += __chunk) {
+      const void* __values[__chunk];
+      for (size_t __j = 0; __j < __chunk; ++__j)
+        __values[__j] = __recs[__i + __j]->__value_.load(__order);
+      for (const void* __v : __values)
+        if (__v != nullptr && !__set.__insert(__v))
+          return false;
+    }
+    for (; __i < __count; ++__i) {
+      const void* __v = __recs[__i]->__value_.load(__order);
+      if (__v != nullptr && !__set.__insert(__v))
+        return false;
+    }
+    std::atomic_thread_fence(memory_order_acquire);
+    return true;
+  }
+
+  // Allocation-free fallback: is __n currently held by any record? O(records).
+  bool __protected_linear(const __node* __n) const noexcept {
+    int __handed_out        = __record_count_.load(memory_order_relaxed);
+    size_t __count          = __handed_out > 0 ? static_cast<size_t>(__handed_out) : 0;
+    __record* const* __recs = __records_.__pointers(__count);
+    for (size_t __i = 0; __i < __count; ++__i)
+      if (__recs[__i]->__value_.load(memory_order_acquire) == static_cast<const void*>(__n))
+        return true;
+    return false;
+  }
+
+  // Reclaims the unprotected extracted nodes and pushes the protected ones back. Returns the number
+  // reclaimed; sets __done = false when new nodes were retired meanwhile (e.g. by a deleter).
+  int __match_reclaim(__node* __retired[__shard_count], const __protected_set* __set, bool& __done) noexcept {
+    __done          = true;
+    int __reclaimed = 0;
+    __node_list __not_reclaimed[__shard_count];
+    __node* __heads[__shard_count];
+    for (int __s = 0; __s < __shard_count; ++__s)
+      __heads[__s] = __retired[__s];
+    for (bool __more = true; __more;) { // walk the shards in lock-step (instruction-level parallelism)
+      __more = false;
+      for (int __s = 0; __s < __shard_count; ++__s) {
+        __node* __n = __heads[__s];
+        if (__n == nullptr)
+          continue;
+        __more              = true;
+        __heads[__s]        = __n->__next_; // read before the deleter may free __n
+        bool __is_protected = __set != nullptr ? __set->__contains(__n) : __protected_linear(__n);
+        if (__is_protected) {
+          __not_reclaimed[__s].__push(__n);
+        } else {
+          ++__reclaimed;
+          __n->__reclaim_(__n);
+        }
+      }
+    }
+    if (!__retired_empty())
+      __done = false;
+    for (int __s = 0; __s < __shard_count; ++__s)
+      __retired_[__s].__push(__not_reclaimed[__s]);
+    return __reclaimed;
+  }
+
+  // The bulk reclamation pass. __count is the number of retirements claimed by the trigger; whatever is
+  // not reclaimed is added back to the retired count (which may transiently make it negative when objects
+  // retired concurrently were reclaimed without having been counted yet).
+  void __do_reclamation(int __count) noexcept {
+    while (true) {
+      __node* __retired[__shard_count];
+      bool __done = true;
+      if (__extract_retired(__retired)) {
+        // Reclaimer-side fence: pairs with the readers' fence in try_protect() and the retirers' fence in
+        // __retire(). See <__hazard_pointer/domain.h>.
+        std::atomic_thread_fence(memory_order_seq_cst);
+        __protected_set __set;
+        const __protected_set* __set_ptr = __load_protected(__set) ? &__set : nullptr;
+        __count -= __match_reclaim(__retired, __set_ptr, __done);
+      }
+      if (__count != 0)
+        __retired_count_.fetch_add(__count, memory_order_release);
+      __count = __check_count_threshold();
+      if (__count == 0 && __done)
+        break;
+    }
+  }
+
+public:
   static void _LIBCPP_TLS_DESTRUCTOR_CC __at_thread_exit(void* __p);
 
 private:
@@ -432,7 +734,9 @@ _LIBCPP_EXPORTED_FROM_ABI void __hazard_pointer_release(__hazard_pointer_slot* _
   __domain.__release(__slot);
 }
 
-// [Task 3: __hazard_pointer_retire definition]
+_LIBCPP_EXPORTED_FROM_ABI void __hazard_pointer_retire(__hazard_pointer_obj_node* __node) noexcept {
+  __domain.__retire(__node);
+}
 
 _LIBCPP_END_EXPLICIT_ABI_ANNOTATIONS
 
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp
new file mode 100644
index 0000000000000..063ac1e4ae077
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp
@@ -0,0 +1,134 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// Implementation-specific reclamation behaviour: libc++ runs a reclamation pass inline in retire()
+// once max(1000, 2 * <number of hazard pointer records>) objects have been retired since the previous
+// pass, so retiring a few thousand objects from a thread with a handful of hazard pointers guarantees
+// passes. Numbers below leave generous slack so tuning the constants does not break the test.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+#include <vector>
+
+#include "test_macros.h"
+
+struct Dummy : std::hazard_pointer_obj_base<Dummy> {
+  std::atomic<int>* deleted;
+  explicit Dummy(std::atomic<int>* d) : deleted(d) {}
+  ~Dummy() { ++*deleted; }
+};
+
+struct Node : std::hazard_pointer_obj_base<Node> {
+  std::atomic<bool>* deleted;
+  explicit Node(std::atomic<bool>* d) : deleted(d) {}
+  ~Node() { deleted->store(true); }
+};
+
+// Retire enough objects to guarantee at least two passes.
+void force_passes(std::atomic<int>& deleted) {
+  for (int i = 0; i < 5000; ++i)
+    (new Dummy(&deleted))->retire();
+}
+
+// A deleter that itself retires another object and uses hazard pointers (re-entrancy).
+std::atomic<int> nested_deleted{0};
+struct Nested : std::hazard_pointer_obj_base<Nested> {
+  int depth;
+  explicit Nested(int d) : depth(d) {}
+  ~Nested() {
+    ++nested_deleted;
+    if (depth > 0)
+      (new Nested(depth - 1))->retire();
+    std::hazard_pointer h = std::make_hazard_pointer(); // acquire/release inside a deleter
+    std::atomic<Nested*> src{nullptr};
+    (void)h.protect(src);
+  }
+};
+
+struct Noop {
+  void operator()(struct StackObj*) const noexcept {}
+};
+struct StackObj : std::hazard_pointer_obj_base<StackObj, Noop> {};
+
+int main(int, char**) {
+  {
+    // Unprotected objects get reclaimed: after 5000 retirements at most ~1000 (+ slack) are still pending.
+    std::atomic<int> deleted{0};
+    force_passes(deleted);
+    assert(deleted.load() >= 3000);
+  }
+  {
+    // A protected object survives passes and is reclaimed once unprotected and another pass runs.
+    std::atomic<bool> node_deleted{false};
+    std::atomic<int> deleted{0};
+    Node* n               = new Node(&node_deleted);
+    std::hazard_pointer h = std::make_hazard_pointer();
+    h.reset_protection(n);
+    n->retire();
+    force_passes(deleted);
+    assert(!node_deleted.load());
+    h.reset_protection();
+    force_passes(deleted);
+    assert(node_deleted.load());
+  }
+  {
+    // ~hazard_pointer() ends the epoch as well.
+    std::atomic<bool> node_deleted{false};
+    std::atomic<int> deleted{0};
+    Node* n = new Node(&node_deleted);
+    {
+      std::hazard_pointer h = std::make_hazard_pointer();
+      h.reset_protection(n);
+      n->retire();
+      force_passes(deleted);
+      assert(!node_deleted.load());
+    }
+    force_passes(deleted);
+    assert(node_deleted.load());
+  }
+  {
+    // Move-assignment over a nonempty hazard_pointer ends its epoch.
+    std::atomic<bool> node_deleted{false};
+    std::atomic<int> deleted{0};
+    Node* n               = new Node(&node_deleted);
+    std::hazard_pointer h = std::make_hazard_pointer();
+    h.reset_protection(n);
+    n->retire();
+    h = std::hazard_pointer(); // releases the associated hazard pointer
+    force_passes(deleted);
+    assert(node_deleted.load());
+  }
+  {
+    // Deleters may retire and use hazard pointers (reclamation is re-entrant).
+    std::atomic<int> deleted{0};
+    (new Nested(3))->retire();
+    force_passes(deleted);
+    force_passes(deleted);
+    assert(nested_deleted.load() >= 1);
+    // Drain: everything retired so far is unprotected, so a couple more passes reclaim the chain.
+    force_passes(deleted);
+    force_passes(deleted);
+    assert(nested_deleted.load() == 4);
+  }
+  {
+    // Objects with automatic storage and a no-op deleter: retire, force a pass, then they may go away.
+    std::atomic<int> deleted{0};
+    std::vector<StackObj> objs(100);
+    for (StackObj& o : objs)
+      o.retire();
+    force_passes(deleted); // reclaims (no-op deleter) and unlinks them: destroying `objs` is now safe
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
new file mode 100644
index 0000000000000..8c799829c86bc
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
@@ -0,0 +1,95 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// [saferecl.hp.general]: an object retired while some hazard pointer is associated with it is not
+// reclaimed until that protection epoch ends; the number of possibly-reclaimable objects is bounded.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+
+#include "test_macros.h"
+
+struct Dummy : std::hazard_pointer_obj_base<Dummy> {
+  std::atomic<int>* deleted;
+  explicit Dummy(std::atomic<int>* d) : deleted(d) {}
+  ~Dummy() { ++*deleted; }
+};
+
+struct Node : std::hazard_pointer_obj_base<Node> {
+  std::atomic<bool>* deleted;
+  int value = 42;
+  explicit Node(std::atomic<bool>* d) : deleted(d) {}
+  ~Node() { deleted->store(true); }
+};
+
+constexpr int N = 100000;
+
+void retire_dummies(std::atomic<int>& deleted) {
+  for (int i = 0; i < N; ++i)
+    (new Dummy(&deleted))->retire();
+}
+
+int main(int, char**) {
+  {
+    // Protected via protect(): survives reclamation passes; readable throughout.
+    std::atomic<bool> node_deleted{false};
+    std::atomic<int> dummies_deleted{0};
+    Node* n = new Node(&node_deleted);
+    std::atomic<Node*> src{n};
+    std::hazard_pointer h = std::make_hazard_pointer();
+    Node* p               = h.protect(src);
+    assert(p == n);
+    src.store(nullptr);
+    n->retire();
+    retire_dummies(dummies_deleted);
+    assert(dummies_deleted.load() > 0); // reclamation did happen ...
+    assert(!node_deleted.load());       // ... but not of the protected object
+    assert(p->value == 42);
+    h.reset_protection(); // epoch ends; n becomes possibly-reclaimable
+    retire_dummies(dummies_deleted);
+    // Whether n has been reclaimed by now is unspecified (see libcxx/.../reclamation.pass.cpp).
+  }
+  {
+    // Protected via reset_protection(ptr) then released by ~hazard_pointer(): same guarantee.
+    std::atomic<bool> node_deleted{false};
+    std::atomic<int> dummies_deleted{0};
+    Node* n = new Node(&node_deleted);
+    {
+      std::hazard_pointer h = std::make_hazard_pointer();
+      h.reset_protection(n);
+      n->retire();
+      retire_dummies(dummies_deleted);
+      assert(!node_deleted.load());
+      assert(n->value == 42);
+    } // h destroyed: epoch ends
+    retire_dummies(dummies_deleted);
+  }
+  {
+    // Two hazard pointers on the same object: it stays protected until both epochs end.
+    std::atomic<bool> node_deleted{false};
+    std::atomic<int> dummies_deleted{0};
+    Node* n                = new Node(&node_deleted);
+    std::hazard_pointer h1 = std::make_hazard_pointer(), h2 = std::make_hazard_pointer();
+    h1.reset_protection(n);
+    h2.reset_protection(n);
+    n->retire();
+    h1.reset_protection();
+    retire_dummies(dummies_deleted);
+    assert(!node_deleted.load()); // still protected by h2
+    h2.reset_protection();
+    retire_dummies(dummies_deleted);
+  }
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/readers_writers.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/readers_writers.pass.cpp
new file mode 100644
index 0000000000000..aebf97f57e027
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/readers_writers.pass.cpp
@@ -0,0 +1,88 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// Concurrent readers protecting a published object while writers replace and retire it. Every reader
+// checks an invariant of the payload; under ASan/TSan this test also acts as a use-after-free / data-race
+// detector for the implementation. No timing assumptions.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+#include <cstdint>
+#include <thread>
+#include <vector>
+
+#include "make_test_thread.h"
+#include "test_macros.h"
+
+struct Payload : std::hazard_pointer_obj_base<Payload> {
+  std::uint64_t a;
+  std::uint64_t b; // invariant: b == ~a while the object is alive
+  explicit Payload(std::uint64_t v) : a(v), b(~v) {}
+  ~Payload() {
+    a = 0xDEADBEEF;
+    b = 0xDEADBEEF; // break the invariant on destruction
+  }
+};
+
+#if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
+constexpr int kReads  = 20000;
+constexpr int kWrites = 2000;
+#else
+constexpr int kReads  = 200000;
+constexpr int kWrites = 20000;
+#endif
+constexpr int kReaders = 4;
+constexpr int kWriters = 2;
+
+int main(int, char**) {
+  std::atomic<Payload*> current{new Payload(1)};
+  std::atomic<bool> go{false};
+
+  std::vector<std::thread> threads;
+  for (int r = 0; r < kReaders; ++r) {
+    threads.push_back(support::make_test_thread([&] {
+      while (!go.load(std::memory_order_acquire)) {
+      }
+      for (int i = 0; i < kReads; ++i) {
+        std::hazard_pointer h = std::make_hazard_pointer();
+        Payload* p            = h.protect(current);
+        if (p != nullptr) {
+          std::uint64_t a = p->a;
+          std::uint64_t b = p->b;
+          assert(b == ~a);
+        }
+      }
+    }));
+  }
+  for (int w = 0; w < kWriters; ++w) {
+    threads.push_back(support::make_test_thread([&, w] {
+      while (!go.load(std::memory_order_acquire)) {
+      }
+      for (int i = 0; i < kWrites; ++i) {
+        Payload* fresh = new Payload(static_cast<std::uint64_t>(w) * kWrites + i + 2);
+        Payload* old   = current.exchange(fresh, std::memory_order_acq_rel);
+        if (old != nullptr)
+          old->retire();
+      }
+    }));
+  }
+  go.store(true, std::memory_order_release);
+  for (std::thread& t : threads)
+    t.join();
+  Payload* last = current.exchange(nullptr);
+  if (last != nullptr)
+    last->retire();
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
new file mode 100644
index 0000000000000..c0cbf41c616c4
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
@@ -0,0 +1,83 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// void retire(D d = D()) noexcept;
+//   Move-assigns d to the deleter, then retires *this. When the object is reclaimed its deleter is
+//   invoked with a pointer to the object. When that happens is unspecified; this test only relies on
+//   the fact that the number of possibly-reclaimable objects is bounded, so retiring a large number of
+//   objects makes *some* reclamation happen.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+#include <memory>
+
+#include "test_macros.h"
+
+constexpr int N = 100000; // comfortably above any sane reclamation bound
+
+// 1. The default deleter: default_delete<T>, i.e. `delete p`.
+std::atomic<int> default_deleted{0};
+struct Plain : std::hazard_pointer_obj_base<Plain> {
+  ~Plain() { ++default_deleted; }
+};
+
+// 2. A stateful deleter, move-assigned by retire(); it checks it receives the object it was given.
+struct Tally {
+  std::atomic<int> calls{0};
+  std::atomic<int> mismatches{0};
+};
+struct Recorded;
+struct RecordingDeleter {
+  Tally* tally = nullptr;
+  void operator()(Recorded* p) const noexcept;
+};
+struct Recorded : std::hazard_pointer_obj_base<Recorded, RecordingDeleter> {
+  Recorded* self = this;
+  int payload    = 7;
+};
+void RecordingDeleter::operator()(Recorded* p) const noexcept {
+  ++tally->calls;
+  if (p->self != p || p->payload != 7)
+    ++tally->mismatches;
+  delete p;
+}
+
+// 3. A function pointer is a valid function object type for D.
+struct FnObj;
+using FnDeleter = void (*)(FnObj*);
+struct FnObj : std::hazard_pointer_obj_base<FnObj, FnDeleter> {};
+std::atomic<int> fn_calls{0};
+void fn_delete(FnObj* p) {
+  ++fn_calls;
+  delete p;
+}
+
+int main(int, char**) {
+  for (int i = 0; i < N; ++i)
+    (new Plain)->retire();
+  assert(default_deleted.load() > 0);
+
+  Tally tally;
+  for (int i = 0; i < N; ++i)
+    (new Recorded)->retire(RecordingDeleter{&tally});
+  assert(tally.calls.load() > 0);
+  assert(tally.mismatches.load() == 0);
+
+  for (int i = 0; i < N; ++i)
+    (new FnObj)->retire(&fn_delete);
+  assert(fn_calls.load() > 0);
+
+  return 0;
+}

>From cdd6922e6f4f3143c3c06d1df53e840885950d9a Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 03:01:40 +0300
Subject: [PATCH 05/17] [libc++] hazard_pointer: fix lifetime and
 slow-environment scaling in reclamation tests

---
 .../saferecl/saferecl.hp/reclamation.pass.cpp | 60 +++++++++----------
 .../saferecl/saferecl.hp/protection.pass.cpp  | 53 ++++++++--------
 .../saferecl.hp.base/retire.pass.cpp          |  4 ++
 3 files changed, 60 insertions(+), 57 deletions(-)

diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp
index 063ac1e4ae077..c6ac0cbe110d2 100644
--- a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp
@@ -24,22 +24,23 @@
 
 #include "test_macros.h"
 
+// Namespace-scope counters: a deleter may run well after the block that retired the object has
+// exited (the engine leaves up to `threshold` retirements pending at any time), so no deleter may
+// write through a pointer to an automatic-storage-duration object.
+std::atomic<int> dummies_deleted{0};
 struct Dummy : std::hazard_pointer_obj_base<Dummy> {
-  std::atomic<int>* deleted;
-  explicit Dummy(std::atomic<int>* d) : deleted(d) {}
-  ~Dummy() { ++*deleted; }
+  ~Dummy() { ++dummies_deleted; }
 };
 
+std::atomic<bool> node_deleted{false};
 struct Node : std::hazard_pointer_obj_base<Node> {
-  std::atomic<bool>* deleted;
-  explicit Node(std::atomic<bool>* d) : deleted(d) {}
-  ~Node() { deleted->store(true); }
+  ~Node() { node_deleted.store(true); }
 };
 
 // Retire enough objects to guarantee at least two passes.
-void force_passes(std::atomic<int>& deleted) {
+void force_passes() {
   for (int i = 0; i < 5000; ++i)
-    (new Dummy(&deleted))->retire();
+    (new Dummy)->retire();
 }
 
 // A deleter that itself retires another object and uses hazard pointers (re-entrancy).
@@ -65,70 +66,65 @@ struct StackObj : std::hazard_pointer_obj_base<StackObj, Noop> {};
 int main(int, char**) {
   {
     // Unprotected objects get reclaimed: after 5000 retirements at most ~1000 (+ slack) are still pending.
-    std::atomic<int> deleted{0};
-    force_passes(deleted);
-    assert(deleted.load() >= 3000);
+    int before = dummies_deleted.load();
+    force_passes();
+    assert(dummies_deleted.load() - before >= 3000);
   }
   {
     // A protected object survives passes and is reclaimed once unprotected and another pass runs.
-    std::atomic<bool> node_deleted{false};
-    std::atomic<int> deleted{0};
-    Node* n               = new Node(&node_deleted);
+    node_deleted          = false;
+    Node* n               = new Node();
     std::hazard_pointer h = std::make_hazard_pointer();
     h.reset_protection(n);
     n->retire();
-    force_passes(deleted);
+    force_passes();
     assert(!node_deleted.load());
     h.reset_protection();
-    force_passes(deleted);
+    force_passes();
     assert(node_deleted.load());
   }
   {
     // ~hazard_pointer() ends the epoch as well.
-    std::atomic<bool> node_deleted{false};
-    std::atomic<int> deleted{0};
-    Node* n = new Node(&node_deleted);
+    node_deleted = false;
+    Node* n      = new Node();
     {
       std::hazard_pointer h = std::make_hazard_pointer();
       h.reset_protection(n);
       n->retire();
-      force_passes(deleted);
+      force_passes();
       assert(!node_deleted.load());
     }
-    force_passes(deleted);
+    force_passes();
     assert(node_deleted.load());
   }
   {
     // Move-assignment over a nonempty hazard_pointer ends its epoch.
-    std::atomic<bool> node_deleted{false};
-    std::atomic<int> deleted{0};
-    Node* n               = new Node(&node_deleted);
+    node_deleted          = false;
+    Node* n               = new Node();
     std::hazard_pointer h = std::make_hazard_pointer();
     h.reset_protection(n);
     n->retire();
     h = std::hazard_pointer(); // releases the associated hazard pointer
-    force_passes(deleted);
+    force_passes();
     assert(node_deleted.load());
   }
   {
     // Deleters may retire and use hazard pointers (reclamation is re-entrant).
-    std::atomic<int> deleted{0};
     (new Nested(3))->retire();
-    force_passes(deleted);
-    force_passes(deleted);
+    force_passes();
+    force_passes();
     assert(nested_deleted.load() >= 1);
     // Drain: everything retired so far is unprotected, so a couple more passes reclaim the chain.
-    force_passes(deleted);
-    force_passes(deleted);
+    force_passes();
+    force_passes();
     assert(nested_deleted.load() == 4);
   }
   {
     // Objects with automatic storage and a no-op deleter: retire, force a pass, then they may go away.
-    std::atomic<int> deleted{0};
     std::vector<StackObj> objs(100);
     for (StackObj& o : objs)
       o.retire();
-    force_passes(deleted); // reclaims (no-op deleter) and unlinks them: destroying `objs` is now safe
+    force_passes(); // reclaims (no-op deleter) and unlinks them: destroying `objs` is now safe
   }
   return 0;
 }
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
index 8c799829c86bc..cad8b570271f0 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
@@ -21,75 +21,78 @@
 
 #include "test_macros.h"
 
+// Namespace-scope counters: a deleter may run well after the block that retired the object has
+// exited (the engine leaves up to `threshold` retirements pending at any time), so no deleter may
+// write through a pointer to an automatic-storage-duration object.
+std::atomic<int> dummies_deleted{0};
 struct Dummy : std::hazard_pointer_obj_base<Dummy> {
-  std::atomic<int>* deleted;
-  explicit Dummy(std::atomic<int>* d) : deleted(d) {}
-  ~Dummy() { ++*deleted; }
+  ~Dummy() { ++dummies_deleted; }
 };
 
+std::atomic<bool> node_deleted{false};
 struct Node : std::hazard_pointer_obj_base<Node> {
-  std::atomic<bool>* deleted;
   int value = 42;
-  explicit Node(std::atomic<bool>* d) : deleted(d) {}
-  ~Node() { deleted->store(true); }
+  ~Node() { node_deleted.store(true); }
 };
 
+#if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
+constexpr int N = 5000;
+#else
 constexpr int N = 100000;
+#endif
 
-void retire_dummies(std::atomic<int>& deleted) {
+void retire_dummies() {
   for (int i = 0; i < N; ++i)
-    (new Dummy(&deleted))->retire();
+    (new Dummy)->retire();
 }
 
 int main(int, char**) {
   {
     // Protected via protect(): survives reclamation passes; readable throughout.
-    std::atomic<bool> node_deleted{false};
-    std::atomic<int> dummies_deleted{0};
-    Node* n = new Node(&node_deleted);
+    node_deleted = false;
+    int before   = dummies_deleted.load();
+    Node* n      = new Node();
     std::atomic<Node*> src{n};
     std::hazard_pointer h = std::make_hazard_pointer();
     Node* p               = h.protect(src);
     assert(p == n);
     src.store(nullptr);
     n->retire();
-    retire_dummies(dummies_deleted);
-    assert(dummies_deleted.load() > 0); // reclamation did happen ...
-    assert(!node_deleted.load());       // ... but not of the protected object
+    retire_dummies();
+    assert(dummies_deleted.load() - before > 0); // reclamation did happen ...
+    assert(!node_deleted.load());                // ... but not of the protected object
     assert(p->value == 42);
     h.reset_protection(); // epoch ends; n becomes possibly-reclaimable
-    retire_dummies(dummies_deleted);
+    retire_dummies();
     // Whether n has been reclaimed by now is unspecified (see libcxx/.../reclamation.pass.cpp).
   }
   {
     // Protected via reset_protection(ptr) then released by ~hazard_pointer(): same guarantee.
-    std::atomic<bool> node_deleted{false};
-    std::atomic<int> dummies_deleted{0};
-    Node* n = new Node(&node_deleted);
+    node_deleted = false;
+    Node* n      = new Node();
     {
       std::hazard_pointer h = std::make_hazard_pointer();
       h.reset_protection(n);
       n->retire();
-      retire_dummies(dummies_deleted);
+      retire_dummies();
       assert(!node_deleted.load());
       assert(n->value == 42);
     } // h destroyed: epoch ends
-    retire_dummies(dummies_deleted);
+    retire_dummies();
   }
   {
     // Two hazard pointers on the same object: it stays protected until both epochs end.
-    std::atomic<bool> node_deleted{false};
-    std::atomic<int> dummies_deleted{0};
-    Node* n                = new Node(&node_deleted);
+    node_deleted           = false;
+    Node* n                = new Node();
     std::hazard_pointer h1 = std::make_hazard_pointer(), h2 = std::make_hazard_pointer();
     h1.reset_protection(n);
     h2.reset_protection(n);
     n->retire();
     h1.reset_protection();
-    retire_dummies(dummies_deleted);
+    retire_dummies();
     assert(!node_deleted.load()); // still protected by h2
     h2.reset_protection();
-    retire_dummies(dummies_deleted);
+    retire_dummies();
   }
   return 0;
 }
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
index c0cbf41c616c4..364330aeb341c 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
@@ -25,7 +25,11 @@
 
 #include "test_macros.h"
 
+#if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
+constexpr int N = 5000;
+#else
 constexpr int N = 100000; // comfortably above any sane reclamation bound
+#endif
 
 // 1. The default deleter: default_delete<T>, i.e. `delete p`.
 std::atomic<int> default_deleted{0};

>From bcc84c10181fc88dfa128939ac7fafd7f84c7b05 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 03:18:43 +0300
Subject: [PATCH 06/17] [libc++] hazard_pointer: conformance and diagnostics
 tests

---
 .../saferecl.hp/hazard_protectable.verify.cpp | 92 +++++++++++++++++++
 .../saferecl/saferecl.hp/nodiscard.verify.cpp | 30 ++++++
 .../incomplete_type.compile.pass.cpp          | 38 ++++++++
 .../saferecl.hp.base/special_members.pass.cpp | 87 ++++++++++++++++++
 .../copy.delete.compile.pass.cpp              | 23 +++++
 5 files changed, 270 insertions(+)
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/copy.delete.compile.pass.cpp

diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
new file mode 100644
index 0000000000000..02a67ab2ad714
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
@@ -0,0 +1,92 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+
+// <hazard_pointer>
+
+// The Mandates of try_protect/reset_protection/retire: T must be hazard-protectable, i.e. a class type
+// with exactly one hazard_pointer_obj_base<T, D> base, public and non-virtual, and no other
+// hazard_pointer_obj_base<T2, D2> base. Also checks the D requirements static_assert.
+
+#include <hazard_pointer>
+#include <atomic>
+
+// Positive cases: no diagnostics.
+struct Good : std::hazard_pointer_obj_base<Good> {};
+static_assert(std::__hazard_protectable<Good>);
+struct GoodDeleter {
+  void operator()(struct GoodD*) const noexcept {}
+};
+struct GoodD : std::hazard_pointer_obj_base<GoodD, GoodDeleter> {};
+static_assert(std::__hazard_protectable<GoodD>);
+struct Other {};
+struct GoodMulti : Other, std::hazard_pointer_obj_base<GoodMulti> {}; // an unrelated base is fine
+static_assert(std::__hazard_protectable<GoodMulti>);
+
+// Negative cases for the concept.
+struct NoBase {};
+static_assert(!std::__hazard_protectable<NoBase>);
+struct PrivateBase : private std::hazard_pointer_obj_base<PrivateBase> {};
+static_assert(!std::__hazard_protectable<PrivateBase>);
+struct VirtualBase : virtual std::hazard_pointer_obj_base<VirtualBase> {};
+static_assert(!std::__hazard_protectable<VirtualBase>);
+struct TwoBases : std::hazard_pointer_obj_base<TwoBases>, std::hazard_pointer_obj_base<Good> {};
+static_assert(!std::__hazard_protectable<TwoBases>);
+struct Base : std::hazard_pointer_obj_base<Base> {};
+struct Derived : Base {}; // Derived's obj_base base is hazard_pointer_obj_base<Base>, not <Derived>
+static_assert(!std::__hazard_protectable<Derived>);
+static_assert(!std::__hazard_protectable<const Good>); // strict reading of [saferecl.hp.general]/2
+static_assert(!std::__hazard_protectable<int>);
+
+void diagnostics() {
+  std::hazard_pointer h;
+  std::atomic<NoBase*> src{nullptr};
+  // try_protect's own static_assert fires, and try_protect's body unconditionally calls
+  // reset_protection(__old), whose static_assert fires too: 2 diagnostics from this one call.
+  (void)h.protect(src); // expected-error@*:* 2 {{static assertion failed}}
+  std::atomic<Derived*> src2{nullptr};
+  Derived* p = nullptr;
+  // Same double-diagnostic shape as above: try_protect<Derived> then reset_protection<Derived>.
+  (void)h.try_protect(p, src2);                         // expected-error@*:* 2 {{static assertion failed}}
+  h.reset_protection(static_cast<const int*>(nullptr)); // expected-error@*:* {{static assertion failed}}
+
+  // protect() rejects a T that isn't itself hazard-protectable, even when T is merely const-qualified:
+  // const Good has no hazard_pointer_obj_base<const Good, D> base. (Only try_protect's own static_assert
+  // fires here: the internal reset_protection(__old) call deduces its template parameter as plain Good --
+  // stripping the const already accounted for by that parameter's own `const _Tp*` declaration -- and
+  // Good is hazard-protectable, so no second diagnostic cascades.)
+  std::atomic<const Good*> csrc{nullptr};
+  (void)h.protect(csrc); // expected-error@*:* {{static assertion failed}}
+
+  // retire() is inherited unambiguously from Base; the instantiated specialization is
+  // hazard_pointer_obj_base<Base, D>, and Base -- unlike Derived -- IS hazard-protectable, so this is
+  // well-formed: no diagnostic.
+  Derived d;
+  d.retire();
+}
+
+// retire()'s Mandate fires when the hazard_pointer_obj_base<T, D> specialization itself names a T that
+// is not hazard-protectable (as opposed to the most-derived object being retired). Because retire()'s
+// body isn't guarded the way reset_protection()'s is, the failed static_assert is followed by an
+// unrelated hard error from the downcast in its reclamation lambda.
+struct Mismatched : std::hazard_pointer_obj_base<NoBase> {};
+void bad_retire() {
+  Mismatched m;
+  m.retire(); // expected-error@*:* {{static assertion failed}}
+              // expected-error@*:* {{static_cast from}}
+}
+
+// D requirements.
+struct NotDefaultConstructible {
+  NotDefaultConstructible(int);
+  void operator()(struct BadD*) const noexcept {}
+};
+struct BadD : std::hazard_pointer_obj_base<BadD, NotDefaultConstructible> {
+}; // expected-error@*:* {{static assertion failed}}
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp
new file mode 100644
index 0000000000000..6ce9c77763790
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp
@@ -0,0 +1,30 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+
+// <hazard_pointer>
+
+// [[nodiscard]] is applied as a libc++ extension to hazard_pointer::empty(), protect(), try_protect() and
+// make_hazard_pointer(): discarding these results is almost certainly a bug.
+
+#include <hazard_pointer>
+#include <atomic>
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+void test() {
+  std::hazard_pointer h;
+  std::atomic<Node*> src{nullptr};
+  Node* p = nullptr;
+  h.empty();                  // expected-warning {{ignoring return value of function}}
+  h.protect(src);             // expected-warning {{ignoring return value of function}}
+  h.try_protect(p, src);      // expected-warning {{ignoring return value of function}}
+  std::make_hazard_pointer(); // expected-warning {{ignoring return value of function}}
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp
new file mode 100644
index 0000000000000..a5026def6f167
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp
@@ -0,0 +1,38 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+
+// <hazard_pointer>
+
+// [saferecl.hp.base]/4: T may be an incomplete type. It shall be complete before any member of the
+// resulting specialization of hazard_pointer_obj_base is referenced.
+
+#include <hazard_pointer>
+
+#include "test_macros.h"
+
+struct Incomplete;
+std::hazard_pointer_obj_base<Incomplete>* declared_only(); // the specialization is named while T is incomplete
+
+struct Node : std::hazard_pointer_obj_base<Node> { // T is incomplete while the base is instantiated
+  Node* next = nullptr;
+};
+
+struct Deleter;
+struct WithForwardDeclaredT;
+struct Deleter {
+  void operator()(WithForwardDeclaredT*) const noexcept;
+};
+struct WithForwardDeclaredT : std::hazard_pointer_obj_base<WithForwardDeclaredT, Deleter> {};
+
+void use() {
+  Node n;
+  n.retire(); // T is complete here
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp
new file mode 100644
index 0000000000000..6958a9f48eb5a
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp
@@ -0,0 +1,87 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// hazard_pointer_obj_base's protected special members are defaulted. A copy or move of an object --
+// even of one that has already been retired but is still protected -- is a fresh object that can itself
+// be retired; assignment never affects retirement state.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+#include <utility>
+
+#include "test_macros.h"
+
+std::atomic<int> deleted{0};
+
+struct Node : std::hazard_pointer_obj_base<Node> {
+  int value;
+  explicit Node(int v) : value(v) {}
+  Node(const Node&)            = default;
+  Node(Node&&)                 = default;
+  Node& operator=(const Node&) = default;
+  Node& operator=(Node&&)      = default;
+  ~Node() { ++deleted; }
+};
+
+void force_reclamation() {
+  struct Dummy : std::hazard_pointer_obj_base<Dummy> {};
+  for (int i = 0; i < 100000; ++i)
+    (new Dummy)->retire();
+}
+
+int main(int, char**) {
+  {
+    // Copy of a live object, then retire both.
+    Node* a = new Node(1);
+    Node* b = new Node(*a);
+    assert(b->value == 1);
+    a->retire();
+    b->retire();
+  }
+  {
+    // Copy and move of a retired-but-protected object.
+    Node* a               = new Node(2);
+    std::hazard_pointer h = std::make_hazard_pointer();
+    h.reset_protection(a);
+    a->retire();
+    Node* copy  = new Node(*a);            // fresh, unretired
+    Node* moved = new Node(std::move(*a)); // fresh, unretired
+    assert(copy->value == 2 && moved->value == 2);
+    copy->retire();
+    moved->retire();
+    h.reset_protection();
+  }
+  {
+    // Assignment from a retired object into a live one leaves the target retirable.
+    Node* a               = new Node(3);
+    Node* b               = new Node(4);
+    std::hazard_pointer h = std::make_hazard_pointer();
+    h.reset_protection(a);
+    a->retire();
+    *b = *a;
+    assert(b->value == 3);
+    b->retire();
+    Node* c = new Node(5);
+    *c      = std::move(*a);
+    assert(c->value == 3);
+    c->retire();
+    h.reset_protection();
+  }
+  int before = deleted.load();
+  force_reclamation();
+  assert(deleted.load() > before); // some of the objects above were reclaimed exactly once each; the
+                                   // total is checked by ASan (no double free) rather than by count.
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/copy.delete.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/copy.delete.compile.pass.cpp
new file mode 100644
index 0000000000000..d36fc30db4a9a
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/copy.delete.compile.pass.cpp
@@ -0,0 +1,23 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+
+// <hazard_pointer>
+
+// hazard_pointer(const hazard_pointer&) = delete;   (implicitly, since move members are declared)
+// hazard_pointer& operator=(const hazard_pointer&) = delete;
+
+#include <hazard_pointer>
+#include <type_traits>
+
+static_assert(!std::is_copy_constructible_v<std::hazard_pointer>);
+static_assert(!std::is_copy_assignable_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_move_constructible_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_move_assignable_v<std::hazard_pointer>);

>From eb45f13ae706653bd7aebf5fc794573adef45ea5 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 03:31:15 +0300
Subject: [PATCH 07/17] [libc++] hazard_pointer: hardening assertion tests

---
 .../saferecl.hp/assert.protect.pass.cpp       | 32 +++++++++++++++++
 .../assert.reset_protection.pass.cpp          | 36 +++++++++++++++++++
 .../saferecl.hp/assert.retire.pass.cpp        | 34 ++++++++++++++++++
 .../saferecl.hp/assert.try_protect.pass.cpp   | 34 ++++++++++++++++++
 4 files changed, 136 insertions(+)
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.protect.pass.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.reset_protection.pass.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.retire.pass.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.try_protect.pass.cpp

diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.protect.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.protect.pass.cpp
new file mode 100644
index 0000000000000..956d008633e20
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.protect.pass.cpp
@@ -0,0 +1,32 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// REQUIRES: has-unix-headers
+// REQUIRES: libcpp-hardening-mode={{extensive|debug}}
+// XFAIL: libcpp-hardening-mode=debug && availability-verbose_abort-missing
+
+// <hazard_pointer>
+// template<class T> T* protect(const atomic<T*>& src) noexcept;   Preconditions: *this is not empty.
+
+#include <hazard_pointer>
+#include <atomic>
+
+#include "check_assertion.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+int main(int, char**) {
+  std::hazard_pointer empty;
+  std::atomic<Node*> src{nullptr};
+  TEST_LIBCPP_ASSERT_FAILURE((void)empty.protect(src), "hazard_pointer::reset_protection(): hazard_pointer is empty");
+  return 0;
+}
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.reset_protection.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.reset_protection.pass.cpp
new file mode 100644
index 0000000000000..3bcb4169dfda0
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.reset_protection.pass.cpp
@@ -0,0 +1,36 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// REQUIRES: has-unix-headers
+// REQUIRES: libcpp-hardening-mode={{extensive|debug}}
+// XFAIL: libcpp-hardening-mode=debug && availability-verbose_abort-missing
+
+// <hazard_pointer>
+// template<class T> void reset_protection(const T* ptr) noexcept;
+// void reset_protection(nullptr_t = nullptr) noexcept;              Preconditions: *this is not empty.
+
+#include <hazard_pointer>
+
+#include "check_assertion.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+int main(int, char**) {
+  std::hazard_pointer empty;
+  Node node;
+  TEST_LIBCPP_ASSERT_FAILURE(
+      empty.reset_protection(&node), "hazard_pointer::reset_protection(): hazard_pointer is empty");
+  TEST_LIBCPP_ASSERT_FAILURE(empty.reset_protection(), "hazard_pointer::reset_protection(): hazard_pointer is empty");
+  TEST_LIBCPP_ASSERT_FAILURE(
+      empty.reset_protection(nullptr), "hazard_pointer::reset_protection(): hazard_pointer is empty");
+  return 0;
+}
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.retire.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.retire.pass.cpp
new file mode 100644
index 0000000000000..5437b3069e614
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.retire.pass.cpp
@@ -0,0 +1,34 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// REQUIRES: has-unix-headers
+// REQUIRES: libcpp-hardening-mode={{extensive|debug}}
+// XFAIL: libcpp-hardening-mode=debug && availability-verbose_abort-missing
+
+// <hazard_pointer>
+// void retire(D d = D()) noexcept;   Preconditions: *this is not retired.
+
+#include <hazard_pointer>
+
+#include "check_assertion.h"
+
+struct Noop {
+  void operator()(struct Node*) const noexcept {}
+};
+struct Node : std::hazard_pointer_obj_base<Node, Noop> {};
+
+int main(int, char**) {
+  Node node; // never reclaimed with a real deleter, so retiring it twice is observable and harmless here
+  node.retire();
+  TEST_LIBCPP_ASSERT_FAILURE(node.retire(), "hazard_pointer_obj_base::retire(): object has already been retired");
+  return 0;
+}
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.try_protect.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.try_protect.pass.cpp
new file mode 100644
index 0000000000000..196a081e16414
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/assert.try_protect.pass.cpp
@@ -0,0 +1,34 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// REQUIRES: has-unix-headers
+// REQUIRES: libcpp-hardening-mode={{extensive|debug}}
+// XFAIL: libcpp-hardening-mode=debug && availability-verbose_abort-missing
+
+// <hazard_pointer>
+// template<class T> bool try_protect(T*& ptr, const atomic<T*>& src) noexcept;   Preconditions: *this is not empty.
+
+#include <hazard_pointer>
+#include <atomic>
+
+#include "check_assertion.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+int main(int, char**) {
+  std::hazard_pointer empty;
+  std::atomic<Node*> src{nullptr};
+  Node* p = nullptr;
+  TEST_LIBCPP_ASSERT_FAILURE(
+      (void)empty.try_protect(p, src), "hazard_pointer::reset_protection(): hazard_pointer is empty");
+  return 0;
+}

>From 70d0e195a5919d679429c1f4ab04f7c292e5c489 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 03:37:36 +0300
Subject: [PATCH 08/17] [libc++] hazard_pointer: thread-exit and
 static-destruction tests

---
 .../saferecl.hp/static_destruction.pass.cpp   | 43 +++++++++++
 .../saferecl/saferecl.hp/thread_exit.pass.cpp | 74 +++++++++++++++++++
 .../thread_exit_late_release.pass.cpp         | 55 ++++++++++++++
 3 files changed, 172 insertions(+)
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/static_destruction.pass.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp

diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/static_destruction.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/static_destruction.pass.cpp
new file mode 100644
index 0000000000000..9bceb7995980d
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/static_destruction.pass.cpp
@@ -0,0 +1,43 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// hazard_pointer objects with static storage duration are destroyed during program termination, and
+// objects may be retired from destructors of static objects: the domain must outlive them (it is never
+// destroyed).
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+std::hazard_pointer global_hp; // released during static destruction
+struct RetiresAtExit {
+  Node* node = new Node;
+  ~RetiresAtExit() {
+    node->retire();                                        // retire during static destruction
+    std::hazard_pointer late = std::make_hazard_pointer(); // and acquire/release
+    assert(!late.empty());
+  }
+} retires_at_exit;
+
+int main(int, char**) {
+  global_hp = std::make_hazard_pointer();
+  Node n;
+  global_hp.reset_protection(&n);
+  global_hp.reset_protection();
+  return 0;
+}
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp
new file mode 100644
index 0000000000000..12bb6acc424b9
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp
@@ -0,0 +1,74 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// The per-thread record cache is returned to the domain when a thread exits, and a hazard_pointer that
+// is destroyed *after* the cache is gone (e.g. owned by a thread_local destroyed later) must still be
+// released correctly. Also exercises many threads acquiring/releasing to churn the caches and the
+// available list. Nothing here is timing dependent; ASan/TSan make it a real detector.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+#include <thread>
+#include <vector>
+
+#include "make_test_thread.h"
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+// A thread_local whose destructor runs during thread teardown and destroys a hazard_pointer then.
+struct LateHolder {
+  std::hazard_pointer hp;
+  ~LateHolder() {
+    // Acquire another one during teardown too: acquire must also work when the cache is dead/absent.
+    std::hazard_pointer late = std::make_hazard_pointer();
+    assert(!late.empty());
+    Node n;
+    late.reset_protection(&n);
+    late.reset_protection();
+  }
+};
+
+void thread_body(int rounds) {
+  static thread_local LateHolder late;
+  late.hp = std::make_hazard_pointer(); // hazard_pointer released after the cache's own destructor
+  for (int i = 0; i < rounds; ++i) {
+    std::hazard_pointer hps[20]; // more than the cache capacity (9): overflow into the domain
+    for (std::hazard_pointer& h : hps)
+      h = std::make_hazard_pointer();
+    for (std::hazard_pointer& h : hps)
+      assert(!h.empty());
+  }
+}
+
+int main(int, char**) {
+#if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
+  const int threads = 8, rounds = 200;
+#else
+  const int threads = 32, rounds = 2000;
+#endif
+  for (int wave = 0; wave < 3; ++wave) {
+    std::vector<std::thread> ts;
+    for (int i = 0; i < threads; ++i)
+      ts.push_back(support::make_test_thread(thread_body, rounds));
+    for (std::thread& t : ts)
+      t.join();
+  }
+  // Records left over from exited threads are reusable by the main thread.
+  std::vector<std::hazard_pointer> hps;
+  for (int i = 0; i < 1000; ++i)
+    hps.push_back(std::make_hazard_pointer());
+  return 0;
+}
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp
new file mode 100644
index 0000000000000..22b92cfea8a06
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp
@@ -0,0 +1,55 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+// REQUIRES: has-unix-headers
+
+// A pthread key destructor that destroys (and creates) hazard pointers: this may run after libc++'s own
+// per-thread record cache has been torn down, exercising the path where release/acquire bypass the cache.
+
+#include <hazard_pointer>
+#include <cassert>
+#include <pthread.h>
+#include <thread>
+
+#include "make_test_thread.h"
+#include "test_macros.h"
+
+struct Box {
+  std::hazard_pointer hp;
+};
+
+pthread_key_t key;
+
+void destroy_box(void* p) {
+  Box* box                 = static_cast<Box*>(p);
+  std::hazard_pointer late = std::make_hazard_pointer(); // acquire after the cache may be gone
+  assert(!late.empty());
+  delete box; // release after the cache may be gone
+}
+
+int main(int, char**) {
+  {
+    std::hazard_pointer first = std::make_hazard_pointer();
+  } // create libc++'s key before ours
+  assert(pthread_key_create(&key, &destroy_box) == 0);
+  for (int i = 0; i < 64; ++i) {
+    std::thread t = support::make_test_thread([] {
+      Box* box = new Box;
+      box->hp  = std::make_hazard_pointer();
+      assert(pthread_setspecific(key, box) == 0);
+      std::hazard_pointer cached[12]; // fill the cache so eviction has work to do
+      for (std::hazard_pointer& h : cached)
+        h = std::make_hazard_pointer();
+    });
+    t.join();
+  }
+  return 0;
+}

>From b19d179d9b12222b63d2a0e28fd6c0bd734882a2 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 03:48:06 +0300
Subject: [PATCH 09/17] [libc++] hazard_pointer: correct the thread_exit test's
 coverage comment

---
 .../thread/saferecl/saferecl.hp/thread_exit.pass.cpp   | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp
index 12bb6acc424b9..6edb41bd91c8a 100644
--- a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp
@@ -12,10 +12,12 @@
 
 // <hazard_pointer>
 
-// The per-thread record cache is returned to the domain when a thread exits, and a hazard_pointer that
-// is destroyed *after* the cache is gone (e.g. owned by a thread_local destroyed later) must still be
-// released correctly. Also exercises many threads acquiring/releasing to churn the caches and the
-// available list. Nothing here is timing dependent; ASan/TSan make it a real detector.
+// The per-thread record cache is returned to the domain when a thread exits. A hazard_pointer owned by a
+// thread_local is destroyed during thread teardown, before or after that cache is gone depending on the
+// platform's ordering of thread_local destructors vs. TSD key destructors (on glibc: before); both orders
+// must be crash-free. The "cache already gone" path is exercised deterministically by
+// thread_exit_late_release.pass.cpp. Also exercises many threads acquiring/releasing to churn the caches
+// and the available list. Nothing here is timing dependent; ASan/TSan make it a real detector.
 
 #include <hazard_pointer>
 #include <atomic>

>From 8d5dff878762c0c244d73490c9558d911a0b3b81 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 04:06:52 +0300
Subject: [PATCH 10/17] [libc++] hazard_pointer: feature-test macro, module
 export, generated files, docs, ABI list

---
 libcxx/docs/FeatureTestMacroTable.rst         |  2 +-
 libcxx/docs/ReleaseNotes/23.rst               |  1 +
 libcxx/docs/Status/Cxx26Papers.csv            |  2 +-
 libcxx/include/version                        |  4 +-
 libcxx/lib/abi/CHANGELOG.TXT                  | 20 ++++++
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |  5 ++
 libcxx/modules/std.compat.cppm.in             |  3 -
 libcxx/modules/std.cppm.in                    |  4 +-
 libcxx/modules/std/hazard_pointer.inc         | 15 ++--
 libcxx/test/libcxx/transitive_includes.gen.py |  1 +
 .../test/libcxx/transitive_includes/cxx03.csv |  8 +++
 .../test/libcxx/transitive_includes/cxx11.csv |  8 +++
 .../test/libcxx/transitive_includes/cxx14.csv |  8 +++
 .../test/libcxx/transitive_includes/cxx17.csv |  8 +++
 .../test/libcxx/transitive_includes/cxx20.csv |  8 +++
 .../test/libcxx/transitive_includes/cxx23.csv |  7 ++
 .../test/libcxx/transitive_includes/cxx26.csv |  7 ++
 .../hazard_pointer.version.compile.pass.cpp   | 70 +++++++++++++++++++
 .../version.version.compile.pass.cpp          |  4 +-
 .../generate_feature_test_macro_components.py |  8 +--
 .../libcxx/test/features/availability.py      |  8 +++
 21 files changed, 178 insertions(+), 23 deletions(-)
 create mode 100644 libcxx/test/std/language.support/support.limits/support.limits.general/hazard_pointer.version.compile.pass.cpp

diff --git a/libcxx/docs/FeatureTestMacroTable.rst b/libcxx/docs/FeatureTestMacroTable.rst
index 8e26bdcd860c1..4d265bf5f049c 100644
--- a/libcxx/docs/FeatureTestMacroTable.rst
+++ b/libcxx/docs/FeatureTestMacroTable.rst
@@ -482,7 +482,7 @@ Status
     ---------------------------------------------------------- -----------------
     ``__cpp_lib_generate_random``                              *unimplemented*
     ---------------------------------------------------------- -----------------
-    ``__cpp_lib_hazard_pointer``                               *unimplemented*
+    ``__cpp_lib_hazard_pointer``                               ``202306L``
     ---------------------------------------------------------- -----------------
     ``__cpp_lib_inplace_vector``                               *unimplemented*
     ---------------------------------------------------------- -----------------
diff --git a/libcxx/docs/ReleaseNotes/23.rst b/libcxx/docs/ReleaseNotes/23.rst
index 7823c2db39867..54591e65b7e7d 100644
--- a/libcxx/docs/ReleaseNotes/23.rst
+++ b/libcxx/docs/ReleaseNotes/23.rst
@@ -52,6 +52,7 @@ Implemented Papers
 - P3383R3: ``mdspan.at()`` (`Github <https://llvm.org/PR175213>`__)
 - P3369R0: constexpr for ``uninitialized_default_construct`` (`Github <https://llvm.org/PR118380>`__)
 - P3508R0: Wording for "constexpr for specialized memory algorithms" (`Github <https://llvm.org/PR118379>`__)
+- P2530R3: Hazard Pointers for C++26 (`Github <https://llvm.org/PR105362>`__)
 
 Improvements and New Features
 -----------------------------
diff --git a/libcxx/docs/Status/Cxx26Papers.csv b/libcxx/docs/Status/Cxx26Papers.csv
index a951ac4c6c833..40ca9b7929567 100644
--- a/libcxx/docs/Status/Cxx26Papers.csv
+++ b/libcxx/docs/Status/Cxx26Papers.csv
@@ -4,7 +4,7 @@
 "`P2587R3 <https://wg21.link/P2587R3>`__","``to_string`` or not ``to_string``","2023-06 (Varna)","","","`#105359 <https://github.com/llvm/llvm-project/issues/105359>`__",""
 "`P2562R1 <https://wg21.link/P2562R1>`__","``constexpr`` Stable Sorting","2023-06 (Varna)","|Complete|","21","`#105360 <https://github.com/llvm/llvm-project/issues/105360>`__",""
 "`P2545R4 <https://wg21.link/P2545R4>`__","Read-Copy Update (RCU)","2023-06 (Varna)","","","`#105361 <https://github.com/llvm/llvm-project/issues/105361>`__",""
-"`P2530R3 <https://wg21.link/P2530R3>`__","Hazard Pointers for C++26","2023-06 (Varna)","","","`#105362 <https://github.com/llvm/llvm-project/issues/105362>`__",""
+"`P2530R3 <https://wg21.link/P2530R3>`__","Hazard Pointers for C++26","2023-06 (Varna)","|Complete|","23","`#105362 <https://github.com/llvm/llvm-project/issues/105362>`__",""
 "`P2538R1 <https://wg21.link/P2538R1>`__","ADL-proof ``std::projected``","2023-06 (Varna)","|Complete|","18","`#105363 <https://github.com/llvm/llvm-project/issues/105363>`__",""
 "`P2495R3 <https://wg21.link/P2495R3>`__","Interfacing ``stringstream``\s with ``string_view``","2023-06 (Varna)","|Complete|","19","`#105364 <https://github.com/llvm/llvm-project/issues/105364>`__",""
 "`P2510R3 <https://wg21.link/P2510R3>`__","Formatting pointers","2023-06 (Varna)","|Complete|","17","`#105365 <https://github.com/llvm/llvm-project/issues/105365>`__","Implemented as a DR against C++20. (MSVC STL and libstdc++ will do the same.)"
diff --git a/libcxx/include/version b/libcxx/include/version
index 7f2dc9e4b72ab..bc8956abe9bfc 100644
--- a/libcxx/include/version
+++ b/libcxx/include/version
@@ -593,7 +593,9 @@ __cpp_lib_void_t                                        201411L <type_traits>
 # endif
 // # define __cpp_lib_function_ref                         202306L
 // # define __cpp_lib_generate_random                      202403L
-// # define __cpp_lib_hazard_pointer                       202306L
+# if _LIBCPP_HAS_THREADS && _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER
+#   define __cpp_lib_hazard_pointer                     202306L
+# endif
 // # define __cpp_lib_inplace_vector                       202406L
 # undef  __cpp_lib_integer_sequence
 # define __cpp_lib_integer_sequence                     202511L
diff --git a/libcxx/lib/abi/CHANGELOG.TXT b/libcxx/lib/abi/CHANGELOG.TXT
index 6182a396ee2c7..e2bf0ee088610 100644
--- a/libcxx/lib/abi/CHANGELOG.TXT
+++ b/libcxx/lib/abi/CHANGELOG.TXT
@@ -16,6 +16,26 @@ New entries should be added directly below the "Version" header.
 Version 23.0
 ------------
 
+* [libc++] Implement P2530R3 hazard pointers
+
+  <hazard_pointer> keeps its domain (hazard pointer records, retired lists, reclamation) in the built
+  library, behind three exported functions used by the inline hazard_pointer/hazard_pointer_obj_base.
+
+  All platforms
+  -------------
+  Symbol added: _ZNSt3__124__hazard_pointer_acquireEv
+  Symbol added: _ZNSt3__124__hazard_pointer_releaseEPNS_21__hazard_pointer_slotE
+  Symbol added: _ZNSt3__123__hazard_pointer_retireEPNS_25__hazard_pointer_obj_nodeE
+
+  <arch>-unknown-linux-gnu
+  -------------------------
+  The built library's hazard pointer domain allocates using nothrow new, which is the first use of
+  these overloads anywhere in libc++.so itself; this pulls in two previously-unreferenced external
+  symbols resolved from libc++abi at load time.
+
+  Symbol added: _ZnwmRKSt9nothrow_t
+  Symbol added: _ZnamRKSt9nothrow_t
+
 * [libc++] Remove ios_base::__xindex_ from the ABI
 
   `__xindex_` is only ever used from the dylib from a single function. We can simplify the code a bit by making the
diff --git a/libcxx/lib/abi/x86_64-unknown-linux-gnu.libcxxabi.v1.stable.exceptions.nonew.abilist b/libcxx/lib/abi/x86_64-unknown-linux-gnu.libcxxabi.v1.stable.exceptions.nonew.abilist
index 717d4c9bee726..77dfb4dab9892 100644
--- a/libcxx/lib/abi/x86_64-unknown-linux-gnu.libcxxabi.v1.stable.exceptions.nonew.abilist
+++ b/libcxx/lib/abi/x86_64-unknown-linux-gnu.libcxxabi.v1.stable.exceptions.nonew.abilist
@@ -41,8 +41,10 @@
 {'is_defined': False, 'name': '_ZdlPvm', 'type': 'FUNC'}
 {'is_defined': False, 'name': '_ZdlPvmSt11align_val_t', 'type': 'FUNC'}
 {'is_defined': False, 'name': '_Znam', 'type': 'FUNC'}
+{'is_defined': False, 'name': '_ZnamRKSt9nothrow_t', 'type': 'FUNC'}
 {'is_defined': False, 'name': '_ZnamSt11align_val_t', 'type': 'FUNC'}
 {'is_defined': False, 'name': '_Znwm', 'type': 'FUNC'}
+{'is_defined': False, 'name': '_ZnwmRKSt9nothrow_t', 'type': 'FUNC'}
 {'is_defined': False, 'name': '_ZnwmSt11align_val_t', 'type': 'FUNC'}
 {'is_defined': False, 'name': '__cxa_allocate_exception', 'type': 'FUNC'}
 {'is_defined': False, 'name': '__cxa_begin_catch', 'type': 'FUNC'}
@@ -1224,8 +1226,11 @@
 {'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_allEPVKv', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_oneEPVKNS_17__cxx_atomic_implIiNS_22__cxx_atomic_base_implIiEEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_oneEPVKv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt3__123__hazard_pointer_retireEPNS_25__hazard_pointer_obj_nodeE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt3__123__libcpp_atomic_monitorEPVKNS_17__cxx_atomic_implIiNS_22__cxx_atomic_base_implIiEEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt3__123__libcpp_atomic_monitorEPVKv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt3__124__hazard_pointer_acquireEv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt3__124__hazard_pointer_releaseEPNS_21__hazard_pointer_slotE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt3__125notify_all_at_thread_exitERNS_18condition_variableENS_11unique_lockINS_5mutexEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt3__126__atomic_notify_all_nativeILm4EEEvPKv', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt3__126__atomic_notify_one_nativeILm4EEEvPKv', 'type': 'FUNC'}
diff --git a/libcxx/modules/std.compat.cppm.in b/libcxx/modules/std.compat.cppm.in
index dd7385bf33a42..71bd1df595b1f 100644
--- a/libcxx/modules/std.compat.cppm.in
+++ b/libcxx/modules/std.compat.cppm.in
@@ -54,9 +54,6 @@ module;
 #  if __has_include(<generator>)
 #    error "please update the header information for <generator> in headers_not_available in utils/libcxx/header_information.py"
 #  endif // __has_include(<generator>)
-#  if __has_include(<hazard_pointer>)
-#    error "please update the header information for <hazard_pointer> in headers_not_available in utils/libcxx/header_information.py"
-#  endif // __has_include(<hazard_pointer>)
 #  if __has_include(<inplace_vector>)
 #    error "please update the header information for <inplace_vector> in headers_not_available in utils/libcxx/header_information.py"
 #  endif // __has_include(<inplace_vector>)
diff --git a/libcxx/modules/std.cppm.in b/libcxx/modules/std.cppm.in
index 6de6369c3387f..91a752d921704 100644
--- a/libcxx/modules/std.cppm.in
+++ b/libcxx/modules/std.cppm.in
@@ -65,6 +65,7 @@ module;
 #include <fstream>
 #include <functional>
 #include <future>
+#include <hazard_pointer>
 #include <initializer_list>
 #include <iomanip>
 #include <ios>
@@ -136,9 +137,6 @@ module;
 #  if __has_include(<generator>)
 #    error "please update the header information for <generator> in headers_not_available in utils/libcxx/header_information.py"
 #  endif // __has_include(<generator>)
-#  if __has_include(<hazard_pointer>)
-#    error "please update the header information for <hazard_pointer> in headers_not_available in utils/libcxx/header_information.py"
-#  endif // __has_include(<hazard_pointer>)
 #  if __has_include(<inplace_vector>)
 #    error "please update the header information for <inplace_vector> in headers_not_available in utils/libcxx/header_information.py"
 #  endif // __has_include(<inplace_vector>)
diff --git a/libcxx/modules/std/hazard_pointer.inc b/libcxx/modules/std/hazard_pointer.inc
index e56f4d0bf4363..0f9b833cc6e2b 100644
--- a/libcxx/modules/std/hazard_pointer.inc
+++ b/libcxx/modules/std/hazard_pointer.inc
@@ -8,16 +8,15 @@
 //===----------------------------------------------------------------------===//
 
 export namespace std {
-#if 0
-#  if _LIBCPP_STD_VER >= 23
-  // 4.1.3, class template hazard_pointer_obj_base
+#if _LIBCPP_STD_VER >= 26 && _LIBCPP_HAS_THREADS
+  // [saferecl.hp.base], class template hazard_pointer_obj_base
   using std::hazard_pointer_obj_base;
-  // 4.1.4, class hazard_pointer
+
+  // [saferecl.hp.holder], class hazard_pointer
   using std::hazard_pointer;
-  // 4.1.5, Construct non-empty hazard_pointer
+
+  // [saferecl.hp.holder.nonmem], non-member functions
   using std::make_hazard_pointer;
-  // 4.1.6, Hazard pointer swap
   using std::swap;
-#  endif // _LIBCPP_STD_VER >= 23
-#endif
+#endif // _LIBCPP_STD_VER >= 26 && _LIBCPP_HAS_THREADS
 } // namespace std
diff --git a/libcxx/test/libcxx/transitive_includes.gen.py b/libcxx/test/libcxx/transitive_includes.gen.py
index 198a155ac0ee5..3dd8f05748e39 100644
--- a/libcxx/test/libcxx/transitive_includes.gen.py
+++ b/libcxx/test/libcxx/transitive_includes.gen.py
@@ -39,6 +39,7 @@
     "experimental/utility": "// UNSUPPORTED: c++03",
     "filesystem": "// UNSUPPORTED: no-filesystem, c++03, c++11, c++14",
     "future": "// UNSUPPORTED: no-threads, c++03",
+    "hazard_pointer": "// UNSUPPORTED: no-threads, c++03, c++11, c++14, c++17, c++20, c++23",
     "latch": "// UNSUPPORTED: no-threads, c++03, c++11, c++14, c++17",
     "mutex": "// UNSUPPORTED: no-threads, c++03",
     "print": "// UNSUPPORTED: no-filesystem, c++03, c++11, c++14, c++17, c++20, availability-fp_to_chars-missing",  # TODO PRINT investigate
diff --git a/libcxx/test/libcxx/transitive_includes/cxx03.csv b/libcxx/test/libcxx/transitive_includes/cxx03.csv
index c0031543e47bc..e9f5903442e3f 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx03.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx03.csv
@@ -945,6 +945,14 @@ future utility
 future variant
 future vector
 future version
+hazard_pointer climits
+hazard_pointer cstdint
+hazard_pointer cstring
+hazard_pointer ctime
+hazard_pointer limits
+hazard_pointer ratio
+hazard_pointer type_traits
+hazard_pointer version
 initializer_list cstddef
 initializer_list version
 iomanip algorithm
diff --git a/libcxx/test/libcxx/transitive_includes/cxx11.csv b/libcxx/test/libcxx/transitive_includes/cxx11.csv
index c0031543e47bc..e9f5903442e3f 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx11.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx11.csv
@@ -945,6 +945,14 @@ future utility
 future variant
 future vector
 future version
+hazard_pointer climits
+hazard_pointer cstdint
+hazard_pointer cstring
+hazard_pointer ctime
+hazard_pointer limits
+hazard_pointer ratio
+hazard_pointer type_traits
+hazard_pointer version
 initializer_list cstddef
 initializer_list version
 iomanip algorithm
diff --git a/libcxx/test/libcxx/transitive_includes/cxx14.csv b/libcxx/test/libcxx/transitive_includes/cxx14.csv
index c2eb5b44e8d7a..ac50e37f3c5fe 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx14.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx14.csv
@@ -968,6 +968,14 @@ future utility
 future variant
 future vector
 future version
+hazard_pointer climits
+hazard_pointer cstdint
+hazard_pointer cstring
+hazard_pointer ctime
+hazard_pointer limits
+hazard_pointer ratio
+hazard_pointer type_traits
+hazard_pointer version
 initializer_list cstddef
 initializer_list version
 iomanip algorithm
diff --git a/libcxx/test/libcxx/transitive_includes/cxx17.csv b/libcxx/test/libcxx/transitive_includes/cxx17.csv
index 332cb62f35b5f..236db88b5b17e 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx17.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx17.csv
@@ -971,6 +971,14 @@ future utility
 future variant
 future vector
 future version
+hazard_pointer climits
+hazard_pointer cstdint
+hazard_pointer cstring
+hazard_pointer ctime
+hazard_pointer limits
+hazard_pointer ratio
+hazard_pointer type_traits
+hazard_pointer version
 initializer_list cstddef
 initializer_list version
 iomanip algorithm
diff --git a/libcxx/test/libcxx/transitive_includes/cxx20.csv b/libcxx/test/libcxx/transitive_includes/cxx20.csv
index 55c79acff5a8f..9edc866b85dfd 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx20.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx20.csv
@@ -965,6 +965,14 @@ future utility
 future variant
 future vector
 future version
+hazard_pointer climits
+hazard_pointer cstdint
+hazard_pointer cstring
+hazard_pointer ctime
+hazard_pointer limits
+hazard_pointer ratio
+hazard_pointer type_traits
+hazard_pointer version
 initializer_list cstddef
 initializer_list version
 iomanip algorithm
diff --git a/libcxx/test/libcxx/transitive_includes/cxx23.csv b/libcxx/test/libcxx/transitive_includes/cxx23.csv
index 073f698786117..a276d398fdd8b 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx23.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx23.csv
@@ -466,6 +466,13 @@ future string_view
 future tuple
 future typeinfo
 future version
+hazard_pointer climits
+hazard_pointer cstdint
+hazard_pointer cstring
+hazard_pointer ctime
+hazard_pointer limits
+hazard_pointer ratio
+hazard_pointer version
 initializer_list version
 iomanip cctype
 iomanip cerrno
diff --git a/libcxx/test/libcxx/transitive_includes/cxx26.csv b/libcxx/test/libcxx/transitive_includes/cxx26.csv
index 1450a13e125ef..76eb017ed4fac 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx26.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx26.csv
@@ -442,6 +442,13 @@ future string_view
 future tuple
 future typeinfo
 future version
+hazard_pointer climits
+hazard_pointer cstdint
+hazard_pointer cstring
+hazard_pointer ctime
+hazard_pointer limits
+hazard_pointer ratio
+hazard_pointer version
 initializer_list version
 iomanip cctype
 iomanip cerrno
diff --git a/libcxx/test/std/language.support/support.limits/support.limits.general/hazard_pointer.version.compile.pass.cpp b/libcxx/test/std/language.support/support.limits/support.limits.general/hazard_pointer.version.compile.pass.cpp
new file mode 100644
index 0000000000000..8aff5871c313f
--- /dev/null
+++ b/libcxx/test/std/language.support/support.limits/support.limits.general/hazard_pointer.version.compile.pass.cpp
@@ -0,0 +1,70 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// WARNING: This test was generated by generate_feature_test_macro_components.py
+// and should not be edited manually.
+
+// UNSUPPORTED: no-threads
+
+// <hazard_pointer>
+
+// Test the feature test macros defined by <hazard_pointer>
+
+// clang-format off
+
+#include <hazard_pointer>
+#include "test_macros.h"
+
+#if TEST_STD_VER < 14
+
+#  ifdef __cpp_lib_hazard_pointer
+#    error "__cpp_lib_hazard_pointer should not be defined before c++26"
+#  endif
+
+#elif TEST_STD_VER == 14
+
+#  ifdef __cpp_lib_hazard_pointer
+#    error "__cpp_lib_hazard_pointer should not be defined before c++26"
+#  endif
+
+#elif TEST_STD_VER == 17
+
+#  ifdef __cpp_lib_hazard_pointer
+#    error "__cpp_lib_hazard_pointer should not be defined before c++26"
+#  endif
+
+#elif TEST_STD_VER == 20
+
+#  ifdef __cpp_lib_hazard_pointer
+#    error "__cpp_lib_hazard_pointer should not be defined before c++26"
+#  endif
+
+#elif TEST_STD_VER == 23
+
+#  ifdef __cpp_lib_hazard_pointer
+#    error "__cpp_lib_hazard_pointer should not be defined before c++26"
+#  endif
+
+#elif TEST_STD_VER > 23
+
+#  if !defined(_LIBCPP_VERSION) || (_LIBCPP_HAS_THREADS && _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER)
+#    ifndef __cpp_lib_hazard_pointer
+#      error "__cpp_lib_hazard_pointer should be defined in c++26"
+#    endif
+#    if __cpp_lib_hazard_pointer != 202306L
+#      error "__cpp_lib_hazard_pointer should have the value 202306L in c++26"
+#    endif
+#  else
+#    ifdef __cpp_lib_hazard_pointer
+#      error "__cpp_lib_hazard_pointer should not be defined when the requirement '!defined(_LIBCPP_VERSION) || (_LIBCPP_HAS_THREADS && _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER)' is not met!"
+#    endif
+#  endif
+
+#endif // TEST_STD_VER > 23
+
+// clang-format on
diff --git a/libcxx/test/std/language.support/support.limits/support.limits.general/version.version.compile.pass.cpp b/libcxx/test/std/language.support/support.limits/support.limits.general/version.version.compile.pass.cpp
index d9c78b73f7e23..aba071d0026f3 100644
--- a/libcxx/test/std/language.support/support.limits/support.limits.general/version.version.compile.pass.cpp
+++ b/libcxx/test/std/language.support/support.limits/support.limits.general/version.version.compile.pass.cpp
@@ -7172,7 +7172,7 @@
 #    error "__cpp_lib_has_unique_object_representations should have the value 201606L in c++26"
 #  endif
 
-#  if !defined(_LIBCPP_VERSION)
+#  if !defined(_LIBCPP_VERSION) || (_LIBCPP_HAS_THREADS && _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER)
 #    ifndef __cpp_lib_hazard_pointer
 #      error "__cpp_lib_hazard_pointer should be defined in c++26"
 #    endif
@@ -7181,7 +7181,7 @@
 #    endif
 #  else
 #    ifdef __cpp_lib_hazard_pointer
-#      error "__cpp_lib_hazard_pointer should not be defined because it is unimplemented in libc++!"
+#      error "__cpp_lib_hazard_pointer should not be defined when the requirement '!defined(_LIBCPP_VERSION) || (_LIBCPP_HAS_THREADS && _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER)' is not met!"
 #    endif
 #  endif
 
diff --git a/libcxx/utils/generate_feature_test_macro_components.py b/libcxx/utils/generate_feature_test_macro_components.py
index d764a1f677ba6..c9084419e533a 100644
--- a/libcxx/utils/generate_feature_test_macro_components.py
+++ b/libcxx/utils/generate_feature_test_macro_components.py
@@ -742,10 +742,9 @@ def add_version_header(tc):
         {
             "name": "__cpp_lib_hazard_pointer",
             "values": {"c++26": 202306},  # P2530R3 Hazard Pointers for C++26
-            "headers": [
-                "hazard_pointer"  # TODO verify this entry since the paper was underspecified.
-            ],
-            "unimplemented": True,
+            "headers": ["hazard_pointer"],
+            "test_suite_guard": "!defined(_LIBCPP_VERSION) || (_LIBCPP_HAS_THREADS && _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER)",
+            "libcxx_guard": "_LIBCPP_HAS_THREADS && _LIBCPP_AVAILABILITY_HAS_HAZARD_POINTER",
         },
         {
             "name": "__cpp_lib_hypot",
@@ -1547,6 +1546,7 @@ def add_version_header(tc):
     "barrier": ["UNSUPPORTED: no-threads"],
     "filesystem": ["UNSUPPORTED: no-filesystem"],
     "fstream": ["UNSUPPORTED: no-localization"],
+    "hazard_pointer": ["UNSUPPORTED: no-threads"],
     "iomanip": ["UNSUPPORTED: no-localization"],
     "ios": ["UNSUPPORTED: no-localization"],
     "iostream": ["UNSUPPORTED: no-localization"],
diff --git a/libcxx/utils/libcxx/test/features/availability.py b/libcxx/utils/libcxx/test/features/availability.py
index bae7b33d2130b..a4ba23c229c82 100644
--- a/libcxx/utils/libcxx/test/features/availability.py
+++ b/libcxx/utils/libcxx/test/features/availability.py
@@ -187,6 +187,14 @@
             cfg.available_features,
         ),
     ),
+    # Tests that require the hazard pointer domain (<hazard_pointer>) in the built library.
+    Feature(
+        name="availability-hazard_pointer-missing",
+        when=lambda cfg: BooleanExpression.evaluate(
+            "!libcpp-has-no-availability-markup && (stdlib=apple-libc++ && !_target-has-llvm-23)",
+            cfg.available_features,
+        ),
+    ),
     # Tests that require time zone database support in the built library
     Feature(
         name="availability-tzdb-missing",

>From b0e66ea2732b7c07760dc83ee63b04b21adc3c43 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 04:19:03 +0300
Subject: [PATCH 11/17] [libc++] hazard_pointer: add benchmark

---
 .../test/benchmarks/hazard_pointer.bench.cpp  | 129 ++++++++++++++++++
 1 file changed, 129 insertions(+)
 create mode 100644 libcxx/test/benchmarks/hazard_pointer.bench.cpp

diff --git a/libcxx/test/benchmarks/hazard_pointer.bench.cpp b/libcxx/test/benchmarks/hazard_pointer.bench.cpp
new file mode 100644
index 0000000000000..210ce21ccc449
--- /dev/null
+++ b/libcxx/test/benchmarks/hazard_pointer.bench.cpp
@@ -0,0 +1,129 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cstddef>
+#include <memory>
+#include <vector>
+
+#include "benchmark/benchmark.h"
+
+namespace {
+
+struct Node : std::hazard_pointer_obj_base<Node> {
+  std::size_t value = 42;
+};
+
+// make + destroy a nonempty hazard_pointer per iteration (thread-cache hit in the steady state).
+void BM_make_destroy(benchmark::State& state) {
+  for (auto _ : state) {
+    std::hazard_pointer h = std::make_hazard_pointer();
+    benchmark::DoNotOptimize(h);
+  }
+}
+
+// protect() per iteration with one hazard_pointer made once; the protection is left in place.
+void BM_protect_combined(benchmark::State& state) {
+  auto own = std::make_unique<Node>();
+  std::atomic<Node*> src{own.get()};
+  benchmark::DoNotOptimize(src);
+  std::hazard_pointer h = std::make_hazard_pointer();
+  std::size_t sum       = 0;
+  for (auto _ : state) {
+    Node* p = h.protect(src);
+    benchmark::DoNotOptimize(p->value);
+    sum += p->value;
+  }
+  h.reset_protection();
+  benchmark::DoNotOptimize(sum);
+}
+
+// protect() + reset_protection() per iteration.
+void BM_protect_separate(benchmark::State& state) {
+  auto own = std::make_unique<Node>();
+  std::atomic<Node*> src{own.get()};
+  benchmark::DoNotOptimize(src);
+  std::hazard_pointer h = std::make_hazard_pointer();
+  std::size_t sum       = 0;
+  for (auto _ : state) {
+    Node* p = h.protect(src);
+    benchmark::DoNotOptimize(p->value);
+    sum += p->value;
+    h.reset_protection();
+  }
+  benchmark::DoNotOptimize(sum);
+}
+
+// make + protect + destroy per iteration.
+void BM_make_protect(benchmark::State& state) {
+  auto own = std::make_unique<Node>();
+  std::atomic<Node*> src{own.get()};
+  benchmark::DoNotOptimize(src);
+  std::size_t sum = 0;
+  for (auto _ : state) {
+    std::hazard_pointer h = std::make_hazard_pointer();
+    Node* p               = h.protect(src);
+    benchmark::DoNotOptimize(p->value);
+    sum += p->value;
+  }
+  benchmark::DoNotOptimize(sum);
+}
+
+// retire() of a pre-allocated object per iteration (allocation happens in untimed batches); reclamation
+// runs inline every ~1000 retirements and is part of the measurement, as it is in real use.
+void BM_retire(benchmark::State& state) {
+  constexpr std::size_t kBatch = std::size_t{1} << 16;
+  std::vector<std::unique_ptr<Node>> objs;
+  std::size_t next = kBatch;
+  for (auto _ : state) {
+    if (next == kBatch) {
+      state.PauseTiming();
+      objs.clear();
+      objs.reserve(kBatch);
+      for (std::size_t i = 0; i < kBatch; ++i)
+        objs.push_back(std::make_unique<Node>());
+      next = 0;
+      state.ResumeTiming();
+    }
+    Node* n = objs[next++].release();
+    n->retire();
+  }
+}
+
+// Multi-threaded: every thread makes and destroys hazard pointers.
+void BM_mt_make_destroy(benchmark::State& state) {
+  for (auto _ : state) {
+    std::hazard_pointer h = std::make_hazard_pointer();
+    benchmark::DoNotOptimize(h);
+  }
+}
+
+// Multi-threaded: every thread allocates and retires objects (contended retired lists + reclamation).
+void BM_mt_retire(benchmark::State& state) {
+  for (auto _ : state) {
+    Node* n = new Node;
+    n->retire();
+  }
+}
+
+} // namespace
+
+BENCHMARK(BM_make_destroy);
+BENCHMARK(BM_protect_combined);
+BENCHMARK(BM_protect_separate);
+BENCHMARK(BM_make_protect);
+BENCHMARK(BM_retire);
+BENCHMARK(BM_mt_make_destroy)->Threads(1)->Threads(2)->Threads(4)->Threads(8);
+BENCHMARK(BM_mt_retire)->Threads(1)->Threads(2)->Threads(4)->Threads(8);
+
+BENCHMARK_MAIN();

>From 455a55dac74133102967c044915ba327f231a97e Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 06:17:57 +0300
Subject: [PATCH 12/17] [libc++] hazard_pointer: availability markup on swap;
 if-constexpr Mandates guards

The free swap(hazard_pointer&, hazard_pointer&) named an availability-annotated
type without carrying the annotation itself. Where
_LIBCPP_INTRODUCED_IN_LLVM_23_ATTRIBUTE expands to __attribute__((unavailable)),
that is a hard error inside the header, even in a system header; add
_LIBCPP_AVAILABILITY_HAZARD_POINTER the way <__chrono/time_zone.h> does for the
free operators.

Guard the bodies of try_protect(), reset_protection(const T*) and retire() with
`if constexpr (__hazard_protectable<T>)` so a Mandates violation produces exactly
one diagnostic instead of a cascade, and make reset_protection()'s static_assert
message name the function like the other two do.
---
 .../include/__hazard_pointer/hazard_pointer.h | 34 +++++++++++++------
 .../hazard_pointer_obj_base.h                 | 20 ++++++-----
 .../saferecl.hp/hazard_protectable.verify.cpp | 30 +++++++++-------
 3 files changed, 52 insertions(+), 32 deletions(-)

diff --git a/libcxx/include/__hazard_pointer/hazard_pointer.h b/libcxx/include/__hazard_pointer/hazard_pointer.h
index 136569f6b922e..82593990781d0 100644
--- a/libcxx/include/__hazard_pointer/hazard_pointer.h
+++ b/libcxx/include/__hazard_pointer/hazard_pointer.h
@@ -84,22 +84,28 @@ class _LIBCPP_AVAILABILITY_HAZARD_POINTER hazard_pointer {
         __hazard_protectable<_Tp>,
         "hazard_pointer::try_protect(): T must be a hazard-protectable type (a class with exactly one public, "
         "non-virtual hazard_pointer_obj_base<T, D> base)");
-    _Tp* __old = __ptr;
-    reset_protection(__old);
-    std::__hazard_pointer_reader_fence();
-    __ptr = __src.load(memory_order_acquire);
-    if (__old != __ptr) {
-      reset_protection();
-      return false;
+    // Guarding the body keeps a Mandates violation down to the one static_assert above instead of also
+    // diagnosing every construct below that depends on the requirement.
+    if constexpr (__hazard_protectable<_Tp>) {
+      _Tp* __old = __ptr;
+      reset_protection(__old);
+      std::__hazard_pointer_reader_fence();
+      __ptr = __src.load(memory_order_acquire);
+      if (__old != __ptr) {
+        reset_protection();
+        return false;
+      }
+      return true;
+    } else {
+      return false; // unreachable: the static_assert above already fired
     }
-    return true;
   }
 
   template <class _Tp>
   _LIBCPP_HIDE_FROM_ABI void reset_protection(const _Tp* __ptr) noexcept {
     static_assert(__hazard_protectable<_Tp>,
-                  "hazard_pointer: T must be a hazard-protectable type (a class with exactly one public, "
-                  "non-virtual hazard_pointer_obj_base<T, D> base)");
+                  "hazard_pointer::reset_protection(): T must be a hazard-protectable type (a class with exactly one "
+                  "public, non-virtual hazard_pointer_obj_base<T, D> base)");
     if constexpr (__hazard_protectable<_Tp>) {
       _LIBCPP_ASSERT_NON_NULL(__slot_ != nullptr, "hazard_pointer::reset_protection(): hazard_pointer is empty");
       __slot_->__value_.store(__node_of(__ptr), memory_order_release);
@@ -118,7 +124,13 @@ class _LIBCPP_AVAILABILITY_HAZARD_POINTER hazard_pointer {
   return hazard_pointer(std::__hazard_pointer_acquire());
 }
 
-_LIBCPP_HIDE_FROM_ABI inline void swap(hazard_pointer& __a, hazard_pointer& __b) noexcept { __a.swap(__b); }
+// The availability attribute is required, not just informative: where it expands to
+// __attribute__((unavailable)), naming hazard_pointer in a declaration that is not itself unavailable is an
+// error even inside a system header.
+_LIBCPP_HIDE_FROM_ABI _LIBCPP_AVAILABILITY_HAZARD_POINTER inline void
+swap(hazard_pointer& __a, hazard_pointer& __b) noexcept {
+  __a.swap(__b);
+}
 
 _LIBCPP_END_NAMESPACE_STD
 
diff --git a/libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h b/libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h
index 25f81f82fc186..2247b7fc4e0f1 100644
--- a/libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h
+++ b/libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h
@@ -67,14 +67,18 @@ class _LIBCPP_AVAILABILITY_HAZARD_POINTER hazard_pointer_obj_base : private __ha
     static_assert(__hazard_protectable<_Tp>,
                   "hazard_pointer_obj_base<T, D>::retire(): T must be a hazard-protectable type (a class with exactly "
                   "one public, non-virtual hazard_pointer_obj_base<T, D> base)");
-    _LIBCPP_ASSERT_VALID_DEALLOCATION(
-        this->__next_ == this, "hazard_pointer_obj_base::retire(): object has already been retired");
-    __deleter_       = std::move(__d);
-    this->__reclaim_ = [](__hazard_pointer_obj_node* __node) noexcept {
-      hazard_pointer_obj_base* __base = static_cast<hazard_pointer_obj_base*>(__node);
-      __base->__deleter_(static_cast<_Tp*>(__base));
-    };
-    std::__hazard_pointer_retire(this);
+    // Guarding the body keeps a Mandates violation down to the one static_assert above instead of also
+    // diagnosing the derived-to-base conversion inside the reclamation function below.
+    if constexpr (__hazard_protectable<_Tp>) {
+      _LIBCPP_ASSERT_VALID_DEALLOCATION(
+          this->__next_ == this, "hazard_pointer_obj_base::retire(): object has already been retired");
+      __deleter_       = std::move(__d);
+      this->__reclaim_ = [](__hazard_pointer_obj_node* __node) noexcept {
+        hazard_pointer_obj_base* __base = static_cast<hazard_pointer_obj_base*>(__node);
+        __base->__deleter_(static_cast<_Tp*>(__base));
+      };
+      std::__hazard_pointer_retire(this);
+    }
   }
 
 protected:
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
index 02a67ab2ad714..1dd3f127a913b 100644
--- a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
@@ -35,6 +35,8 @@ struct NoBase {};
 static_assert(!std::__hazard_protectable<NoBase>);
 struct PrivateBase : private std::hazard_pointer_obj_base<PrivateBase> {};
 static_assert(!std::__hazard_protectable<PrivateBase>);
+struct ProtectedBase : protected std::hazard_pointer_obj_base<ProtectedBase> {};
+static_assert(!std::__hazard_protectable<ProtectedBase>);
 struct VirtualBase : virtual std::hazard_pointer_obj_base<VirtualBase> {};
 static_assert(!std::__hazard_protectable<VirtualBase>);
 struct TwoBases : std::hazard_pointer_obj_base<TwoBases>, std::hazard_pointer_obj_base<Good> {};
@@ -48,20 +50,17 @@ static_assert(!std::__hazard_protectable<int>);
 void diagnostics() {
   std::hazard_pointer h;
   std::atomic<NoBase*> src{nullptr};
-  // try_protect's own static_assert fires, and try_protect's body unconditionally calls
-  // reset_protection(__old), whose static_assert fires too: 2 diagnostics from this one call.
-  (void)h.protect(src); // expected-error@*:* 2 {{static assertion failed}}
+  // Exactly one diagnostic per offending call: the bodies of try_protect and reset_protection are guarded
+  // by `if constexpr (__hazard_protectable<T>)`, so nothing downstream of the failed Mandate is
+  // instantiated.
+  (void)h.protect(src); // expected-error@*:* {{static assertion failed}}
   std::atomic<Derived*> src2{nullptr};
   Derived* p = nullptr;
-  // Same double-diagnostic shape as above: try_protect<Derived> then reset_protection<Derived>.
-  (void)h.try_protect(p, src2);                         // expected-error@*:* 2 {{static assertion failed}}
+  (void)h.try_protect(p, src2);                         // expected-error@*:* {{static assertion failed}}
   h.reset_protection(static_cast<const int*>(nullptr)); // expected-error@*:* {{static assertion failed}}
 
   // protect() rejects a T that isn't itself hazard-protectable, even when T is merely const-qualified:
-  // const Good has no hazard_pointer_obj_base<const Good, D> base. (Only try_protect's own static_assert
-  // fires here: the internal reset_protection(__old) call deduces its template parameter as plain Good --
-  // stripping the const already accounted for by that parameter's own `const _Tp*` declaration -- and
-  // Good is hazard-protectable, so no second diagnostic cascades.)
+  // const Good has no hazard_pointer_obj_base<const Good, D> base.
   std::atomic<const Good*> csrc{nullptr};
   (void)h.protect(csrc); // expected-error@*:* {{static assertion failed}}
 
@@ -73,14 +72,12 @@ void diagnostics() {
 }
 
 // retire()'s Mandate fires when the hazard_pointer_obj_base<T, D> specialization itself names a T that
-// is not hazard-protectable (as opposed to the most-derived object being retired). Because retire()'s
-// body isn't guarded the way reset_protection()'s is, the failed static_assert is followed by an
-// unrelated hard error from the downcast in its reclamation lambda.
+// is not hazard-protectable (as opposed to the most-derived object being retired). Its body is guarded
+// too, so the downcast in the reclamation lambda does not produce a second, unrelated error.
 struct Mismatched : std::hazard_pointer_obj_base<NoBase> {};
 void bad_retire() {
   Mismatched m;
   m.retire(); // expected-error@*:* {{static assertion failed}}
-              // expected-error@*:* {{static_cast from}}
 }
 
 // D requirements.
@@ -90,3 +87,10 @@ struct NotDefaultConstructible {
 };
 struct BadD : std::hazard_pointer_obj_base<BadD, NotDefaultConstructible> {
 }; // expected-error@*:* {{static assertion failed}}
+struct NotMoveAssignable {
+  NotMoveAssignable()                               = default;
+  NotMoveAssignable& operator=(NotMoveAssignable&&) = delete;
+  void operator()(struct BadMoveD*) const noexcept {}
+};
+struct BadMoveD : std::hazard_pointer_obj_base<BadMoveD, NotMoveAssignable> {
+}; // expected-error@*:* {{static assertion failed}}

>From b7478d5655baeab011bb4e7f3f5909b3ef29b48f Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 06:18:08 +0300
Subject: [PATCH 13/17] [libc++] hazard_pointer: engine comments and a yield in
 the reclamation loop

Comment corrections only, plus one behavioural nicety: when a reclamation pass
finds the retired lists already emptied by another thread but the counter is
still over the threshold, yield instead of spinning on that cache line. The
loop's termination logic is unchanged.

The comments now say what the code does: __grow_array's __size() is not
monotonic (a delayed store from a smaller round can lower it) but is always a
lower bound; the thread cache removes atomics on the domain's shared state, not
all atomics; the batch parameter of __acquire_records/__try_pop_available is
only ever 1 today and is kept for a future batch-acquire entry point; the
happens-before argument in __create_record relies on seq_cst fences being
cumulative; __due_time_ starting at 0 makes the process's first retire() run a
pass; __do_reclamation terminates because the residue added back is bounded by
the record count, which is below the threshold; and __create_record may
over-count by one if the grow array throws.

The note above __dead_thread_cache now also records why the sentinel is armed
exactly once: a TSD destructor that re-arms itself runs in the implementation's
final destructor round, which is the round in which ThreadSanitizer finalizes
the thread from an earlier-created key, so the re-arming destructor would
execute instrumented code on a torn-down ThreadState.
---
 libcxx/src/hazard_pointer.cpp | 72 ++++++++++++++++++++++++++---------
 1 file changed, 54 insertions(+), 18 deletions(-)

diff --git a/libcxx/src/hazard_pointer.cpp b/libcxx/src/hazard_pointer.cpp
index 8fdd121c8f7f2..ef15ab70f2757 100644
--- a/libcxx/src/hazard_pointer.cpp
+++ b/libcxx/src/hazard_pointer.cpp
@@ -92,8 +92,10 @@ struct alignas(__cache_line_size) __hazard_pointer_record : __hazard_pointer_slo
 // valid for every i: if i >= __size() the array grows to bit_ceil(i + 1), default-constructing every
 // element up to the new capacity. Growth is a race decided by one CAS; losers destroy what they built.
 // Each growth round is one allocation holding {header}{_Tp* list[capacity]}{_Tp slab[capacity - base]};
-// older rounds stay alive on a chain, so element addresses never change. __size() is monotonic and always
-// a lower bound. Never destroyed (the domain is immortal). _Tp must be nothrow default constructible.
+// older rounds stay alive on a chain, so element addresses never change. __size() is *not* monotonic -- a
+// delayed store from a smaller round can lower it after a larger one published -- but it is always a lower
+// bound on what has been constructed, which is all operator[]'s fast path needs. Never destroyed (the domain
+// is immortal). _Tp must be nothrow default constructible.
 template <class _Tp>
 class __grow_array {
   struct __round {
@@ -208,8 +210,9 @@ class __grow_array {
 // ---- per-thread cache of free records ---------------------------------------------------------------
 
 // A per-thread stack of up to 9 free records. It sits in front of the domain's available list:
-// __hazard_pointer_acquire() pops from it and __hazard_pointer_release() pushes to it, so the steady
-// state costs no atomics at all. Reached through a pthread key (see __hazard_pointer_domain::__thread_cache).
+// __hazard_pointer_acquire() pops from it and __hazard_pointer_release() pushes to it, so the steady state
+// costs no atomics on the domain's shared state (the call_once fast path and __libcpp_tls_get remain).
+// Reached through a pthread key (see __hazard_pointer_domain::__thread_cache).
 struct __hazard_pointer_thread_cache {
   __hazard_pointer_record* __entries_[__thread_cache_capacity] = {};
   uint8_t __count_                                             = 0;
@@ -225,10 +228,22 @@ struct __hazard_pointer_thread_cache {
   }
 };
 
-// Sentinel stored in the TLS key by __at_thread_exit once a thread's cache has been torn down. It
-// keeps a hazard_pointer destroyed later in the same destructor round (or acquired then) from
-// touching the freed cache; TSD implementations reset the value to null before each further
-// destructor round, which is why __release never creates a cache.
+// Sentinel stored in the TLS key by __at_thread_exit once a thread's cache has been torn down. While it is
+// in the key, __thread_cache() reports "no cache", so a hazard_pointer destroyed by a destructor that runs
+// after ours releases its record straight to the domain instead of touching the freed cache.
+//
+// It is armed exactly once. POSIX clears a TSD value before calling its destructor, so the sentinel is gone
+// again by the round after the one that stored it: round 1 gets the cache and arms the sentinel, round 2
+// gets the sentinel and leaves the key null, and the key is not visited again.
+//
+// It is tempting to re-arm it in round 2 and keep the key dead for every remaining round, so that a
+// hazard_pointer *acquired* by some other key's destructor could never build a fresh cache that nothing
+// tears down. Don't: a destructor that re-arms itself is called in the implementation's final round
+// (PTHREAD_DESTRUCTOR_ITERATIONS, 4 on glibc), and ThreadSanitizer finalizes the thread from its own TSD
+// destructor in that same round -- from a key created before ours, hence run before ours -- so our
+// destructor would execute instrumented code on a torn-down ThreadState and crash inside the TSan runtime.
+// What we give up is small and bounded: such a late cache holds at most __thread_cache_capacity records,
+// it is evicted normally if any destructor round is left, and records are immortal anyway.
 __hazard_pointer_thread_cache __dead_thread_cache;
 
 // ---- intrusive lists of retired nodes -----------------------------------------------------------------
@@ -364,7 +379,9 @@ class __hazard_pointer_domain {
   // ---- retired objects ------------------------------------------------------------------------------
   __shared_node_list __retired_[__shard_count]; // sharded by node address to spread contention
   atomic<int> __retired_count_{0};              // signed: transiently negative during a pass (see below)
-  atomic<uint64_t> __due_time_{0};              // steady_clock nanoseconds; a pass also runs when now >= due
+  // steady_clock nanoseconds; a pass also runs when now >= due. Starting at 0 means the very first retire()
+  // in the process is always due and runs a (nearly empty) pass, which is what arms the real due time.
+  atomic<uint64_t> __due_time_{0};
 
 public:
   constexpr __hazard_pointer_domain() noexcept                       = default;
@@ -560,8 +577,9 @@ class __hazard_pointer_domain {
   void __do_reclamation(int __count) noexcept {
     while (true) {
       __node* __retired[__shard_count];
-      bool __done = true;
-      if (__extract_retired(__retired)) {
+      bool __done      = true;
+      bool __extracted = __extract_retired(__retired);
+      if (__extracted) {
         // Reclaimer-side fence: pairs with the readers' fence in try_protect() and the retirers' fence in
         // __retire(). See <__hazard_pointer/domain.h>.
         std::atomic_thread_fence(memory_order_seq_cst);
@@ -569,11 +587,19 @@ class __hazard_pointer_domain {
         const __protected_set* __set_ptr = __load_protected(__set) ? &__set : nullptr;
         __count -= __match_reclaim(__retired, __set_ptr, __done);
       }
+      // Adding the residue back cannot by itself re-arm the trigger, which is what makes the loop
+      // terminate: a node survives a pass only while some record protects it, distinct nodes have distinct
+      // addresses, so residue <= records < max(1000, 2 * records) == __threshold(). Any further iteration is
+      // therefore paid for by retirements from other threads (or from a deleter we just ran).
       if (__count != 0)
         __retired_count_.fetch_add(__count, memory_order_release);
       __count = __check_count_threshold();
       if (__count == 0 && __done)
         break;
+      if (!__extracted)
+        // Another thread holds the nodes and is running deleters; spinning on the counter would just
+        // ping-pong its cache line.
+        std::__libcpp_thread_yield();
     }
   }
 
@@ -590,6 +616,8 @@ class __hazard_pointer_domain {
   }
 
   // Pops up to __count records; returns the head of a __next_available_-linked chain and sets __popped.
+  // __count is only ever 1 today; the batch capability is kept for a future batch-acquire entry point
+  // (P3428R4's hazard_pointer_array).
   __record* __try_pop_available(uint8_t __count, uint8_t& __popped) noexcept {
     while (true) {
       uintptr_t __available = __load_available();
@@ -648,13 +676,20 @@ class __hazard_pointer_domain {
   __record* __create_record() { // may throw bad_alloc
     // This increment happens-before the loads of any scan that must see the new record: acquiring a
     // hazard pointer precedes the reader's seq_cst fence in try_protect(), which synchronizes with the
-    // reclaimer's seq_cst fence before the scan.
+    // reclaimer's seq_cst fence before the scan. That argument relies on seq_cst fences being cumulative
+    // (they order stores the fencing thread only observed, not just its own) -- guaranteed by RC11 and by
+    // every hardware mapping we target; folly::hazptr rests on the same property.
+    //
+    // The count is bumped before the element exists. If __records_[] throws, the domain has counted one
+    // record more than it built; the scan clamps to what the grow array actually holds (see __pointers),
+    // so the over-count only makes __threshold() marginally larger.
     int __index = __record_count_.fetch_add(1, memory_order_relaxed);
     return &__records_[static_cast<size_t>(__index)];
   }
 
   // Pops __count records from the available list, creating the shortfall. Returns the head of a
-  // __next_available_-linked chain of exactly __count records.
+  // __next_available_-linked chain of exactly __count records. As with __try_pop_available, __count is
+  // always 1 today; batching is kept for a future batch-acquire entry point (P3428R4).
   __record* __acquire_records(uint8_t __count) {
     uint8_t __popped = 0;
     __record* __head = __try_pop_available(__count, __popped);
@@ -712,9 +747,10 @@ class __hazard_pointer_domain {
 
 constinit __hazard_pointer_domain __domain;
 
-// Runs at thread exit with the thread's cache: return its records to the domain and mark the thread as
-// past its cache. Storing the sentinel schedules one more destructor round (POSIX semantics), which
-// returns immediately below.
+// The TSD destructor of the thread cache. POSIX clears the key's value before each destructor round, so
+// round 1 arrives with the cache -- return its records to the domain, free it, and arm the sentinel -- and
+// round 2 arrives with the sentinel and does nothing, which leaves the key null for good. Deliberately no
+// re-arming: see the note above __dead_thread_cache.
 void _LIBCPP_TLS_DESTRUCTOR_CC __hazard_pointer_domain::__at_thread_exit(void* __p) {
   if (__p == nullptr || __p == static_cast<void*>(&__dead_thread_cache))
     return;
@@ -734,8 +770,8 @@ _LIBCPP_EXPORTED_FROM_ABI void __hazard_pointer_release(__hazard_pointer_slot* _
   __domain.__release(__slot);
 }
 
-_LIBCPP_EXPORTED_FROM_ABI void __hazard_pointer_retire(__hazard_pointer_obj_node* __node) noexcept {
-  __domain.__retire(__node);
+_LIBCPP_EXPORTED_FROM_ABI void __hazard_pointer_retire(__hazard_pointer_obj_node* __n) noexcept {
+  __domain.__retire(__n);
 }
 
 _LIBCPP_END_EXPLICIT_ABI_ANNOTATIONS

>From c55d10866b6feaa230da863b0deef82a28ce1e13 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 06:18:20 +0300
Subject: [PATCH 14/17] [libc++] hazard_pointer: test fixes and additional
 coverage

Fixes: include <atomic> where std::atomic is used; scale
special_members.pass.cpp's reclamation loop under
TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT like its siblings; no side effects inside
assert() in thread_exit_late_release.pass.cpp, which also gained its missing
`// <hazard_pointer>` banner; give each block of protection.pass.cpp its own
reclamation flag so one block's assertions no longer depend on when an earlier
block's object happened to be reclaimed; move the class hazard_pointer
static_asserts out of saferecl.hp.base/types.compile.pass.cpp into a new
saferecl.hp.holder/types.compile.pass.cpp; add the missing alignof check for the
stateful-deleter case in abi.compile.pass.cpp.

New coverage: a hazard-protectable type whose hazard_pointer_obj_base subobject
is not at offset 0; protect() through a const atomic object; a nonempty
hazard_pointer moved to another thread and used and destroyed there; a deleter
that checks retire() move-assigned it; and negative cases for protected
inheritance and for a D that is not Cpp17MoveAssignable.
---
 .../test/benchmarks/hazard_pointer.bench.cpp  |   3 +-
 .../saferecl/saferecl.hp/abi.compile.pass.cpp |   1 +
 .../thread_exit_late_release.pass.cpp         |   8 +-
 .../saferecl/saferecl.hp/protection.pass.cpp  |  29 +++--
 .../nonzero_offset_base.pass.cpp              | 104 ++++++++++++++++++
 .../saferecl.hp.base/retire.pass.cpp          |  30 ++++-
 .../saferecl.hp.base/special_members.pass.cpp |   8 +-
 .../saferecl.hp.base/types.compile.pass.cpp   |  25 -----
 .../moved_between_threads.pass.cpp            |  82 ++++++++++++++
 .../assign.move.pass.cpp                      |   1 +
 .../saferecl.hp.holder.ctor/dtor.pass.cpp     |   1 +
 .../saferecl.hp.holder.ctor/move.pass.cpp     |   1 +
 .../saferecl.hp.holder.mem/protect.pass.cpp   |   6 +
 .../saferecl.hp.holder/types.compile.pass.cpp |  51 +++++++++
 14 files changed, 308 insertions(+), 42 deletions(-)
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/nonzero_offset_base.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/moved_between_threads.pass.cpp
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp

diff --git a/libcxx/test/benchmarks/hazard_pointer.bench.cpp b/libcxx/test/benchmarks/hazard_pointer.bench.cpp
index 210ce21ccc449..518a349573405 100644
--- a/libcxx/test/benchmarks/hazard_pointer.bench.cpp
+++ b/libcxx/test/benchmarks/hazard_pointer.bench.cpp
@@ -100,7 +100,8 @@ void BM_retire(benchmark::State& state) {
   }
 }
 
-// Multi-threaded: every thread makes and destroys hazard pointers.
+// Multi-threaded: every thread makes and destroys hazard pointers. Deliberately the same body as
+// BM_make_destroy, so the two are directly comparable: the difference is the cost of contention.
 void BM_mt_make_destroy(benchmark::State& state) {
   for (auto _ : state) {
     std::hazard_pointer h = std::make_hazard_pointer();
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
index 3e38e4d09b63f..13a44c7ea1382 100644
--- a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
@@ -42,3 +42,4 @@ static_assert(alignof(std::hazard_pointer_obj_base<Node>) == alignof(void*));
 static_assert(sizeof(Node) == 2 * sizeof(void*));
 
 static_assert(sizeof(std::hazard_pointer_obj_base<Big, StatefulDeleter>) == 3 * sizeof(void*));
+static_assert(alignof(std::hazard_pointer_obj_base<Big, StatefulDeleter>) == alignof(void*));
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp
index 22b92cfea8a06..6e4458d6d12db 100644
--- a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp
@@ -11,6 +11,8 @@
 // XFAIL: availability-hazard_pointer-missing
 // REQUIRES: has-unix-headers
 
+// <hazard_pointer>
+
 // A pthread key destructor that destroys (and creates) hazard pointers: this may run after libc++'s own
 // per-thread record cache has been torn down, exercising the path where release/acquire bypass the cache.
 
@@ -39,12 +41,14 @@ int main(int, char**) {
   {
     std::hazard_pointer first = std::make_hazard_pointer();
   } // create libc++'s key before ours
-  assert(pthread_key_create(&key, &destroy_box) == 0);
+  int created = pthread_key_create(&key, &destroy_box);
+  assert(created == 0);
   for (int i = 0; i < 64; ++i) {
     std::thread t = support::make_test_thread([] {
       Box* box = new Box;
       box->hp  = std::make_hazard_pointer();
-      assert(pthread_setspecific(key, box) == 0);
+      int set  = pthread_setspecific(key, box);
+      assert(set == 0);
       std::hazard_pointer cached[12]; // fill the cache so eviction has work to do
       for (std::hazard_pointer& h : cached)
         h = std::make_hazard_pointer();
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
index cad8b570271f0..30ac6e3503ee5 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
@@ -29,10 +29,18 @@ struct Dummy : std::hazard_pointer_obj_base<Dummy> {
   ~Dummy() { ++dummies_deleted; }
 };
 
-std::atomic<bool> node_deleted{false};
+// One flag per block, and each Node reports to the flag it was handed: an object retired by an earlier
+// block may still be reclaimed during a later one (when that happens is unspecified), so a single shared
+// flag would make one block's assertions depend on another block's reclamation timing.
+std::atomic<bool> node_deleted_1{false};
+std::atomic<bool> node_deleted_2{false};
+std::atomic<bool> node_deleted_3{false};
+
 struct Node : std::hazard_pointer_obj_base<Node> {
+  explicit Node(std::atomic<bool>& deleted) : deleted_(&deleted) {}
+  std::atomic<bool>* deleted_;
   int value = 42;
-  ~Node() { node_deleted.store(true); }
+  ~Node() { deleted_->store(true); }
 };
 
 #if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
@@ -49,9 +57,8 @@ void retire_dummies() {
 int main(int, char**) {
   {
     // Protected via protect(): survives reclamation passes; readable throughout.
-    node_deleted = false;
-    int before   = dummies_deleted.load();
-    Node* n      = new Node();
+    int before = dummies_deleted.load();
+    Node* n    = new Node(node_deleted_1);
     std::atomic<Node*> src{n};
     std::hazard_pointer h = std::make_hazard_pointer();
     Node* p               = h.protect(src);
@@ -60,7 +67,7 @@ int main(int, char**) {
     n->retire();
     retire_dummies();
     assert(dummies_deleted.load() - before > 0); // reclamation did happen ...
-    assert(!node_deleted.load());                // ... but not of the protected object
+    assert(!node_deleted_1.load());              // ... but not of the protected object
     assert(p->value == 42);
     h.reset_protection(); // epoch ends; n becomes possibly-reclaimable
     retire_dummies();
@@ -68,29 +75,27 @@ int main(int, char**) {
   }
   {
     // Protected via reset_protection(ptr) then released by ~hazard_pointer(): same guarantee.
-    node_deleted = false;
-    Node* n      = new Node();
+    Node* n = new Node(node_deleted_2);
     {
       std::hazard_pointer h = std::make_hazard_pointer();
       h.reset_protection(n);
       n->retire();
       retire_dummies();
-      assert(!node_deleted.load());
+      assert(!node_deleted_2.load());
       assert(n->value == 42);
     } // h destroyed: epoch ends
     retire_dummies();
   }
   {
     // Two hazard pointers on the same object: it stays protected until both epochs end.
-    node_deleted           = false;
-    Node* n                = new Node();
+    Node* n                = new Node(node_deleted_3);
     std::hazard_pointer h1 = std::make_hazard_pointer(), h2 = std::make_hazard_pointer();
     h1.reset_protection(n);
     h2.reset_protection(n);
     n->retire();
     h1.reset_protection();
     retire_dummies();
-    assert(!node_deleted.load()); // still protected by h2
+    assert(!node_deleted_3.load()); // still protected by h2
     h2.reset_protection();
     retire_dummies();
   }
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/nonzero_offset_base.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/nonzero_offset_base.pass.cpp
new file mode 100644
index 0000000000000..801a1b8dcb6eb
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/nonzero_offset_base.pass.cpp
@@ -0,0 +1,104 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// The hazard_pointer_obj_base<T, D> subobject need not sit at offset 0 of T: here a polymorphic base
+// comes first. Protection, retirement and the deleter's argument must all still be expressed in terms of
+// the T*, not of the base subobject, so a protected object survives reclamation passes and the deleter
+// is eventually called with the same pointer retire() was called on.
+//
+// When an object is reclaimed is unspecified; like retire.pass.cpp this test only relies on the number
+// of possibly-reclaimable objects being bounded, so retiring many objects makes *some* reclamation
+// happen.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+#include <cstdint>
+
+#include "test_macros.h"
+
+#if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
+constexpr int N = 5000;
+#else
+constexpr int N = 100000; // comfortably above any sane reclamation bound
+#endif
+
+struct Prefix {
+  virtual ~Prefix() = default; // makes Node polymorphic: the obj_base base cannot be at offset 0
+  int tag           = 11;
+};
+
+struct Node;
+struct NodeDeleter {
+  void operator()(Node* p) const noexcept;
+};
+
+struct Node : Prefix, std::hazard_pointer_obj_base<Node, NodeDeleter> {
+  Node* self_   = this;
+  int value     = 42;
+  bool watched_ = false; // the one object the test protects reports where it went
+};
+
+// Namespace scope: a deleter may run long after the block that retired the object has exited.
+std::atomic<int> calls{0};
+std::atomic<int> mismatches{0};
+std::atomic<std::uintptr_t> watched_received{0};
+
+void NodeDeleter::operator()(Node* p) const noexcept {
+  ++calls;
+  if (p->self_ != p || p->value != 42 || p->tag != 11)
+    ++mismatches;
+  if (p->watched_)
+    watched_received.store(reinterpret_cast<std::uintptr_t>(p));
+  delete p;
+}
+
+void retire_many() {
+  for (int i = 0; i < N; ++i)
+    (new Node)->retire();
+}
+
+int main(int, char**) {
+  {
+    // The premise of the test: the base subobject really is at a non-zero offset.
+    Node probe;
+    const void* derived = &probe;
+    const void* base    = static_cast<std::hazard_pointer_obj_base<Node, NodeDeleter>*>(&probe);
+    assert(derived != base);
+  }
+
+  Node* watched                     = new Node;
+  watched->watched_                 = true;
+  const std::uintptr_t watched_addr = reinterpret_cast<std::uintptr_t>(watched);
+  {
+    std::atomic<Node*> src{watched};
+    std::hazard_pointer h = std::make_hazard_pointer();
+    Node* p               = h.protect(src);
+    assert(p == watched);
+    src.store(nullptr);
+    watched->retire();
+    retire_many();
+    assert(calls.load() > 0);             // reclamation did happen ...
+    assert(watched_received.load() == 0); // ... but not of the protected object
+    assert(watched->value == 42 && watched->tag == 11);
+    h.reset_protection(); // epoch ends: watched becomes possibly-reclaimable
+  }
+  retire_many();
+  // If it has been reclaimed by now -- unspecified, but the usual case -- the deleter got the T* that
+  // retire() was called on, not the address of the base subobject.
+  std::uintptr_t got = watched_received.load();
+  assert(got == 0 || got == watched_addr);
+  assert(mismatches.load() == 0);
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
index 364330aeb341c..3cc1b232356e1 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
@@ -58,7 +58,30 @@ void RecordingDeleter::operator()(Recorded* p) const noexcept {
   delete p;
 }
 
-// 3. A function pointer is a valid function object type for D.
+// 3. retire(d) move-assigns d into the object's stored deleter: the deleter that eventually runs is the
+//    move-assignment target, and it must have observed the assignment.
+struct MoveTracked;
+std::atomic<int> tracked_calls{0};
+std::atomic<int> tracked_not_move_assigned{0};
+struct MoveTrackingDeleter {
+  bool assigned_by_move                      = false;
+  MoveTrackingDeleter()                      = default;
+  MoveTrackingDeleter(MoveTrackingDeleter&&) = default;
+  MoveTrackingDeleter& operator=(MoveTrackingDeleter&&) noexcept {
+    assigned_by_move = true;
+    return *this;
+  }
+  void operator()(MoveTracked* p) const noexcept;
+};
+struct MoveTracked : std::hazard_pointer_obj_base<MoveTracked, MoveTrackingDeleter> {};
+void MoveTrackingDeleter::operator()(MoveTracked* p) const noexcept {
+  ++tracked_calls;
+  if (!assigned_by_move)
+    ++tracked_not_move_assigned;
+  delete p;
+}
+
+// 4. A function pointer is a valid function object type for D.
 struct FnObj;
 using FnDeleter = void (*)(FnObj*);
 struct FnObj : std::hazard_pointer_obj_base<FnObj, FnDeleter> {};
@@ -79,6 +102,11 @@ int main(int, char**) {
   assert(tally.calls.load() > 0);
   assert(tally.mismatches.load() == 0);
 
+  for (int i = 0; i < N; ++i)
+    (new MoveTracked)->retire(MoveTrackingDeleter{});
+  assert(tracked_calls.load() > 0);
+  assert(tracked_not_move_assigned.load() == 0);
+
   for (int i = 0; i < N; ++i)
     (new FnObj)->retire(&fn_delete);
   assert(fn_calls.load() > 0);
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp
index 6958a9f48eb5a..7c81b7ae674f7 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp
@@ -23,6 +23,12 @@
 
 #include "test_macros.h"
 
+#if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
+constexpr int N = 5000;
+#else
+constexpr int N = 100000; // comfortably above any sane reclamation bound
+#endif
+
 std::atomic<int> deleted{0};
 
 struct Node : std::hazard_pointer_obj_base<Node> {
@@ -37,7 +43,7 @@ struct Node : std::hazard_pointer_obj_base<Node> {
 
 void force_reclamation() {
   struct Dummy : std::hazard_pointer_obj_base<Dummy> {};
-  for (int i = 0; i < 100000; ++i)
+  for (int i = 0; i < N; ++i)
     (new Dummy)->retire();
 }
 
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp
index 7f40ca7a69636..a744f60bcc370 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp
@@ -48,28 +48,3 @@ static_assert(std::is_move_constructible_v<Node>);
 static_assert(std::is_copy_assignable_v<Node>);
 static_assert(std::is_move_assignable_v<Node>);
 static_assert(std::is_destructible_v<Node>);
-
-// class hazard_pointer: move-only, all documented members present with the right noexcept-ness.
-static_assert(std::is_nothrow_default_constructible_v<std::hazard_pointer>);
-static_assert(std::is_nothrow_move_constructible_v<std::hazard_pointer>);
-static_assert(std::is_nothrow_move_assignable_v<std::hazard_pointer>);
-static_assert(!std::is_copy_constructible_v<std::hazard_pointer>);
-static_assert(!std::is_copy_assignable_v<std::hazard_pointer>);
-static_assert(std::is_nothrow_swappable_v<std::hazard_pointer>);
-static_assert(noexcept(std::declval<const std::hazard_pointer&>().empty()));
-static_assert(noexcept(std::declval<std::hazard_pointer&>().protect(std::declval<const std::atomic<Node*>&>())));
-static_assert(noexcept(std::declval<std::hazard_pointer&>().try_protect(
-    std::declval<Node*&>(), std::declval<const std::atomic<Node*>&>())));
-static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection(std::declval<const Node*>())));
-static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection()));
-static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection(nullptr)));
-static_assert(noexcept(std::declval<std::hazard_pointer&>().swap(std::declval<std::hazard_pointer&>())));
-static_assert(noexcept(std::swap(std::declval<std::hazard_pointer&>(), std::declval<std::hazard_pointer&>())));
-static_assert(!noexcept(std::make_hazard_pointer()));
-static_assert(std::is_same_v<decltype(std::make_hazard_pointer()), std::hazard_pointer>);
-static_assert(
-    std::is_same_v<decltype(std::declval<std::hazard_pointer&>().protect(std::declval<const std::atomic<Node*>&>())),
-                   Node*>);
-static_assert(std::is_same_v<decltype(std::declval<std::hazard_pointer&>().try_protect(
-                                 std::declval<Node*&>(), std::declval<const std::atomic<Node*>&>())),
-                             bool>);
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/moved_between_threads.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/moved_between_threads.pass.cpp
new file mode 100644
index 0000000000000..880ca60bec3bb
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/moved_between_threads.pass.cpp
@@ -0,0 +1,82 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// A hazard pointer is owned by an object, not by the thread that made it: a nonempty hazard_pointer may
+// be moved to another thread and used and destroyed there. The protection epoch is unaffected by the
+// move, so an object retired while the moved-to hazard pointer protects it is not reclaimed.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+#include <thread>
+#include <utility>
+
+#include "make_test_thread.h"
+#include "test_macros.h"
+
+#if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
+constexpr int N = 5000;
+#else
+constexpr int N = 100000;
+#endif
+
+// Namespace scope: a deleter may run long after the block that retired the object has exited.
+std::atomic<bool> node_deleted{false};
+
+struct Node : std::hazard_pointer_obj_base<Node> {
+  int value = 42;
+  ~Node() { node_deleted.store(true); }
+};
+
+struct Spare : std::hazard_pointer_obj_base<Spare> {
+  int value = 7;
+};
+
+struct Dummy : std::hazard_pointer_obj_base<Dummy> {};
+
+void retire_dummies() {
+  for (int i = 0; i < N; ++i)
+    (new Dummy)->retire();
+}
+
+int main(int, char**) {
+  Node* n = new Node;
+  std::atomic<Node*> src{n};
+
+  Spare spare;
+  std::atomic<Spare*> spare_src{&spare};
+
+  std::hazard_pointer h = std::make_hazard_pointer();
+  assert(h.protect(src) == n);
+  src.store(nullptr);
+  n->retire(); // retired while protected by a hazard pointer that is about to change threads
+
+  std::thread t = support::make_test_thread([moved = std::move(h), &spare_src, &spare, n]() mutable {
+    // The epoch survived the move: n is still protected here, on a thread that never acquired it.
+    assert(!moved.empty());
+    retire_dummies();
+    assert(!node_deleted.load());
+    assert(n->value == 42);
+
+    // The moved-to hazard pointer is fully usable on this thread.
+    assert(moved.protect(spare_src) == &spare); // ends n's epoch and starts one for spare
+    assert(spare.value == 7);
+    moved.reset_protection();
+  }); // ~hazard_pointer runs here: the record is released by a thread that did not acquire it
+  t.join();
+
+  assert(h.empty()); // moved from
+  retire_dummies();  // n is unprotected now; whether it has been reclaimed is unspecified
+  return 0;
+}
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.pass.cpp
index cc922aa958ac9..3a112957401c7 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.pass.cpp
@@ -17,6 +17,7 @@
 //   by *this. Postconditions: *this owns other's hazard pointer (if any); other is empty.
 
 #include <hazard_pointer>
+#include <atomic>
 #include <cassert>
 #include <utility>
 
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.pass.cpp
index a02dcb507781e..63fe26948e67b 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.pass.cpp
@@ -17,6 +17,7 @@
 //   protection epoch. (Reclaimability after the epoch ends is tested in ../../protection.pass.cpp.)
 
 #include <hazard_pointer>
+#include <atomic>
 #include <cassert>
 #include <utility>
 
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp
index a66cb1627d566..dd22a859d6b95 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp
@@ -17,6 +17,7 @@
 //   originally owned by other; other is empty.
 
 #include <hazard_pointer>
+#include <atomic>
 #include <cassert>
 #include <utility>
 
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp
index 692eecdf29e24..268bec3942b97 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp
@@ -57,5 +57,11 @@ int main(int, char**) {
     assert(h.protect(src) == &a);
     assert(h2.protect(src) == &a);
   }
+  {
+    // src may be a const object, not merely a reference to const.
+    Node a(4);
+    const std::atomic<Node*> csrc{&a};
+    assert(h.protect(csrc) == &a);
+  }
   return 0;
 }
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp
new file mode 100644
index 0000000000000..aa5f25a5eebbc
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp
@@ -0,0 +1,51 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+
+// <hazard_pointer>
+
+// class hazard_pointer;
+//   The class is move-only and every member listed in [saferecl.hp.holder] is present with the return
+//   type and noexcept-ness the synopsis gives: the default constructor, the move constructor, move
+//   assignment, empty(), protect(), try_protect(), both reset_protection() overloads, swap(), the free
+//   swap() and make_hazard_pointer().
+
+#include <hazard_pointer>
+#include <atomic>
+#include <type_traits>
+#include <utility>
+
+#include "test_macros.h"
+
+struct Node : std::hazard_pointer_obj_base<Node> {};
+
+static_assert(std::is_nothrow_default_constructible_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_move_constructible_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_move_assignable_v<std::hazard_pointer>);
+static_assert(!std::is_copy_constructible_v<std::hazard_pointer>);
+static_assert(!std::is_copy_assignable_v<std::hazard_pointer>);
+static_assert(std::is_nothrow_swappable_v<std::hazard_pointer>);
+static_assert(noexcept(std::declval<const std::hazard_pointer&>().empty()));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().protect(std::declval<const std::atomic<Node*>&>())));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().try_protect(
+    std::declval<Node*&>(), std::declval<const std::atomic<Node*>&>())));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection(std::declval<const Node*>())));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection()));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().reset_protection(nullptr)));
+static_assert(noexcept(std::declval<std::hazard_pointer&>().swap(std::declval<std::hazard_pointer&>())));
+static_assert(noexcept(std::swap(std::declval<std::hazard_pointer&>(), std::declval<std::hazard_pointer&>())));
+static_assert(!noexcept(std::make_hazard_pointer()));
+static_assert(std::is_same_v<decltype(std::make_hazard_pointer()), std::hazard_pointer>);
+static_assert(
+    std::is_same_v<decltype(std::declval<std::hazard_pointer&>().protect(std::declval<const std::atomic<Node*>&>())),
+                   Node*>);
+static_assert(std::is_same_v<decltype(std::declval<std::hazard_pointer&>().try_protect(
+                                 std::declval<Node*&>(), std::declval<const std::atomic<Node*>&>())),
+                             bool>);

>From 2475f0aff1cbb87229d987646ef4df09734a0085 Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 06:18:21 +0300
Subject: [PATCH 15/17] [libc++] hazard_pointer: release-notes and ABI
 CHANGELOG touch-ups

Announce <hazard_pointer> under "Improvements and New Features", noting that the
domain lives in the built library and so the facility is unavailable when
back-deploying, and fix the underline length of the <arch>-unknown-linux-gnu
heading in lib/abi/CHANGELOG.TXT.
---
 libcxx/docs/ReleaseNotes/23.rst | 3 +++
 libcxx/lib/abi/CHANGELOG.TXT    | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/libcxx/docs/ReleaseNotes/23.rst b/libcxx/docs/ReleaseNotes/23.rst
index 54591e65b7e7d..73e4f35fdffcb 100644
--- a/libcxx/docs/ReleaseNotes/23.rst
+++ b/libcxx/docs/ReleaseNotes/23.rst
@@ -66,6 +66,9 @@ Improvements and New Features
   ``"std::visit: variant is valueless"``, ``"std::get: variant is valueless"``, or
   ``"std::get: wrong alternative for variant"``. The standard only requires ``what()`` to return an
   unspecified non-null string, so user code that does not match on the exact message remains correct.
+- The C++26 ``<hazard_pointer>`` header (P2530R3) is now implemented. The hazard pointer domain lives in the
+  built library, so the facility requires a libc++ 23 dylib and is unavailable when back-deploying to
+  earlier releases.
 
 Deprecations and Removals
 -------------------------
diff --git a/libcxx/lib/abi/CHANGELOG.TXT b/libcxx/lib/abi/CHANGELOG.TXT
index e2bf0ee088610..dabc040bc3f06 100644
--- a/libcxx/lib/abi/CHANGELOG.TXT
+++ b/libcxx/lib/abi/CHANGELOG.TXT
@@ -28,7 +28,7 @@ Version 23.0
   Symbol added: _ZNSt3__123__hazard_pointer_retireEPNS_25__hazard_pointer_obj_nodeE
 
   <arch>-unknown-linux-gnu
-  -------------------------
+  ------------------------
   The built library's hazard pointer domain allocates using nothrow new, which is the first use of
   these overloads anywhere in libc++.so itself; this pulls in two previously-unreferenced external
   symbols resolved from libc++abi at load time.

>From a30106b0064bfc08048524fbd3b07b662d01114b Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Wed, 19 Aug 2026 06:38:14 +0300
Subject: [PATCH 16/17] [libc++] hazard_pointer: XFAIL compile-only tests on
 missing availability; comment precision

Add XFAIL: availability-hazard_pointer-missing to the seven compile-only
and verify tests that name std::hazard_pointer / hazard_pointer_obj_base
directly, since the class is availability-unavailable and these tests
would hard-error instead of skipping where that applies (matching the
XFAIL: availability-tzdb-missing precedent).

Also tighten three comments in libcxx/src/hazard_pointer.cpp: the note
above __dead_thread_cache no longer makes the absolute "key is not
visited again" claim that contradicted the later foreign-destructor
re-acquire scenario, the __release doc comment states the reason a
cache is never allocated instead of pointing elsewhere, and the
residue-back termination comment in __do_reclamation notes that its
bound is exact only single-threaded. No behavior changes.
---
 libcxx/src/hazard_pointer.cpp                 | 31 +++++++++----------
 .../saferecl/saferecl.hp/abi.compile.pass.cpp |  1 +
 .../saferecl.hp/hazard_protectable.verify.cpp |  1 +
 .../saferecl/saferecl.hp/nodiscard.verify.cpp |  1 +
 .../incomplete_type.compile.pass.cpp          |  1 +
 .../saferecl.hp.base/types.compile.pass.cpp   |  1 +
 .../copy.delete.compile.pass.cpp              |  1 +
 .../saferecl.hp.holder/types.compile.pass.cpp |  1 +
 8 files changed, 21 insertions(+), 17 deletions(-)

diff --git a/libcxx/src/hazard_pointer.cpp b/libcxx/src/hazard_pointer.cpp
index ef15ab70f2757..7b8a9215c433a 100644
--- a/libcxx/src/hazard_pointer.cpp
+++ b/libcxx/src/hazard_pointer.cpp
@@ -232,18 +232,12 @@ struct __hazard_pointer_thread_cache {
 // in the key, __thread_cache() reports "no cache", so a hazard_pointer destroyed by a destructor that runs
 // after ours releases its record straight to the domain instead of touching the freed cache.
 //
-// It is armed exactly once. POSIX clears a TSD value before calling its destructor, so the sentinel is gone
-// again by the round after the one that stored it: round 1 gets the cache and arms the sentinel, round 2
-// gets the sentinel and leaves the key null, and the key is not visited again.
-//
-// It is tempting to re-arm it in round 2 and keep the key dead for every remaining round, so that a
-// hazard_pointer *acquired* by some other key's destructor could never build a fresh cache that nothing
-// tears down. Don't: a destructor that re-arms itself is called in the implementation's final round
-// (PTHREAD_DESTRUCTOR_ITERATIONS, 4 on glibc), and ThreadSanitizer finalizes the thread from its own TSD
-// destructor in that same round -- from a key created before ours, hence run before ours -- so our
-// destructor would execute instrumented code on a torn-down ThreadState and crash inside the TSan runtime.
-// What we give up is small and bounded: such a late cache holds at most __thread_cache_capacity records,
-// it is evicted normally if any destructor round is left, and records are immortal anyway.
+// The sentinel is armed when the cache is torn down. POSIX clears a TSD value before each destructor
+// round, so a later round sees null again, not the sentinel. We deliberately do not re-arm it: re-arming
+// would keep this thread in destructor rounds after sanitizer runtimes have already finalized it.
+// Consequently, a make_hazard_pointer() called from a foreign TSD destructor in a later round may build a
+// fresh cache that is never evicted -- a bounded leak of that cache object plus up to
+// __thread_cache_capacity records. Records are immortal, so the cache object is the actual leak.
 __hazard_pointer_thread_cache __dead_thread_cache;
 
 // ---- intrusive lists of retired nodes -----------------------------------------------------------------
@@ -399,7 +393,9 @@ class __hazard_pointer_domain {
   }
 
   // Ends the epoch of __slot and returns its record to the calling thread's cache, if it has one,
-  // else to the domain. Never allocates a cache: see the note above __dead_thread_cache.
+  // else to the domain. Never allocates a cache: a thread that only ever releases (e.g. a
+  // hazard_pointer moved in from another thread, or one destroyed during thread teardown) must not
+  // leave records stranded in a cache nothing will evict.
   void __release(__hazard_pointer_slot* __slot) noexcept {
     __record* __rec = static_cast<__record*>(__slot);
     __rec->__value_.store(nullptr, memory_order_release);
@@ -587,10 +583,11 @@ class __hazard_pointer_domain {
         const __protected_set* __set_ptr = __load_protected(__set) ? &__set : nullptr;
         __count -= __match_reclaim(__retired, __set_ptr, __done);
       }
-      // Adding the residue back cannot by itself re-arm the trigger, which is what makes the loop
-      // terminate: a node survives a pass only while some record protects it, distinct nodes have distinct
-      // addresses, so residue <= records < max(1000, 2 * records) == __threshold(). Any further iteration is
-      // therefore paid for by retirements from other threads (or from a deleter we just ran).
+      // This residue-back bound -- residue <= records < max(1000, 2 * records) == __threshold(), because a
+      // node survives a pass only while some record protects it and distinct nodes have distinct addresses
+      // -- is exact only single-threaded. Under concurrency __count here is claimed - reclaimed, and adding
+      // it back may re-arm the trigger once more; the yield below mitigates the resulting spin, and the
+      // negative-residue accounting (see the comment above __do_reclamation) guarantees it still settles.
       if (__count != 0)
         __retired_count_.fetch_add(__count, memory_order_release);
       __count = __check_count_threshold();
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
index 13a44c7ea1382..9ce59ee567125 100644
--- a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
@@ -8,6 +8,7 @@
 
 // UNSUPPORTED: no-threads
 // UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
 
 // <hazard_pointer>
 
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
index 1dd3f127a913b..e3bce64cfb66b 100644
--- a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
@@ -8,6 +8,7 @@
 
 // UNSUPPORTED: no-threads
 // UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
 
 // <hazard_pointer>
 
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp
index 6ce9c77763790..4106e310e94ec 100644
--- a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp
@@ -8,6 +8,7 @@
 
 // UNSUPPORTED: no-threads
 // UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
 
 // <hazard_pointer>
 
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp
index a5026def6f167..b6c4e73374b43 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp
@@ -8,6 +8,7 @@
 
 // UNSUPPORTED: no-threads
 // UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
 
 // <hazard_pointer>
 
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp
index a744f60bcc370..8724614517815 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/types.compile.pass.cpp
@@ -8,6 +8,7 @@
 
 // UNSUPPORTED: no-threads
 // UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
 
 // <hazard_pointer>
 
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/copy.delete.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/copy.delete.compile.pass.cpp
index d36fc30db4a9a..61468aa61186a 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/copy.delete.compile.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/copy.delete.compile.pass.cpp
@@ -8,6 +8,7 @@
 
 // UNSUPPORTED: no-threads
 // UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
 
 // <hazard_pointer>
 
diff --git a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp
index aa5f25a5eebbc..cd67868b5535a 100644
--- a/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp
@@ -8,6 +8,7 @@
 
 // UNSUPPORTED: no-threads
 // UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
 
 // <hazard_pointer>
 

>From 1c2993cf735281a329e152985c950e4a41eb201c Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Sun, 23 Aug 2026 11:18:53 +0300
Subject: [PATCH 17/17] [libc++] Add deallocation/allocation and bulk
 reclamation semantics

---
 libcxx/src/hazard_pointer.cpp                 | 99 ++++++++++---------
 .../saferecl.hp/concurrent_retire.pass.cpp    | 79 +++++++++++++++
 2 files changed, 134 insertions(+), 44 deletions(-)
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/concurrent_retire.pass.cpp

diff --git a/libcxx/src/hazard_pointer.cpp b/libcxx/src/hazard_pointer.cpp
index 7b8a9215c433a..14098d7c67ed6 100644
--- a/libcxx/src/hazard_pointer.cpp
+++ b/libcxx/src/hazard_pointer.cpp
@@ -57,7 +57,11 @@ constexpr int __shard_mask                = __shard_count - 1;
 constexpr int __ignored_low_bits          = 8;
 constexpr uintptr_t __lock_bit            = 1;
 constexpr uint8_t __thread_cache_capacity = 9;
-#if defined(__GCC_DESTRUCTIVE_SIZE)
+// Records are laid out one per cache line (see __hazard_pointer_record). Where over-aligned allocation is
+// not available, std::__libcpp_allocate can only promise fundamental alignment, so fall back to that.
+#if !_LIBCPP_HAS_ALIGNED_ALLOCATION
+constexpr size_t __cache_line_size = alignof(max_align_t);
+#elif defined(__GCC_DESTRUCTIVE_SIZE)
 constexpr size_t __cache_line_size = __GCC_DESTRUCTIVE_SIZE;
 #else
 constexpr size_t __cache_line_size = 64;
@@ -79,8 +83,9 @@ inline size_t __mix_pointer(uintptr_t __x) noexcept {
 
 // ---- record ----------------------------------------------------------------------------------------
 
-// One cache line per record: records of different threads must not share a line (every protect()
-// writes to it). Records are created by the domain's grow array and never destroyed.
+// One cache line per record: records of different threads must not share a line (every protect() writes
+// to it); targets without over-aligned allocation fall back to fundamental alignment. Records are created
+// by the domain's grow array and never destroyed.
 struct alignas(__cache_line_size) __hazard_pointer_record : __hazard_pointer_slot {
   __hazard_pointer_record* __next_available_ = nullptr; // available-list link; meaningful only while free
   __hazard_pointer_record() noexcept : __hazard_pointer_slot{nullptr} {}
@@ -126,7 +131,7 @@ class __grow_array {
   // another thread published a round meanwhile. May throw bad_alloc.
   __round* __new_round(size_t __capacity, __round*& __next) {
     size_t __base   = __next ? __next->__size_ : 0;
-    void* __raw     = ::operator new(__bytes(__capacity, __base), align_val_t{__align()});
+    void* __raw     = std::__libcpp_allocate<byte>(__element_count(__bytes(__capacity, __base)), __align());
     __round* __curr = ::new (__raw) __round{};
     __curr->__next_ = __next;
     __curr->__size_ = __capacity;
@@ -158,7 +163,7 @@ class __grow_array {
       --__built;
     for (size_t __i = 0; __i < __built - __base; ++__i)
       __list[__built - 1 - __i]->~_Tp();
-    ::operator delete(static_cast<void*>(__curr), __size, align_val_t{__align()});
+    std::__libcpp_deallocate<byte>(reinterpret_cast<byte*>(__curr), __element_count(__size), __align());
   }
 
   __round* __at_slow(size_t __index) {
@@ -482,14 +487,15 @@ class __hazard_pointer_domain {
     return true;
   }
 
-  bool __extract_retired(__node* __out[__shard_count]) noexcept {
-    bool __any = false;
+  // Pops every shard into __out; returns the number of nodes taken.
+  int __extract_retired(__node* __out[__shard_count]) noexcept {
+    int __taken = 0;
     for (int __s = 0; __s < __shard_count; ++__s) {
       __out[__s] = __retired_[__s].__pop_all();
-      if (__out[__s] != nullptr)
-        __any = true;
+      for (__node* __n = __out[__s]; __n != nullptr; __n = __n->__next_)
+        ++__taken;
     }
-    return __any;
+    return __taken;
   }
 
   // Loads every non-null slot value into __set. Returns false when the set could not be built (allocation
@@ -535,10 +541,9 @@ class __hazard_pointer_domain {
   }
 
   // Reclaims the unprotected extracted nodes and pushes the protected ones back. Returns the number
-  // reclaimed; sets __done = false when new nodes were retired meanwhile (e.g. by a deleter).
+  // pushed back; sets __done = false when new nodes were retired meanwhile (e.g. by a deleter).
   int __match_reclaim(__node* __retired[__shard_count], const __protected_set* __set, bool& __done) noexcept {
-    __done          = true;
-    int __reclaimed = 0;
+    __done = true;
     __node_list __not_reclaimed[__shard_count];
     __node* __heads[__shard_count];
     for (int __s = 0; __s < __shard_count; ++__s)
@@ -552,51 +557,56 @@ class __hazard_pointer_domain {
         __more              = true;
         __heads[__s]        = __n->__next_; // read before the deleter may free __n
         bool __is_protected = __set != nullptr ? __set->__contains(__n) : __protected_linear(__n);
-        if (__is_protected) {
+        if (__is_protected)
           __not_reclaimed[__s].__push(__n);
-        } else {
-          ++__reclaimed;
+        else
           __n->__reclaim_(__n);
-        }
       }
     }
     if (!__retired_empty())
       __done = false;
-    for (int __s = 0; __s < __shard_count; ++__s)
+    int __residue = 0;
+    for (int __s = 0; __s < __shard_count; ++__s) {
+      __residue += __not_reclaimed[__s].__count_;
       __retired_[__s].__push(__not_reclaimed[__s]);
-    return __reclaimed;
+    }
+    return __residue;
   }
 
-  // The bulk reclamation pass. __count is the number of retirements claimed by the trigger; whatever is
-  // not reclaimed is added back to the retired count (which may transiently make it negative when objects
-  // retired concurrently were reclaimed without having been counted yet).
+  // The bulk reclamation pass. __count is the number of retirements claimed by the trigger. The claim is
+  // settled against what the pass actually extracted before any deleter runs, so that from then on
+  // __retired_count_ again counts the nodes sitting in the shards (transiently negative when nodes retired
+  // concurrently were extracted before their retire() counted them); the protected residue is added back
+  // when it is returned to the shards.
   void __do_reclamation(int __count) noexcept {
     while (true) {
       __node* __retired[__shard_count];
-      bool __done      = true;
-      bool __extracted = __extract_retired(__retired);
-      if (__extracted) {
+      bool __done = true;
+      if (int __taken = __extract_retired(__retired)) {
+        // Settling now rather than after the deleters matters when two passes overlap: the credit a pass
+        // claimed for nodes another pass then extracted would otherwise sit in the counter, above the
+        // threshold, for the whole duration of that other pass, and every retire() in between would run
+        // a fruitless full pass.
+        __count -= __taken;
+        if (__count != 0)
+          __retired_count_.fetch_add(__count, memory_order_release);
         // Reclaimer-side fence: pairs with the readers' fence in try_protect() and the retirers' fence in
         // __retire(). See <__hazard_pointer/domain.h>.
         std::atomic_thread_fence(memory_order_seq_cst);
         __protected_set __set;
         const __protected_set* __set_ptr = __load_protected(__set) ? &__set : nullptr;
-        __count -= __match_reclaim(__retired, __set_ptr, __done);
-      }
-      // This residue-back bound -- residue <= records < max(1000, 2 * records) == __threshold(), because a
-      // node survives a pass only while some record protects it and distinct nodes have distinct addresses
-      // -- is exact only single-threaded. Under concurrency __count here is claimed - reclaimed, and adding
-      // it back may re-arm the trigger once more; the yield below mitigates the resulting spin, and the
-      // negative-residue accounting (see the comment above __do_reclamation) guarantees it still settles.
-      if (__count != 0)
+        // Residue bound: residue <= records < max(1000, 2 * records) == __threshold(), because a node
+        // survives a pass only while some record protects it and distinct nodes have distinct addresses.
+        if (int __residue = __match_reclaim(__retired, __set_ptr, __done))
+          __retired_count_.fetch_add(__residue, memory_order_release);
+      } else if (__count != 0) {
+        // Nothing to hold the claim against (another pass took the nodes): give it back and return
+        // rather than claiming it again; the owning pass has settled or will settle its own extract.
         __retired_count_.fetch_add(__count, memory_order_release);
-      __count = __check_count_threshold();
-      if (__count == 0 && __done)
+      }
+      if (__done)
         break;
-      if (!__extracted)
-        // Another thread holds the nodes and is running deleters; spinning on the counter would just
-        // ping-pong its cache line.
-        std::__libcpp_thread_yield();
+      __count = __check_count_threshold();
     }
   }
 
@@ -671,11 +681,12 @@ class __hazard_pointer_domain {
   // ---- record creation ------------------------------------------------------------------------------
 
   __record* __create_record() { // may throw bad_alloc
-    // This increment happens-before the loads of any scan that must see the new record: acquiring a
-    // hazard pointer precedes the reader's seq_cst fence in try_protect(), which synchronizes with the
-    // reclaimer's seq_cst fence before the scan. That argument relies on seq_cst fences being cumulative
-    // (they order stores the fencing thread only observed, not just its own) -- guaranteed by RC11 and by
-    // every hardware mapping we target; folly::hazptr rests on the same property.
+    // Any scan that must see the new record sees this increment and the grow-array round holding the
+    // record: acquiring a hazard pointer precedes the reader's seq_cst fence in try_protect(), and when
+    // that fence precedes the reclaimer's seq_cst fence in the fences' total order, the scan's loads observe
+    // every write that happens-before the reader's fence -- including the round-publishing CAS of another
+    // thread that this one merely observed (C++20 [atomics.order]/4.4, P0668; the same cumulativity RC11
+    // and every hardware mapping we target provide, and that folly::hazptr rests on).
     //
     // The count is bumped before the element exists. If __records_[] throws, the domain has counted one
     // record more than it built; the scan clamps to what the grow array actually holds (see __pointers),
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/concurrent_retire.pass.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/concurrent_retire.pass.cpp
new file mode 100644
index 0000000000000..60921253f3404
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/concurrent_retire.pass.cpp
@@ -0,0 +1,79 @@
+//===----------------------------------------------------------------------===//
+//
+// 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: no-threads
+// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20, c++23
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// Many threads retiring concurrently, none of them protecting a retired object: reclamation passes race
+// with each other and with retire() on the shared retired-object count, and the accounting must stay
+// consistent. libc++ runs a pass inline in retire() once max(1000, 2 * <records>) objects are pending, so
+// once every thread has finished, the number of objects still pending is bounded by that threshold plus a
+// claim in flight per thread, and one more burst of retirements from a single thread drains them all.
+// Under ASan/TSan this is also a use-after-free / data-race detector for overlapping passes.
+
+#include <hazard_pointer>
+#include <atomic>
+#include <cassert>
+#include <thread>
+#include <vector>
+
+#include "make_test_thread.h"
+#include "test_macros.h"
+
+// Namespace scope: deleters run on whichever thread's retire() happens to trigger the pass.
+std::atomic<int> deleted{0};
+struct Node : std::hazard_pointer_obj_base<Node> {
+  ~Node() { ++deleted; }
+};
+
+// Never retired: gives every pass some real protections to scan past.
+struct Live : std::hazard_pointer_obj_base<Live> {};
+
+int main(int, char**) {
+#if defined(TEST_IS_EXECUTED_IN_A_SLOW_ENVIRONMENT)
+  const int threads = 4, per_thread = 5000;
+#else
+  const int threads = 8, per_thread = 50000;
+#endif
+  constexpr int threshold = 1000; // records stay well below 500, so the threshold is the fixed floor
+
+  std::atomic<bool> go{false};
+  std::vector<std::thread> ts;
+  for (int t = 0; t < threads; ++t) {
+    ts.push_back(support::make_test_thread([&] {
+      Live live;
+      std::atomic<Live*> src{&live};
+      std::hazard_pointer hps[4];
+      for (std::hazard_pointer& h : hps) {
+        h = std::make_hazard_pointer();
+        (void)h.protect(src);
+      }
+      while (!go.load(std::memory_order_acquire)) {
+      }
+      for (int i = 0; i < per_thread; ++i)
+        (new Node)->retire();
+    }));
+  }
+  go.store(true, std::memory_order_release);
+  for (std::thread& t : ts)
+    t.join();
+
+  const int retired = threads * per_thread;
+  const int pending = retired - deleted.load();
+  assert(pending >= 0);                         // nothing reclaimed twice
+  assert(pending <= threshold * (threads + 1)); // bounded: no credit leaked out of the counter
+
+  // Everything retired above is unprotected; the first pass triggered here extracts and reclaims it all.
+  for (int i = 0; i < 3 * threshold; ++i)
+    (new Node)->retire();
+  assert(deleted.load() >= retired);
+  return 0;
+}



More information about the libcxx-commits mailing list