You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #7744 is still an open draft and was never merged. Its current head is da6f6bfa. git cherry reports that none of its non-merge commits landed patch-equivalently on main, although later PRs independently addressed parts of the same problem space.
The assessment is based on the PR description, commits, review comments, current code, and git history. It is a static analysis; a reduced-suppression TSAN run is still needed to confirm each lock-cycle report dynamically.
Summary
Result
Count
Meaning
Still applicable
6
The problematic ownership, synchronization, or lock ordering remains on main.
Partially superseded
1
A later PR removed the unsafe visibility window, but the nested startup lock ordering remains.
Fixed independently
1
A later merged PR removed the problem.
The three targeted suppressions also remain in tsan_env_suppressions: two deadlock suppressions covering store.h and untyped_map.h, and one race suppression covering node/rpc/frontend.h.
Still Applicable
1. LedgerSecrets takes its mutex before acquiring a KV dependency
Status: Still applicable.
get_latest(), get_latest_and_penultimate(), and get() still acquire LedgerSecrets::lock before calling take_dependency_on_secrets(). The latter opens and reads a KV handle, establishing the order LedgerSecrets::lock -> KV locks.
PR #7744's focused change is still appropriate: establish the transaction dependency first, then take the local ledger-secrets mutex only while reading the in-memory map.
2. RpcFrontend races on consensus and history
Status: Still applicable.
Both fields remain mutable raw pointers. update_consensus() and update_history() write them without synchronization, while request and tick paths read them concurrently. process(), process_forwarded(), and tick() can all call update_consensus().
The final #7744 direction is sound: explicitly publish consensus/history to each frontend and use synchronized pointer publication. The implementation must load each pointer once per operation and handle nullptr, especially in redirect resolution.
3. Certificate state has both lock-order and ownership problems
Status: Still applicable.
self_signed_node_cert and endorsed_node_cert are protected by the broad NodeState::lock. Certificate map/global hooks take that lock while executing from KV hook paths. This contributes to the KV maps_lock -> NodeState::lock side of the reported lock inversion.
Separately, NodeClient stores references to both mutable certificate objects, and HTTPNodeClient::make_request() reads them without taking NodeState::lock. Certificate renewal can replace those objects concurrently.
Use a dedicated certificate mutex for both mutable certificate fields, return copies from locked accessors, and make NodeClient own immutable certificate values rather than references. Keep the certificate lock scope narrow and do not hold it while deserializing snapshots, opening frontends, or performing RPCs.
4. Member and user frontends are opened synchronously from KV global hooks
Status: Still applicable.
The endorsed-certificate global hook opens the member frontend while holding NodeState::lock; the service global hook opens the user frontend directly. Global hooks execute during KV commit/compaction paths, where the store may hold maps_lock. RpcFrontend::open() takes open_lock and initializes endpoint handlers, so these calls extend an already sensitive lock chain.
Defer member/user frontend opening to an idempotent task after the hook returns. Keep the node frontend synchronous: #7744 briefly made it asynchronous, but boot and in-process node RPCs require it to be open before their tasks run.
5. transition_service_to_open() takes NodeState::lock before using a transaction
Status: Still applicable.
The method takes NodeState::lock and then accesses several KV maps through the supplied transaction, establishing NodeState::lock -> KV locks. Governance execution can reach the same objects from the opposite direction.
The #7744 audit found that this function's member access is transaction-owned, immutable after startup, or independently synchronized. Remove the broad lock after revalidating that assumption against current code; add narrower synchronization only for any mutable member that now requires it.
6. The snapshot-evidence global hook takes NodeState::lock
Status: Still applicable. This was found during review of #7744 rather than in its initial description.
The SnapshotEvidence global hook takes NodeState::lock to inspect and replace backup_snapshot_fetch_task. Since the hook can run under Store::maps_lock, this recreates the same maps_lock -> NodeState::lock ordering that #7744 was trying to remove. The task destructor also uses the main lock when clearing the owner pointer.
Protect only backup_snapshot_fetch_task with a dedicated mutex. Create/update the pointer under that mutex, then enqueue work after releasing it. The task completion path should use the same dedicated mutex.
Partially Superseded
Consensus role initialization after publication
Status: The unsafe observation window is largely fixed by PR #8060, but the nested startup locking remains relevant to TSAN cleanup.
Current startup still constructs and publishes consensus before calling force_become_primary() or init_as_backup():
#8060 added store readiness and node-state gates so ordinary transactions, ticks, and inbound consensus messages cannot use the object before role initialization completes. That removes the strongest correctness argument for moving role activation into the constructor.
However, startup still takes NodeState::lock and then the AFT state lock. If removing the deadlock suppressions still reports this ordering, use a two-phase factory or initialization API:
Construct an unpublished AFT object.
Seed passive term/index/history/ledger state without externally visible side effects.
Publish it to the store and frontends.
Activate leader-only behavior after wiring is complete and without holding NodeState::lock.
Do not call become_leader(true) from the constructor. Review correctly noted that it can enqueue retired-node cleanup and perform leader-side effects before Store::set_consensus() and frontend setup are complete.
The #7744 review found that snapshot_evidence.version - 1 could underflow at zero. #7775 changed backup snapshot fetching to closed/inclusive semantics and now passes snapshot_evidence.version directly.
Run frontend_test, governance_test, and the partition tests through build-tsan/tests.sh, first with the existing suppressions and then with only the relevant suppression removed.
Save one reproducer or TSAN stack pair for each suppression before changing code. This prevents unrelated lock-order reports from being conflated.
Phase 2: Land narrow lock-order fixes
Move the three ledger-secret dependency reads before LedgerSecrets::lock.
Remove the broad lock from transition_service_to_open() after a current member-access audit.
Add a dedicated mutex for backup_snapshot_fetch_task and remove NodeState::lock from its hook/completion paths.
Rerun the focused TSAN tests after each change.
Phase 3: Fix ownership and publication races
Introduce a dedicated certificate mutex for both node certificate fields.
Make NodeClient own immutable certificate snapshots rather than references.
Publish RpcFrontend consensus/history pointers explicitly, with atomic or otherwise synchronized access.
Add tests for null consensus during startup, forwarding on primary/backup, certificate renewal, and retired-node cleanup.
Phase 4: Remove hook-side work and revisit AFT startup
Defer member/user frontend opening until after global hooks return; keep node frontend opening synchronous.
Re-run TSAN without the deadlock suppressions.
Only if AFT startup ordering is still reported, introduce passive pre-publication state seeding plus post-publication leader activation. Do not port TSAN fixes: Refactor mutex handling #7744's constructor-side leader effects unchanged.
Phase 5: Remove suppressions
Remove the three targeted entries one at a time, with the corresponding focused test green before each removal. Finish with the long-test TSAN configuration and the normal C++ unit/e2e checks affected by startup, governance, recovery, snapshot fetching, and forwarding.
Suggested Change Split
Keep this as several reviewable PRs rather than reviving #7744 wholesale:
Ledger-secret ordering and its suppression evidence.
Frontend consensus/history publication and race suppression removal.
Certificate locking plus NodeClient value ownership.
Snapshot-task and governance lock-order cleanup.
Deferred member/user frontend opening.
AFT startup changes only if the preceding fixes do not eliminate the remaining deadlock reports.
This ordering starts with local, behavior-preserving changes and leaves the highest-risk consensus startup work until TSAN demonstrates that it is still necessary.
Follow up to #7744, updated against head of the repo
Scope
This report assesses PR #7744, "TSAN fixes: Refactor mutex handling" against
mainat commit0f8233cc, inspected on 2026-08-06.PR #7744 is still an open draft and was never merged. Its current head is
da6f6bfa.git cherryreports that none of its non-merge commits landed patch-equivalently onmain, although later PRs independently addressed parts of the same problem space.The assessment is based on the PR description, commits, review comments, current code, and git history. It is a static analysis; a reduced-suppression TSAN run is still needed to confirm each lock-cycle report dynamically.
Summary
main.The three targeted suppressions also remain in
tsan_env_suppressions: two deadlock suppressions coveringstore.handuntyped_map.h, and one race suppression coveringnode/rpc/frontend.h.Still Applicable
1.
LedgerSecretstakes its mutex before acquiring a KV dependencyStatus: Still applicable.
get_latest(),get_latest_and_penultimate(), andget()still acquireLedgerSecrets::lockbefore callingtake_dependency_on_secrets(). The latter opens and reads a KV handle, establishing the orderLedgerSecrets::lock -> KV locks.Evidence:
ledger_secrets.hlines 171-215.PR #7744's focused change is still appropriate: establish the transaction dependency first, then take the local ledger-secrets mutex only while reading the in-memory map.
2.
RpcFrontendraces onconsensusandhistoryStatus: Still applicable.
Both fields remain mutable raw pointers.
update_consensus()andupdate_history()write them without synchronization, while request and tick paths read them concurrently.process(),process_forwarded(), andtick()can all callupdate_consensus().Evidence:
frontend.hlines 41-70frontend.hlines 1126-1161tsan_env_suppressionsline 12.The final #7744 direction is sound: explicitly publish consensus/history to each frontend and use synchronized pointer publication. The implementation must load each pointer once per operation and handle
nullptr, especially in redirect resolution.3. Certificate state has both lock-order and ownership problems
Status: Still applicable.
self_signed_node_certandendorsed_node_certare protected by the broadNodeState::lock. Certificate map/global hooks take that lock while executing from KV hook paths. This contributes to theKV maps_lock -> NodeState::lockside of the reported lock inversion.Separately,
NodeClientstores references to both mutable certificate objects, andHTTPNodeClient::make_request()reads them without takingNodeState::lock. Certificate renewal can replace those objects concurrently.Evidence:
node_state.hlines 401-416node_state.hlines 3241-3363node_client.hlines 12-30http_node_client.hlines 25-35Use a dedicated certificate mutex for both mutable certificate fields, return copies from locked accessors, and make
NodeClientown immutable certificate values rather than references. Keep the certificate lock scope narrow and do not hold it while deserializing snapshots, opening frontends, or performing RPCs.4. Member and user frontends are opened synchronously from KV global hooks
Status: Still applicable.
The endorsed-certificate global hook opens the member frontend while holding
NodeState::lock; the service global hook opens the user frontend directly. Global hooks execute during KV commit/compaction paths, where the store may holdmaps_lock.RpcFrontend::open()takesopen_lockand initializes endpoint handlers, so these calls extend an already sensitive lock chain.Evidence:
node_state.hlines 3291-3363node_state.hlines 3366-3403maps_lock:store.hlines 569-588Defer member/user frontend opening to an idempotent task after the hook returns. Keep the node frontend synchronous: #7744 briefly made it asynchronous, but boot and in-process node RPCs require it to be open before their tasks run.
5.
transition_service_to_open()takesNodeState::lockbefore using a transactionStatus: Still applicable.
The method takes
NodeState::lockand then accesses several KV maps through the supplied transaction, establishingNodeState::lock -> KV locks. Governance execution can reach the same objects from the opposite direction.Evidence:
node_state.hlines 2464-2533.The #7744 audit found that this function's member access is transaction-owned, immutable after startup, or independently synchronized. Remove the broad lock after revalidating that assumption against current code; add narrower synchronization only for any mutable member that now requires it.
6. The snapshot-evidence global hook takes
NodeState::lockStatus: Still applicable. This was found during review of #7744 rather than in its initial description.
The
SnapshotEvidenceglobal hook takesNodeState::lockto inspect and replacebackup_snapshot_fetch_task. Since the hook can run underStore::maps_lock, this recreates the samemaps_lock -> NodeState::lockordering that #7744 was trying to remove. The task destructor also uses the main lock when clearing the owner pointer.Evidence:
node_state.hlines 250-279node_state.hlines 3586-3625Protect only
backup_snapshot_fetch_taskwith a dedicated mutex. Create/update the pointer under that mutex, then enqueue work after releasing it. The task completion path should use the same dedicated mutex.Partially Superseded
Consensus role initialization after publication
Status: The unsafe observation window is largely fixed by PR #8060, but the nested startup locking remains relevant to TSAN cleanup.
Current startup still constructs and publishes consensus before calling
force_become_primary()orinit_as_backup():node_state.hlines 1179-1184node_state.hlines 1628-1719node_state.hlines 2139-2146setup_consensus().#8060 added store readiness and node-state gates so ordinary transactions, ticks, and inbound consensus messages cannot use the object before role initialization completes. That removes the strongest correctness argument for moving role activation into the constructor.
However, startup still takes
NodeState::lockand then the AFT state lock. If removing the deadlock suppressions still reports this ordering, use a two-phase factory or initialization API:NodeState::lock.Do not call
become_leader(true)from the constructor. Review correctly noted that it can enqueue retired-node cleanup and perform leader-side effects beforeStore::set_consensus()and frontend setup are complete.Fixed Independently
Snapshot fetch
version - 1underflowStatus: Fixed by PR #7775.
The #7744 review found that
snapshot_evidence.version - 1could underflow at zero. #7775 changed backup snapshot fetching to closed/inclusive semantics and now passessnapshot_evidence.versiondirectly.Current evidence:
node_state.hlines 3615-3622.No further #7744 work is needed for this item.
Draft-Only Hazards
These review findings apply to #7744's proposed implementation, not to current
main, but any replacement must avoid them:become_leader(true)in the AFT constructor runs leader side effects before consensus is fully wired.consensusin redirect resolution without a null check.The review's
NodeClientreference-race and snapshot-hook lock findings are not draft-only; they remain current-main issues and are listed above.Recommended Plan
Phase 1: Establish focused TSAN baselines
Create a dedicated TSAN build using the CI configuration:
cmake -S . -B build-tsan -GNinja \ -DCMAKE_BUILD_TYPE=Debug \ -DLONG_TESTS=OFF \ -DTSAN=ON \ -DUSE_SNMALLOC=OFF \ -DWORKER_THREADS=2 ninja -C build-tsanRun
frontend_test,governance_test, and the partition tests throughbuild-tsan/tests.sh, first with the existing suppressions and then with only the relevant suppression removed.Save one reproducer or TSAN stack pair for each suppression before changing code. This prevents unrelated lock-order reports from being conflated.
Phase 2: Land narrow lock-order fixes
LedgerSecrets::lock.transition_service_to_open()after a current member-access audit.backup_snapshot_fetch_taskand removeNodeState::lockfrom its hook/completion paths.Phase 3: Fix ownership and publication races
NodeClientown immutable certificate snapshots rather than references.RpcFrontendconsensus/history pointers explicitly, with atomic or otherwise synchronized access.Phase 4: Remove hook-side work and revisit AFT startup
Phase 5: Remove suppressions
Remove the three targeted entries one at a time, with the corresponding focused test green before each removal. Finish with the long-test TSAN configuration and the normal C++ unit/e2e checks affected by startup, governance, recovery, snapshot fetching, and forwarding.
Suggested Change Split
Keep this as several reviewable PRs rather than reviving #7744 wholesale:
NodeClientvalue ownership.This ordering starts with local, behavior-preserving changes and leaves the highest-risk consensus startup work until TSAN demonstrates that it is still necessary.