Skip to content

qt: Closing transaction details after wallet unload can crash #964

Description

@l0rinc

After Rob's wallet-creation finding, I created a Windows VM and asked Codex desktop to try 500 GUI scenarios it considered important. This finding came from that exploration.
This is a locally reproduced issue, the details were given to an AI to convert into an issue, treat everything as a hint to investigate, not as final truth.

Current behaviour

Transaction details windows remain open after their wallet is closed. Closing one of those detached windows can crash bitcoin-qt.

In the Windows VM exploration, two transaction details windows remained readable after their wallet was closed. Closing one was the last GUI action before bitcoin-qt disappeared. The Windows Application log records an access violation in PID 7888 at 2026-09-07 06:07:16 UTC:

Faulting application: bitcoin-qt.exe 31.99.0.0
Exception code:       0xc0000005
Fault offset:         0x0000000000108d29
Application path:     C:\Users\Public\bitcoin-gui-260907b\bin\bitcoin-qt.exe

The debug log ends after Releasing wallet encrypted-restored-155.. and has no clean-shutdown entry.

The Windows dump was not symbolized, but a focused AddressSanitizer reproduction of the same lifetime sequence on macOS confirms a heap-use-after-free. TransactionView is deleted while its details window remains alive. When the details window is later destroyed, its callback calls m_opened_dialogs.removeOne() through the deleted TransactionView.

Expected behaviour

Closing a wallet should close its transaction details windows. Destroying a details window must not invoke a callback whose TransactionView has already been destroyed.

Steps to reproduce

Grouped regtest setup and manual reproduction

From an unpatched source checkout with the GUI build dependencies installed, run the following in bash or zsh. It launches a fresh regtest instance, creates a wallet, and adds a transaction that can be opened in the GUI.

{ cmake -B build-gui-wallet-unload -DBUILD_GUI=ON && cmake --build build-gui-wallet-unload -j10 --target bitcoin-qt bitcoin-cli; } >/dev/null 2>&1 &&
{
    gui_datadir=$(mktemp -d)
    gui_rpc() { build-gui-wallet-unload/bin/bitcoin-cli -regtest -datadir="$gui_datadir" -rpcport=19577 "$@"; }
    build-gui-wallet-unload/bin/bitcoin-qt -regtest -datadir="$gui_datadir" -server -rpcport=19577 \
        -noconnect -nolisten -discover=0 -fallbackfee=0.00001 >"$gui_datadir/gui.log" 2>&1 &
    gui_rpc -rpcwait -rpcwaittimeout=30 createwallet repro &&
    gui_address=$(gui_rpc -rpcwallet=repro getnewaddress) &&
    gui_rpc -rpcwallet=repro generatetoaddress 101 "$gui_address" >/dev/null &&
    gui_rpc -rpcwallet=repro sendtoaddress "$gui_address" 1 >/dev/null &&
    gui_rpc -rpcwallet=repro generatetoaddress 1 "$gui_address" >/dev/null
}

In the GUI:

  1. Open Transactions and double-click the Sent to entry for the 1 BTC payment.
  2. Leave the transaction details open.
  3. Choose File > Close Wallet and confirm closing repro.
  4. Observe that the transaction details remain open, then close that window.

The native crash may depend on when Qt processes the deferred deletion. Running the same sequence under AddressSanitizer reports the heap-use-after-free directly.

Stop the temporary instance when finished:

gui_rpc stop

How did you obtain Bitcoin Core?

Compiled from source.

What version of Bitcoin Core are you using?

master@a174dd4017cf97f283b1848bb31998bfa28465e9.

Operating system and version

The native crash occurred on Windows Server 2025 Datacenter 10.0.26100 x64 in a VM with Qt 6.11.2. The AddressSanitizer reproduction used macOS 26.5, Qt 6.11.2 and Apple LLVM 21.

Additional context

TransactionView::showDetails() creates each TransactionDescDialog as a top-level window and stores it in m_opened_dialogs. Its destroyed callback captures this without a QObject context. Closing a wallet deletes WalletView and its TransactionView, but the top-level details windows survive. Destroying one later runs the callback against the freed view.

The focused AddressSanitizer trace reports the invalid access in this path:

QList<TransactionDescDialog*>::removeOne()
TransactionView::showDetails()::$_0::operator()()
QObject::destroyed(QObject*)
TransactionDescDialog::~TransactionDescDialog()

The closest existing work I found is related but does not fix this lifetime:

  • #941 closes details when switching wallets, but it does not close them from TransactionView destruction or add a lifetime context to the callback.
  • #817 limits multiple details windows for one transaction, but retains the callback without a context.
  • #835 fixed a different crash caused by removing the same wallet model twice.
Suggested patch (optional)

This is the lifetime part of the local patch and is a possible starting point. Closing the owned dialogs handles the visible stale windows, while using the view as the callback context prevents the callback from outliving it.

diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp
index 0cb9b43b84..cf753f5c73 100644
--- a/src/qt/transactionview.cpp
+++ b/src/qt/transactionview.cpp
@@ -191,6 +191,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa
 
 TransactionView::~TransactionView()
 {
+    closeOpenedDialogs();
     QSettings settings;
     // Rename this cache when adding or removing columns.
     settings.setValue("TransactionViewHeaderState-2025", transactionView->horizontalHeader()->saveState());
@@ -501,7 +502,7 @@ void TransactionView::showDetails()
         TransactionDescDialog *dlg = new TransactionDescDialog(selection.at(0));
         dlg->setAttribute(Qt::WA_DeleteOnClose);
         m_opened_dialogs.append(dlg);
-        connect(dlg, &QObject::destroyed, [this, dlg] {
+        connect(dlg, &QObject::destroyed, this, [this, dlg] {
             m_opened_dialogs.removeOne(dlg);
         });
         dlg->show();

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions