script: Fix undefined behavior in Clone() -- std::transform writes past end of empty vector
What changed, and why it matters
This commit fixes a programming bug in Bitcoin Core's descriptor code where a vector was reserved but never resized before being written to. Using std::transform with providers.begin() on an empty vector causes undefined behavior because it writes past the end of the vector. The fix uses std::back_inserter to safely append elements. The bug could cause crashes or memory corruption when cloning certain wallet descriptors, but it is not a remote exploit by itself.
Apply the patch. It is a low-risk, correct fix. Users running affected versions should upgrade to a release containing this fix, especially if using descriptor wallets with multisig or Taproot descriptors, to avoid potential crashes or memory corruption during descriptor cloning.
Security signals we found
Undefined behavior due to writing past end of empty vector
Memory corruption potential in descriptor cloning
Fix uses std::back_inserter after reserve()
No explicit security advisory or CVE in commit metadata
Evidence from the diff
In descriptor.cpp, two Clone() implementations (MultisigDescriptor and TRDescriptor) called reserve() on an empty std::vector and then passed begin() to std::transform. Because reserve only allocates capacity without changing size, std::transform writes through an iterator that is not valid for writing the requested number of elements, resulting in undefined behavior. The patch replaces providers.begin()/subdescs.begin() with std::back_inserter, which correctly increases the vector’s size as elements are appended. This is a local correctness/robustness fix in wallet descriptor handling.
Changed components
src/script/descriptor.cppMultisigDescriptor::Clone()TRDescriptor::Clone()Inspect captured patch +2 / −2
diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
index 5bbf7517..19315dd6 100644
--- a/src/script/descriptor.cpp
+++ b/src/script/descriptor.cpp
@@ -1310,7 +1310,7 @@ public:
{
std::vector<std::unique_ptr<PubkeyProvider>> providers;
providers.reserve(m_pubkey_args.size());
- std::transform(m_pubkey_args.begin(), m_pubkey_args.end(), providers.begin(), [](const std::unique_ptr<PubkeyProvider>& p) { return p->Clone(); });
+ std::transform(m_pubkey_args.begin(), m_pubkey_args.end(), std::back_inserter(providers), [](const std::unique_ptr<PubkeyProvider>& p) { return p->Clone(); });
return std::make_unique<MultisigDescriptor>(m_threshold, std::move(providers), m_sorted);
}
};
@@ -1533,7 +1533,7 @@ public:
{
std::vector<std::unique_ptr<DescriptorImpl>> subdescs;
subdescs.reserve(m_subdescriptor_args.size());
- std::transform(m_subdescriptor_args.begin(), m_subdescriptor_args.end(), subdescs.begin(), [](const std::unique_ptr<DescriptorImpl>& d) { return d->Clone(); });
+ std::transform(m_subdescriptor_args.begin(), m_subdescriptor_args.end(), std::back_inserter(subdescs), [](const std::unique_ptr<DescriptorImpl>& d) { return d->Clone(); });
return std::make_unique<TRDescriptor>(m_pubkey_args.at(0)->Clone(), std::move(subdescs), m_depths);
}
};
Why this scored 49/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.