Files
AILang/crates/ail/tests/embed/rc_global_stats_race.c
T
Brummel 427b687b95 test(rc): RED — non-atomic global g_rc_* stats counters race under a multi-threaded host
bugfix-rc-global-stats-race, RED stage (audit trail; GREEN follows
separately via implement mini-mode).

`g_rc_alloc_count`/`g_rc_free_count` (runtime/rc.c:90-91) are plain
`static uint64_t`; the `__ail_tls_ctx == NULL` fallback at rc.c:161
and rc.c:212 does a non-atomic `++`. Concurrent host-side
ailang_rc_alloc/ailang_rc_dec outside a bound ctx race the
read-modify-write and silently drop updates. New C host (8 threads
x 2_000_000 alloc-then-dec, no ailang_ctx_new so the global path is
taken, no box crosses a thread) + integration test asserting the
atexit Σ is exact. Boss-verified RED: allocs=2141382 expected
16000000, live=-131242 (deterministic-fail under this contention,
not flaky).

NOT a memory bug — Ctx:!Send keeps every box on one thread, the
per-object refcount header op is correct, programs are bit-exact;
the only defect is the under-counted statistics Σ (the M5 iter 2
symbol-fan leak-proof finding, b724cd1). Existing green per-ctx
harnesses (embed_swarm_tsan.rs, embed_rc_accounting_tsan.rs) always
bind __ail_tls_ctx, exercising the zero-contention per-thread
counters and never this global fallback — which is why the suite is
green while the bug ships.
2026-05-19 01:50:36 +02:00

61 lines
2.5 KiB
C

/* bugfix-rc-global-stats-race: RED host.
*
* Drives the GLOBAL RC-stats fallback path (`g_rc_alloc_count` /
* `g_rc_free_count`, runtime/rc.c:90-91) under high thread contention.
*
* Crucially: NO `ailang_ctx_new`, so `__ail_tls_ctx` stays NULL in every
* worker and both `ailang_rc_alloc` (rc.c:161) and the to-zero branch of
* `ailang_rc_dec` (rc.c:212) take the `else g_rc_*count++;` arm. Those
* two counters are plain `static uint64_t` with a non-atomic `++`, so
* concurrent increments lose updates (classic read-modify-write race).
*
* Each worker does NCYCLES of alloc-immediately-dec on a 16-byte box.
* No box ever crosses a thread (alloc + dec in the same loop body), so
* the per-object refcount header op is correct and the program is
* bit-exact every run. The ONLY observable defect is the under-counted
* global Σ printed by the atexit handler.
*
* The integration test (crates/ail/tests/embed_rc_global_stats_race.rs)
* parses the atexit `ailang_rc_stats:` line and asserts
* allocs == NTHREADS*NCYCLES AND frees == NTHREADS*NCYCLES.
* Pre-fix: reliably fails (allocs/frees short by lost increments).
* Post-fix (atomic global counters): deterministic exact equality.
*
* This host links libailang_rt.a directly; it does not call any kernel.
*/
#include <stddef.h>
#include <stdio.h>
#include <pthread.h>
extern void *ailang_rc_alloc(size_t);
extern void ailang_rc_dec(void *);
/* High contention: 8 threads * 2_000_000 cycles = 16_000_000 expected
* global allocs and 16_000_000 global frees. At this contention the
* non-atomic `++` reliably loses updates on every observed run, so the
* RED is deterministic (it fails pre-fix, not merely flaky). */
#define NTHREADS 8
#define NCYCLES 2000000
static void *worker(void *_a) {
(void)_a;
/* __ail_tls_ctx left NULL on purpose -> global fallback path. */
for (int i = 0; i < NCYCLES; i++) {
void *p = ailang_rc_alloc(16);
ailang_rc_dec(p); /* refcount 1 -> 0, frees, g_rc_free_count++ */
}
return NULL;
}
int main(void) {
pthread_t t[NTHREADS];
for (int i = 0; i < NTHREADS; i++) pthread_create(&t[i], NULL, worker, NULL);
for (int i = 0; i < NTHREADS; i++) pthread_join(t[i], NULL);
/* All threads joined before main returns -> the atexit reader sees
* a fully quiesced counter (no reader/writer ordering concern; the
* defect is purely the lost writer-vs-writer increments). */
printf("rc_global_stats_race: ran %d threads x %d cycles\n",
NTHREADS, NCYCLES);
return 0;
}