Is there an existing issue for this?
- I have searched the existing issues
Current behaviour
Branch/tag: 30.x (v30.2), commit 1fb642e
Root cause
SetThread at proxy.cpp#L319 dereferences connection->m_loop->m_thread_id. The connection pointer, server.m_context.connection, captured at type-context.h#L95 into a deferred sync callback, is already dangling when the callback executes. The same pattern repeats at type-context.h#L112, L129, and L135.
ProxyClientBase avoids this with a disconnect cleanup at proxy-io.h#L461-L468 that sets m_context.connection = nullptr. ProxyServerBase has no equivalent: there is no spot in ~ProxyServerBase() or during construction that registers cleanup. When the Connection is destroyed, every ProxyServer that held a pointer to it is left with a dangling Connection*.
Backtrace
* thread [#3](/bitcoin-bitcoin/3/), name = 'b-capnp-loop', stop reason = hit program assert
* frame [#0](/bitcoin-bitcoin/0/): libsystem_kernel.dylib`__pthread_kill + 8
frame [#1](/bitcoin-bitcoin/1/): libsystem_pthread.dylib`pthread_kill + 296
frame [#2](/bitcoin-bitcoin/2/): libsystem_c.dylib`abort + 124
frame [#3](/bitcoin-bitcoin/3/): libsystem_c.dylib`__assert_rtn + 284
frame [#4](/bitcoin-bitcoin/4/): bitcoin-node`mp::EventLoopRef::operator->(this=0x0000000137808210) const at proxy.h:60:37
frame [#5](/bitcoin-bitcoin/5/): bitcoin-node`mp::SetThread(..., connection=0x0000000137808210, ...) at proxy.cpp:319:5
frame [#6](/bitcoin-bitcoin/6/): bitcoin-node`...PassField...::'lambda'()::operator()()::'lambda'()::operator()(this=0x0000600001b9a148) const at type-context.h:95:62
frame [#7](/bitcoin-bitcoin/7/): bitcoin-node`kj::Function<void ()>::Impl<...>::operator() at function.h:142:14
frame [#8](/bitcoin-bitcoin/8/): bitcoin-node`kj::Function<void ()>::operator() at function.h:119:12
frame [#9](/bitcoin-bitcoin/9/): bitcoin-node`void mp::Unlock<mp::Lock, kj::Function<void ()>&>(...) at util.h:209:5
frame [#10](/bitcoin-bitcoin/10/): bitcoin-node`mp::EventLoop::loop(this=0x0000000154004098) at proxy.cpp:244:13
frame [#11](/bitcoin-bitcoin/11/): bitcoin-node`...CapnpProtocol::startLoop(...)::'lambda'()::operator() const at protocol.cpp:130:21
frame [#14](/bitcoin-bitcoin/14/): ...std::__1::__thread_proxy... at thread.h:214:3
frame [#15](/bitcoin-bitcoin/15/): libsystem_pthread.dylib`_pthread_start + 136
Affected code
proxy-io.h#L461-L468—ProxyClientBasedisconnect cleanup nullsm_context.connection. Servers need this.proxy-io.h#L539-L565—~ProxyServerBase()destructor, no connection cleanup.proxy.cpp#L81—ProxyContext()constructor stores rawConnection*, never cleared.proxy.cpp#L319—SetThread()crash site:connection->m_loop->m_thread_id.type-context.h#L94-L97— capturesserver.m_context.connectioninto deferredsynccallback.proxy.h#L60—EventLoopRef::operator->()whereassert(m_loop)fails.
Any interface method with context :Proxy.Context is a trigger.
/cc @ViniciusCestarii helped me debug this.
Expected behaviour
Allow the other client to continue running and not crash
Steps to reproduce
Two separate client processes each call init.makeMining(), each getting their own ProxyClient<Mining>. One client makes any call (with context :Proxy.Context) and is then killed before the RPC completes. The surviving client keeps calling methods on its mining proxy — its activity on the event loop eventually dequeues the killed client's deferred sync callback, which dereferences the now-destroyed Connection, and then it crashes.
I'm using bitcoin-capnp to test. Running just run-logger in one terminal and just run-tui in another, then killing one of the processes and after a while the crash occurs.
Tested both in a linux container (alpine/musl) and on macos (arm64) with these compile options:
cmake -B build \
-DWITH_MINIUPNPC=OFF \
-DBUILD_GUI=OFF \
-DBUILD_TESTS=OFF \
-DBUILD_BENCH=OFF \
-DENABLE_WALLET=ON \
-DBUILD_FUZZ_BINARY=OFF \
-DENABLE_IPC=ON \
-DWITH_ZMQ=ON \
-DBUILD_CLI=ON \
-DBUILD_UTIL=ON \
-DCMAKE_BUILD_TYPE=Debug --fresh
Relevant log output
2026-06-16T19:32:39Z [capnp-loop] [ipc/capnp/protocol.cpp:61] [void ipc::capnp::(anonymous namespace)::IpcLogFn(mp::LogMessage)] [ipc] {bitcoin-node-70358/b-capnp-loop-1568722} IPC server send response [#16](/bitcoin-bitcoin/16/) Mining.waitTipChanged$Results
2026-06-16T19:32:39Z [capnp-loop] [ipc/capnp/protocol.cpp:61] [void ipc::capnp::(anonymous namespace)::IpcLogFn(mp::LogMessage)] [ipc:info] {bitcoin-node-70358/b-capnp-loop-1568722} IPC server destroy N2mp11ProxyServerIN3ipc5capnp8messages6MiningEEE
Assertion failed: (m_loop), function operator->, file proxy.h, line 60.
with debug=ipc and logips=1,logsourcelocations=1,logthreadnames=1,logtimestamps=1
How did you obtain Bitcoin Core
Compiled from source
What version of Bitcoin Core are you using?
v30.2.0
Operating system and version
linux container (alpine/musl - orbkit) and on macos (arm64)
Machine specifications
MacOS 15.7.3 - arm64