--- id: "bug-1972" status: "fixed" status_updated: "2026-10-26" status_note: "" component: "controller" severity: "crash" timestamp: "2026-10-26T09:46:58Z" last_seen: "2026-10-26T09:46:58Z" occurrences: 4 files: ["src/controller/testsuite/test_job_live_gc.c"] tags: ["test-only", "uaf", "gc"] related: [] fix_commits: ["f780a166"] --- # bug-1972: test_job_live_gc asserted destruction by reading freed memory ## Description **Observed:** `controller_unit_test` aborts under ASAN at `test_sweep_purges_expired_done_job` (test_job_live_gc.c:169): ``` ==27834==ERROR: AddressSanitizer: heap-use-after-free on address 0x5110000da290 READ of size 8 at 0x5110000da290 thread T0 #0 job_get_refcount ../lib/jobj.c:715 #1 test_sweep_purges_expired_done_job test_job_live_gc.c:169 freed by: #3 job_dereference ../lib/jobj.c:705 #5 job_live_gc_sweep ../controller/job.c:4777 ``` **Expected:** a correct sweep dereferences the purged job down to refcount 0 and destroys it; the test must verify destruction without touching the freed object. **Impact:** controller suite aborts (killed by SIGABRT); the whole `controller test` meson entry fails. Test-only — the production sweep (job.c:4765-4777, double dereference: collection ref + table's makeJob ref) is correct. **Found by:** ASAN, first full-suite run after d028c8c3 (the archives' earlier runs all predate that commit). ## Environment - OS: Ubuntu 24.04.2 LTS; kernel Linux 6.8.0-147-generic x86_64 - libc: glibc 2.39 (host ldd) - Toolchain: gcc (GCC) 15.2.0, meson 1.9.1 - Build: asan (xce.build) - Code: feature/license-resources @ 68eabd8a-dirty (introduced in d028c8c3) - Deployment: n/a (unit test) ## Reproduction 1. `cd src/xce.build && ASAN_OPTIONS=detect_leaks=0 ./controller/controller_unit_test` - Frequency: deterministic (4/4 runs, 2026-10-26) - Evidence: /home/lchen/code/xce/src/testlogs.archive/xce.build/this-20261026-082318-rc1 ## Root cause The assertion `TEST_ASSERT_EQUAL_UINT32(0, job_get_refcount(expired))` runs AFTER `job_live_gc_sweep` has already dereferenced the job to 0 and freed the entire RTTI object (header + private + data in one calloc block). Reading the refcount through the dangling `job_t*` is itself the use-after-free: the stronger the sweep does its job, the more surely the test crashes. The same pattern is absent from test_apply_pump.c's refcount test because there the job is never destroyed. ## Fix Move the refcount probe BEFORE the sweep and invert its meaning: assert refcount == 1 (table's seed reference, nothing else) before `job_live_gc_sweep`, then verify destruction by absence — `jlg_find_job(81001) == NULL` plus the idempotent second sweep (both already present). `expired` is never dereferenced after the sweep; a comment marks the last safe point. ## Verification - Test: test_sweep_purges_expired_done_job (same file) — RED before (deterministic ASAN abort 4/4), GREEN after (rc=0, 1354/1354 controller unity tests, zero ASAN reports). - Suites: controller_unit_test 1354 tests 0 failures (ASAN, detect_leaks=0); sched_repl_trace_baseline_test 4/4 with regenerated fixtures (separate issue, bug-free fixture staleness from fc33a5da's schema change, handled in the same working tree). - Live: n/a (test-only defect; production sweep unchanged). ## References - Related: none