Skip to content

wallet: close races around CWallet::EncryptWallet - #183

Merged
kiwidream merged 2 commits into
1.x.xfrom
kiwidream/encrypt-wallet-races
Sep 28, 2026
Merged

kiwidream merged 2 commits into
1.x.xfrom
kiwidream/encrypt-wallet-races

Conversation

@kiwidream

@kiwidream kiwidream commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Summary

This closes three races around CWallet::EncryptWallet from #179. They predate #178, but #178's asynchronous GUI encryption makes it easier for a GUI encryption and an encryptwallet RPC to overlap.

  1. Check-then-encrypt was not atomic. The already-encrypted and PQC-validation checks ran before LOCK2. An encryptwallet RPC overlapping the GUI worker could pass them too, double-encrypt and hit assert(false).
    • EncryptWallet now makes both checks under LOCK2(m_relock_mutex, cs_wallet), after the key derivation, which still runs unlocked on the GUI and interfaces path.
    • The RPC takes its locks before its own check, so an overlapping or repeated call now fails deterministically with -15 RPC_WALLET_WRONG_ENC_STATE ("running with an encrypted wallet") instead of -16 "Failed to encrypt". -16 now means a real database failure. This is a user-visible RPC error-code change.
  2. A master key leaked into memory on TxnBegin failure. The key now enters mapMasterKeys and nMasterKeyMaxID only after TxnBegin and WriteMasterKey both succeed. A failed WriteMasterKey now aborts the transaction. Post-commit behaviour is unchanged.
  3. Unlocked readers of the SPKM maps.
    • GetActiveScriptPubKeyMans, IsActiveScriptPubKeyMan and ConnectScriptPubKeyManNotifiers now require cs_wallet, via EXCLUSIVE_LOCKS_REQUIRED plus AssertLockHeld.
    • IsHDEnabled now locks internally.
    • The two unlocked callers, AddWallet and CWallet::Create, are fixed.
    • The only GUI caller of hdEnabled is already skipped while encrypting (qt: keep Encrypt Wallet responsive during existing-wallet encryption #178).
  4. GUI follow-up. If another request wins the race, the Encrypt Wallet dialog now says "The wallet was already encrypted by another request. Your passphrase was not used." Before, it said the wallet was not encrypted. It reads the status after the worker has returned.

There are no wallet format, encryption semantics or consensus changes.

Fixes #179.

Testing

  • Built locally.

  • Ran focused unit or functional tests for the changed area.

  • Ran lint or formatting checks relevant to this change.

  • Not run. Reason:

  • New wallet_encryption_tests suite, 3 deterministic cases using a test-only hook between key derivation and locking, plus the mock database:

    • two overlapping EncryptWallet calls;
    • TxnBegin failure;
    • WriteMasterKey failure.

    Against the base production code these fail with SIGABRT at assert(false), with IsCrypted/mapMasterKeys left set, and with a fatal flag-write error respectively.

  • wallet_encryption.py adds a two-thread overlapping encryptwallet case. It got -16 on 6/6 base runs and gets -15 with the fix.

  • New Qt test TestEncryptWalletAlreadyEncryptedByAnotherRequest covers the dialog message and fails without the fix. The race test now fails instead of hanging if its first assertion fails; verified with a forced failure.

  • Other suites: test_qbit wallet suites, wallet_encryption.py (also with --usecli), wallet_keypool.py and test_qbit-qt (offscreen, including the qt: keep Encrypt Wallet responsive during existing-wallet encryption #178 tests) pass.

  • clang 20 with -Wthread-safety -Wthread-safety-precise -Werror=thread-safety and DEBUG_LOCKORDER: all 65 wallet and wallet-test objects rebuild clean. Its wallet, spend, psbt and feebumper suites pass, and wallet_encryption_tests passes 15/15 repeats.

  • Adversarial review: an independent review traced every caller of the changed accessors for lock-order inversions and GUI freezes, checked every EncryptWallet failure path and the RPC lock scope, and reproduced the revert proofs. It found no blocker; its nits are addressed in 91f83c5.

  • Not run: TSan, Debug-config, macOS/Windows, the Qt translation units under clang thread-safety (the clang build is GUI-off), and a manual GUI+RPC overlap on a populated wallet. .ts translation files weren't regenerated for the new string.

Follow-up (not in this PR): GetAllScriptPubKeyMans, GetScriptPubKeyMan and IsCrypted remain unannotated unlocked readers.

Target Branch

  • This PR targets main or a maintainer-requested release branch such as 0.1.x.

Targets 1.x.x for v1.0.1 (#174). It merges cleanly with #182.

Risk / Review Notes

  • Consensus, script, crypto, wallet, P2P, release, CI, or security-sensitive behavior changed.
  • No consensus, script, crypto, wallet, P2P, release, CI, or security-sensitive behavior changed.

Notes: this changes wallet encryption locking and failure handling, plus lock requirements on SPKM accessors. The RPC error code for an overlapping or repeated encryptwallet changes from -16 to -15, and the v1.0.1 release notes should mention it.

Docs / Process Impact

Choose exactly one:

  • I updated public docs because this PR changes user-visible behavior, integration guidance, release/process guidance, or expected validation.
  • No public docs update needed. Reason: the one user-visible change, the -15 code for an already-encrypted wallet, will be added to the v1.0.1 release notes in release: prepare v1.0.1 #181.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

kiwidream and others added 2 commits September 25, 2026 16:07
EncryptWallet decided whether the wallet could be encrypted before it
took the wallet locks, and the key derivation that follows is tuned to
take a noticeable amount of time. A second EncryptWallet caller in that
window, such as the encryptwallet RPC overlapping the GUI worker added
by #178, passed the same check, encrypted the already encrypted keys
and hit the assert in the encryption loop.

Move the already-encrypted and PQC-validation checks under the wallet
locks, after the key derivation. The derivation still runs unlocked so
it does not stall other wallet users, while the check and the
encryption now happen under one lock acquisition, so the losing caller
returns false without touching the wallet. The encryptwallet RPC takes
the same locks before its own already-encrypted check, so an
overlapping RPC call reports the wallet as already encrypted once the
other encryption has finished, instead of a generic failure.

The new master key was inserted into mapMasterKeys before the database
transaction began, so a failed TxnBegin left the wallet reporting
IsCrypted() with nothing written. Insert it only once the transaction
is open and the master key record has been written; a failed write
aborts the transaction. Post-commit failures are unchanged.

IsHDEnabled and GetActiveScriptPubKeyMans read the active manager maps
without cs_wallet, which EncryptWallet replaces under the lock.
GetActiveScriptPubKeyMans, IsActiveScriptPubKeyMan and
ConnectScriptPubKeyManNotifiers now require cs_wallet; IsHDEnabled and
the two callers that did not hold it take the lock.

A test hook parks one EncryptWallet caller between key derivation and
the locks while another completes. Further unit tests cover the failed
TxnBegin and the failed master key write, and wallet_encryption.py
runs two overlapping encryptwallet calls.

Fixes #179

Co-Authored-By: Claude <noreply@anthropic.com>
When the encryptwallet RPC encrypts the wallet while the GUI's request is
under way, the GUI's request fails, but the wallet is encrypted with the
RPC's passphrase. The Encrypt Wallet dialog still reported that the
wallet was not encrypted. After a failed encryption the dialog now reads
the encryption status, which the model refreshes once the worker has
returned, and says the wallet was already encrypted by another request
and the entered passphrase was not used. A genuinely unencrypted wallet
keeps the existing message. The synthetic Qt wallet now refuses an
already encrypted wallet as CWallet::EncryptWallet does, and a Qt test
covers the new message.

ConcurrentEncryptWalletLoserFailsCleanly hung instead of failing when
the winning encryption's BOOST_REQUIRE failed: the loser's std::async
future was destroyed, and waited for the parked loser, before anything
released it. A scope guard declared after the future now releases the
loser on unwind.

The EncryptWallet comment said the key derivation does not stall other
wallet users, but the encryptwallet RPC holds the wallet locks for the
whole call; only callers that do not hold them, such as the GUI worker,
avoid the stall. Say so.

Fixes #179

Co-Authored-By: Claude <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T20:39:26.043487Z 91f83c5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kiwidream
kiwidream merged commit 7ce1208 into 1.x.x Sep 28, 2026
37 checks passed
kiwidream added a commit that referenced this pull request Oct 22, 2026
#176, #177 and #178 merged into 1.x.x, so drop their pending-merge
markers. #182 and #183 merged after the notes were drafted; add them,
and call out #183's encryptwallet error-code change as a behavior
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kiwidream kiwidream mentioned this pull request Oct 22, 2026
9 of 18 tasks
@djh58 djh58 mentioned this pull request Oct 26, 2026
6 of 15 tasks
kiwidream added a commit that referenced this pull request Oct 27, 2026
Cover every pull request merged to 1.x.x since the signed v1.0.0 tag,
through #252. List the behavior changes first: the -fallbackfee default
(#151), fee-only send entries (#150), and the encryptwallet overlap
error code moving from -16 to -15 (#183).

Record the release's known limitations: the unsigned-platform waiver
(#70), the depth of the fuzz evidence (#173), and scheduled-workflow
deployment (#152, #158). The required P2MR v1 conformance statement
names the signed tag target and the oracle report digest, so the
in-tree notes point to the release page, and the publish-time notes
file carries the exact statement.

Fold the #150 and #151 fragments into these notes and remove them.

Refs #174

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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