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.

@luke-jr luke-jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crACK

@luke-jr luke-jr added this to the 29.4 milestone Jul 25, 2026
luke-jr pushed a commit that referenced this pull request Jul 25, 2026
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 #306

Github-Pull: #330
Rebased-From: d4c389b
luke-jr pushed a commit that referenced this pull request Jul 25, 2026
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.

Github-Pull: #330
Rebased-From: 26ec084
luke-jr pushed a commit that referenced this pull request Jul 25, 2026
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.

Github-Pull: #330
Rebased-From: 191d0fd
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

3 participants