Skip to main content

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(&current.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}