Summary
SignMuSig2() in src/script/sign.cpp derives an attacker-supplied aggregate pubkey along an attacker-supplied BIP32 path (both from untrusted PSBT fields PSBT_IN_TAP_BIP32_DERIVATION and PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS) and then Assert()s the result matches the script pubkey. A mismatched derivation or a hardened index aborts bitcoind via finalizepsbt, analyzepsbt, or descriptorprocesspsbt, with no wallet loaded.
Two crash vectors:
- A hardened index (bit 31 set) in the path hits
assert((nChild >> 31) == 0)inCPubKey::Derive(pubkey.cpp). - A non-hardened path that derives to the wrong pubkey hits
Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey)atsign.cpp:313.
Assert() uses std::abort() in all builds (NDEBUG is #error'd in check.h), so this terminates the process.
Impact
- Attack surface: authenticated RPC only; not P2P-reachable.
- No wallet required:
finalizepsbt,analyzepsbt, anddescriptorprocesspsbtall reach the vulnerable code viaDUMMY_SIGNING_PROVIDER.analyzepsbtmakes even a read-only inspection a crash trigger. - Gate is trivial: the only precondition is a 4-byte fingerprint match between two attacker-supplied PSBT fields.
- Affected versions:
v31.0rc1,v31.0rc2, andmaster. Not present in any GA release. Introduced in commit4a273edda0(PR #29675). This is release-blocking forv31.0GA. - Severity: DoS — instant node abort. No memory corruption, no key or fund compromise.
Reproduction
On master (2d5ab09f0d at the time of testing):
bitcoind -regtest -disablewallet -daemon
# Variant A — non-hardened path, hits sign.cpp:313 Assert
bitcoin-cli -regtest finalizepsbt "cHNidP8BAF4CAAAAAaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqAAAAAAAAAAAAAbiCAQAAAAAAIlEgeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5gAAAAAAAEBK6CGAQAAAAAAIlEgeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5ghFnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYCQAGr9RrAAAAAAEXIHm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYIhoCxgR/lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuUhAvkwigGSWMMQSTRPhfidUim1MchFg2+ZsIYB8RO84Db5AAA="
# Variant B — hardened index m/0', hits pubkey.cpp:343 assert
bitcoin-cli -regtest finalizepsbt "cHNidP8BAF4CAAAAAaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqAAAAAAAAAAAAAbiCAQAAAAAAIlEgeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5gAAAAAAAEBK6CGAQAAAAAAIlEgeb5mfvncu6xVoGKVzocLBwKb/NstzijZWfKBWxb4F5ghFnm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYCQAGr9RrAAAAgAEXIHm+Zn753LusVaBilc6HCwcCm/zbLc4o2VnygVsW+BeYIhoCxgR/lEHtfW0wRUBulcB82Fx3jkuM7zynq6wJuVxwnuUhAvkwigGSWMMQSTRPhfidUim1MchFg2+ZsIYB8RO84Db5AAA="
Both PSBTs pass decodepsbt cleanly and would not look suspicious in a MuSig2 / coinjoin workflow. bitcoind aborts on each call; bitcoin-cli reports "EOF reached". analyzepsbt with the same PSBTs also aborts.
Fix
Two small changes in SignMuSig2() at src/script/sign.cpp:
- Reject hardened derivation indices before calling
CPubKey::Derive. Public-key derivation only supports non-hardened steps; a hardened index is attacker-controlled input that can never produce a valid derivation, so skipping the aggregate is correct. - Replace the fatal
Assert(XOnlyPubKey(extpub.pubkey) == script_pubkey)withif (...) continue;. A pubkey mismatch means this aggregate is not the one that producedscript_pubkey. The correct behavior is to move on to the next candidate, not to abort the process.
The diff is 6 added / 1 removed lines.
Credit
Found by Claude during an automated review conducted by Anthropic; manually validated and patched by Trail of Bits. Reference: ANT-2026-05771.