--- id: "bug-1452" status: "fixed" status_updated: "2026-09-24" status_note: "" component: "controller" severity: "crash" timestamp: "2026-07-18T00:00:00Z" last_seen: "2026-09-19T09:00:00Z" occurrences: 1 files: ["src/controller/commit.c (crash site", "commit_advance_locked); src/controller/persist.c (fsync_worker background thread", "apply_committed_entry/restore_job/makeJob chain also appears in a related non-crashing stack from the same investigation); src/controller/ha_convergence.c + config_repl.c (g_drive_async_demotion_offload / g_drive_async_catchup_offload -- the flags whose flip triggers this)"] tags: ["ha_convergence.c", "config_repl.c", "commit.c", "persist.c", "test_persist_truncate.c", "teardown-ordering", "toctou-race", "commit_engine_fini", "persist_stop", "raft-drive-async-pattern", "xce-audit-invariants", "controller-tests", "ASAN", "SEGV", "FIXED", "root-caused"] related: ["bug-1429", "bug-1450", "bug-1451"] legacy: true gitea_issue: 36 gitea_synced_status: "fixed" --- # bug-1452: AddressSanitizer: SEGV on unknown address 0x000000000038 in commit_advance_locked… ## Description AddressSanitizer: SEGV on unknown address 0x000000000038 in commit_advance_locked (controller/commit.c:208), called from commit_on_durable_advance (commit.c:341) <- fsync_worker (persist.c:399), a background ASAN thread started by persist_start (persist.c:723) inside test_persist_truncate.c's make_fresh_persist / test_truncate_above_resolves_pending_continuations_as_failed. Reproduced by temporarily flipping BOTH g_drive_async_demotion_offload and g_drive_async_catchup_offload (bug-1450/1451) from their default 0 to 1 and running the standalone controller_unit_test binary directly (976-test suite) -- crashed immediately, before completing the suite. Confirmed absent with both flags at their committed default of 0: the same binary/build (just reverted) completed cleanly, 976 Tests 0 Failures 0 Ignored, twice. ## Environment _Not recorded (migrated from buglog.json)._ ## Reproduction _Not recorded (migrated from buglog.json)._ ## Root cause ROOT-CAUSED 2026-07-19: the g_drive_async_demotion_offload/g_drive_async_catchup_offload flags were a RED HERRING -- this exact SEGV signature (NULL deref in the fsync_worker -> commit_on_durable_advance -> commit_advance_locked chain) was already independently observed on 2026-07-07, long before those flags existed (see memory: 'Pre-existing ASAN SEGV in test_persist_truncate Unrelated to Bug-1417 Work'). The flags only perturbed thread-scheduling timing enough to make a PRE-EXISTING, always-latent test-teardown race fire more reliably; they do not participate in the crash's causal chain at all. Actual root cause: in src/controller/testsuite/test_persist_truncate.c, the three continuation-related tests (test_truncate_above_resolves_pending_continuations_as_failed, test_truncate_noop_does_not_resolve_pending_continuations, test_truncate_concurrent_burst_resolves_above_cutoff_exactly_once) called `commit_engine_fini(&ce)` BEFORE `teardown_persist(p)` (which calls `persist_stop(p)`) -- the exact opposite of the contract documented in commit_engine_fini's own comment (commit.c:464-468: 'Callers are expected to have stopped the flusher (persist_stop) first'). Because persist_stop had not yet run, the fsync_worker background thread was still alive and could be mid-flight invoking the durable-advance callback (fsync_worker, persist.c:398-399, calls p->on_durable_advance(p->durable_cb_ctx) with NO lock held) concurrently with commit_engine_fini destroying ce->lock/ce->cond and, as its final step, setting ce->p = NULL. This produces a genuine TOCTOU data race with no synchronization between the two threads: commit_on_durable_advance's own guard (`if (ce == NULL || ce->p == NULL) return;`) can read ce->p as still non-NULL (racing ahead of or concurrent with commit_engine_fini's teardown), pass the guard, and then commit_advance_locked's SEPARATE, later re-read of ce->p (commit.c:208, `atomic_load(&ce->p->committed_lsn)`) observes the NOW-NULLED value written by the concurrently-running commit_engine_fini on the main thread -- producing a NULL-pointer dereference at exactly offset 0x38 (offsetof(persist_t, committed_lsn)), matching the crash address bit-for-bit. persist_set_durable_cb's unhook (called from commit_engine_fini) is 'defensive and cheap' per its own comment but is NOT sufficient on its own without persist_stop's pthread_join guaranteeing the flusher thread cannot be in-flight at all -- confirmed by the fact that persist_set_durable_cb existed and ran in this path the whole time and the crash still occurred. ## Fix FIXED 2026-07-19 (commit pending): added an explicit `persist_stop(p);` call immediately before `commit_engine_fini(&ce);` in all three affected test functions in test_persist_truncate.c (previously commit_engine_fini ran first, then teardown_persist -> persist_stop ran second -- backwards from the documented contract). persist_stop joins the fsync_worker thread, so by the time commit_engine_fini tears down ce->lock/ce->cond/ce->p, no thread can be mid-callback referencing them. teardown_persist's own internal persist_stop(p) call becomes a safe idempotent no-op on the second invocation (fsync_thread_running is already 0 and wal_fd is already -1, so both of its internal branches skip). No production code changed -- this was purely a test-fixture ordering bug, not a bug in commit.c/persist.c itself. Verified: full ASAN controller_unit_test suite (976 tests) run 6 times total (1 via meson + 5 standalone direct binary runs per project convention of not trusting meson's aggregate alone) -- 976 Tests 0 Failures 0 Ignored on every run, zero ASAN reports (SEGV/UAF/etc.), all 8 test_persist_truncate.c tests PASS every run. NOTE for a future session: the identical commit_engine_fini-before-persist_stop ordering also exists in test_demotion_convergence.c (line 233 commit_engine_fini(&g_dc.commit) before line 235 persist_stop(&g_dc.persist), against a real persist_start'd fixture) -- same latent race, not yet observed to crash there (likely a narrower window against a static/global fixture rather than a per-test stack fixture), out of scope for this fix but worth a follow-up sweep of all commit_engine_fini call sites. ## Verification _Not recorded (migrated from buglog.json)._ ## References - Related: bug-1429, bug-1450, bug-1451 - Gitea: #36