atomic_try_update/stack.rs
1//! # Lightweight lock-free stack implementations
2//! It is surprisingly difficult to correctly use and implement a lock-free
3//! stack that provides the standard push/pop interface.
4//!
5//! In particular, pop has to traverse a pointer to obtain the next node in
6//! the stack. It is possible that the target of the pointer changes in
7//! race (due to multiple concurrent stack pops), or even that the old head
8//! was popped and deallocated and then a new entry with the same address
9//! was allocated and pushed back on as the head of the stack. At this point
10//! a compare and swap of the old head with the new one would incorrectly
11//! succeed, leading to an instance of the "ABA problem."
12//!
13//! Naive stack algorithms suffer from the following ABA issue: It is possible
14//! for pop() to read the head pointer "A", then be descheduled. In race, the
15//! head pointer and some other values are popped, then a node with the same
16//! memory address as A is pushed back onto the stack. The CAS will succeed,
17//! even though the current value of head is semantically distinct from the
18//! value the lambda read.
19//!
20//! `Stack` avoids this by providing a `pop_all` method that removes everything
21//! from the stack atomically. This guarantees read set equivalence because it
22//! does not read anything other than the CAS bits.
23//!
24//! `NonceStack` uses a nonce to ensure that no pushes have been performed
25//! in race with pop, which probabilistically guarantees that head was not popped
26//! then pushed back on to the stack in race with a pop.
27
28//!
29use super::{atomic_try_update, Atom, Node, NodeIterator};
30use std::ptr::null_mut;
31
32struct Head<T> {
33 head: *mut Node<T>,
34}
35
36pub struct Stack<T>
37where
38 T: Send,
39{
40 head: Atom<Head<T>, u64>,
41}
42
43impl<T> Default for Stack<T>
44where
45 T: Send,
46{
47 fn default() -> Self {
48 Self {
49 head: Default::default(),
50 }
51 }
52}
53
54impl<T> Stack<T>
55where
56 T: Send,
57{
58 pub fn push(&self, val: T) {
59 let node = Box::into_raw(Box::new(Node {
60 val,
61 next: std::ptr::null_mut(),
62 }));
63
64 unsafe {
65 atomic_try_update(&self.head, |head: &mut Head<T>| {
66 (*node).next = head.head;
67 head.head = node;
68 (true, ())
69 });
70 }
71 }
72 pub fn pop_all(&self) -> NodeIterator<T> {
73 NodeIterator {
74 node: unsafe {
75 atomic_try_update(&self.head, |head: &mut Head<T>| {
76 let ret = head.head;
77 head.head = null_mut();
78 (true, ret)
79 })
80 },
81 }
82 }
83}
84
85impl<T> Drop for Stack<T>
86where
87 T: Send,
88{
89 fn drop(&mut self) {
90 self.pop_all();
91 }
92}
93
94struct NonceHead<T> {
95 head: *mut Node<T>,
96 nonce: u64,
97}
98
99impl<T> Default for NonceStack<T>
100where
101 T: Send,
102{
103 #[allow(unreachable_code)]
104 fn default() -> NonceStack<T> {
105 todo!("This example code contains a use after free.");
106 NonceStack::<T> {
107 head: Default::default(),
108 }
109 }
110}
111
112pub struct NonceStack<T>
113where
114 T: Send,
115{
116 head: Atom<NonceHead<T>, u128>,
117}
118
119impl<T> NonceStack<T>
120where
121 T: Send,
122{
123 #[allow(unused)]
124 pub fn push(&self, val: T) {
125 let node = Box::into_raw(Box::new(Node {
126 val,
127 next: std::ptr::null_mut(),
128 }));
129
130 unsafe {
131 atomic_try_update(&self.head, |head| {
132 (*node).next = head.head;
133 head.nonce += 1;
134 head.head = node;
135 (true, ())
136 })
137 }
138 }
139
140 /// Almost-correct example of using a nonce to implement pop().
141 ///
142 /// This method contains a use-after-free on the line `head.head=(*ret).next`.
143 ///
144 /// This would be safe in a garbage collected system, or in an embedded
145 /// system without memory protection, since head.head will be discarded
146 /// if the stack has been changed, and *ret can only be freed after it is
147 /// popped from the stack. Since *ret may have been freed, it may have
148 /// been returned to the operating system, leading to a segmentation fault
149 /// when accessed here.
150 ///
151 /// If you need a stack similar to NonceStack, consider using an epoch
152 /// collector such as crossbeam_epoch, or by maintaining a pool of
153 /// reusable objects. For instance, you could use a second stack to
154 /// store the pool, and return objects to it after they are popped from
155 /// this stack. If the pool stack is empty, then allocate a new object.
156 ///
157 /// Once you are sure that no thread will access either stack, you can
158 /// safely empty both stacks and free the objects they contain.
159 ///
160 /// Stacks with nonces are also sometimes used to implement slot allocators.
161 /// A slot allocator is initialized at startup with a finite number of
162 /// tokens (such as file handles, or some other finite resource). When
163 /// a thread needs a resource, it pops from the stack. If the stack is
164 /// empty, then the thread goes async. Atomically checking that the stack
165 /// is empty and registering oneself for future wakeup is left as an exercise
166 /// to the reader, as it is exactly the sort of thing atomic_try_update excels
167 /// at.
168 ///
169 /// TODO: Implement a double-stack structure and/or slot such as the ones above,
170 /// so we have correct examples of the NonceStack pattern.
171
172 #[allow(unused)]
173 pub fn pop(&self) -> Option<T> {
174 let node = unsafe {
175 atomic_try_update(&self.head, |head: &mut NonceHead<T>| unsafe {
176 head.nonce += 1;
177 let ret = head.head;
178 if ret.is_null() {
179 (false, ret)
180 } else {
181 head.head = (*ret).next;
182 (true, ret)
183 }
184 })
185 };
186
187 if !node.is_null() {
188 Some(unsafe { *Box::from_raw(node) }.val)
189 } else {
190 None
191 }
192 }
193}
194
195impl<T> Drop for NonceStack<T>
196where
197 T: Send,
198{
199 fn drop(&mut self) {
200 while self.pop().is_some() {}
201 }
202}