lockout_hazard/lib.rs
1//! A lock-free hazard pointer implementation for safe memory reclamation.
2//!
3//! Hazard pointers allow threads to safely read shared pointers while other threads
4//! may concurrently remove and deallocate the pointed-to objects. A thread "protects"
5//! a pointer by publishing it in a hazard slot; reclamation of retired objects is
6//! deferred until no thread holds a matching hazard.
7//!
8//! # Example
9//!
10//! ```
11//! use std::sync::atomic::Ordering;
12//! use lockout_hazard::{Domain, AtomicPtr};
13//!
14//! static DOMAIN: Domain = Domain::new();
15//!
16//! let shared = AtomicPtr::new(Box::into_raw(Box::new(42)));
17//!
18//! // Protect the pointer so it won't be reclaimed while we read it.
19//! let guard = DOMAIN.protect(&shared).unwrap();
20//! assert_eq!(*guard, 42);
21//!
22//! // Swap in a new value — returns a Replaced that must be retired.
23//! let old = shared.swap(Box::into_raw(Box::new(100)), Ordering::SeqCst);
24//! guard.clear();
25//! old.retire(&DOMAIN);
26//!
27//! // Clean up the remaining allocation.
28//! shared.swap(std::ptr::null_mut(), Ordering::SeqCst).retire(&DOMAIN);
29//! DOMAIN.collect();
30//! ```
31
32#[cfg(not(loom))]
33use std::sync::atomic::{AtomicPtr as StdAtomicPtr, AtomicU8, AtomicUsize};
34use std::{ops::Deref, ptr::NonNull, sync::atomic::Ordering};
35
36#[cfg(loom)]
37use loom::sync::atomic::{AtomicPtr as StdAtomicPtr, AtomicU8, AtomicUsize};
38
39/// Number of retires before an automatic collection is triggered.
40const DEFAULT_COLLECTION_THRESHOLD: u8 = 8;
41
42/// A node in the domain's lock-free hazard linked list.
43#[derive(Debug, Default)]
44struct HazardNode {
45 hazard: StdAtomicPtr<()>,
46 next: StdAtomicPtr<HazardNode>,
47}
48
49impl HazardNode {
50 #[cfg(not(loom))]
51 const fn new(ptr: *mut ()) -> Self {
52 Self {
53 hazard: StdAtomicPtr::new(ptr),
54 next: StdAtomicPtr::new(std::ptr::null_mut()),
55 }
56 }
57
58 #[cfg(loom)]
59 fn new(ptr: *mut ()) -> Self {
60 Self {
61 hazard: StdAtomicPtr::new(ptr),
62 next: StdAtomicPtr::new(std::ptr::null_mut()),
63 }
64 }
65}
66
67/// A retired pointer with its type-erased deleter, forming a lock-free intrusive stack.
68struct RetiredNode {
69 ptr: *mut (),
70 deleter: unsafe fn(*mut ()),
71 next: *mut RetiredNode,
72}
73
74/// A pointer that has been displaced from a [`AtomicPtr`] and must be retired.
75///
76/// This type cannot be dereferenced — the value is no longer safely accessible
77/// without a hazard guard. The only valid operation is to [`retire`](Replaced::retire)
78/// it through a [`Domain`], which schedules it for deferred reclamation.
79///
80/// Dropping a `Replaced` without retiring it will leak the allocation.
81pub struct Replaced<T> {
82 ptr: Option<NonNull<T>>,
83}
84
85impl<T> Replaced<T> {
86 /// Retires this pointer, scheduling it for deferred reclamation.
87 ///
88 /// The pointed-to value will be deallocated once no hazard slot references it.
89 /// Does nothing if the displaced pointer was null.
90 pub fn retire(self, domain: &Domain) {
91 if let Some(ptr) = self.ptr {
92 unsafe { domain.retire_ptr(ptr.as_ptr()) };
93 }
94 std::mem::forget(self);
95 }
96
97 /// Deliberately discards this `Replaced` without retiring it.
98 ///
99 /// Use this when the old pointer is still reachable (e.g. from another
100 /// atomic) and should not be reclaimed.
101 pub fn forget(self) {
102 std::mem::forget(self);
103 }
104}
105
106impl<T> Drop for Replaced<T> {
107 fn drop(&mut self) {
108 // Intentional leak — if the user drops without retiring, we can't
109 // safely free because another thread may still hold a guard.
110 // This is preferable to a use-after-free.
111 #[cfg(debug_assertions)]
112 eprintln!("lockout_hazard: Replaced<T> dropped without retiring — memory leaked");
113 }
114}
115
116/// A managed atomic pointer type for use with hazard pointer domains.
117///
118/// Wraps [`AtomicPtr`] and returns [`Replaced<T>`] from mutation operations,
119/// ensuring displaced pointers are properly retired.
120#[derive(Debug)]
121pub struct AtomicPtr<T> {
122 inner: StdAtomicPtr<T>,
123}
124
125impl<T> AtomicPtr<T> {
126 /// Creates a new `AtomicPtr` from a raw pointer.
127 #[cfg(not(loom))]
128 pub const fn new(ptr: *mut T) -> Self {
129 Self {
130 inner: StdAtomicPtr::new(ptr),
131 }
132 }
133
134 #[cfg(loom)]
135 pub fn new(ptr: *mut T) -> Self {
136 Self {
137 inner: StdAtomicPtr::new(ptr),
138 }
139 }
140
141 /// Creates a new `AtomicPtr` from a [`Box`].
142 pub fn from_box(val: Box<T>) -> Self {
143 Self::new(Box::into_raw(val))
144 }
145
146 /// Atomically loads the raw pointer.
147 pub fn load(&self, order: Ordering) -> *mut T {
148 self.inner.load(order)
149 }
150
151 /// Atomically swaps the pointer, returning the old value as a [`Replaced<T>`]
152 /// that must be retired.
153 pub fn swap(&self, new: *mut T, order: Ordering) -> Replaced<T> {
154 let old = self.inner.swap(new, order);
155 Replaced {
156 ptr: NonNull::new(old),
157 }
158 }
159
160 /// Atomically compares and exchanges the pointer. On success, returns
161 /// `Ok(Replaced<T>)` containing the old value that must be retired.
162 /// On failure, returns `Err(*mut T)` with the current value.
163 pub fn compare_exchange(
164 &self,
165 current: *mut T,
166 new: *mut T,
167 success: Ordering,
168 failure: Ordering,
169 ) -> Result<Replaced<T>, *mut T> {
170 self.inner
171 .compare_exchange(current, new, success, failure)
172 .map(|old| Replaced {
173 ptr: NonNull::new(old),
174 })
175 }
176}
177
178impl<T> From<StdAtomicPtr<T>> for AtomicPtr<T> {
179 fn from(ptr: StdAtomicPtr<T>) -> Self {
180 Self { inner: ptr }
181 }
182}
183
184impl<T> From<Box<T>> for AtomicPtr<T> {
185 fn from(val: Box<T>) -> Self {
186 Self::from_box(val)
187 }
188}
189
190/// A protected reference to a hazard-pointer-guarded value.
191///
192/// While a `Guard` exists, the underlying pointer is published in a hazard slot,
193/// preventing any concurrent [`Domain::collect`] from reclaiming it.
194///
195/// Implements [`Deref`] for ergonomic access to the protected value.
196/// Dropping the guard (or calling [`clear`](Guard::clear)) releases the hazard slot.
197#[derive(Debug)]
198pub struct Guard<'a, T> {
199 slot: &'a StdAtomicPtr<()>,
200 ptr: *mut T,
201}
202
203// Safety: The guard only provides &T access and the hazard slot is atomic.
204// Sending/sharing a guard across threads is safe as long as T itself is Send + Sync.
205unsafe impl<T: Send + Sync> Send for Guard<'_, T> {}
206unsafe impl<T: Send + Sync> Sync for Guard<'_, T> {}
207
208impl<'a, T> Guard<'a, T> {
209 fn new(slot: &'a StdAtomicPtr<()>, ptr: *mut T) -> Self {
210 Self { slot, ptr }
211 }
212
213 /// Returns a reference to the protected value.
214 pub fn get(&self) -> &T {
215 unsafe { &*self.ptr }
216 }
217
218 /// Returns the underlying raw pointer.
219 pub fn as_raw(&self) -> *mut T {
220 self.ptr
221 }
222
223 fn set_null(&self) {
224 self.slot.store(std::ptr::null_mut(), Ordering::SeqCst);
225 }
226
227 /// Releases the hazard slot without running the destructor twice.
228 ///
229 /// Equivalent to dropping the guard, but can be called explicitly when
230 /// you want to release protection at a specific point.
231 pub fn clear(self) {
232 self.set_null();
233 std::mem::forget(self);
234 }
235}
236
237impl<T> Deref for Guard<'_, T> {
238 type Target = T;
239
240 fn deref(&self) -> &T {
241 self.get()
242 }
243}
244
245impl<T> Drop for Guard<'_, T> {
246 fn drop(&mut self) {
247 self.set_null();
248 }
249}
250
251/// A hazard pointer domain that manages hazard slots and deferred reclamation.
252///
253/// All pointers protected and retired through the same domain share a single
254/// hazard list. Retired pointers are stored in a lock-free Treiber stack and
255/// reclaimed when no hazard slot references them.
256///
257/// Typically one global domain is sufficient:
258///
259/// ```
260/// use lockout_hazard::Domain;
261/// static DOMAIN: Domain = Domain::new();
262/// ```
263#[derive(Debug)]
264pub struct Domain<const COLLECTION_THRESHOLD: u8 = DEFAULT_COLLECTION_THRESHOLD> {
265 hazard_list: HazardNode,
266 hazard_count: AtomicUsize,
267 retired_head: StdAtomicPtr<RetiredNode>,
268 retire_count: AtomicU8,
269}
270
271// Safety: All fields use atomic operations for concurrent access.
272unsafe impl Send for Domain {}
273unsafe impl Sync for Domain {}
274
275impl Default for Domain<DEFAULT_COLLECTION_THRESHOLD> {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281impl<const COLLECTION_THRESHOLD: u8> Drop for Domain<COLLECTION_THRESHOLD> {
282 fn drop(&mut self) {
283 // Free all retired nodes unconditionally — no guards can exist
284 // since they borrow the domain.
285 let mut retired = self.retired_head.load(Ordering::Relaxed);
286 while !retired.is_null() {
287 let node = unsafe { Box::from_raw(retired) };
288 unsafe { (node.deleter)(node.ptr) };
289 retired = node.next;
290 }
291
292 // Free all hazard list nodes.
293 let mut next = self.hazard_list.next.load(Ordering::Relaxed);
294 while !next.is_null() {
295 let node = unsafe { Box::from_raw(next) };
296 next = node.next.load(Ordering::Relaxed);
297 }
298 }
299}
300
301impl Domain {
302 /// Creates a new hazard pointer domain.
303 ///
304 /// This is a `const fn`, so it can be used in `static` declarations.
305 #[cfg(not(loom))]
306 pub const fn new() -> Self {
307 Self::with_threshold()
308 }
309
310 #[cfg(loom)]
311 pub fn new() -> Self {
312 Self::with_threshold()
313 }
314}
315
316impl<const COLLECTION_THRESHOLD: u8> Domain<COLLECTION_THRESHOLD> {
317 /// Creates a new hazard pointer domain with a threshold for automatic pointer reclamation specified as a generic parameter.
318 ///
319 /// This is a `const fn`, so it can be used in `static` declarations.
320 #[cfg(not(loom))]
321 pub const fn with_threshold() -> Self {
322 Self {
323 hazard_list: HazardNode::new(std::ptr::null_mut()),
324 hazard_count: AtomicUsize::new(1),
325 retired_head: StdAtomicPtr::new(std::ptr::null_mut()),
326 retire_count: AtomicU8::new(0),
327 }
328 }
329
330 #[cfg(loom)]
331 pub fn with_threshold() -> Self {
332 Self {
333 hazard_list: HazardNode::new(std::ptr::null_mut()),
334 hazard_count: AtomicUsize::new(1),
335 retired_head: StdAtomicPtr::new(std::ptr::null_mut()),
336 retire_count: AtomicU8::new(0),
337 }
338 }
339
340 /// Protects the pointer stored in a [`AtomicPtr`] by publishing it in a hazard slot.
341 ///
342 /// Uses a load-reserve-verify loop to ensure the returned guard protects
343 /// the value that was in `ptr` at the time of the call. Returns `None` if
344 /// the pointer is null.
345 pub fn protect<T>(&self, ptr: &AtomicPtr<T>) -> Option<Guard<'_, T>> {
346 self.protect_atomic(&ptr.inner)
347 }
348
349 /// Protects the pointer stored in a std [`AtomicPtr`](std::sync::atomic::AtomicPtr) by publishing it in a hazard slot.
350 ///
351 /// Uses a load-reserve-verify loop to ensure the returned guard protects
352 /// the value that was in `ptr` at the time of the call. Returns `None` if
353 /// the pointer is null.
354 fn protect_atomic<T>(&self, ptr: &StdAtomicPtr<T>) -> Option<Guard<'_, T>> {
355 loop {
356 let ptr_before = ptr.load(Ordering::SeqCst);
357 if ptr_before.is_null() {
358 return None;
359 }
360
361 let guard = self.reserve(ptr_before);
362
363 let ptr_after = ptr.load(Ordering::SeqCst);
364 if ptr_after == ptr_before {
365 return Some(guard);
366 }
367
368 #[cfg(loom)]
369 loom::thread::yield_now();
370 }
371 }
372
373 /// Protects an arbitrary raw pointer by publishing it in a hazard slot.
374 ///
375 /// Unlike [`protect`](Domain::protect), this does not verify the pointer
376 /// against an `AtomicPtr` source. The caller must ensure the pointer is
377 /// valid. Returns `None` if the pointer is null.
378 ///
379 /// # Safety
380 ///
381 /// The pointer must point to a valid, live allocation.
382 pub unsafe fn protect_ptr<T>(&self, ptr: *mut T) -> Option<Guard<'_, T>> {
383 if ptr.is_null() {
384 return None;
385 }
386 Some(self.reserve(ptr))
387 }
388
389 /// Reserves a hazard slot for `ptr` by walking the lock-free linked list.
390 ///
391 /// Tries to claim an existing free slot via CAS. If all slots are occupied,
392 /// allocates a new node and appends it to the list.
393 fn reserve<T>(&self, ptr: *mut T) -> Guard<'_, T> {
394 let mut current = &self.hazard_list;
395
396 loop {
397 if current
398 .hazard
399 .compare_exchange_weak(
400 std::ptr::null_mut(),
401 ptr as *mut (),
402 Ordering::SeqCst,
403 Ordering::Relaxed,
404 )
405 .is_ok()
406 {
407 return Guard::new(¤t.hazard, ptr);
408 }
409
410 let next = current.next.load(Ordering::Acquire);
411 if !next.is_null() {
412 current = unsafe { next.as_ref().unwrap_unchecked() };
413 #[cfg(loom)]
414 loom::thread::yield_now();
415 continue;
416 }
417
418 let new_node = Box::into_raw(Box::new(HazardNode::new(ptr as *mut ())));
419 match current.next.compare_exchange(
420 std::ptr::null_mut(),
421 new_node,
422 Ordering::Release,
423 Ordering::Acquire,
424 ) {
425 Ok(_) => {
426 self.hazard_count.fetch_add(1, Ordering::SeqCst);
427 return Guard::new(
428 unsafe { &new_node.as_ref().unwrap_unchecked().hazard },
429 ptr,
430 );
431 }
432 Err(_) => {
433 drop(unsafe { Box::from_raw(new_node) });
434 current = unsafe {
435 current
436 .next
437 .load(Ordering::Acquire)
438 .as_ref()
439 .unwrap_unchecked()
440 };
441 #[cfg(loom)]
442 loom::thread::yield_now();
443 }
444 }
445 }
446 }
447
448 /// Pushes a retired node onto the lock-free Treiber stack.
449 fn push_retired(&self, node: *mut RetiredNode) {
450 loop {
451 let head = self.retired_head.load(Ordering::Relaxed);
452 unsafe { (*node).next = head };
453 if self
454 .retired_head
455 .compare_exchange_weak(head, node, Ordering::Release, Ordering::Relaxed)
456 .is_ok()
457 {
458 return;
459 }
460 #[cfg(loom)]
461 loom::thread::yield_now();
462 }
463 }
464
465 /// Retires a raw pointer, scheduling it for deferred reclamation.
466 ///
467 /// The pointer will be deallocated (via `Box::from_raw`) once no hazard slot
468 /// references it.
469 ///
470 /// # Safety
471 ///
472 /// - The pointer must have been allocated with `Box`.
473 /// - The pointer must no longer be reachable from any shared atomic.
474 /// - The pointer must not be retired more than once.
475 pub unsafe fn retire_ptr<T>(&self, ptr: *mut T) {
476 unsafe fn deleter<T>(p: *mut ()) {
477 drop(unsafe { Box::from_raw(p as *mut T) });
478 }
479
480 let node = Box::into_raw(Box::new(RetiredNode {
481 ptr: ptr as *mut (),
482 deleter: deleter::<T>,
483 next: std::ptr::null_mut(),
484 }));
485 self.push_retired(node);
486
487 let count = self.retire_count.fetch_add(1, Ordering::Relaxed) + 1;
488 if count.is_multiple_of(COLLECTION_THRESHOLD) {
489 self.collect();
490 }
491 }
492
493 /// Scans all hazard slots and reclaims any retired pointers that are not
494 /// currently protected.
495 ///
496 /// This is called automatically every [`COLLECTION_THRESHOLD`] retires, but
497 /// can also be called manually to force reclamation. Resets the retire counter.
498 pub fn collect(&self) {
499 self.retire_count.store(0, Ordering::Relaxed);
500
501 // Claim retired nodes first so this collection only processes pointers
502 // that were already retired at this point.
503 let mut retired = self
504 .retired_head
505 .swap(std::ptr::null_mut(), Ordering::Acquire);
506 if retired.is_null() {
507 return;
508 }
509
510 // Snapshot all active hazard pointers from a stable hazard-list view.
511 let hazard_ptrs = loop {
512 let expected_nodes = self.hazard_count.load(Ordering::SeqCst);
513 let mut seen_nodes = 0usize;
514 let mut hazard_ptrs = Vec::new();
515 let mut current = &self.hazard_list;
516
517 loop {
518 seen_nodes += 1;
519 let ptr = current.hazard.load(Ordering::SeqCst);
520 if !ptr.is_null() {
521 hazard_ptrs.push(ptr);
522 }
523
524 let next = current.next.load(Ordering::SeqCst);
525 if next.is_null() {
526 break;
527 }
528 current = unsafe { &*next };
529 }
530
531 let final_nodes = self.hazard_count.load(Ordering::SeqCst);
532 if seen_nodes == expected_nodes && final_nodes == expected_nodes {
533 break hazard_ptrs;
534 }
535 };
536
537 // Walk the claimed list: free unprotected entries, push back protected ones.
538 while !retired.is_null() {
539 let node = unsafe { Box::from_raw(retired) };
540 retired = node.next;
541
542 if hazard_ptrs.contains(&node.ptr) {
543 let raw = Box::into_raw(node);
544 self.push_retired(raw);
545 } else {
546 unsafe { (node.deleter)(node.ptr) };
547 }
548 }
549 }
550}