use-after-free `EventLoopRef::operator->` during Capnp RPC call #35545

issue johnnyasantoss opened this issue on June 16, 2026
  1. johnnyasantoss commented at 7:46 PM on June 16, 2026: none

    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-L468ProxyClientBase disconnect cleanup nulls m_context.connection. Servers need this.
    • proxy-io.h#L539-L565~ProxyServerBase() destructor, no connection cleanup.
    • proxy.cpp#L81ProxyContext() constructor stores raw Connection*, never cleared.
    • proxy.cpp#L319SetThread() crash site: connection->m_loop->m_thread_id.
    • type-context.h#L94-L97 — captures server.m_context.connection into deferred sync callback.
    • proxy.h#L60EventLoopRef::operator->() where assert(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

  2. fanquake commented at 7:55 PM on June 16, 2026: member
  3. ryanofsky commented at 8:11 PM on June 16, 2026: contributor

    Nice bug report. This sounds like the bug that was fixed https://github.com/bitcoin-core/libmultiprocess/commit/0174450ca2e95a4bd1f22e4fd38d83b1d432ac1f (https://github.com/bitcoin-core/libmultiprocess/pull/240, #34422) so not included in v30.2. Maybe it should be backported?

  4. johnnyasantoss commented at 8:29 PM on June 16, 2026: none

    Just tested on 31.0 and it doesn't happen there.

  5. maflcko commented at 8:32 PM on June 16, 2026: member

    Is there value in back-porting this? The interface is experimental and users are probably better off using the latest major version of Bitcoin Core?

    If this were to be backported (or libmultiprocess was bumped to v8), then there are two more major versions of libmultiprocess (https://github.com/bitcoin-core/libmultiprocess/blob/master/doc/versions.md), so likely someone else is going to report that clang 22 cant' compile and asks for a bump to v9, etc ...

    Just wondering what the approach here should be

  6. ryanofsky commented at 8:36 PM on June 16, 2026: contributor

    I'm also wondering whether or not this is worth backporting. Would want to know more about the use-case that triggered this.

  7. maflcko added this to the milestone 30.3 on Jun 16, 2026
  8. johnnyasantoss commented at 12:23 AM on June 17, 2026: none

    @ryanofsky @maflcko I don't believe we have to backport this as capnp/ipc is still experimental AFAIK. Upgrading to 31.x doesn't seem like a big deal, at least to me.

    For me, I'm developing a general library for all capnp methods and I was simply testing my development with a few examples when I found the bug. I created the mentioned crate to be thread-safer than the alternatives as my goal is to use it on p2poolv2 for the template/mining parts.

    I just felt the need to report so this is known and no one stumbles upon this in "production" in mining infra. Also, reported in the open as ipc is meant to be trusted/internal and a bug like this would be low-severity, at least that's how I understand.

  9. Sjors commented at 7:44 AM on June 17, 2026: member

    I would rather not backport fixes until we mark the interface as no longer experimental. That would also be a good time to introduce backward compatibility tests: #35392.

  10. maflcko commented at 7:51 AM on June 17, 2026: member

    I would rather not backport fixes until we mark the interface as no longer experimental.

    I wonder if the issue here can be closed, or should the docs around the cmake build option be worded stronger about the experimental status and that backport bugfixes are usually done only for the latest major release branch?

  11. ryanofsky commented at 12:17 PM on June 17, 2026: contributor

    Yes, sounds like this issue can be closed. I am glad to have the report though, and hope that documentation about instability would not discourage similar bug reports for old versions from being filed, even if they might be closed without backporting fixes.

    In this case, clients can pretty easily avoid this problem by just not immediately disconnecting after sending requests (ideally they would wait for responses as well). The problem could still happen if a cilent was killed with kill -9 or similar immediately after sending a request, but this should be very unlikely to happen and also might not be sufficient if there aren't also other clients active at the same time. I did write up some more notes about this bug in https://github.com/bitcoin-core/libmultiprocess/pull/240#issuecomment-4729778509 and will close the issue now. Thanks for the report!

  12. ryanofsky closed this on Jun 17, 2026

  13. ryanofsky closed this on Jun 17, 2026


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-06-19 07:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me