Skip to content

GUI/NetWatch: Fix heap corruption from off-thread model mutation#330

Open
kwsantiago wants to merge 3 commits into
bitcoinknots:29.x-knotsfrom
privkeyio:fix-netwatch-gui-thread-race
Open

GUI/NetWatch: Fix heap corruption from off-thread model mutation#330
kwsantiago wants to merge 3 commits into
bitcoinknots:29.x-knotsfrom
privkeyio:fix-netwatch-gui-thread-race

Conversation

@kwsantiago

Copy link
Copy Markdown

Fixes #306.

The Net Watch model was mutated from validation-interface callbacks running on the scheduler thread (BlockConnected, TransactionAddedToMempool), while the GUI thread read and painted the same model. Qt models are single-threaded; the concurrent beginInsertRows/beginRemoveRows/dataChanged calls corrupted Qt-internal selection state, producing the heap corruption in #306. The callbacks now marshal the model mutations onto the GUI thread with a queued invocation, as GuiBlockView already does.

Reproduced on release and ASan builds by flooding the mempool while the Net Watch view painted; deterministic crash before, clean after (ASan clean through churn and shutdown).

Two adjacent memory-safety bugs found in the same file while verifying, fixed as separate commits:

  • setClientModel(nullptr) deleted the validation interface without unregistering it, leaving a dangling pointer dereferenced during shutdown flush (use-after-free).
  • LogEntry::clear() freed ::operator new memory with a delete-expression on uint8_t* (mismatched deallocation, UB under sized delete).

BlockConnected and TransactionAddedToMempool run on the scheduler thread
and mutated the Qt model directly (beginInsertRows/beginRemoveRows/
dataChanged) while the GUI thread painted it, corrupting Qt-internal
selection state and causing heap corruption / crashes.  Marshal the model
mutations to the GUI thread via a queued invocation, as GuiBlockView does.

Fixes bitcoinknots#306
setClientModel(nullptr) deleted m_validation_interface without
unregistering it from ValidationSignals, leaving a dangling pointer that
was dereferenced during shutdown callback flushing (use-after-free).
Unregister first, mirroring the destructor.
m_data is allocated with ::operator new but freed with a delete-expression
on a uint8_t*, which is undefined behaviour (mismatched deallocation size
under sized delete).  Free it with the matching ::operator delete.
@pdath

pdath commented Jul 14, 2026

Copy link
Copy Markdown

tACK 191d0fd

I reviewed the change with Code Rabbit and it found no issues.

I built PR330 on Ubuntu 24.04 running on ARMv8-A (64-bit) with gcc for Windows using guix.

git clone https://github.com/bitcoinknots/bitcoin.git pr330
cd pr330
git fetch origin pull/330/head:pr330
git switch pr330
cmake -B build -DCMAKE_BUILD_TYPE=Release -DRDTS_CONSENT=IMPLICIT
nice cmake --build build -j$(nproc)

I tested for memory leaks, and found none.

build/bin/test_bitcoin --detect_memory_leaks

I then built using guix.

env HOSTS='x86_64-w64-mingw32' ./contrib/guix/guix-build

And tested with:

bitcoin-qt.exe -signet

Everything worked as expected in the "Watch network activity" window.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GUI: Net Watch view crashes with heap corruption while painting (NetWatchLogModel::data) — possible regression/variant of #4

2 participants