[libcxx-commits] [libcxx] [libc++] P2530R3: Implementation of std::hazard_pointer (PR #218218)

Nikita Grivin via libcxx-commits libcxx-commits at lists.llvm.org
Thu Sep 3 02:52:00 PDT 2026


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

>From 3d87b74b79c02aea4f38bd6438710bf993fc09ef Mon Sep 17 00:00:00 2001
From: Nikita Grivin <neuronspectrelin at gmail.com>
Date: Mon, 24 Aug 2026 00:49:56 +0300
Subject: [PATCH] [libc++] Implement P2530R3: Hazard Pointers for C++26

Implements <hazard_pointer>: std::hazard_pointer, std::hazard_pointer_obj_base,
std::make_hazard_pointer and __cpp_lib_hazard_pointer.

The hazard pointer domain (records, per-thread record caches, sharded retired
lists and the reclamation pass) lives in the built library behind three
exported entry points (__hazard_pointer_acquire/release/retire); the class
templates in the headers are inline wrappers around them, so the facility is
availability-marked and requires the LLVM 24 dylib. The engine is a port of
Folly's hazptr (untagged path) reduced to the single default domain the
standard specifies: reclamation runs inline in retire() once
max(1000, 2 * records) objects are pending or two seconds have elapsed, and
deleters never run on a background thread.

RFC: https://discourse.llvm.org/t/rfc-implementing-hazard-pointer-p2530-in-libc/91587

Closes #105362
---
 libcxx/docs/FeatureTestMacroTable.rst         |   2 +-
 libcxx/docs/ReleaseNotes/24.rst               |   1 +
 libcxx/docs/Status/Cxx26Papers.csv            |   2 +-
 libcxx/include/CMakeLists.txt                 |   4 +
 libcxx/include/__configuration/availability.h |  13 +
 libcxx/include/__hazard_pointer/domain.h      |  93 +++
 .../include/__hazard_pointer/hazard_pointer.h | 133 +++
 .../hazard_pointer_obj_base.h                 |  96 +++
 libcxx/include/hazard_pointer                 |  74 ++
 libcxx/include/module.modulemap.in            |   9 +
 libcxx/include/version                        |   4 +-
 libcxx/lib/abi/CHANGELOG.TXT                  |  20 +
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |   5 +-
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |   5 +-
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |   7 +-
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |   7 +-
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |   5 +-
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |   5 +-
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |   7 +-
 ...bcxxabi.v1.stable.exceptions.nonew.abilist |   7 +-
 ...xxabi.v1.stable.noexceptions.nonew.abilist |   7 +-
 libcxx/modules/std.compat.cppm.in             |   3 -
 libcxx/modules/std.cppm.in                    |   4 +-
 libcxx/modules/std/hazard_pointer.inc         |  15 +-
 libcxx/src/CMakeLists.txt                     |   1 +
 libcxx/src/hazard_pointer.cpp                 | 789 ++++++++++++++++++
 .../test/benchmarks/hazard_pointer.bench.cpp  | 140 ++++
 .../saferecl/saferecl.hp/abi.compile.pass.cpp |  46 +
 .../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 +
 .../saferecl.hp/concurrent_retire.pass.cpp    |  79 ++
 .../saferecl.hp/hazard_protectable.verify.cpp | 101 +++
 .../saferecl.hp/no_specializations.verify.cpp |  29 +
 .../saferecl/saferecl.hp/nodiscard.verify.cpp |  31 +
 .../saferecl/saferecl.hp/reclamation.pass.cpp | 130 +++
 .../saferecl.hp/static_destruction.pass.cpp   |  43 +
 .../saferecl/saferecl.hp/thread_exit.pass.cpp |  76 ++
 .../thread_exit_late_release.pass.cpp         |  59 ++
 libcxx/test/libcxx/transitive_includes.gen.py |   1 +
 .../test/libcxx/transitive_includes/cxx03.csv |   1 +
 .../test/libcxx/transitive_includes/cxx11.csv |   1 +
 .../test/libcxx/transitive_includes/cxx14.csv |   1 +
 .../test/libcxx/transitive_includes/cxx17.csv |   1 +
 .../test/libcxx/transitive_includes/cxx20.csv |   1 +
 .../test/libcxx/transitive_includes/cxx23.csv |   1 +
 .../test/libcxx/transitive_includes/cxx26.csv |   7 +
 .../test/libcxx/transitive_includes/cxx29.csv |   7 +
 .../hazard_pointer.version.compile.pass.cpp   |  85 ++
 .../version.version.compile.pass.cpp          |   8 +-
 .../saferecl/saferecl.hp/protection.pass.cpp  | 103 +++
 .../saferecl.hp/readers_writers.pass.cpp      |  88 ++
 .../incomplete_type.compile.pass.cpp          |  39 +
 .../nonzero_offset_base.pass.cpp              | 104 +++
 .../saferecl.hp.base/retire.pass.cpp          | 115 +++
 .../saferecl.hp.base/special_members.pass.cpp |  93 +++
 .../saferecl.hp.base/types.compile.pass.cpp   |  51 ++
 .../moved_between_threads.pass.cpp            |  82 ++
 .../assign.move.pass.cpp                      |  79 ++
 .../copy.delete.compile.pass.cpp              |  24 +
 .../saferecl.hp.holder.ctor/default.pass.cpp  |  28 +
 .../saferecl.hp.holder.ctor/dtor.pass.cpp     |  51 ++
 .../saferecl.hp.holder.ctor/move.pass.cpp     |  52 ++
 .../saferecl.hp.holder.mem/empty.pass.cpp     |  32 +
 .../saferecl.hp.holder.mem/protect.pass.cpp   |  67 ++
 .../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 +
 .../saferecl.hp.holder/types.compile.pass.cpp |  52 ++
 .../generate_feature_test_macro_components.py |   4 +-
 libcxx/utils/libcxx/header_information.py     |   1 -
 .../libcxx/test/features/availability.py      |  17 +-
 75 files changed, 3600 insertions(+), 33 deletions(-)
 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/src/hazard_pointer.cpp
 create mode 100644 libcxx/test/benchmarks/hazard_pointer.bench.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
 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
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/concurrent_retire.pass.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/no_specializations.verify.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp
 create mode 100644 libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp
 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
 create mode 100644 libcxx/test/std/language.support/support.limits/support.limits.general/hazard_pointer.version.compile.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/incomplete_type.compile.pass.cpp
 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.base/retire.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.base/types.compile.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/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/copy.delete.compile.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
 create mode 100644 libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp

diff --git a/libcxx/docs/FeatureTestMacroTable.rst b/libcxx/docs/FeatureTestMacroTable.rst
index 7035b01904a19..6f15c70e86efc 100644
--- a/libcxx/docs/FeatureTestMacroTable.rst
+++ b/libcxx/docs/FeatureTestMacroTable.rst
@@ -488,7 +488,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/24.rst b/libcxx/docs/ReleaseNotes/24.rst
index a7b9e8731d555..ddd3708eed11c 100644
--- a/libcxx/docs/ReleaseNotes/24.rst
+++ b/libcxx/docs/ReleaseNotes/24.rst
@@ -52,6 +52,7 @@ Implemented Papers
 - P2830R10: Standardized Constexpr Type Ordering (`Github <https://llvm.org/PR216616>`__)
 - P3778R0: Fix for ``type_order`` template definition (`Github <https://llvm.org/PR216616>`__)
 - P4140R0: Proposed resolution for US70-126: allow incomplete types in type_order (`Github <https://llvm.org/PR216616>`__)
+- 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 fc38b0a47070e..5578c17d7245a 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|","24","`#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."
diff --git a/libcxx/include/CMakeLists.txt b/libcxx/include/CMakeLists.txt
index b559309929a67..4c79684e4bce9 100644
--- a/libcxx/include/CMakeLists.txt
+++ b/libcxx/include/CMakeLists.txt
@@ -491,6 +491,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
@@ -1071,6 +1074,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 db6c38ddedb95..9dad855fe6e29 100644
--- a/libcxx/include/__configuration/availability.h
+++ b/libcxx/include/__configuration/availability.h
@@ -39,6 +39,9 @@
 // in all versions of the library are available.
 #if !_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS
 
+#  define _LIBCPP_INTRODUCED_IN_LLVM_24 1
+#  define _LIBCPP_INTRODUCED_IN_LLVM_24_ATTRIBUTE /* nothing */
+
 #  define _LIBCPP_INTRODUCED_IN_LLVM_23 1
 #  define _LIBCPP_INTRODUCED_IN_LLVM_23_ATTRIBUTE /* nothing */
 
@@ -73,6 +76,11 @@
 
 // clang-format off
 
+// LLVM 24
+// TODO: Fill this in
+#  define _LIBCPP_INTRODUCED_IN_LLVM_24 0
+#  define _LIBCPP_INTRODUCED_IN_LLVM_24_ATTRIBUTE __attribute__((unavailable))
+
 // LLVM 23
 // TODO: Fill this in
 #  define _LIBCPP_INTRODUCED_IN_LLVM_23 0
@@ -253,6 +261,11 @@
 
 #endif
 
+// 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_24
+#define _LIBCPP_AVAILABILITY_HAZARD_POINTER _LIBCPP_INTRODUCED_IN_LLVM_24_ATTRIBUTE
+
 // This determines whether we assume that the internal std::__bad_variant_access_with_msg class
 // (which carries a message describing the cause of the failure in bad_variant_access::what())
 // provides a key function in the dylib. This allows centralizing its vtable and typeinfo instead
diff --git a/libcxx/include/__hazard_pointer/domain.h b/libcxx/include/__hazard_pointer/domain.h
new file mode 100644
index 0000000000000..0341b147497f9
--- /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 24)
+//
+// - __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..43f5d48c26f8e
--- /dev/null
+++ b/libcxx/include/__hazard_pointer/hazard_pointer.h
@@ -0,0 +1,133 @@
+// -*- 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::reset_protection(): T must be a hazard-protectable type (a class with exactly one "
+                  "public, non-virtual hazard_pointer_obj_base<T, D> base)");
+    _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());
+}
+
+// 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
+
+#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..e6a1bb0658b9b
--- /dev/null
+++ b/libcxx/include/__hazard_pointer/hazard_pointer_obj_base.h
@@ -0,0 +1,96 @@
+// -*- 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 _LIBCPP_NO_SPECIALIZATIONS 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 _LIBCPP_NO_SPECIALIZATIONS 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..46788e8f9ca6e
--- /dev/null
+++ b/libcxx/include/hazard_pointer
@@ -0,0 +1,74 @@
+// -*- 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_STD_VER >= 26 && _LIBCPP_HAS_THREADS
+#    include <__hazard_pointer/domain.h>
+#    include <__hazard_pointer/hazard_pointer.h>
+#    include <__hazard_pointer/hazard_pointer_obj_base.h>
+#  endif
+
+#  include <version>
+
+#  if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
+#    pragma GCC system_header
+#  endif
+#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 f0defc3040ef9..92e20d23e1ff6 100644
--- a/libcxx/include/module.modulemap.in
+++ b/libcxx/include/module.modulemap.in
@@ -1490,6 +1490,15 @@ module std {
     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/include/version b/libcxx/include/version
index c89c56fce45f5..27c3a3d5d2bf5 100644
--- a/libcxx/include/version
+++ b/libcxx/include/version
@@ -610,7 +610,9 @@ __cpp_lib_void_t                                        201411L <type_traits>
 # endif
 # define __cpp_lib_function_ref                         202603L
 // # 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 30bb3d6254965..6eba5af1876b8 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 24.0
 ------------
 
+* [libc++] Implement P2530R3: Hazard Pointers for C++26
+
+  <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
+
+  x86_64-unknown-linux-gnu, x86_64-unknown-freebsd, powerpc-ibm-aix, powerpc64-ibm-aix
+  -----------------------------------------------------------------------------------
+  The hazard pointer domain allocates with nothrow new, which the built library did not use before.
+  Where libc++abi is a separate library this adds two undefined symbols resolved from it at load time.
+  (The Apple lists already carried these imports; on Android they are defined in libc++_shared.so.)
+
+  Symbol added: _ZnwmRKSt9nothrow_t
+  Symbol added: _ZnamRKSt9nothrow_t
+
 * [libc++] Remove locale::id::__next_id from the ABI
 
   `__next_id` is only ever used inside the dylib, so we can avoid making it public.
diff --git a/libcxx/lib/abi/arm64-apple-darwin.libcxxabi.v1.stable.exceptions.nonew.abilist b/libcxx/lib/abi/arm64-apple-darwin.libcxxabi.v1.stable.exceptions.nonew.abilist
index fad3441d59ab7..da9dde7d7c2cd 100644
--- a/libcxx/lib/abi/arm64-apple-darwin.libcxxabi.v1.stable.exceptions.nonew.abilist
+++ b/libcxx/lib/abi/arm64-apple-darwin.libcxxabi.v1.stable.exceptions.nonew.abilist
@@ -1577,8 +1577,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_implIxNS_22__cxx_atomic_base_implIxEEEE', '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_implIxNS_22__cxx_atomic_base_implIxEEEE', '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_nativeILm8EEEvPKv', 'type': 'FUNC'}
 {'is_defined': True, 'name': '__ZNSt3__126__atomic_notify_one_nativeILm8EEEvPKv', 'type': 'FUNC'}
@@ -2655,4 +2658,4 @@
 {'is_defined': True, 'name': '___cxa_vec_new2', 'type': 'I'}
 {'is_defined': True, 'name': '___cxa_vec_new3', 'type': 'I'}
 {'is_defined': True, 'name': '___dynamic_cast', 'type': 'I'}
-{'is_defined': True, 'name': '___gxx_personality_v0', 'type': 'I'}
\ No newline at end of file
+{'is_defined': True, 'name': '___gxx_personality_v0', 'type': 'I'}
diff --git a/libcxx/lib/abi/i686-linux-android23.libcxxabi.v1.stable.exceptions.nonew.abilist b/libcxx/lib/abi/i686-linux-android23.libcxxabi.v1.stable.exceptions.nonew.abilist
index 4a16243a17501..b78d7342bb567 100644
--- a/libcxx/lib/abi/i686-linux-android23.libcxxabi.v1.stable.exceptions.nonew.abilist
+++ b/libcxx/lib/abi/i686-linux-android23.libcxxabi.v1.stable.exceptions.nonew.abilist
@@ -1214,8 +1214,11 @@
 {'is_defined': True, 'name': '_ZNSt6__ndk123__cxx_atomic_notify_allEPVKv', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk123__cxx_atomic_notify_oneEPVKNS_17__cxx_atomic_implIiNS_22__cxx_atomic_base_implIiEEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk123__cxx_atomic_notify_oneEPVKv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt6__ndk123__hazard_pointer_retireEPNS_25__hazard_pointer_obj_nodeE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk123__libcpp_atomic_monitorEPVKNS_17__cxx_atomic_implIiNS_22__cxx_atomic_base_implIiEEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk123__libcpp_atomic_monitorEPVKv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt6__ndk124__hazard_pointer_acquireEv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt6__ndk124__hazard_pointer_releaseEPNS_21__hazard_pointer_slotE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk125notify_all_at_thread_exitERNS_18condition_variableENS_11unique_lockINS_5mutexEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk126__atomic_notify_all_nativeILj4EEEvPKv', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk126__atomic_notify_one_nativeILj4EEEvPKv', 'type': 'FUNC'}
@@ -2341,4 +2344,4 @@
 {'is_defined': True, 'name': '__cxa_vec_dtor', 'type': 'FUNC'}
 {'is_defined': True, 'name': '__cxa_vec_new', 'type': 'FUNC'}
 {'is_defined': True, 'name': '__cxa_vec_new2', 'type': 'FUNC'}
-{'is_defined': True, 'name': '__cxa_vec_new3', 'type': 'FUNC'}
\ No newline at end of file
+{'is_defined': True, 'name': '__cxa_vec_new3', 'type': 'FUNC'}
diff --git a/libcxx/lib/abi/powerpc-ibm-aix.libcxxabi.v1.stable.exceptions.nonew.abilist b/libcxx/lib/abi/powerpc-ibm-aix.libcxxabi.v1.stable.exceptions.nonew.abilist
index 65984090a5246..be1108da395f8 100644
--- a/libcxx/lib/abi/powerpc-ibm-aix.libcxxabi.v1.stable.exceptions.nonew.abilist
+++ b/libcxx/lib/abi/powerpc-ibm-aix.libcxxabi.v1.stable.exceptions.nonew.abilist
@@ -569,8 +569,11 @@
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_allEPVKv', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_oneEPVKNS_17__cxx_atomic_implIiNS_22__cxx_atomic_base_implIiEEEE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_oneEPVKv', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__hazard_pointer_retireEPNS_25__hazard_pointer_obj_nodeE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__libcpp_atomic_monitorEPVKNS_17__cxx_atomic_implIiNS_22__cxx_atomic_base_implIiEEEE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__libcpp_atomic_monitorEPVKv', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__124__hazard_pointer_acquireEv', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__124__hazard_pointer_releaseEPNS_21__hazard_pointer_slotE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__125notify_all_at_thread_exitERNS_18condition_variableENS_11unique_lockINS_5mutexEEE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__126__atomic_wait_global_tableEPKvi', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__131__arrive_barrier_algorithm_baseEPNS_24__barrier_algorithm_baseEh', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
@@ -2030,6 +2033,8 @@
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_ZdlPvm', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_ZdlPvmSt11align_val_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_Znam', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnamRKSt9nothrow_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnamSt11align_val_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_Znwm', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
-{'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnwmSt11align_val_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
\ No newline at end of file
+{'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnwmRKSt9nothrow_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnwmSt11align_val_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
diff --git a/libcxx/lib/abi/powerpc64-ibm-aix.libcxxabi.v1.stable.exceptions.nonew.abilist b/libcxx/lib/abi/powerpc64-ibm-aix.libcxxabi.v1.stable.exceptions.nonew.abilist
index 96396605a8412..eb542c2a6abfd 100644
--- a/libcxx/lib/abi/powerpc64-ibm-aix.libcxxabi.v1.stable.exceptions.nonew.abilist
+++ b/libcxx/lib/abi/powerpc64-ibm-aix.libcxxabi.v1.stable.exceptions.nonew.abilist
@@ -569,8 +569,11 @@
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_allEPVKv', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_oneEPVKNS_17__cxx_atomic_implIlNS_22__cxx_atomic_base_implIlEEEE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__cxx_atomic_notify_oneEPVKv', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__hazard_pointer_retireEPNS_25__hazard_pointer_obj_nodeE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__libcpp_atomic_monitorEPVKNS_17__cxx_atomic_implIlNS_22__cxx_atomic_base_implIlEEEE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__123__libcpp_atomic_monitorEPVKv', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__124__hazard_pointer_acquireEv', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__124__hazard_pointer_releaseEPNS_21__hazard_pointer_slotE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__125notify_all_at_thread_exitERNS_18condition_variableENS_11unique_lockINS_5mutexEEE', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__126__atomic_wait_global_tableEPKvl', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'EXP', 'is_defined': True, 'name': '_ZNSt3__131__arrive_barrier_algorithm_baseEPNS_24__barrier_algorithm_baseEh', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
@@ -2030,6 +2033,8 @@
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_ZdlPvm', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_ZdlPvmSt11align_val_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_Znam', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnamRKSt9nothrow_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnamSt11align_val_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
 {'import_export': 'wIMP', 'is_defined': False, 'name': '_Znwm', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
-{'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnwmSt11align_val_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
\ No newline at end of file
+{'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnwmRKSt9nothrow_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
+{'import_export': 'wIMP', 'is_defined': False, 'name': '_ZnwmSt11align_val_t', 'storage_mapping_class': 'DS', 'type': 'FUNC'}
diff --git a/libcxx/lib/abi/x86_64-apple-darwin.libcxxabi.v1.stable.exceptions.nonew.abilist b/libcxx/lib/abi/x86_64-apple-darwin.libcxxabi.v1.stable.exceptions.nonew.abilist
index be1d6382a7983..70e8725ea357c 100644
--- a/libcxx/lib/abi/x86_64-apple-darwin.libcxxabi.v1.stable.exceptions.nonew.abilist
+++ b/libcxx/lib/abi/x86_64-apple-darwin.libcxxabi.v1.stable.exceptions.nonew.abilist
@@ -1573,8 +1573,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_implIxNS_22__cxx_atomic_base_implIxEEEE', '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_implIxNS_22__cxx_atomic_base_implIxEEEE', '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__127__from_chars_floating_pointEPKcS1_RdNS_12chars_formatE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '__ZNSt3__127__from_chars_floating_pointEPKcS1_RfNS_12chars_formatE', 'type': 'FUNC'}
@@ -2674,4 +2677,4 @@
 {'is_defined': True, 'name': '___cxa_vec_new2', 'type': 'I'}
 {'is_defined': True, 'name': '___cxa_vec_new3', 'type': 'I'}
 {'is_defined': True, 'name': '___dynamic_cast', 'type': 'I'}
-{'is_defined': True, 'name': '___gxx_personality_v0', 'type': 'I'}
\ No newline at end of file
+{'is_defined': True, 'name': '___gxx_personality_v0', 'type': 'I'}
diff --git a/libcxx/lib/abi/x86_64-linux-android23.libcxxabi.v1.stable.exceptions.nonew.abilist b/libcxx/lib/abi/x86_64-linux-android23.libcxxabi.v1.stable.exceptions.nonew.abilist
index 979c5b9d98509..26c670f55b9f9 100644
--- a/libcxx/lib/abi/x86_64-linux-android23.libcxxabi.v1.stable.exceptions.nonew.abilist
+++ b/libcxx/lib/abi/x86_64-linux-android23.libcxxabi.v1.stable.exceptions.nonew.abilist
@@ -1214,8 +1214,11 @@
 {'is_defined': True, 'name': '_ZNSt6__ndk123__cxx_atomic_notify_allEPVKv', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk123__cxx_atomic_notify_oneEPVKNS_17__cxx_atomic_implIiNS_22__cxx_atomic_base_implIiEEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk123__cxx_atomic_notify_oneEPVKv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt6__ndk123__hazard_pointer_retireEPNS_25__hazard_pointer_obj_nodeE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk123__libcpp_atomic_monitorEPVKNS_17__cxx_atomic_implIiNS_22__cxx_atomic_base_implIiEEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk123__libcpp_atomic_monitorEPVKv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt6__ndk124__hazard_pointer_acquireEv', 'type': 'FUNC'}
+{'is_defined': True, 'name': '_ZNSt6__ndk124__hazard_pointer_releaseEPNS_21__hazard_pointer_slotE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk125notify_all_at_thread_exitERNS_18condition_variableENS_11unique_lockINS_5mutexEEE', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk126__atomic_notify_all_nativeILm4EEEvPKv', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt6__ndk126__atomic_notify_one_nativeILm4EEEvPKv', 'type': 'FUNC'}
@@ -2335,4 +2338,4 @@
 {'is_defined': True, 'name': '__cxa_vec_dtor', 'type': 'FUNC'}
 {'is_defined': True, 'name': '__cxa_vec_new', 'type': 'FUNC'}
 {'is_defined': True, 'name': '__cxa_vec_new2', 'type': 'FUNC'}
-{'is_defined': True, 'name': '__cxa_vec_new3', 'type': 'FUNC'}
\ No newline at end of file
+{'is_defined': True, 'name': '__cxa_vec_new3', 'type': 'FUNC'}
diff --git a/libcxx/lib/abi/x86_64-unknown-freebsd.libcxxabi.v1.stable.exceptions.nonew.abilist b/libcxx/lib/abi/x86_64-unknown-freebsd.libcxxabi.v1.stable.exceptions.nonew.abilist
index cb77431bc4f50..6a06b331e1422 100644
--- a/libcxx/lib/abi/x86_64-unknown-freebsd.libcxxabi.v1.stable.exceptions.nonew.abilist
+++ b/libcxx/lib/abi/x86_64-unknown-freebsd.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_atexit at FBSD_1.0', 'type': 'FUNC'}
@@ -1227,8 +1229,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_implIlNS_22__cxx_atomic_base_implIlEEEE', '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_implIlNS_22__cxx_atomic_base_implIlEEEE', '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_nativeILm8EEEvPKv', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZNSt3__126__atomic_notify_one_nativeILm8EEEvPKv', 'type': 'FUNC'}
@@ -2034,4 +2039,4 @@
 {'is_defined': True, 'name': '_ZTv0_n24_NSt3__114basic_iostreamIcNS_11char_traitsIcEEED0Ev', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZTv0_n24_NSt3__114basic_iostreamIcNS_11char_traitsIcEEED1Ev', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZTv0_n24_NSt3__19strstreamD0Ev', 'type': 'FUNC'}
-{'is_defined': True, 'name': '_ZTv0_n24_NSt3__19strstreamD1Ev', 'type': 'FUNC'}
\ No newline at end of file
+{'is_defined': True, 'name': '_ZTv0_n24_NSt3__19strstreamD1Ev', 'type': 'FUNC'}
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 c727c028d21e6..77b44b3664c54 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'}
@@ -1225,8 +1227,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'}
@@ -2033,4 +2038,4 @@
 {'is_defined': True, 'name': '_ZTv0_n24_NSt3__114basic_iostreamIcNS_11char_traitsIcEEED0Ev', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZTv0_n24_NSt3__114basic_iostreamIcNS_11char_traitsIcEEED1Ev', 'type': 'FUNC'}
 {'is_defined': True, 'name': '_ZTv0_n24_NSt3__19strstreamD0Ev', 'type': 'FUNC'}
-{'is_defined': True, 'name': '_ZTv0_n24_NSt3__19strstreamD1Ev', 'type': 'FUNC'}
\ No newline at end of file
+{'is_defined': True, 'name': '_ZTv0_n24_NSt3__19strstreamD1Ev', 'type': 'FUNC'}
diff --git a/libcxx/lib/abi/x86_64-unknown-linux-gnu.libcxxabi.v1.stable.noexceptions.nonew.abilist b/libcxx/lib/abi/x86_64-unknown-linux-gnu.libcxxabi.v1.stable.noexceptions.nonew.abilist
index 86c548d5aa627..165279e97d7a1 100644
--- a/libcxx/lib/abi/x86_64-unknown-linux-gnu.libcxxabi.v1.stable.noexceptions.nonew.abilist
+++ b/libcxx/lib/abi/x86_64-unknown-linux-gnu.libcxxabi.v1.stable.noexceptions.nonew.abilist
@@ -20,8 +20,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_current_primary_exception', 'type': 'FUNC'}
 {'is_defined': False, 'name': '__cxa_decrement_exception_refcount', 'type': 'FUNC'}
@@ -1196,8 +1198,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'}
@@ -2004,4 +2009,4 @@
 {'is_defined': True, 'name': '_ZTv0_n24_NSt3__19strstreamD1Ev', 'type': 'FUNC'}
 {'name': '_ZTINSt3__113basic_filebufIwNS_11char_traitsIwEEEE', 'size': 24, 'type': 'OBJECT', 'is_defined': True}
 {'name': '_ZTSNSt3__113basic_filebufIwNS_11char_traitsIwEEEE', 'size': 47, 'type': 'OBJECT', 'is_defined': True}
-{'name': '_ZTVNSt3__113basic_filebufIwNS_11char_traitsIwEEEE', 'size': 128, 'type': 'OBJECT', 'is_defined': True}
\ No newline at end of file
+{'name': '_ZTVNSt3__113basic_filebufIwNS_11char_traitsIwEEEE', 'size': 128, 'type': 'OBJECT', 'is_defined': True}
diff --git a/libcxx/modules/std.compat.cppm.in b/libcxx/modules/std.compat.cppm.in
index ab08125c4c4e4..1ad9520cd15b2 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 b9a491653659a..0b8711319e651 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>
@@ -137,9 +138,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/src/CMakeLists.txt b/libcxx/src/CMakeLists.txt
index e029f9f29d5ea..d9bacf812a5ac 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..16041b7c46093
--- /dev/null
+++ b/libcxx/src/hazard_pointer.cpp
@@ -0,0 +1,789 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+
+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 uintptr_t __sync_time_period_s  = 2;    // ... 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;
+// 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;
+#endif
+
+// 64-bit mixer shared by the shard function and the protected set.
+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); 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} {}
+};
+
+// 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 *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 {
+    __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 __alignment() 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*), __alignment()) +
+           __ceil_to((__capacity - __base) * sizeof(_Tp), __alignment());
+  }
+  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), __alignment()));
+  }
+
+  // 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     = std::__libcpp_allocate<byte>(__element_count(__bytes(__capacity, __base)), __alignment());
+    __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();
+    std::__libcpp_deallocate<byte>(reinterpret_cast<byte*>(__curr), __element_count(__size), __alignment());
+  }
+
+  __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 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;
+
+  __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 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.
+//
+// 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
+
+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 {
+  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;
+
+  // 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)
+  // steady_clock seconds, kept word-sized: a 64-bit atomic is not lock-free on every 32-bit target, and
+  // 2^32 seconds of uptime is over a century, so seconds never wrap. 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<uintptr_t> __due_time_{0};
+
+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_slot() {
+    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, if it has one,
+  // 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_slot(__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=*/false))
+      if (__cache->__try_put(__rec))
+        return;
+    __push_available(__rec, __rec);
+  }
+
+  // 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 uintptr_t __now_s() noexcept {
+    return static_cast<uintptr_t>(
+        chrono::duration_cast<chrono::seconds>(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_s() + __sync_time_period_s, 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 {
+    uintptr_t __time = __now_s();
+    uintptr_t __due  = __due_time_.load(memory_order_acquire);
+    if (__time < __due || !__due_time_.compare_exchange_strong(
+                              __due, __time + __sync_time_period_s, 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;
+  }
+
+  // Pops every shard into __extracted; returns the number of nodes taken.
+  int __extract_retired(__node* __extracted[__shard_count]) noexcept {
+    int __taken = 0;
+    for (int __s = 0; __s < __shard_count; ++__s) {
+      __extracted[__s] = __retired_[__s].__pop_all();
+      for (__node* __n = __extracted[__s]; __n != nullptr; __n = __n->__next_)
+        ++__taken;
+    }
+    return __taken;
+  }
+
+  // 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
+  // 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;
+    __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
+          __n->__reclaim_(__n);
+      }
+    }
+    if (!__retired_empty())
+      __done = false;
+    int __residue = 0;
+    for (int __s = 0; __s < __shard_count; ++__s) {
+      __residue += __not_reclaimed[__s].__count_;
+      __retired_[__s].__push(__not_reclaimed[__s]);
+    }
+    return __residue;
+  }
+
+  // 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;
+      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;
+        // 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);
+      }
+      if (__done)
+        break;
+      __count = __check_count_threshold();
+    }
+  }
+
+public:
+  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.
+  // __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();
+      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
+    // 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),
+    // 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. 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);
+    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;
+
+// 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;
+  __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_slot(); }
+
+_LIBCPP_EXPORTED_FROM_ABI void __hazard_pointer_release(__hazard_pointer_slot* __slot) noexcept {
+  __domain.__release_slot(__slot);
+}
+
+_LIBCPP_EXPORTED_FROM_ABI void __hazard_pointer_retire(__hazard_pointer_obj_node* __n) noexcept {
+  __domain.__retire(__n);
+}
+
+_LIBCPP_END_EXPLICIT_ABI_ANNOTATIONS
+
+_LIBCPP_END_NAMESPACE_STD
diff --git a/libcxx/test/benchmarks/hazard_pointer.bench.cpp b/libcxx/test/benchmarks/hazard_pointer.bench.cpp
new file mode 100644
index 0000000000000..78fed17d597ad
--- /dev/null
+++ b/libcxx/test/benchmarks/hazard_pointer.bench.cpp
@@ -0,0 +1,140 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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. 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();
+    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)->Name("std::make_hazard_pointer() (make + destroy)");
+BENCHMARK(BM_protect_combined)->Name("std::hazard_pointer::protect(const atomic<T*>&) (protection kept)");
+BENCHMARK(BM_protect_separate)->Name("std::hazard_pointer::protect(const atomic<T*>&) (+ reset_protection())");
+BENCHMARK(BM_make_protect)->Name("std::make_hazard_pointer() (+ protect(), destroy)");
+BENCHMARK(BM_retire)->Name("std::hazard_pointer_obj_base<T>::retire()");
+BENCHMARK(BM_mt_make_destroy)
+    ->Name("std::make_hazard_pointer() (make + destroy, contended)")
+    ->Threads(1)
+    ->Threads(2)
+    ->Threads(4)
+    ->Threads(8);
+BENCHMARK(BM_mt_retire)
+    ->Name("std::hazard_pointer_obj_base<T>::retire() (contended)")
+    ->Threads(1)
+    ->Threads(2)
+    ->Threads(4)
+    ->Threads(8);
+
+BENCHMARK_MAIN();
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..b7724891a990e
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/abi.compile.pass.cpp
@@ -0,0 +1,46 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// XFAIL: availability-hazard_pointer-missing
+
+// <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*));
+static_assert(alignof(std::hazard_pointer_obj_base<Big, StatefulDeleter>) == alignof(void*));
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..1ee0faf26e20b
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..69710f8da729e
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..ed793994624a6
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..ef60061aa015d
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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;
+}
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..6f77867654e42
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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;
+}
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..adb4d61b3fc2a
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/hazard_protectable.verify.cpp
@@ -0,0 +1,101 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// XFAIL: availability-hazard_pointer-missing
+
+// <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 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> {};
+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};
+  // The static_assert is the diagnostic under test; the follow-on errors from the body are expected too:
+  // try_protect() calls reset_protection(), whose own Mandate fires, and __node_of() has no viable
+  // derived-to-base conversion when T has no hazard_pointer_obj_base base at all.
+  (void)h.protect(src); // expected-error@*:* 2 {{static assertion failed}}
+  // expected-error@*:* {{no matching function for call to '__node_of'}}
+  std::atomic<Derived*> src2{nullptr};
+  Derived* p = nullptr;
+  // Derived converts to hazard_pointer_obj_base<Base, D>, so __node_of() itself is fine here.
+  (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}}
+  // expected-error@*:* {{no matching function for call to '__node_of'}}
+
+  // 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.
+  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); the downcast in the
+// reclamation function then fails as well.
+struct Mismatched : std::hazard_pointer_obj_base<NoBase> {};
+void bad_retire() {
+  Mismatched m;
+  m.retire(); // expected-error@*:* {{static assertion failed}}
+  // expected-error@*:* {{not related by inheritance}}
+}
+
+// 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}}
+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}}
diff --git a/libcxx/test/libcxx/thread/saferecl/saferecl.hp/no_specializations.verify.cpp b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/no_specializations.verify.cpp
new file mode 100644
index 0000000000000..551dd99289ce2
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/no_specializations.verify.cpp
@@ -0,0 +1,29 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+
+// <hazard_pointer>
+
+// Check that user-specializations of hazard_pointer_obj_base are diagnosed. A program-defined
+// specialization cannot meet the requirements of the original template ([namespace.std]/2): retire()
+// must hand the object to the library's hazard pointer domain.
+
+#include <hazard_pointer>
+
+#include "test_macros.h"
+
+#if !__has_warning("-Winvalid-specialization")
+// expected-no-diagnostics
+#else
+struct S {};
+
+template <>
+class std::hazard_pointer_obj_base<S>; // expected-error {{cannot be specialized}}
+#endif
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..c0fea4e89601f
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/nodiscard.verify.cpp
@@ -0,0 +1,31 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// XFAIL: availability-hazard_pointer-missing
+
+// <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/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..e0a4b8d435c40
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/reclamation.pass.cpp
@@ -0,0 +1,130 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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"
+
+// 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> {
+  ~Dummy() { ++dummies_deleted; }
+};
+
+std::atomic<bool> node_deleted{false};
+struct Node : std::hazard_pointer_obj_base<Node> {
+  ~Node() { node_deleted.store(true); }
+};
+
+// Retire enough objects to guarantee at least two passes.
+void force_passes() {
+  for (int i = 0; i < 5000; ++i)
+    (new Dummy)->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.
+    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.
+    node_deleted          = false;
+    Node* n               = new Node();
+    std::hazard_pointer h = std::make_hazard_pointer();
+    h.reset_protection(n);
+    n->retire();
+    force_passes();
+    assert(!node_deleted.load());
+    h.reset_protection();
+    force_passes();
+    assert(node_deleted.load());
+  }
+  {
+    // ~hazard_pointer() ends the epoch as well.
+    node_deleted = false;
+    Node* n      = new Node();
+    {
+      std::hazard_pointer h = std::make_hazard_pointer();
+      h.reset_protection(n);
+      n->retire();
+      force_passes();
+      assert(!node_deleted.load());
+    }
+    force_passes();
+    assert(node_deleted.load());
+  }
+  {
+    // Move-assignment over a nonempty hazard_pointer ends its epoch.
+    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();
+    assert(node_deleted.load());
+  }
+  {
+    // Deleters may retire and use hazard pointers (reclamation is re-entrant).
+    (new Nested(3))->retire();
+    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();
+    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::vector<StackObj> objs(100);
+    for (StackObj& o : objs)
+      o.retire();
+    force_passes(); // reclaims (no-op deleter) and unlinks them: destroying `objs` is now safe
+  }
+  return 0;
+}
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..030cf1f61f00c
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..ad8293828641a
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit.pass.cpp
@@ -0,0 +1,76 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// XFAIL: availability-hazard_pointer-missing
+
+// <hazard_pointer>
+
+// 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>
+#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..a654d60141589
--- /dev/null
+++ b/libcxx/test/libcxx/thread/saferecl/saferecl.hp/thread_exit_late_release.pass.cpp
@@ -0,0 +1,59 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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.
+
+#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
+  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();
+      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();
+    });
+    t.join();
+  }
+  return 0;
+}
diff --git a/libcxx/test/libcxx/transitive_includes.gen.py b/libcxx/test/libcxx/transitive_includes.gen.py
index 20d5e981ccba4..1b542a7e6c02e 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 ad2f90062c078..1ed35ba3b1ce1 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx03.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx03.csv
@@ -316,6 +316,7 @@ future string_view
 future tuple
 future typeinfo
 future version
+hazard_pointer version
 initializer_list version
 iomanip cctype
 iomanip cerrno
diff --git a/libcxx/test/libcxx/transitive_includes/cxx11.csv b/libcxx/test/libcxx/transitive_includes/cxx11.csv
index 672df0a488c9e..1e9ecec1bbfe7 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx11.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx11.csv
@@ -316,6 +316,7 @@ future string_view
 future tuple
 future typeinfo
 future version
+hazard_pointer version
 initializer_list version
 iomanip cctype
 iomanip cerrno
diff --git a/libcxx/test/libcxx/transitive_includes/cxx14.csv b/libcxx/test/libcxx/transitive_includes/cxx14.csv
index f496a00270386..5f21d8fc7f6f8 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx14.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx14.csv
@@ -320,6 +320,7 @@ future string_view
 future tuple
 future typeinfo
 future version
+hazard_pointer version
 initializer_list version
 iomanip cctype
 iomanip cerrno
diff --git a/libcxx/test/libcxx/transitive_includes/cxx17.csv b/libcxx/test/libcxx/transitive_includes/cxx17.csv
index e0a92259202c4..f44f72425ad2f 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx17.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx17.csv
@@ -361,6 +361,7 @@ future string_view
 future tuple
 future typeinfo
 future version
+hazard_pointer version
 initializer_list version
 iomanip cctype
 iomanip cerrno
diff --git a/libcxx/test/libcxx/transitive_includes/cxx20.csv b/libcxx/test/libcxx/transitive_includes/cxx20.csv
index 3c5e9439f100c..3a08c0e243b41 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx20.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx20.csv
@@ -426,6 +426,7 @@ future string_view
 future tuple
 future typeinfo
 future version
+hazard_pointer version
 initializer_list version
 iomanip cctype
 iomanip cerrno
diff --git a/libcxx/test/libcxx/transitive_includes/cxx23.csv b/libcxx/test/libcxx/transitive_includes/cxx23.csv
index 8c03f1c3c2251..376ba2009483b 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx23.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx23.csv
@@ -445,6 +445,7 @@ future string_view
 future tuple
 future typeinfo
 future version
+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 3f4daee66625a..af0fa7c1fa4f1 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx26.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx26.csv
@@ -440,6 +440,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/cxx29.csv b/libcxx/test/libcxx/transitive_includes/cxx29.csv
index 3f4daee66625a..af0fa7c1fa4f1 100644
--- a/libcxx/test/libcxx/transitive_includes/cxx29.csv
+++ b/libcxx/test/libcxx/transitive_includes/cxx29.csv
@@ -440,6 +440,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..7a3f9fd2227a7
--- /dev/null
+++ b/libcxx/test/std/language.support/support.limits/support.limits.general/hazard_pointer.version.compile.pass.cpp
@@ -0,0 +1,85 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 == 26
+
+#  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
+
+#elif TEST_STD_VER > 26
+
+#  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++29"
+#    endif
+#    if __cpp_lib_hazard_pointer != 202306L
+#      error "__cpp_lib_hazard_pointer should have the value 202306L in c++29"
+#    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 > 26
+
+// 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 34900f78a8e92..fc0be95b8501b 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
@@ -7451,7 +7451,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
@@ -7460,7 +7460,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
 
@@ -9537,7 +9537,7 @@
 #    error "__cpp_lib_has_unique_object_representations should have the value 201606L in c++29"
 #  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++29"
 #    endif
@@ -9546,7 +9546,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/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..815236aa8a3c1
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/protection.pass.cpp
@@ -0,0 +1,103 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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"
+
+// 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> {
+  ~Dummy() { ++dummies_deleted; }
+};
+
+// 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() { 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() {
+  for (int i = 0; i < N; ++i)
+    (new Dummy)->retire();
+}
+
+int main(int, char**) {
+  {
+    // Protected via protect(): survives reclamation passes; readable throughout.
+    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);
+    assert(p == n);
+    src.store(nullptr);
+    n->retire();
+    retire_dummies();
+    assert(dummies_deleted.load() - before > 0); // reclamation did happen ...
+    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();
+    // 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.
+    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_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* 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_3.load()); // still protected by h2
+    h2.reset_protection();
+    retire_dummies();
+  }
+  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..4a13bc1808b49
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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/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..6118e577d17fa
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/incomplete_type.compile.pass.cpp
@@ -0,0 +1,39 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// XFAIL: availability-hazard_pointer-missing
+
+// <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/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..8ea32d8408f8e
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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
new file mode 100644
index 0000000000000..c80d0b7bb2372
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/retire.pass.cpp
@@ -0,0 +1,115 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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"
+
+#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};
+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. 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> {};
+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 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);
+
+  return 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
new file mode 100644
index 0000000000000..f7b15e02e1b38
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/special_members.pass.cpp
@@ -0,0 +1,93 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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"
+
+#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> {
+  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 < N; ++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.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..c66ab7b07572b
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.base/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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// XFAIL: availability-hazard_pointer-missing
+
+// <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>);
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..96a73730e8039
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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
new file mode 100644
index 0000000000000..e09f469d211db
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/assign.move.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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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 <atomic>
+#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/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..eb64f20866b6c
--- /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,24 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// XFAIL: availability-hazard_pointer-missing
+
+// <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>);
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..0a1bdd61647b3
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..f53c267bce1ad
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/dtor.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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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 <atomic>
+#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..e0cb1dcd02c8e
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.ctor/move.pass.cpp
@@ -0,0 +1,52 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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 <atomic>
+#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..42e4241eebde8
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..1258f85f809fa
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/saferecl.hp.holder.mem/protect.pass.cpp
@@ -0,0 +1,67 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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);
+  }
+  {
+    // 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/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..0d9549c8aff88
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..726659dc85c72
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..4c4c69d23d89f
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..093e45d6f7117
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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..096d2b94c0629
--- /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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// 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;
+}
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..7f5a396d8f88d
--- /dev/null
+++ b/libcxx/test/std/thread/saferecl/saferecl.hp/saferecl.hp.holder/types.compile.pass.cpp
@@ -0,0 +1,52 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: std-at-least-c++26
+// UNSUPPORTED: no-threads
+// XFAIL: availability-hazard_pointer-missing
+
+// <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>);
diff --git a/libcxx/utils/generate_feature_test_macro_components.py b/libcxx/utils/generate_feature_test_macro_components.py
index 8cc54971c57c9..bc6c6db58bf22 100755
--- a/libcxx/utils/generate_feature_test_macro_components.py
+++ b/libcxx/utils/generate_feature_test_macro_components.py
@@ -756,7 +756,8 @@ def add_version_header(tc):
                 # "c++29": 202606,  # P3428R4 Hazard Pointer Batches
             },
             "headers": ["hazard_pointer"],
-            "unimplemented": True,
+            "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",
@@ -1638,6 +1639,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/header_information.py b/libcxx/utils/libcxx/header_information.py
index 7316662171b77..b5635d3242c3d 100644
--- a/libcxx/utils/libcxx/header_information.py
+++ b/libcxx/utils/libcxx/header_information.py
@@ -159,7 +159,6 @@ def __hash__(self) -> int:
 headers_not_available = list(map(Header, [
     "debugging",
     "generator",
-    "hazard_pointer",
     "inplace_vector",
     "linalg",
     "rcu",
diff --git a/libcxx/utils/libcxx/test/features/availability.py b/libcxx/utils/libcxx/test/features/availability.py
index 65cf19f0a7617..66ab2a504ceb2 100644
--- a/libcxx/utils/libcxx/test/features/availability.py
+++ b/libcxx/utils/libcxx/test/features/availability.py
@@ -13,12 +13,19 @@
 # Those are used for backdeployment features below, do not use directly in tests.
 features = [
     Feature(
-        name="_target-has-llvm-23",
+        name="_target-has-llvm-24",
         when=lambda cfg: BooleanExpression.evaluate(
             "TBD",
             cfg.available_features,
         ),
     ),
+    Feature(
+        name="_target-has-llvm-23",
+        when=lambda cfg: BooleanExpression.evaluate(
+            "_target-has-llvm-24",
+            cfg.available_features,
+        ),
+    ),
     Feature(
         name="_target-has-llvm-22",
         when=lambda cfg: BooleanExpression.evaluate(
@@ -187,6 +194,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-24)",
+            cfg.available_features,
+        ),
+    ),
     # Tests that require time zone database support in the built library
     Feature(
         name="availability-tzdb-missing",



More information about the libcxx-commits mailing list