fix(p2p): serialize access to p2pCtx/p2pCancel (#10839)#10861
Conversation
StopP2P() read and wrote a.p2pCtx/a.p2pCancel without holding a.p2pMutex, and StartP2P() reassigned both fields with no lock at all -- including when RestartP2P() calls it from a background goroutine after releasing the mutex. Both paths are reachable from POST /api/settings (empty p2p_token -> StopP2P, non-empty -> RestartP2P), so concurrent requests race on the same fields. Take a.p2pMutex in StopP2P and around the field publication in StartP2P, factor the shared teardown into stopP2PLocked() so RestartP2P reuses it, and route the goroutine error path through StopP2P instead of touching a.p2pCancel unlocked. Signed-off-by: Anai-Guo <antai12232931@anaiguo.com>
c06382a to
326f3f5
Compare
|
Reviewed this against issue #10839. Verification done:
One pre-existing quirk this PR does not (and does not need to) fix: because CI is green and the branch is mergeable. @mudler this looks good to merge: it closes the exact StopP2P/RestartP2P/StartP2P data race from the issue by putting every access to the shared fields under the existing mutex. |
|
Reviewed against issue #10839 and the surrounding code in Verdict: the race fix is correct and minimal. Good to merge from my side, pinging @mudler for the final call. What I checked:
Two notes, neither blocking:
One housekeeping nit: per |
Fixes #10839.
The race
Application.p2pCtx/p2pCancelare mutated from three places with inconsistent locking:StopP2P()read and wrote both fields without ever takinga.p2pMutex.StartP2P()reassigned both fields with no lock at all (p2p.go:40-41), andRestartP2P()invokes it from a background goroutine — so the write happens afterRestartP2Phas released the mutex.a.p2pCancelunlocked as well.Both entry points are reachable from
POST /api/settings(emptyp2p_token→StopP2P, non-empty →RestartP2P), so two concurrent requests race on the same struct fields (CWE-362), as the reporter's-racebuild shows.The fix
StopP2P()now takesa.p2pMutex.stopP2PLocked()(documented as caller-holds-the-lock) soRestartP2P()reuses it instead of duplicating the cancel/clear/sleep block.StartP2P()takes the mutex around the publication ofctx/cancelonly. The node and discoverer setup below works off the localctxvariable, so the lock is deliberately not held across those network calls — this keepsStopP2Pfrom blocking for the duration of a P2P bring-up.RestartP2Pgoroutine's error path now goes throughStopP2P()rather than touchinga.p2pCanceldirectly.No deadlock is introduced:
RestartP2Pholds the mutex only until it returns, and it does not wait on the goroutine it spawns, so the goroutine'sStartP2Plock acquisition just waits for that unlock.This mirrors the
mitmMutexconvention already used inApplication("serializes Stop+Start").🤖 Generated with Claude Code