SendRecipientsListModel::clear() (qml/models/sendrecipientslistmodel.cpp) deletes each SendRecipient in a loop while its pointer is still in m_recipients; the list is only cleared after the delete loop. During the surrounding beginResetModel()/endResetModel() signal emissions, QML re-enters WalletQmlModel (for example through a coin-control or fee binding) and calls scheduleFeeEstimates(), which runs BuildRecipients() over m_recipients and dereferences the freed recipients.
ASAN on an unmodified qt6 build (99618b0) confirms the use-after-free: freed in SendRecipient::~SendRecipient via clear(), used in BuildRecipients via scheduleFeeEstimates invoked from QML.
clearToFront() in the same file has the same delete-before-remove shape, and additionally removes rows without beginRemoveRows/endRemoveRows (it only emits countChanged), a model-contract violation that desyncs any attached view.
Impact: reproduces intermittently (roughly 3 to 7 percent of runs) when importing a multi-recipient PSBT, which is the recurring qml_test_psbt.py CI failure at the "multi-recipient PSBT submitted" step. It is also reachable from transaction discard and the normal Send page teardown, so it can crash the app during ordinary send flows. #825 (crash on send, with a cascade of null-property TypeErrors from Send.qml during post-send teardown) and #841 (intermittent crash sending a labeled multi-recipient transaction, not reproducible on demand) are consistent with this defect: multi-recipient, teardown-timed, and intermittent.
Suggested fix: detach the container before destroying elements (swap-then-clear, or deleteLater) in both clear() and clearToFront(), and add the missing row-removal notifications to clearToFront() (mirroring the existing remove()).
Related: #762 proposes separating imported-PSBT state from the send draft, which would remove the entanglement at the root; the memory-safety fix stands on its own and is needed regardless.
SendRecipientsListModel::clear() (qml/models/sendrecipientslistmodel.cpp) deletes each SendRecipient in a loop while its pointer is still in m_recipients; the list is only cleared after the delete loop. During the surrounding beginResetModel()/endResetModel() signal emissions, QML re-enters WalletQmlModel (for example through a coin-control or fee binding) and calls scheduleFeeEstimates(), which runs BuildRecipients() over m_recipients and dereferences the freed recipients.
ASAN on an unmodified qt6 build (99618b0) confirms the use-after-free: freed in SendRecipient::~SendRecipient via clear(), used in BuildRecipients via scheduleFeeEstimates invoked from QML.
clearToFront() in the same file has the same delete-before-remove shape, and additionally removes rows without beginRemoveRows/endRemoveRows (it only emits countChanged), a model-contract violation that desyncs any attached view.
Impact: reproduces intermittently (roughly 3 to 7 percent of runs) when importing a multi-recipient PSBT, which is the recurring qml_test_psbt.py CI failure at the "multi-recipient PSBT submitted" step. It is also reachable from transaction discard and the normal Send page teardown, so it can crash the app during ordinary send flows. #825 (crash on send, with a cascade of null-property TypeErrors from Send.qml during post-send teardown) and #841 (intermittent crash sending a labeled multi-recipient transaction, not reproducible on demand) are consistent with this defect: multi-recipient, teardown-timed, and intermittent.
Suggested fix: detach the container before destroying elements (swap-then-clear, or deleteLater) in both clear() and clearToFront(), and add the missing row-removal notifications to clearToFront() (mirroring the existing remove()).
Related: #762 proposes separating imported-PSBT state from the send draft, which would remove the entanglement at the root; the memory-safety fix stands on its own and is needed regardless.