Am 9/19/2024 um 2:12 AM schrieb Jann Horn:
On Tue, Sep 17, 2024 at 4:33 PM Boqun Feng <boqun.f...@gmail.com> wrote:
Hazard pointers [1] provide a way to dynamically distribute refcounting
and can be used to improve the scalability of refcounting without
significant space cost.

+static inline void *__hazptr_tryprotect(hazptr_t *hzp,
+                                       void *const *p,
+                                       unsigned long head_offset)
+{
+       void *ptr;
+       struct callback_head *head;
+
+       ptr = READ_ONCE(*p);
+
+       if (ptr == NULL)
+               return NULL;
+
+       head = (struct callback_head *)(ptr + head_offset);
+
+       WRITE_ONCE(*hzp, head);
+       smp_mb();
+
+       ptr = READ_ONCE(*p); // read again
+
+       if (ptr + head_offset != head) { // pointer changed
+               WRITE_ONCE(*hzp, NULL);  // reset hazard pointer
+               return NULL;
+       } else
+               return ptr;
+}

I got nerdsniped by the Plumbers talk. So, about that smp_mb()...

I think you should be able to avoid the smp_mb() using relaxed atomics
(on architectures that have those), at the cost of something like a
cmpxchg-acquire sandwiched between a load-acquire and a relaxed load?
I'm not sure how their cost compares to an smp_mb() though.



We have done a similar scheme before, and on some architectures (not x86) the RMW is slightly cheaper than the mb.

Your reasoning is a bit simplified because it seems to assume a stronger concept of ordering than LKMM has, but even with LKMM's ordering your code seems fine.

I feel it can even be simplified a little, the hazard bit does not seem necessary.

Assuming atomic operations for everything racy, relaxed unless stated otherwise:

(R)eader:

old = read p // I don't think this needs acq, because of address dependencies (*)
   haz ||=_acq old
   if (read p != old) retry;
   *old

(W)riter:

   p =_??? ... // assuming we don't set it to null this needs a rel
   --- mb ---
   haz ||= 0
   while (read_acq haz == old) retry;
   delete old


In order to get a use-after-free, both of the R:read p need to read before W:p = ... , so because of the W:mb, they execute before W:haz||=0 .

Also, for the use-after-free, W:read_acq haz needs to read before (the write part of) R:haz||=_acq old .


Then the W:haz ||= 0 also needs to read before (the write part of) R:haz||=_acq old because of coherence on the same location.

Since both of them are atomic RMW, the W:haz||= 0 also needs to write before (the write part of) R:haz||=_acq old , and in the absence of non-RMW writes (so assuming no thread will just store to the hazard pointer), this implies that the latter reads-from an rmw-sequence-later store than W:haz||=0 , which therefore executes before R:haz||=_acq old .

But because of the acquire barrier, R:haz||=_acq old executes before the second R:read p .


This gives a cycle
   (2nd)R:read p  ->xb  W:haz||=0  ->xb  R:haz||=_acq  ->xb  (2nd)R:read p

and therefore is forbidden.

Note that in this argument, the two R:read p are not necessarily reading from the same store. Because of ABA, it can happen that they read from distinct stores and see the same value.

It does require clearing hazard pointers with an RMW like atomic_and(0) (**). The combination of the two RMW (for setting & clearing the hazard pointer) might in total be slower again than an mb.


(I took the liberty to add an acquire barrier in the writer's while loop for the case where we read from the (not shown) release store of the reader that clears the hazard pointer. It's arguable whether that acq is needed since one could argue that a delete is a kind of store, in which case control dependency would handle it.)

have fun,
  jonas


(*
you talk about sandwiching, and the data dependency does guarantee some weaker form of sandwiching than the acq, but I don't think sandwiching is required at all. If you happened to be able to set the hazard pointer before reading the pointer, that would just extend the protected area, wouldn't it?
)

(**
I think if you clear the pointer with a store, the hazard bit does not help. You could be overwriting the hazard bit, and the RMWs that set the hazard bit might never propagate to your CPU.

Also in your code you probably meant to clear the whole hazard pointer in the retry code of the reader, not just the hazard bit.)


Reply via email to