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

issue l0rinc opened this issue on September 7, 2026
  1. l0rinc commented at 7:29 PM on September 7, 2026: contributor

    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

    <details> <summary>Grouped regtest setup and manual reproduction</summary>

    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
    

    </details>

    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.

    <details> <summary>Suggested patch (optional)</summary>

    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();
    

    </details>


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/gui. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-08 07:57 UTC