86 lines
3.2 KiB
Markdown
86 lines
3.2 KiB
Markdown
---
|
|
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
|