The move-assignment operator for btck::Handle<> in src/kernel/bitcoinkernel_wrapper.h unconditionally called DestroyFunc(m_ptr) before reading the source pointer. On a self-move (h = std::move(h)), this destroys the held resource and then restores the now-dangling pointer via std::exchange(other.m_ptr, nullptr) (since &other == this), which leaves m_ptr pointing at freed memory. The destructor then calls DestroyFunc again on it, resulting in a double-free.
Trace of h = std::move(h) with the old code, where h.m_ptr == P:
DestroyFunc(m_ptr)->delete P.this->m_ptrstill literally stores the now-dangling valueP.std::exchange(other.m_ptr, nullptr)— because&other == this, this reads the danglingPback, writesnullptrtom_ptr, and returnsP.m_ptr = Prestores the dangling pointer.~Handle()later runsDestroyFunc(P)-> double free, UB.
The copy-assignment operator already guards against self-assignment with if (this != &other); the move variant should be symmetric. The standard library requires moved-from objects to be in a valid (at minimum, safely destructible) state, which the previous implementation violated when source and destination alias.
Handle<> is the base class of 16 public types in the kernel C++ API wrapper (Transaction, Block, BlockHeader, ChainParams, Context, Coin, BlockValidationState, ScriptPubkey, TransactionOutput, Txid, OutPoint, TransactionInput, PrecomputedTransactionData, BlockHash, BlockSpentOutputs, TransactionSpentOutputs), so self-move can arise from generic algorithms operating on containers of these types (std::sort, std::remove, erase-remove idioms, etc.).
Fix: mirror the copy-assignment pattern by guarding the move-assignment body with if (this != &other), making a self-move a no-op.
Also extend CheckHandle in src/test/kernel/test_kernel.cpp to exercise self-move-assignment for every Handle-derived type, checking that the stored pointer and the serialized bytes (where applicable) are unchanged.