Skip to content

Fix cross-lock clobber that could reissue an in-flight nonce - #273

Draft
damilolaedwards wants to merge 1 commit into
ethpandaops:masterfrom
damilolaedwards:fix/nonce-cross-lock-clobber
Draft

Fix cross-lock clobber that could reissue an in-flight nonce#273
damilolaedwards wants to merge 1 commit into
ethpandaops:masterfrom
damilolaedwards:fix/nonce-cross-lock-clobber

Conversation

@damilolaedwards

Copy link
Copy Markdown
Contributor

Summary

  • Nonce allocation and confirmation tracking each take their own lock, and the confirmation path was doing a plain load-then-store on the pending nonce counter. A confirmation landing in between another goroutine's load and store could get its update overwritten with a stale, lower value, opening a window for the same nonce to be handed out twice.
  • Replaced the load-then-store with a compare-and-swap retry loop, extracted as a small helper on the wallet, so the update can never move the counter backwards regardless of how the two paths interleave.

Test plan

  • Added deterministic tests of the compare-and-swap helper covering both directions (already ahead, needs to advance).
  • Added a concurrent smoke test under -race for general memory safety.
  • Note: a goroutine stress test could not reliably reproduce the original race even with the bug present, since the vulnerable window sits between two adjacent atomic operations with no blocking call in between. The deterministic tests of the decision logic are the real regression coverage here.

GetNextNonce advances pendingTxCount with an atomic Add under nonceMutex.
The confirmation path advances the same counter under a different lock
(txNonceMutex) with a plain Load-then-Store, which is not atomic as a
whole: a Store computed from a stale Load could clobber a concurrent Add
and roll the counter backwards, handing a later caller a nonce that was
already allocated to an in-flight transaction. Only one of the two ever
lands on chain, and the tool's own record of what it sent versus confirmed
goes quietly out of sync.

Replaced the Load-then-Store with a CompareAndSwap retry loop so the update
only ever lands against the value it was actually computed from, extracted
into its own method since it is one half of a race with GetNextNonce in the
same file.

This bug's trigger window sits between two adjacent atomic operations with
no blocking call in between, which turns out to be too narrow for a
goroutine stress test to reproduce reliably even with the bug still present
(confirmed while writing the tests). The fix is verified with direct,
deterministic checks of the update logic instead, which is what
CompareAndSwap's own atomicity guarantee makes sufficient here, plus a
general concurrent-access smoke test under the race detector.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant