Skip to content

Commit 94ca3cb

Browse files
committed
waddrmgr: fix data race between Manager lock/unlock and ScopedKeyManager
Manager.lock() and Manager.Unlock() directly accessed ScopedKeyManager fields (acctInfo, addrs, deriveOnUnlock) while holding only m.mtx. ScopedKeyManager methods accessed those same fields while holding only s.mtx. Since these are different mutexes, there was no mutual exclusion on the shared fields. The fix moves responsibility for ScopedKeyManager's field lifecycle into ScopedKeyManager itself via Lock(), Unlock(), Close(), and ConvertToWatchingOnly() methods. Each acquires s.mtx internally, ensuring all access to the shared fields is consistently protected by s.mtx. Manager delegates to these methods instead of manipulating fields directly. A KeyManagerRoot interface replaces the *Manager back-reference in ScopedKeyManager, preventing it from directly accessing Manager's mutex or internal fields. syncState is moved to a dedicated syncStateMtx, and FetchScopedKeyManager is made lock-free (the scopedManagers map is stable after wallet initialization), removing all s.mtx -> m.mtx lock ordering violations.
1 parent 70a94ea commit 94ca3cb

6 files changed

Lines changed: 320 additions & 208 deletions

File tree

waddrmgr/address.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ func (a *managedAddress) PrivKey() (*btcec.PrivateKey, error) {
401401
// Decrypt the key as needed. Also, make sure it's a copy since the
402402
// private key stored in memory can be cleared at any time. Otherwise
403403
// the returned private key could be invalidated from under the caller.
404-
privKeyCopy, err := a.unlock(a.manager.rootManager.cryptoKeyPriv)
404+
privKeyCopy, err := a.unlock(a.manager.rootManager.CryptoKeyPriv())
405405
if err != nil {
406406
return nil, err
407407
}
@@ -421,7 +421,7 @@ func (a *managedAddress) ExportPrivKey() (*btcutil.WIF, error) {
421421
return nil, err
422422
}
423423

424-
return btcutil.NewWIF(pk, a.manager.rootManager.chainParams, a.compressed)
424+
return btcutil.NewWIF(pk, a.manager.rootManager.ChainParams(), a.compressed)
425425
}
426426

427427
// DerivationInfo contains the information required to derive the key that
@@ -559,7 +559,7 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager,
559559

560560
// First, we'll generate a normal p2wkh address from the pubkey hash.
561561
witAddr, err := btcutil.NewAddressWitnessPubKeyHash(
562-
pubKeyHash, m.rootManager.chainParams,
562+
pubKeyHash, m.rootManager.ChainParams(),
563563
)
564564
if err != nil {
565565
return nil, err
@@ -577,23 +577,23 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager,
577577
// witnessProgram as the sigScript, then present the proper
578578
// <sig, pubkey> pair as the witness.
579579
address, err = btcutil.NewAddressScriptHash(
580-
witnessProgram, m.rootManager.chainParams,
580+
witnessProgram, m.rootManager.ChainParams(),
581581
)
582582
if err != nil {
583583
return nil, err
584584
}
585585

586586
case PubKeyHash:
587587
address, err = btcutil.NewAddressPubKeyHash(
588-
pubKeyHash, m.rootManager.chainParams,
588+
pubKeyHash, m.rootManager.ChainParams(),
589589
)
590590
if err != nil {
591591
return nil, err
592592
}
593593

594594
case WitnessPubKey:
595595
address, err = btcutil.NewAddressWitnessPubKeyHash(
596-
pubKeyHash, m.rootManager.chainParams,
596+
pubKeyHash, m.rootManager.ChainParams(),
597597
)
598598
if err != nil {
599599
return nil, err
@@ -602,7 +602,7 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager,
602602
case TaprootPubKey:
603603
tapKey := txscript.ComputeTaprootKeyNoScript(pubKey)
604604
address, err = btcutil.NewAddressTaproot(
605-
schnorr.SerializePubKey(tapKey), m.rootManager.chainParams,
605+
schnorr.SerializePubKey(tapKey), m.rootManager.ChainParams(),
606606
)
607607
if err != nil {
608608
return nil, err
@@ -635,7 +635,7 @@ func newManagedAddress(s *ScopedKeyManager, derivationPath DerivationPath,
635635
// NOTE: The privKeyBytes here are set into the managed address which
636636
// are cleared when locked, so they aren't cleared here.
637637
privKeyBytes := privKey.Serialize()
638-
privKeyEncrypted, err := s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes)
638+
privKeyEncrypted, err := s.rootManager.CryptoKeyPriv().Encrypt(privKeyBytes)
639639
if err != nil {
640640
str := "failed to encrypt private key"
641641
return nil, managerError(ErrCrypto, str, err)
@@ -894,7 +894,7 @@ func (a *scriptAddress) Script() ([]byte, error) {
894894
// Decrypt the script as needed. Also, make sure it's a copy since the
895895
// script stored in memory can be cleared at any time. Otherwise,
896896
// the returned script could be invalidated from under the caller.
897-
return a.unlock(a.manager.rootManager.cryptoKeyScript)
897+
return a.unlock(a.manager.rootManager.CryptoKeyScript())
898898
}
899899

900900
// Enforce scriptAddress satisfies the ManagedScriptAddress interface.
@@ -905,7 +905,7 @@ func newScriptAddress(m *ScopedKeyManager, account uint32, scriptHash,
905905
scriptEncrypted []byte) (*scriptAddress, error) {
906906

907907
address, err := btcutil.NewAddressScriptHashFromHash(
908-
scriptHash, m.rootManager.chainParams,
908+
scriptHash, m.rootManager.ChainParams(),
909909
)
910910
if err != nil {
911911
return nil, err
@@ -989,9 +989,9 @@ func (a *witnessScriptAddress) Script() ([]byte, error) {
989989
return nil, managerError(ErrLocked, errLocked, nil)
990990
}
991991

992-
cryptoKey := a.manager.rootManager.cryptoKeyScript
992+
cryptoKey := a.manager.rootManager.CryptoKeyScript()
993993
if !a.isSecretScript {
994-
cryptoKey = a.manager.rootManager.cryptoKeyPub
994+
cryptoKey = a.manager.rootManager.CryptoKeyPub()
995995
}
996996

997997
// Decrypt the script as needed. Also, make sure it's a copy since the
@@ -1012,7 +1012,7 @@ func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptIdent,
10121012
switch witnessVersion {
10131013
case witnessVersionV0:
10141014
address, err := btcutil.NewAddressWitnessScriptHash(
1015-
scriptIdent, m.rootManager.chainParams,
1015+
scriptIdent, m.rootManager.ChainParams(),
10161016
)
10171017
if err != nil {
10181018
return nil, err
@@ -1031,7 +1031,7 @@ func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptIdent,
10311031

10321032
case witnessVersionV1:
10331033
address, err := btcutil.NewAddressTaproot(
1034-
scriptIdent, m.rootManager.chainParams,
1034+
scriptIdent, m.rootManager.ChainParams(),
10351035
)
10361036
if err != nil {
10371037
return nil, err

waddrmgr/manager.go

Lines changed: 49 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ type Manager struct {
345345
externalAddrSchemas map[AddressType][]KeyScope
346346
internalAddrSchemas map[AddressType][]KeyScope
347347

348+
syncStateMtx sync.RWMutex
348349
syncState syncState
349350
watchingOnly atomic.Bool
350351
birthday time.Time
@@ -426,42 +427,25 @@ func (m *Manager) IsWatchOnlyAccount(ns walletdb.ReadBucket, keyScope KeyScope,
426427
//
427428
// TODO(yy): Rename this to wipe memory for priv keys.
428429
func (m *Manager) lock() {
429-
for _, manager := range m.scopedManagers {
430-
// Clear all of the account private keys.
431-
for _, acctInfo := range manager.acctInfo {
432-
if acctInfo.acctKeyPriv != nil {
433-
acctInfo.acctKeyPriv.Zero()
434-
}
435-
acctInfo.acctKeyPriv = nil
436-
}
437-
}
430+
// Mark locked first so callers checking IsLocked() see the locked
431+
// state before we begin zeroing keys.
432+
m.locked.Store(true)
438433

439-
// Remove clear text private keys and scripts from all address entries.
440-
for _, manager := range m.scopedManagers {
441-
for _, ma := range manager.addrs {
442-
switch addr := ma.(type) {
443-
case *managedAddress:
444-
addr.lock()
445-
case *scriptAddress:
446-
addr.lock()
447-
}
448-
}
434+
// Zero private key material in each scoped manager under s.mtx.
435+
for _, sm := range m.scopedManagers {
436+
sm.Lock()
449437
}
450438

451-
// Remove clear text private master and crypto keys from memory.
439+
// Zero manager-level crypto keys.
452440
m.cryptoKeyScript.Zero()
453441
m.cryptoKeyPriv.Zero()
454442
m.masterKeyPriv.Zero()
455-
456-
// Zero the hashed passphrase.
457443
zero.Bytea64(&m.hashedPrivPassphrase)
458444

459445
// NOTE: m.cryptoKeyPub is intentionally not cleared here as the address
460446
// manager needs to be able to continue to read and decrypt public data
461447
// which uses a separate derived key from the database even when it is
462448
// locked.
463-
464-
m.locked.Store(true)
465449
}
466450

467451
// Close cleanly shuts down the manager. It makes a best try effort to remove
@@ -878,6 +862,42 @@ func (m *Manager) ChainParams() *chaincfg.Params {
878862
return m.chainParams
879863
}
880864

865+
// CryptoKeyPub returns the public crypto key used to encrypt/decrypt public
866+
// extended keys and addresses.
867+
func (m *Manager) CryptoKeyPub() EncryptorDecryptor {
868+
return m.cryptoKeyPub
869+
}
870+
871+
// CryptoKeyPriv returns the private crypto key used to encrypt/decrypt private
872+
// key material. The underlying key is zeroed when the manager is locked.
873+
func (m *Manager) CryptoKeyPriv() EncryptorDecryptor {
874+
return m.cryptoKeyPriv
875+
}
876+
877+
// CryptoKeyScript returns the script crypto key used to encrypt/decrypt script
878+
// data. The underlying key is zeroed when the manager is locked.
879+
func (m *Manager) CryptoKeyScript() EncryptorDecryptor {
880+
return m.cryptoKeyScript
881+
}
882+
883+
// SetStartBlock updates the start block if the given block is earlier than the
884+
// current start block. It persists the change to the database and updates
885+
// the in-memory state atomically.
886+
func (m *Manager) SetStartBlock(ns walletdb.ReadWriteBucket,
887+
bs *BlockStamp) error {
888+
889+
m.syncStateMtx.Lock()
890+
defer m.syncStateMtx.Unlock()
891+
892+
if bs.Height < m.syncState.startBlock.Height {
893+
if err := putStartBlock(ns, bs); err != nil {
894+
return err
895+
}
896+
m.syncState.startBlock = *bs
897+
}
898+
return nil
899+
}
900+
881901
// ChangePassphrase changes either the public or private passphrase to the
882902
// provided value depending on the private flag. In order to change the
883903
// private password, the address manager must not be watching-only. The new
@@ -1076,32 +1096,9 @@ func (m *Manager) ConvertToWatchingOnly(ns walletdb.ReadWriteBucket) error {
10761096
m.lock()
10771097
}
10781098

1079-
// This section clears and removes the encrypted private key material
1080-
// that is ordinarily used to unlock the manager. Since the the manager
1081-
// is being converted to watching-only, the encrypted private key
1082-
// material is no longer needed.
1083-
1084-
// Clear and remove all of the encrypted acount private keys.
1099+
// Clear encrypted private key material from each scoped manager.
10851100
for _, manager := range m.scopedManagers {
1086-
for _, acctInfo := range manager.acctInfo {
1087-
zero.Bytes(acctInfo.acctKeyEncrypted)
1088-
acctInfo.acctKeyEncrypted = nil
1089-
}
1090-
}
1091-
1092-
// Clear and remove encrypted private keys and encrypted scripts from
1093-
// all address entries.
1094-
for _, manager := range m.scopedManagers {
1095-
for _, ma := range manager.addrs {
1096-
switch addr := ma.(type) {
1097-
case *managedAddress:
1098-
zero.Bytes(addr.privKeyEncrypted)
1099-
addr.privKeyEncrypted = nil
1100-
case *scriptAddress:
1101-
zero.Bytes(addr.scriptEncrypted)
1102-
addr.scriptEncrypted = nil
1103-
}
1104-
}
1101+
manager.ConvertToWatchingOnly()
11051102
}
11061103

11071104
// Clear and remove encrypted private and script crypto keys.
@@ -1210,64 +1207,9 @@ func (m *Manager) Unlock(ns walletdb.ReadBucket, passphrase []byte) error {
12101207
// Use the crypto private key to decrypt all of the account private
12111208
// extended keys.
12121209
for _, manager := range m.scopedManagers {
1213-
for account, acctInfo := range manager.acctInfo {
1214-
decrypted, err := m.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted)
1215-
if err != nil {
1216-
m.lock()
1217-
str := fmt.Sprintf("failed to decrypt account %d "+
1218-
"private key", account)
1219-
return managerError(ErrCrypto, str, err)
1220-
}
1221-
1222-
acctKeyPriv, err := hdkeychain.NewKeyFromString(string(decrypted))
1223-
zero.Bytes(decrypted)
1224-
if err != nil {
1225-
m.lock()
1226-
str := fmt.Sprintf("failed to regenerate account %d "+
1227-
"extended key", account)
1228-
return managerError(ErrKeyChain, str, err)
1229-
}
1230-
acctInfo.acctKeyPriv = acctKeyPriv
1231-
}
1232-
1233-
// We'll also derive any private keys that are pending due to
1234-
// them being created while the address manager was locked.
1235-
for _, info := range manager.deriveOnUnlock {
1236-
addressKey, _, _, err := manager.deriveKeyFromPath(
1237-
ns, info.managedAddr.InternalAccount(),
1238-
info.branch, info.index, true,
1239-
)
1240-
if err != nil {
1241-
m.lock()
1242-
return err
1243-
}
1244-
1245-
// It's ok to ignore the error here since it can only
1246-
// fail if the extended key is not private, however it
1247-
// was just derived as a private key.
1248-
privKey, _ := addressKey.ECPrivKey()
1249-
addressKey.Zero()
1250-
1251-
privKeyBytes := privKey.Serialize()
1252-
privKeyEncrypted, err := m.cryptoKeyPriv.Encrypt(privKeyBytes)
1253-
privKey.Zero()
1254-
if err != nil {
1255-
m.lock()
1256-
str := fmt.Sprintf("failed to encrypt private key for "+
1257-
"address %s", info.managedAddr.Address())
1258-
return managerError(ErrCrypto, str, err)
1259-
}
1260-
1261-
switch a := info.managedAddr.(type) {
1262-
case *managedAddress:
1263-
a.privKeyEncrypted = privKeyEncrypted
1264-
a.privKeyCT = privKeyBytes
1265-
case *scriptAddress:
1266-
}
1267-
1268-
// Avoid re-deriving this key on subsequent unlocks.
1269-
manager.deriveOnUnlock[0] = nil
1270-
manager.deriveOnUnlock = manager.deriveOnUnlock[1:]
1210+
if err := manager.Unlock(m.cryptoKeyPriv, ns); err != nil {
1211+
m.lock()
1212+
return err
12711213
}
12721214
}
12731215

waddrmgr/manager_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"os"
1414
"reflect"
15+
"sync"
1516
"testing"
1617
"time"
1718

@@ -3294,3 +3295,51 @@ func TestManagedAddressValidation(t *testing.T) {
32943295
}
32953296

32963297
}
3298+
3299+
// TestLockUnlockRace verifies there is no data race between Manager.Lock()
3300+
// and concurrent ScopedKeyManager.NextExternalAddresses calls.
3301+
func TestLockUnlockRace(t *testing.T) {
3302+
teardown, db, mgr := setupManager(t)
3303+
defer teardown()
3304+
3305+
err := walletdb.View(db, func(tx walletdb.ReadTx) error {
3306+
ns := tx.ReadBucket(waddrmgrNamespaceKey)
3307+
return mgr.Unlock(ns, privPassphrase)
3308+
})
3309+
require.NoError(t, err)
3310+
3311+
scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0086)
3312+
require.NoError(t, err)
3313+
3314+
const iterations = 50
3315+
3316+
var wg sync.WaitGroup
3317+
wg.Add(2)
3318+
3319+
go func() {
3320+
defer wg.Done()
3321+
for i := 0; i < iterations; i++ {
3322+
_ = walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
3323+
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
3324+
_, err := scopedMgr.NextExternalAddresses(
3325+
ns, 0, 1,
3326+
)
3327+
return err
3328+
})
3329+
}
3330+
}()
3331+
3332+
go func() {
3333+
defer wg.Done()
3334+
for i := 0; i < iterations; i++ {
3335+
_ = mgr.Lock()
3336+
3337+
_ = walletdb.View(db, func(tx walletdb.ReadTx) error {
3338+
ns := tx.ReadBucket(waddrmgrNamespaceKey)
3339+
return mgr.Unlock(ns, privPassphrase)
3340+
})
3341+
}
3342+
}()
3343+
3344+
wg.Wait()
3345+
}

0 commit comments

Comments
 (0)