427b687b95
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.
112 lines
4.8 KiB
Rust
112 lines
4.8 KiB
Rust
//! RED for `bugfix-rc-global-stats-race`.
|
|
//!
|
|
//! Named property protected: under a multi-threaded host that uses the
|
|
//! NULL-ctx GLOBAL RC-stats fallback path (`__ail_tls_ctx == NULL` ->
|
|
//! `g_rc_alloc_count` / `g_rc_free_count`, runtime/rc.c:90-91,161,212),
|
|
//! the `AILANG_RC_STATS` atexit Σ is EXACT: it counts every
|
|
//! `ailang_rc_alloc` and every to-zero `ailang_rc_dec`, with no lost
|
|
//! increments. The two global counters are plain `static uint64_t`
|
|
//! incremented by a non-atomic `++`; concurrent workers race on the
|
|
//! read-modify-write and silently drop updates, so the printed total
|
|
//! under-counts non-deterministically (M5 iter 2's symbol-fan swarm:
|
|
//! allocs jitter 5993321..5999318, never the true 6_000_000).
|
|
//!
|
|
//! This is NOT a memory bug: no box crosses a thread (alloc + dec in
|
|
//! the same loop body), the per-object refcount header op is correct,
|
|
//! and the program is bit-exact every run. The ONLY defect is the
|
|
//! under-counted statistics Σ — exactly the deferred atomic-counter
|
|
//! decision rc.c's own header (rc.c:44-49,85) names, with M5 the first
|
|
//! concurrent consumer.
|
|
//!
|
|
//! The existing per-ctx swarm/accounting harnesses
|
|
//! (embed_swarm_tsan.rs, embed_rc_accounting_tsan.rs) always set
|
|
//! `__ail_tls_ctx = ctx`, so they exercise the per-thread (zero-
|
|
//! contention) counters and never the global fallback — that is why
|
|
//! the suite is green while this bug ships.
|
|
//!
|
|
//! Build/link mechanism mirrors `embed_swarm_tsan.rs`: a staticlib
|
|
//! build of any kernel emits `libailang_rt.a`; the C host links the
|
|
//! runtime archive directly and never calls a kernel.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
fn ws_root() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
|
|
}
|
|
fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") }
|
|
|
|
/// 8 threads * 2_000_000 cycles — must match the `#define`s in
|
|
/// `crates/ail/tests/embed/rc_global_stats_race.c`.
|
|
const NTHREADS: u64 = 8;
|
|
const NCYCLES: u64 = 2_000_000;
|
|
const EXPECTED: u64 = NTHREADS * NCYCLES;
|
|
|
|
#[test]
|
|
fn global_rc_stats_counters_are_exact_under_thread_contention() {
|
|
let outdir = std::env::temp_dir().join(format!(
|
|
"ail-rc-global-stats-race-{}", std::process::id()));
|
|
std::fs::create_dir_all(&outdir).unwrap();
|
|
let root = ws_root();
|
|
|
|
// Any kernel staticlib build emits libailang_rt.a alongside it.
|
|
let st = Command::new(ail_bin())
|
|
.args(["build", "examples/embed_backtest_step_tick.ail",
|
|
"--emit=staticlib", "--alloc=rc",
|
|
"-o", outdir.to_str().unwrap()])
|
|
.current_dir(&root).status().expect("ail build");
|
|
assert!(st.success(), "ail build --emit=staticlib failed");
|
|
|
|
let rt_archive = outdir.join("libailang_rt.a");
|
|
assert!(rt_archive.exists(), "libailang_rt.a not emitted");
|
|
|
|
let host_c = root.join("crates/ail/tests/embed/rc_global_stats_race.c");
|
|
let bin = outdir.join("rc_global_stats_race");
|
|
let st = Command::new("cc")
|
|
.args([
|
|
"-O2", "-pthread",
|
|
host_c.to_str().unwrap(),
|
|
rt_archive.to_str().unwrap(),
|
|
"-o", bin.to_str().unwrap(),
|
|
])
|
|
.status().expect("cc");
|
|
assert!(st.success(), "linking rc_global_stats_race.c failed");
|
|
|
|
let out = Command::new(&bin)
|
|
.env("AILANG_RC_STATS", "1")
|
|
.output().expect("run rc_global_stats_race");
|
|
assert!(out.status.success(),
|
|
"host must exit 0; stderr:\n{}",
|
|
String::from_utf8_lossy(&out.stderr));
|
|
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
let line = stderr.lines()
|
|
.find(|l| l.starts_with("ailang_rc_stats:"))
|
|
.unwrap_or_else(|| panic!(
|
|
"missing ailang_rc_stats line; stderr was:\n{stderr}"));
|
|
|
|
let mut allocs: Option<u64> = None;
|
|
let mut frees: Option<u64> = None;
|
|
for tok in line.split_whitespace() {
|
|
if let Some(v) = tok.strip_prefix("allocs=") {
|
|
allocs = Some(v.parse().expect("allocs= u64"));
|
|
} else if let Some(v) = tok.strip_prefix("frees=") {
|
|
frees = Some(v.parse().expect("frees= u64"));
|
|
}
|
|
}
|
|
let allocs = allocs.expect("allocs= present");
|
|
let frees = frees.expect("frees= present");
|
|
|
|
// Pre-fix: the non-atomic `g_rc_*count++` loses updates under
|
|
// contention, so allocs/frees are < EXPECTED and jitter run-to-run.
|
|
// Post-fix (atomic global counters): exact equality, deterministic.
|
|
assert_eq!(allocs, EXPECTED,
|
|
"global RC alloc counter must count every ailang_rc_alloc on \
|
|
the NULL-ctx fallback path with no lost increments \
|
|
(allocs={allocs}, expected {EXPECTED}); line: {line:?}");
|
|
assert_eq!(frees, EXPECTED,
|
|
"global RC free counter must count every to-zero ailang_rc_dec \
|
|
on the NULL-ctx fallback path with no lost increments \
|
|
(frees={frees}, expected {EXPECTED}); line: {line:?}");
|
|
}
|