Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: C/C++ CI

on:
push:
branches: [ "HP" ]
pull_request:
branches: [ "HP" ]

jobs:
build:

runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v4
- name: make
run: make
- name: make test
run: make test
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@
/Visual Stdio/.vs/lfqueue/v14
/Visual Stdio/Release
/Visual Stdio/Debug

# Build artifacts
bin/
*.o
*.a
*.so
*.so.*

# CodeQL artifacts
_codeql_detected_source_root
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ CFLAGS=-std=gnu99 -O3 -Wall -Wextra -g
LDFLAGS=-g
LOADLIBS=-lpthread

all : bin/test_p1c1 bin/test_p4c4 bin/test_p100c10 bin/test_p10c100 bin/example
all : bin/test_p1c1 bin/test_p4c4 bin/test_p100c10 bin/test_p10c100 bin/test_aba bin/example

bin/example: example.c liblfq.so.1.0.0
gcc $(CFLAGS) $(LDFLAGS) example.c lfq.c -o bin/example -lpthread
Expand All @@ -20,17 +20,21 @@ bin/test_p100c10: liblfq.so.1.0.0 test_multithread.c
bin/test_p10c100: liblfq.so.1.0.0 test_multithread.c
gcc $(CFLAGS) $(LDFLAGS) test_multithread.c -o bin/test_p10c100 -L. -Wl,-Bstatic -llfq -Wl,-Bdynamic -lpthread -D MAX_PRODUCER=10 -D MAX_CONSUMER=100

bin/test_aba: liblfq.so.1.0.0 test_aba.c
gcc $(CFLAGS) $(LDFLAGS) test_aba.c -o bin/test_aba -L. -Wl,-Bstatic -llfq -Wl,-Bdynamic -lpthread

liblfq.so.1.0.0: lfq.c lfq.h cross-platform.h
gcc $(CFLAGS) $(CPPFLAGS) -c lfq.c # -fno-pie for static linking?
ar rcs liblfq.a lfq.o
gcc $(CFLAGS) $(CPPFLAGS) -fPIC -c lfq.c
gcc $(LDFLAGS) -shared -o liblfq.so.1.0.0 lfq.o

test: bin/test_p1c1 bin/test_p4c4 bin/test_p100c10 bin/test_p10c100
test: bin/test_p1c1 bin/test_p4c4 bin/test_p100c10 bin/test_p10c100 bin/test_aba
$(TESTWRAPPER) bin/test_p1c1
$(TESTWRAPPER) bin/test_p4c4
$(TESTWRAPPER) bin/test_p100c10
$(TESTWRAPPER) bin/test_p10c100
$(TESTWRAPPER) bin/test_aba

clean:
rm -rf *.o bin/* liblfq.so.1.0.0 liblfq.a
5 changes: 4 additions & 1 deletion cross-platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
#define lmb() asm volatile("":::"memory") // compiler barrier only. runtime reordering already impossible on x86
#define smb() asm volatile("":::"memory")
// "mfence" for lmb and smb makes assertion failures rarer, but doesn't eliminate, so it's just papering over the symptoms
#endif // else no definition
#else
#define lmb() mb()
#define smb() mb()
#endif

// thread
#include <pthread.h>
Expand Down
8 changes: 5 additions & 3 deletions lfq.c
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,16 @@ int lfq_init(struct lfq_ctx *ctx, int max_consume_thread) {
return -errno;

struct lfq_node * free_pool_node = calloc(1,sizeof(struct lfq_node));
if (!free_pool_node)
if (!free_pool_node) {
free(tmpnode);
return -errno;
}

tmpnode->can_free = free_pool_node->can_free = true;
memset(ctx, 0, sizeof(struct lfq_ctx));
ctx->MAXHPSIZE = max_consume_thread;
ctx->HP = calloc(max_consume_thread,sizeof(struct lfq_node));
ctx->tid_map = calloc(max_consume_thread,sizeof(struct lfq_node));
ctx->HP = calloc(max_consume_thread,sizeof(struct lfq_node *));
ctx->tid_map = calloc(max_consume_thread,sizeof(int));
ctx->head = ctx->tail=tmpnode;
ctx->fph = ctx->fpt=free_pool_node;

Expand Down
166 changes: 166 additions & 0 deletions test_aba.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* ABA stress test for lfqueue
*
* The ABA problem: a CAS on a pointer can succeed spuriously when:
* 1. Thread 1 reads head = node_A
* 2. Thread 2 dequeues A, frees it; malloc() returns the same address for
* a new node B; B is enqueued, making head cycle back to address A
* 3. Thread 1's CAS(&head, A, A->next) succeeds even though the node at
* address A is now logically B — A->next is stale/garbage.
*
* To maximize ABA probability this test uses:
* - All threads as both producers AND consumers (tight enq+deq loops)
* - No thread_yield between enqueue and dequeue (maximises racing)
* - Very small queue depth (each thread keeps at most 1 item in-flight)
* - Many threads competing on the same head pointer
* - Magic values in each node to detect data corruption caused by ABA
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#include "lfq.h"
#include "cross-platform.h"

#if defined __GNUC__ || defined __CYGWIN__ || defined __MINGW32__
#include <unistd.h>
#include <pthread.h>
#else
#include <windows.h>
#endif

#ifndef ABA_THREADS
#define ABA_THREADS 16
#endif

#ifndef ABA_ITERATIONS
#define ABA_ITERATIONS 500000
#endif

/* Two distinct magic values — LIVE (in queue) and DEAD (already freed).
* A read of DEAD_MAGIC on a dequeued item means use-after-free / ABA
* corruption. */
#define LIVE_MAGIC 0xAB1AB1EFUL
#define DEAD_MAGIC 0xDEADBEEFUL

struct aba_item {
volatile uint32_t magic;
};

static volatile uint64_t total_enqueued = 0;
static volatile uint64_t total_dequeued = 0;
static volatile int errors = 0;
static volatile int cn_t = 0; /* tid allocator */

static struct lfq_ctx ctx;

/*
* Each thread alternates between enqueue and spin-dequeue in a tight loop.
* This keeps the queue depth near zero, so the same pointer addresses cycle
* through head rapidly — exactly the condition that triggers ABA.
*/
THREAD_FN aba_thread(void *arg) {
(void)arg;
/* Allocate a dedicated hazard-pointer slot for this thread */
int tid = ATOMIC_ADD(&cn_t, 1) - 1;

uint64_t local_enq = 0, local_deq = 0;

for (int i = 0; i < ABA_ITERATIONS && !errors; i++) {
/* --- Enqueue a fresh item --- */
struct aba_item *item = malloc(sizeof(struct aba_item));
if (!item) {
ATOMIC_ADD(&errors, 1);
break;
}
item->magic = LIVE_MAGIC;

if (lfq_enqueue(&ctx, item) != 0) {
free(item);
ATOMIC_ADD(&errors, 1);
break;
}
local_enq++;

/*
* --- Spin-dequeue (no yield) ---
* By immediately spinning for a result with no sleep we keep
* maximum pressure on the head CAS, creating the window for ABA:
* our enqueued node may be stolen and its memory recycled before
* we manage to dequeue it ourselves.
*/
struct aba_item *got;
do {
got = lfq_dequeue_tid(&ctx, tid);
} while (got == NULL);

/* Validate: corruption means ABA or use-after-free occurred */
if (got->magic != LIVE_MAGIC) {
printf("ABA corruption detected! "
"magic=0x%08X (expected 0x%08X) iter=%d tid=%d\n",
got->magic, (uint32_t)LIVE_MAGIC, i, tid);
ATOMIC_ADD(&errors, 1);
}
/* Poison before free to detect future use-after-free reads */
got->magic = DEAD_MAGIC;
free(got);
local_deq++;
}

ATOMIC_ADD64(&total_enqueued, local_enq);
ATOMIC_ADD64(&total_dequeued, local_deq);
return 0;
}

int main(void) {
printf("ABA stress test: %d threads x %d iterations each\n",
ABA_THREADS, ABA_ITERATIONS);
printf("(tight enq+deq loops, no yield — maximises head-pointer reuse)\n");

if (lfq_init(&ctx, ABA_THREADS) != 0) {
fprintf(stderr, "lfq_init failed\n");
return 1;
}

THREAD_TOKEN threads[ABA_THREADS];
for (int i = 0; i < ABA_THREADS; i++) {
#if defined __GNUC__ || defined __CYGWIN__ || defined __MINGW32__
pthread_create(&threads[i], NULL, aba_thread, NULL);
#else
#pragma warning(disable:4133)
threads[i] = CreateThread(NULL, 0, aba_thread, NULL, 0, 0);
#endif
}

for (int i = 0; i < ABA_THREADS; i++)
THREAD_WAIT(threads[i]);

/* Drain any items left in the queue after all threads exit */
struct aba_item *item;
int drain_tid = 0; /* safe: all thread tids have been released */
while ((item = lfq_dequeue_tid(&ctx, drain_tid)) != NULL) {
if (item->magic != LIVE_MAGIC) {
printf("ABA corruption in drain! magic=0x%08X\n", item->magic);
errors++;
}
item->magic = DEAD_MAGIC;
free(item);
total_dequeued++;
}

long freecount = lfg_count_freelist(&ctx);
int clean = lfq_clean(&ctx);

printf("Enqueued=%" PRId64 " Dequeued=%" PRId64
" freelist=%ld clean=%d\n",
total_enqueued, total_dequeued, freecount, clean);

if (errors)
printf("ABA Test FAILED!! (%d corruption(s) detected)\n", errors);
else
printf("ABA Test PASS!!\n");

return errors != 0;
}
2 changes: 1 addition & 1 deletion test_multithread.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ THREAD_FN addq( void * data ) {
THREAD_FN delq(void * data) {
struct lfq_ctx * ctx = data;
struct user_data * p;
int tid = ATOMIC_ADD(&cn_t, 1);
int tid = ATOMIC_ADD(&cn_t, 1) - 1;

long deleted = 0;
while(1) {
Expand Down