diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6e86fc2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index ba6acd0..4dfedf0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile index 5fc12a6..ddef0e7 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 diff --git a/cross-platform.h b/cross-platform.h index 104947a..2a97882 100644 --- a/cross-platform.h +++ b/cross-platform.h @@ -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 diff --git a/lfq.c b/lfq.c index 4d94075..5a6e728 100755 --- a/lfq.c +++ b/lfq.c @@ -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; diff --git a/test_aba.c b/test_aba.c new file mode 100644 index 0000000..0125295 --- /dev/null +++ b/test_aba.c @@ -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 +#include +#include +#include +#include +#include "lfq.h" +#include "cross-platform.h" + +#if defined __GNUC__ || defined __CYGWIN__ || defined __MINGW32__ +#include +#include +#else +#include +#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; +} diff --git a/test_multithread.c b/test_multithread.c index 7c239b8..50658f0 100644 --- a/test_multithread.c +++ b/test_multithread.c @@ -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) {