File Coverage

deque.h
Criterion Covered Total %
statement 344 389 88.4
branch 152 288 52.7
condition n/a
subroutine n/a
pod n/a
total 496 677 73.2


line stmt bran cond sub pod time code
1             /*
2             * deque.h -- Fixed-size shared-memory double-ended queue for Linux
3             *
4             * Ring buffer with CAS-based push/pop at both ends, plus a per-slot
5             * publication state machine for MPMC safety:
6             *
7             * EMPTY -> WRITING (pusher claims slot)
8             * WRITING-> FILLED (pusher publishes value)
9             * FILLED -> READING (popper claims slot)
10             * READING-> EMPTY (popper releases, generation bumps)
11             *
12             * The slot's ctl word encodes (generation << 2) | state_bits, and
13             * transitions are made via CAS. Head/tail still serialize the POSITION
14             * handout; the per-slot state machine serializes the VALUE handoff.
15             * A consumer that wins the head/tail CAS always waits for the matching
16             * publisher's transition to FILLED before reading.
17             *
18             * Futex blocking when empty or full.
19             */
20              
21             #ifndef DEQUE_H
22             #define DEQUE_H
23              
24             #include
25             #include
26             #include
27             #include
28             #include
29             #include
30             #include
31             #include
32             #include
33             #include
34             #include
35             #include
36             #include
37             #include
38             #include
39              
40             #define DEQ_MAGIC 0x44455132U /* "DEQ2" — v2 layout (per-slot ctl) */
41             #define DEQ_VERSION 2
42             #define DEQ_ERR_BUFLEN 256
43              
44             /* Drain-time recovery: how long to wait for a slot stuck in WRITING before
45             * declaring its pusher dead and force-skipping. Matches the slot-stuck
46             * recovery timeout used in sister Data-*-Shared modules (e.g. Stack). */
47             #define DEQ_DRAIN_RECOVERY_SEC 2
48              
49             #define DEQ_VAR_INT 0
50             #define DEQ_VAR_STR 1
51              
52             /* Slot state (low 2 bits of ctl word). Upper 62 bits = generation. */
53             #define DEQ_SLOT_EMPTY 0u
54             #define DEQ_SLOT_WRITING 1u
55             #define DEQ_SLOT_FILLED 2u
56             #define DEQ_SLOT_READING 3u
57             #define DEQ_SLOT_STATE_MASK 3u
58             #define DEQ_SLOT_STATE(c) ((uint32_t)((c) & DEQ_SLOT_STATE_MASK))
59             #define DEQ_SLOT_GEN(c) ((c) >> 2)
60              
61             /* Combined cursor: upper 32 bits = head, lower 32 bits = tail. A single
62             * 64-bit CAS atomically updates both ends, so push_front vs push_back (or
63             * pop_front vs pop_back) cannot both succeed when they share a boundary
64             * slot. Capacity is bounded by 2^31 elements. Head and tail wrap mod 2^32
65             * after 4B ops; size = (tail - head) treated as uint32.
66             */
67             typedef struct {
68             uint32_t magic;
69             uint32_t version;
70             uint32_t elem_size;
71             uint32_t variant_id;
72             uint64_t capacity;
73             uint64_t total_size;
74             uint64_t data_off;
75             uint64_t ctl_off; /* offset to per-slot ctl array */
76             uint8_t _pad0[16];
77              
78             uint64_t cursor; /* 64: (head<<32)|tail */
79             uint32_t waiters_push; /* 72 */
80             uint32_t waiters_pop; /* 76 */
81             uint64_t stat_pushes; /* 80 */
82             uint64_t stat_pops; /* 88 */
83             uint64_t stat_waits; /* 96 */
84             uint64_t stat_timeouts; /* 104 */
85             uint32_t push_wake_seq; /* 112: bumped by every pop, futex word for pushers */
86             uint32_t pop_wake_seq; /* 116: bumped by every push, futex word for poppers */
87             uint64_t stat_recoveries; /* 120: drain-time recovery of stuck slots (WRITING or EMPTY) */
88             } DeqHeader;
89              
90             #define DEQ_CURSOR(head, tail) (((uint64_t)(head) << 32) | (uint32_t)(tail))
91             #define DEQ_CURSOR_HEAD(c) ((uint32_t)((c) >> 32))
92             #define DEQ_CURSOR_TAIL(c) ((uint32_t)(c))
93             #define DEQ_CURSOR_SIZE(c) ((uint32_t)(DEQ_CURSOR_TAIL(c) - DEQ_CURSOR_HEAD(c)))
94              
95             #if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
96             _Static_assert(sizeof(DeqHeader) == 128, "DeqHeader must be 128 bytes");
97             #endif
98              
99             typedef struct {
100             DeqHeader *hdr;
101             uint8_t *data;
102             uint64_t *ctl; /* per-slot state+generation word */
103             size_t mmap_size;
104             uint32_t elem_size;
105             char *path;
106             int notify_fd;
107             int backing_fd;
108             } DeqHandle;
109              
110             /* ================================================================ */
111              
112 7           static inline void deq_make_deadline(double t, struct timespec *dl) {
113 7           clock_gettime(CLOCK_MONOTONIC, dl);
114 7           dl->tv_sec += (time_t)t;
115 7           dl->tv_nsec += (long)((t - (double)(time_t)t) * 1e9);
116 7 100         if (dl->tv_nsec >= 1000000000L) { dl->tv_sec++; dl->tv_nsec -= 1000000000L; }
117 7           }
118              
119 19087           static inline int deq_remaining(const struct timespec *dl, struct timespec *rem) {
120             struct timespec now;
121 19087           clock_gettime(CLOCK_MONOTONIC, &now);
122 19087           rem->tv_sec = dl->tv_sec - now.tv_sec;
123 19087           rem->tv_nsec = dl->tv_nsec - now.tv_nsec;
124 19087 100         if (rem->tv_nsec < 0) { rem->tv_sec--; rem->tv_nsec += 1000000000L; }
125 19087           return rem->tv_sec >= 0;
126             }
127              
128 18193           static inline uint8_t *deq_slot(DeqHandle *h, uint32_t idx) {
129 18193           return h->data + (size_t)(idx % (uint32_t)h->hdr->capacity) * h->elem_size;
130             }
131              
132 25           static inline uint32_t deq_size(DeqHandle *h) {
133 25           uint64_t c = __atomic_load_n(&h->hdr->cursor, __ATOMIC_ACQUIRE);
134 25           return DEQ_CURSOR_SIZE(c);
135             }
136              
137 1220992           static inline void deq_spin_pause(void) {
138             #if defined(__x86_64__) || defined(__i386__)
139 1220992           __asm__ volatile("pause" ::: "memory");
140             #elif defined(__aarch64__)
141             __asm__ volatile("yield" ::: "memory");
142             #endif
143 1220992           }
144              
145             /* --- per-slot state machine helpers ---
146             * Claim a slot for writing: spin CAS until we observe EMPTY and can mark
147             * WRITING. Returns the generation that was observed, for the matching
148             * publish. Caller holds the position CAS so at most one pusher targets
149             * this slot, but a pending popper from the previous cycle may still be
150             * finishing; the spin is bounded by that popper's READING -> EMPTY store.
151             */
152 127           static inline uint64_t deq_slot_claim_write(uint64_t *ctl_word) {
153 0           for (;;) {
154 127           uint64_t c = __atomic_load_n(ctl_word, __ATOMIC_ACQUIRE);
155 127 50         if (DEQ_SLOT_STATE(c) == DEQ_SLOT_EMPTY) {
156 127           uint64_t nc = (DEQ_SLOT_GEN(c) << 2) | DEQ_SLOT_WRITING;
157 127 50         if (__atomic_compare_exchange_n(ctl_word, &c, nc,
158             0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
159 127           return DEQ_SLOT_GEN(c);
160             }
161 0           deq_spin_pause();
162             }
163             }
164              
165             /* Publish written value: WRITING@gen -> FILLED@gen. Implemented as CAS
166             * (not a plain store) so that if deq_drain force-recovered the slot mid-
167             * write — bumping it to EMPTY@(gen+1) — this publish is a no-op rather
168             * than clobbering the recovered state back to FILLED@gen. That would
169             * leave a phantom FILLED at a stale gen which the next pusher's
170             * deq_slot_claim_write (waits on EMPTY) could never advance past,
171             * deadlocking that slot forever. The caller's cursor CAS was already
172             * committed, so on lost-race the value is silently dropped — matching
173             * the documented drain-recovery semantics. */
174 127           static inline void deq_slot_publish(uint64_t *ctl_word, uint64_t gen) {
175 127           uint64_t expected = (gen << 2) | DEQ_SLOT_WRITING;
176 127           uint64_t desired = (gen << 2) | DEQ_SLOT_FILLED;
177 127           (void)__atomic_compare_exchange_n(ctl_word, &expected, desired,
178             0, __ATOMIC_RELEASE, __ATOMIC_RELAXED);
179 127           }
180              
181             /* Claim a slot for reading: spin CAS until we observe FILLED and mark READING. */
182 18066           static inline uint64_t deq_slot_claim_read(uint64_t *ctl_word) {
183 0           for (;;) {
184 18066           uint64_t c = __atomic_load_n(ctl_word, __ATOMIC_ACQUIRE);
185 18066 50         if (DEQ_SLOT_STATE(c) == DEQ_SLOT_FILLED) {
186 18066           uint64_t nc = (DEQ_SLOT_GEN(c) << 2) | DEQ_SLOT_READING;
187 18066 50         if (__atomic_compare_exchange_n(ctl_word, &c, nc,
188             0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
189 18066           return DEQ_SLOT_GEN(c);
190             }
191 0           deq_spin_pause();
192             }
193             }
194              
195             /* Release slot after read: READING@gen -> EMPTY@gen+1, via CAS not a blind
196             * store (a drain force-recovery may have reused the slot; mirror publish-CAS). */
197 18077           static inline void deq_slot_release(uint64_t *ctl_word, uint64_t gen) {
198 18077           uint64_t expected = (gen << 2) | DEQ_SLOT_READING;
199 18077           uint64_t desired = ((gen + 1) << 2) | DEQ_SLOT_EMPTY;
200 18077           __atomic_compare_exchange_n(ctl_word, &expected, desired,
201             0, __ATOMIC_RELEASE, __ATOMIC_RELAXED);
202 18077           }
203              
204             /* ================================================================
205             * Push back (tail++)
206             * ================================================================ */
207              
208 122           static inline int deq_try_push_back(DeqHandle *h, const void *val, uint32_t vlen) {
209 122           DeqHeader *hdr = h->hdr;
210 122           uint32_t cap = (uint32_t)hdr->capacity;
211 0           for (;;) {
212 122           uint64_t c = __atomic_load_n(&hdr->cursor, __ATOMIC_ACQUIRE);
213 122           uint32_t hd = DEQ_CURSOR_HEAD(c), t = DEQ_CURSOR_TAIL(c);
214 239 100         if ((uint32_t)(t - hd) >= cap) return 0;
215 117           uint64_t nc = DEQ_CURSOR(hd, t + 1);
216 117 50         if (__atomic_compare_exchange_n(&hdr->cursor, &c, nc,
217             1, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) {
218 117           uint32_t sz = h->elem_size;
219 117           uint32_t cp = vlen < sz ? vlen : sz;
220 117           uint32_t idx = t % cap;
221 117           uint64_t gen = deq_slot_claim_write(&h->ctl[idx]);
222 117           memcpy(deq_slot(h, t), val, cp);
223 117 50         if (cp < sz) memset(deq_slot(h, t) + cp, 0, sz - cp);
224 117           deq_slot_publish(&h->ctl[idx], gen);
225 117           __atomic_add_fetch(&hdr->stat_pushes, 1, __ATOMIC_RELAXED);
226             /* StoreLoad: publish our slot/cursor change before reading waiters. */
227 117           __atomic_thread_fence(__ATOMIC_SEQ_CST);
228 117 50         if (__atomic_load_n(&hdr->waiters_pop, __ATOMIC_RELAXED) > 0) {
229 0           __atomic_add_fetch(&hdr->pop_wake_seq, 1, __ATOMIC_RELEASE);
230 0           syscall(SYS_futex, &hdr->pop_wake_seq, FUTEX_WAKE, 1, NULL, NULL, 0);
231             }
232 117           return 1;
233             }
234             }
235             }
236              
237             /* ================================================================
238             * Push front (head--)
239             * ================================================================ */
240              
241 15           static inline int deq_try_push_front(DeqHandle *h, const void *val, uint32_t vlen) {
242 15           DeqHeader *hdr = h->hdr;
243 15           uint32_t cap = (uint32_t)hdr->capacity;
244 0           for (;;) {
245 15           uint64_t c = __atomic_load_n(&hdr->cursor, __ATOMIC_ACQUIRE);
246 15           uint32_t hd = DEQ_CURSOR_HEAD(c), t = DEQ_CURSOR_TAIL(c);
247 25 100         if ((uint32_t)(t - hd) >= cap) return 0;
248 10           uint32_t new_hd = hd - 1;
249 10           uint64_t nc = DEQ_CURSOR(new_hd, t);
250 10 50         if (__atomic_compare_exchange_n(&hdr->cursor, &c, nc,
251             1, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) {
252 10           uint32_t sz = h->elem_size;
253 10           uint32_t cp = vlen < sz ? vlen : sz;
254 10           uint32_t idx = new_hd % cap;
255 10           uint64_t gen = deq_slot_claim_write(&h->ctl[idx]);
256 10           memcpy(deq_slot(h, new_hd), val, cp);
257 10 50         if (cp < sz) memset(deq_slot(h, new_hd) + cp, 0, sz - cp);
258 10           deq_slot_publish(&h->ctl[idx], gen);
259 10           __atomic_add_fetch(&hdr->stat_pushes, 1, __ATOMIC_RELAXED);
260             /* StoreLoad: publish our slot/cursor change before reading waiters. */
261 10           __atomic_thread_fence(__ATOMIC_SEQ_CST);
262 10 50         if (__atomic_load_n(&hdr->waiters_pop, __ATOMIC_RELAXED) > 0) {
263 0           __atomic_add_fetch(&hdr->pop_wake_seq, 1, __ATOMIC_RELEASE);
264 0           syscall(SYS_futex, &hdr->pop_wake_seq, FUTEX_WAKE, 1, NULL, NULL, 0);
265             }
266 10           return 1;
267             }
268             }
269             }
270              
271             /* ================================================================
272             * Pop front (head++)
273             * ================================================================ */
274              
275 18062           static inline int deq_try_pop_front(DeqHandle *h, void *out) {
276 18062           DeqHeader *hdr = h->hdr;
277 18062           uint32_t cap = (uint32_t)hdr->capacity;
278 0           for (;;) {
279 18062           uint64_t c = __atomic_load_n(&hdr->cursor, __ATOMIC_ACQUIRE);
280 18062           uint32_t hd = DEQ_CURSOR_HEAD(c), t = DEQ_CURSOR_TAIL(c);
281 36118 100         if ((uint32_t)(t - hd) == 0) return 0;
282 18056           uint64_t nc = DEQ_CURSOR(hd + 1, t);
283 18056 50         if (__atomic_compare_exchange_n(&hdr->cursor, &c, nc,
284             1, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) {
285 18056           uint32_t idx = hd % cap;
286 18056           uint64_t gen = deq_slot_claim_read(&h->ctl[idx]);
287 18056           memcpy(out, deq_slot(h, hd), h->elem_size);
288 18056           deq_slot_release(&h->ctl[idx], gen);
289 18056           __atomic_add_fetch(&hdr->stat_pops, 1, __ATOMIC_RELAXED);
290             /* StoreLoad: publish our slot/cursor change before reading waiters. */
291 18056           __atomic_thread_fence(__ATOMIC_SEQ_CST);
292 18056 50         if (__atomic_load_n(&hdr->waiters_push, __ATOMIC_RELAXED) > 0) {
293 0           __atomic_add_fetch(&hdr->push_wake_seq, 1, __ATOMIC_RELEASE);
294 0           syscall(SYS_futex, &hdr->push_wake_seq, FUTEX_WAKE, 1, NULL, NULL, 0);
295             }
296 18056           return 1;
297             }
298             }
299             }
300              
301             /* ================================================================
302             * Pop back (tail--)
303             * ================================================================ */
304              
305 14           static inline int deq_try_pop_back(DeqHandle *h, void *out) {
306 14           DeqHeader *hdr = h->hdr;
307 14           uint32_t cap = (uint32_t)hdr->capacity;
308 0           for (;;) {
309 14           uint64_t c = __atomic_load_n(&hdr->cursor, __ATOMIC_ACQUIRE);
310 14           uint32_t hd = DEQ_CURSOR_HEAD(c), t = DEQ_CURSOR_TAIL(c);
311 24 100         if ((uint32_t)(t - hd) == 0) return 0;
312 10           uint64_t nc = DEQ_CURSOR(hd, t - 1);
313 10 50         if (__atomic_compare_exchange_n(&hdr->cursor, &c, nc,
314             1, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) {
315 10           uint32_t idx = (t - 1) % cap;
316 10           uint64_t gen = deq_slot_claim_read(&h->ctl[idx]);
317 10           memcpy(out, deq_slot(h, t - 1), h->elem_size);
318 10           deq_slot_release(&h->ctl[idx], gen);
319 10           __atomic_add_fetch(&hdr->stat_pops, 1, __ATOMIC_RELAXED);
320             /* StoreLoad: publish our slot/cursor change before reading waiters. */
321 10           __atomic_thread_fence(__ATOMIC_SEQ_CST);
322 10 50         if (__atomic_load_n(&hdr->waiters_push, __ATOMIC_RELAXED) > 0) {
323 0           __atomic_add_fetch(&hdr->push_wake_seq, 1, __ATOMIC_RELEASE);
324 0           syscall(SYS_futex, &hdr->push_wake_seq, FUTEX_WAKE, 1, NULL, NULL, 0);
325             }
326 10           return 1;
327             }
328             }
329             }
330              
331             /* ================================================================
332             * Blocking push/pop
333             * ================================================================ */
334              
335 2           static inline int deq_push_wait(DeqHandle *h, const void *val, uint32_t vlen,
336             int front, double timeout) {
337 2           int (*try_fn)(DeqHandle*, const void*, uint32_t) =
338 2 100         front ? deq_try_push_front : deq_try_push_back;
339 2 50         if (try_fn(h, val, vlen)) return 1;
340 2 50         if (timeout == 0) return 0;
341              
342 2           DeqHeader *hdr = h->hdr;
343             struct timespec dl, rem;
344 2           int has_dl = (timeout > 0);
345 2 50         if (has_dl) deq_make_deadline(timeout, &dl);
346 2           __atomic_add_fetch(&hdr->stat_waits, 1, __ATOMIC_RELAXED);
347              
348 2           uint32_t cap = (uint32_t)hdr->capacity;
349 0           for (;;) {
350 2           uint32_t wseq = __atomic_load_n(&hdr->push_wake_seq, __ATOMIC_ACQUIRE);
351 2           __atomic_add_fetch(&hdr->waiters_push, 1, __ATOMIC_RELEASE);
352             /* StoreLoad: publish waiters++ before re-reading the cursor, so a
353             * concurrent waker can't read waiters==0 while we read a stale cursor. */
354 2           __atomic_thread_fence(__ATOMIC_SEQ_CST);
355 2           uint64_t c = __atomic_load_n(&hdr->cursor, __ATOMIC_ACQUIRE);
356 2 50         if (DEQ_CURSOR_SIZE(c) >= cap) {
357 2           struct timespec *pts = NULL;
358 2 50         if (has_dl) {
359 2 50         if (!deq_remaining(&dl, &rem)) {
360 0           __atomic_sub_fetch(&hdr->waiters_push, 1, __ATOMIC_RELAXED);
361 0           __atomic_add_fetch(&hdr->stat_timeouts, 1, __ATOMIC_RELAXED);
362 0           return 0;
363             }
364 2           pts = &rem;
365             }
366 2           syscall(SYS_futex, &hdr->push_wake_seq, FUTEX_WAIT, wseq, pts, NULL, 0);
367             }
368 2           __atomic_sub_fetch(&hdr->waiters_push, 1, __ATOMIC_RELAXED);
369 2 50         if (try_fn(h, val, vlen)) return 1;
370 2 50         if (has_dl && !deq_remaining(&dl, &rem)) {
    50          
371 2           __atomic_add_fetch(&hdr->stat_timeouts, 1, __ATOMIC_RELAXED);
372 2           return 0;
373             }
374             }
375             }
376              
377 3           static inline int deq_pop_wait(DeqHandle *h, void *out, int back, double timeout) {
378 3           int (*try_fn)(DeqHandle*, void*) =
379 3 100         back ? deq_try_pop_back : deq_try_pop_front;
380 3 50         if (try_fn(h, out)) return 1;
381 3 50         if (timeout == 0) return 0;
382              
383 3           DeqHeader *hdr = h->hdr;
384             struct timespec dl, rem;
385 3           int has_dl = (timeout > 0);
386 3 50         if (has_dl) deq_make_deadline(timeout, &dl);
387 3           __atomic_add_fetch(&hdr->stat_waits, 1, __ATOMIC_RELAXED);
388              
389 0           for (;;) {
390 3           uint32_t wseq = __atomic_load_n(&hdr->pop_wake_seq, __ATOMIC_ACQUIRE);
391 3           __atomic_add_fetch(&hdr->waiters_pop, 1, __ATOMIC_RELEASE);
392             /* StoreLoad: publish waiters++ before re-reading the cursor, so a
393             * concurrent waker can't read waiters==0 while we read a stale cursor. */
394 3           __atomic_thread_fence(__ATOMIC_SEQ_CST);
395 3           uint64_t c = __atomic_load_n(&hdr->cursor, __ATOMIC_ACQUIRE);
396 3 50         if (DEQ_CURSOR_SIZE(c) == 0) {
397 3           struct timespec *pts = NULL;
398 3 50         if (has_dl) {
399 3 50         if (!deq_remaining(&dl, &rem)) {
400 0           __atomic_sub_fetch(&hdr->waiters_pop, 1, __ATOMIC_RELAXED);
401 0           __atomic_add_fetch(&hdr->stat_timeouts, 1, __ATOMIC_RELAXED);
402 0           return 0;
403             }
404 3           pts = &rem;
405             }
406 3           syscall(SYS_futex, &hdr->pop_wake_seq, FUTEX_WAIT, wseq, pts, NULL, 0);
407             }
408 3           __atomic_sub_fetch(&hdr->waiters_pop, 1, __ATOMIC_RELAXED);
409 3 100         if (try_fn(h, out)) return 1;
410 2 50         if (has_dl && !deq_remaining(&dl, &rem)) {
    50          
411 2           __atomic_add_fetch(&hdr->stat_timeouts, 1, __ATOMIC_RELAXED);
412 2           return 0;
413             }
414             }
415             }
416              
417             /* ================================================================
418             * Create / Open / Close
419             * ================================================================ */
420              
421             #define DEQ_ERR(fmt, ...) do { if (errbuf) snprintf(errbuf, DEQ_ERR_BUFLEN, fmt, ##__VA_ARGS__); } while(0)
422              
423             /* Layout offsets — data array first, then 8-byte-aligned ctl array. */
424 62           static inline uint64_t deq_ctl_offset(uint32_t elem_size, uint64_t capacity) {
425 62           uint64_t data_end = sizeof(DeqHeader) + capacity * elem_size;
426 62           return (data_end + 7u) & ~(uint64_t)7u;
427             }
428              
429 25           static inline uint64_t deq_total_size(uint32_t elem_size, uint64_t capacity) {
430 25           return deq_ctl_offset(elem_size, capacity) + capacity * sizeof(uint64_t);
431             }
432              
433 17           static inline void deq_init_header(void *base, uint64_t total,
434             uint32_t elem_size, uint32_t variant_id,
435             uint64_t capacity) {
436 17           DeqHeader *hdr = (DeqHeader *)base;
437 17           memset(base, 0, (size_t)total); /* zeroes data + ctl → all slots EMPTY, gen=0 */
438 17           hdr->magic = DEQ_MAGIC;
439 17           hdr->version = DEQ_VERSION;
440 17           hdr->elem_size = elem_size;
441 17           hdr->variant_id = variant_id;
442 17           hdr->capacity = capacity;
443 17           hdr->total_size = total;
444 17           hdr->data_off = sizeof(DeqHeader);
445 17           hdr->ctl_off = deq_ctl_offset(elem_size, capacity);
446 17           __atomic_thread_fence(__ATOMIC_SEQ_CST);
447 17           }
448              
449             /* Layout fields are passed in by the caller — either from a validated
450             * header snapshot or locally computed — never re-read from the live
451             * mapping, which a hostile peer could rewrite between validation and
452             * here (double-fetch TOCTOU). */
453 20           static inline DeqHandle *deq_setup(void *base, size_t ms, const char *path, int bfd,
454             uint64_t data_off, uint64_t ctl_off,
455             uint32_t elem_size) {
456 20           DeqHandle *h = (DeqHandle *)calloc(1, sizeof(DeqHandle));
457 20 50         if (!h) { munmap(base, ms); return NULL; }
458 20           h->hdr = (DeqHeader *)base;
459 20           h->data = (uint8_t *)base + data_off;
460 20           h->ctl = (uint64_t *)((uint8_t *)base + ctl_off);
461 20           h->mmap_size = ms;
462 20           h->elem_size = elem_size;
463 20 100         h->path = path ? strdup(path) : NULL;
464 20           h->notify_fd = -1;
465 20           h->backing_fd = bfd;
466 20           return h;
467             }
468              
469             /* Validate a mapped header (shared by deq_create reopen and deq_open_fd). */
470 6           static inline int deq_validate_header(const DeqHeader *hdr, uint64_t file_size,
471             uint32_t expected_variant) {
472 6 100         if (hdr->magic != DEQ_MAGIC) return 0;
473 5 50         if (hdr->version != DEQ_VERSION) return 0;
474 5 50         if (hdr->variant_id != expected_variant) return 0;
475 5 50         if (hdr->elem_size == 0 || hdr->capacity == 0) return 0;
    50          
476 5 50         if (hdr->capacity > 0x80000000u) return 0;
477             /* Variant-specific elem_size sanity: prevents buffer overflows in the
478             * XS push paths if a corrupted/tampered file claims an impossibly-small
479             * elem_size (e.g. < 4 for a Str variant where push writes a 4-byte
480             * length prefix). */
481 5 100         if (expected_variant == DEQ_VAR_INT && hdr->elem_size != sizeof(int64_t))
    100          
482 1           return 0;
483 4 100         if (expected_variant == DEQ_VAR_STR && hdr->elem_size < sizeof(uint32_t) + 1)
    100          
484 1           return 0;
485 3 50         if (hdr->total_size != file_size) return 0;
486 3 50         if (hdr->data_off != sizeof(DeqHeader)) return 0;
487 3 50         if (hdr->ctl_off != deq_ctl_offset(hdr->elem_size, hdr->capacity)) return 0;
488 3 50         if (hdr->total_size != deq_total_size(hdr->elem_size, hdr->capacity)) return 0;
489 3           return 1;
490             }
491              
492             /* Ring slots map via idx % capacity on a free-running 32-bit index; that tiles the
493             2^32 index space correctly only when capacity divides 2^32, i.e. is a power of two,
494             so a non-power-of-2 request would collide slots across the 2^32 wrap seam. Round up. */
495 22           static inline uint64_t deq_next_pow2(uint64_t n) {
496 22 100         if (n <= 1) return 1;
497 21           n--; n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16; n |= n >> 32;
498 21           return n + 1;
499             }
500              
501             /* Securely obtain a fd for the path-based backing store. Either create it fresh
502             * (O_CREAT|O_EXCL|O_NOFOLLOW at `mode`, default 0600 = owner-only), or, if it
503             * already exists, attach to it (O_RDWR|O_NOFOLLOW, no O_CREAT). O_EXCL blocks a
504             * pre-seeded or hard-linked file and O_NOFOLLOW a symlink swap, so a local
505             * attacker can no longer redirect or poison the backing store through the path.
506             * Cross-user sharing is opt-in via a wider `mode` (e.g. 0660); the caller still
507             * validates the file's contents. */
508 10           static int deq_secure_open(const char *path, mode_t mode, char *errbuf) {
509 10 50         for (int attempt = 0; attempt < 100; attempt++) {
510 10           int fd = open(path, O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC, mode);
511 10 100         if (fd >= 0) { (void)fchmod(fd, mode); return fd; } /* exact mode: umask narrowed the O_EXCL create */
512 5 50         if (errno != EEXIST) { DEQ_ERR("create %s: %s", path, strerror(errno)); return -1; }
    0          
513 5           fd = open(path, O_RDWR|O_NOFOLLOW|O_CLOEXEC);
514 5 50         if (fd >= 0) return fd;
515 0 0         if (errno == ENOENT) continue; /* creator unlinked between our two opens; retry */
516 0 0         DEQ_ERR("open %s: %s", path, strerror(errno)); /* ELOOP => symlink rejected */
517 0           return -1;
518             }
519 0 0         DEQ_ERR("open %s: create/attach kept racing", path);
520 0           return -1;
521             }
522              
523 21           static DeqHandle *deq_create(const char *path, uint64_t capacity,
524             uint32_t elem_size, uint32_t variant_id,
525             mode_t mode, char *errbuf) {
526 21 50         if (errbuf) errbuf[0] = '\0';
527 21 50         if (capacity == 0) { DEQ_ERR("capacity must be > 0"); return NULL; }
    0          
528 21 50         if (elem_size == 0) { DEQ_ERR("elem_size must be > 0"); return NULL; }
    0          
529 21           capacity = deq_next_pow2(capacity);
530 21 50         if (capacity == 0 || capacity > 0x80000000u) {
    50          
531 0 0         DEQ_ERR("capacity must be <= 2^31 (32-bit cursor halves)"); return NULL;
532             }
533              
534 21           uint64_t total = deq_total_size(elem_size, capacity);
535 21           int anonymous = (path == NULL);
536 21           int fd = -1;
537             size_t map_size;
538             void *base;
539              
540 21 100         if (anonymous) {
541 11           map_size = (size_t)total;
542 11           base = mmap(NULL, map_size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
543 11 50         if (base == MAP_FAILED) { DEQ_ERR("mmap: %s", strerror(errno)); return NULL; }
    0          
544             } else {
545 10           fd = deq_secure_open(path, mode, errbuf);
546 15 50         if (fd < 0) return NULL;
547 10 50         if (flock(fd, LOCK_EX) < 0) { DEQ_ERR("flock: %s", strerror(errno)); close(fd); return NULL; }
    0          
548             struct stat st;
549 10 50         if (fstat(fd, &st) < 0) {
550 0 0         DEQ_ERR("fstat: %s", strerror(errno)); flock(fd, LOCK_UN); close(fd); return NULL;
551             }
552 10           int is_new = (st.st_size == 0);
553 10 100         if (!is_new && (uint64_t)st.st_size < sizeof(DeqHeader)) {
    50          
554 0 0         DEQ_ERR("%s: file too small (%lld)", path, (long long)st.st_size);
555 0           flock(fd, LOCK_UN); close(fd); return NULL;
556             }
557 10 100         if (is_new && ftruncate(fd, (off_t)total) < 0) {
    50          
558 0 0         DEQ_ERR("ftruncate: %s", strerror(errno)); flock(fd, LOCK_UN); close(fd); return NULL;
559             }
560 10 100         map_size = is_new ? (size_t)total : (size_t)st.st_size;
561 10           base = mmap(NULL, map_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
562 10 50         if (base == MAP_FAILED) { DEQ_ERR("mmap: %s", strerror(errno)); flock(fd, LOCK_UN); close(fd); return NULL; }
    0          
563 10 100         if (!is_new) {
564             DeqHeader snap; /* single fetch: validate + setup use one copy */
565 5           memcpy(&snap, base, sizeof snap);
566 5 100         if (!deq_validate_header(&snap, (uint64_t)st.st_size, variant_id)) {
567 3 50         DEQ_ERR("invalid deque file"); munmap(base, map_size); flock(fd, LOCK_UN); close(fd); return NULL;
568             }
569 2           flock(fd, LOCK_UN); close(fd);
570 2           return deq_setup(base, map_size, path, -1,
571             snap.data_off, snap.ctl_off, snap.elem_size);
572             }
573             }
574 16           deq_init_header(base, total, elem_size, variant_id, capacity);
575 16 100         if (fd >= 0) { flock(fd, LOCK_UN); close(fd); }
576 16           return deq_setup(base, map_size, path, -1,
577             sizeof(DeqHeader), deq_ctl_offset(elem_size, capacity),
578             elem_size);
579             }
580              
581 1           static DeqHandle *deq_create_memfd(const char *name, uint64_t capacity,
582             uint32_t elem_size, uint32_t variant_id,
583             char *errbuf) {
584 1 50         if (errbuf) errbuf[0] = '\0';
585 1 50         if (capacity == 0) { DEQ_ERR("capacity must be > 0"); return NULL; }
    0          
586 1 50         if (elem_size == 0) { DEQ_ERR("elem_size must be > 0"); return NULL; }
    0          
587 1           capacity = deq_next_pow2(capacity);
588 1 50         if (capacity == 0 || capacity > 0x80000000u) {
    50          
589 0 0         DEQ_ERR("capacity must be <= 2^31 (32-bit cursor halves)"); return NULL;
590             }
591 1           uint64_t total = deq_total_size(elem_size, capacity);
592 1 50         int fd = memfd_create(name ? name : "deque", MFD_CLOEXEC | MFD_ALLOW_SEALING);
593 1 50         if (fd < 0) { DEQ_ERR("memfd_create: %s", strerror(errno)); return NULL; }
    0          
594 1 50         if (ftruncate(fd, (off_t)total) < 0) { DEQ_ERR("ftruncate: %s", strerror(errno)); close(fd); return NULL; }
    0          
595 1           (void)fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW);
596 1           void *base = mmap(NULL, (size_t)total, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
597 1 50         if (base == MAP_FAILED) { DEQ_ERR("mmap: %s", strerror(errno)); close(fd); return NULL; }
    0          
598 1           deq_init_header(base, total, elem_size, variant_id, capacity);
599 1           return deq_setup(base, (size_t)total, NULL, fd,
600             sizeof(DeqHeader), deq_ctl_offset(elem_size, capacity),
601             elem_size);
602             }
603              
604 1           static DeqHandle *deq_open_fd(int fd, uint32_t variant_id, char *errbuf) {
605 1 50         if (errbuf) errbuf[0] = '\0';
606             struct stat st;
607 1 50         if (fstat(fd, &st) < 0) { DEQ_ERR("fstat: %s", strerror(errno)); return NULL; }
    0          
608 1 50         if ((uint64_t)st.st_size < sizeof(DeqHeader)) { DEQ_ERR("too small"); return NULL; }
    0          
609 1           size_t ms = (size_t)st.st_size;
610 1           void *base = mmap(NULL, ms, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
611 1 50         if (base == MAP_FAILED) { DEQ_ERR("mmap: %s", strerror(errno)); return NULL; }
    0          
612             DeqHeader snap; /* single fetch: validate + setup use one copy */
613 1           memcpy(&snap, base, sizeof snap);
614 1 50         if (!deq_validate_header(&snap, (uint64_t)st.st_size, variant_id)) {
615 0 0         DEQ_ERR("invalid deque"); munmap(base, ms); return NULL;
616             }
617 1           int myfd = fcntl(fd, F_DUPFD_CLOEXEC, 0);
618 1 50         if (myfd < 0) { DEQ_ERR("fcntl: %s", strerror(errno)); munmap(base, ms); return NULL; }
    0          
619 1           return deq_setup(base, ms, NULL, myfd,
620             snap.data_off, snap.ctl_off, snap.elem_size);
621             }
622              
623 20           static void deq_destroy(DeqHandle *h) {
624 20 50         if (!h) return;
625 20 100         if (h->notify_fd >= 0) close(h->notify_fd);
626 20 100         if (h->backing_fd >= 0) close(h->backing_fd);
627 20 50         if (h->hdr) munmap(h->hdr, h->mmap_size);
628 20           free(h->path);
629 20           free(h);
630             }
631              
632             /* NOT concurrency-safe — use drain() for concurrent scenarios */
633 3           static void deq_clear(DeqHandle *h) {
634 3           __atomic_store_n(&h->hdr->cursor, 0, __ATOMIC_RELEASE);
635             /* Reset all slot ctl to {EMPTY, gen=0}. Safe only when no concurrent
636             * push/pop — which is the documented contract of clear(). */
637 3           memset(h->ctl, 0, (size_t)h->hdr->capacity * sizeof(uint64_t));
638             /* clear() frees the entire deque at once — wake all waiters so they
639             * can re-evaluate state, not just one. */
640             /* StoreLoad: publish our slot/cursor change before reading waiters. */
641 3           __atomic_thread_fence(__ATOMIC_SEQ_CST);
642 3 50         if (__atomic_load_n(&h->hdr->waiters_push, __ATOMIC_RELAXED) > 0) {
643 0           __atomic_add_fetch(&h->hdr->push_wake_seq, 1, __ATOMIC_RELEASE);
644 0           syscall(SYS_futex, &h->hdr->push_wake_seq, FUTEX_WAKE, INT_MAX, NULL, NULL, 0);
645             }
646             /* StoreLoad: publish our slot/cursor change before reading waiters. */
647 3           __atomic_thread_fence(__ATOMIC_SEQ_CST);
648 3 50         if (__atomic_load_n(&h->hdr->waiters_pop, __ATOMIC_RELAXED) > 0) {
649 0           __atomic_add_fetch(&h->hdr->pop_wake_seq, 1, __ATOMIC_RELEASE);
650 0           syscall(SYS_futex, &h->hdr->pop_wake_seq, FUTEX_WAKE, INT_MAX, NULL, NULL, 0);
651             }
652 3           }
653              
654             /* Concurrency-safe drain: CAS cursor to advance head to tail, then release
655             * each drained slot through the state machine so future pushes can reuse.
656             *
657             * Crash-recovery: a pusher that won the cursor CAS but died (SIGKILL/crash)
658             * before completing its write leaves its slot stuck in a non-FILLED state.
659             * Two distinct stall windows:
660             * 1. cursor CAS done, claim_write not yet succeeded — slot is still in
661             * EMPTY@gen (or briefly READING@gen if a prior popper is finishing).
662             * 2. claim_write done, publish not yet done — slot is WRITING@gen.
663             * In either case plain deq_slot_claim_read would spin forever. We bound the
664             * per-slot wait to ~2s; on timeout we CAS the current state -> EMPTY@(gen+1)
665             * so the slot is reclaimed. The lock-free design doesn't track per-slot
666             * owner PID so we cannot distinguish "dead writer" from "live but extremely
667             * slow writer"; a 2s threshold is far longer than any legitimate slot fill.
668             * A falsely-recovered live writer's late deq_slot_publish is a CAS keyed on
669             * the original gen, so it observes the bump and silently no-ops rather than
670             * resurrecting a phantom FILLED slot. */
671 4           static inline uint32_t deq_drain(DeqHandle *h) {
672 4           DeqHeader *hdr = h->hdr;
673 4           uint32_t cap = (uint32_t)hdr->capacity;
674             /* Snapshot how many items are present at entry. We drain at most this many:
675             * concurrent pops may take some, and pushes that arrive after entry are left
676             * for the next call. */
677 4           uint64_t c0 = __atomic_load_n(&hdr->cursor, __ATOMIC_ACQUIRE);
678 4           uint32_t target = (uint32_t)(DEQ_CURSOR_TAIL(c0) - DEQ_CURSOR_HEAD(c0));
679 4           uint32_t drained = 0;
680             /* Reclaim ONE head slot per iteration (not bulk-advance-then-walk): never
681             * expose a freed slot for a wrapping push to reuse before we reclaim it --
682             * that raced drain/pop/push on a reused slot (gen-agnostic claim_read). */
683 17 100         while (drained < target) {
684 13           uint64_t c = __atomic_load_n(&hdr->cursor, __ATOMIC_ACQUIRE);
685 13           uint32_t hd = DEQ_CURSOR_HEAD(c), t = DEQ_CURSOR_TAIL(c);
686 13 50         if ((uint32_t)(t - hd) == 0) break; /* emptied by a concurrent consumer */
687 13           uint64_t nc = DEQ_CURSOR(hd + 1, t);
688 13 50         if (!__atomic_compare_exchange_n(&hdr->cursor, &c, nc,
689             1, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED))
690 0           continue; /* lost the CAS -- re-read + retry */
691             /* We now exclusively own item hd's slot. Reclaim it, with the same
692             * bounded recovery the old drain used for a dead/slow writer's slot. */
693 13           uint32_t idx = hd % cap;
694 13           uint64_t *ctl_word = &h->ctl[idx];
695 13           int recovered = 0, dl_set = 0;
696 13           uint32_t spins = 0;
697             struct timespec dl;
698 1220990           for (;;) {
699 1221003           uint64_t cw = __atomic_load_n(ctl_word, __ATOMIC_ACQUIRE);
700 1221003 100         if (DEQ_SLOT_STATE(cw) == DEQ_SLOT_FILLED) {
701 11           uint64_t nw = (DEQ_SLOT_GEN(cw) << 2) | DEQ_SLOT_READING;
702 11 50         if (__atomic_compare_exchange_n(ctl_word, &cw, nw,
703             0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) {
704 11           deq_slot_release(ctl_word, DEQ_SLOT_GEN(cw));
705 11           break;
706             }
707 0           continue;
708             }
709             /* Non-FILLED: hot-spin, then short sleeps; on timeout force the slot
710             * to EMPTY@(gen+1) (a dead/stalled writer left it WRITING). */
711 1220992           deq_spin_pause();
712 1220992 100         if ((++spins & 0x3F) == 0) {
713 19078 100         if (!dl_set) { deq_make_deadline((double)DEQ_DRAIN_RECOVERY_SEC, &dl); dl_set = 1; }
714             struct timespec rem;
715 19078 100         if (!deq_remaining(&dl, &rem)) {
716 2           uint64_t nw = ((DEQ_SLOT_GEN(cw) + 1) << 2) | DEQ_SLOT_EMPTY;
717 2 50         if (__atomic_compare_exchange_n(ctl_word, &cw, nw,
718 2           0, __ATOMIC_ACQ_REL, __ATOMIC_RELAXED)) { recovered = 1; break; }
719 0           continue; /* CAS lost: state advanced concurrently -- re-observe */
720             }
721 19076           struct timespec ts = { 0, 100000L }; /* 100us */
722 19076           nanosleep(&ts, NULL);
723             }
724             }
725 13 100         if (recovered) __atomic_add_fetch(&hdr->stat_recoveries, 1, __ATOMIC_RELAXED);
726 13           drained++;
727             }
728 4 100         if (drained > 0) {
729             /* StoreLoad: publish our slot/cursor changes before reading waiters. */
730 3           __atomic_thread_fence(__ATOMIC_SEQ_CST);
731 3 50         if (__atomic_load_n(&hdr->waiters_push, __ATOMIC_RELAXED) > 0) {
732 0           __atomic_add_fetch(&hdr->push_wake_seq, 1, __ATOMIC_RELEASE);
733 0 0         syscall(SYS_futex, &hdr->push_wake_seq, FUTEX_WAKE,
734             drained < INT_MAX ? (int)drained : INT_MAX, NULL, NULL, 0);
735             }
736             }
737 4           return drained;
738             }
739              
740 2           static int deq_create_eventfd(DeqHandle *h) {
741 2 50         if (h->notify_fd >= 0) return h->notify_fd;
742 2           int efd = eventfd(0, EFD_NONBLOCK|EFD_CLOEXEC);
743 2 50         if (efd < 0) return -1;
744 2           h->notify_fd = efd; return efd;
745             }
746 2           static int deq_notify(DeqHandle *h) {
747 2 50         if (h->notify_fd < 0) return 0;
748 2           uint64_t v = 1; return write(h->notify_fd, &v, sizeof(v)) == sizeof(v);
749             }
750 2           static int64_t deq_eventfd_consume(DeqHandle *h) {
751 2 50         if (h->notify_fd < 0) return -1;
752 2           uint64_t v = 0;
753 2 50         if (read(h->notify_fd, &v, sizeof(v)) != sizeof(v)) return -1;
754 2           return (int64_t)v;
755             }
756 1           static int deq_msync(DeqHandle *h) { return msync(h->hdr, h->mmap_size, MS_SYNC); }
757              
758             #endif /* DEQUE_H */