Calling an IPC object after or during destroy() makes the server use a null or freed implementation #378

issue ryanofsky opened this issue on October 15, 2026
  1. ryanofsky commented at 4:22 AM on October 15, 2026: collaborator

    A client that keeps using an IPC object after calling its destroy method crashes the server. In Bitcoin Core, a mining client that calls BlockTemplate.destroy and then calls another BlockTemplate method makes bitcoin-node call a virtual method through a null pointer and die with SIGSEGV. A client that calls destroy from one thread while another of its threads is still inside a method on the same object makes the server free the implementation under that call, which is a use-after-free.

    The libmultiprocess C++ client cannot do either, because it sends destroy only from ~ProxyClientBase, after which no C++ code can call that proxy. A Python (pycapnp) or Rust (capnp-rust) client holds a plain capability and can, for example when a stale task still holds a template.

    Where

    How it plays out in Bitcoin Core

    BlockTemplate and Echo declare destroy (mining.capnp line 34).

    • Call after destroy: methods with a Context (getBlock, submitSolution, waitNext, ...) crash on their worker thread. interruptWait, which has no Context, crashes on the event loop thread. A second destroy is harmless.
    • Destroy during a call: the ProxyServer is the only owner of BlockTemplateImpl, so the reset deletes it. The clearest case is waitNext, which loops for up to the client's timeout reading the template's hashPrevBlock and vTxFees. A client that drops a template on a new tip and destroys it without calling interruptWait first turns every later iteration into a use-after-free. Rust Stratum v2 clients already run several calls on one template from separate threads (bitcoin/bitcoin#33923).

    Under Bitcoin Core's trust model a client that can reach the -ipcbind socket already has unauthenticated RPC access, so this gives a hostile client nothing new. The concern is a buggy non-C++ client taking down the node.

    Possible fix

    Make destroy a checked operation, and document dropping the capability as the normal way to release an object.

    • A method called after destroy throws a clear error ("method called after destroy") instead of calling through a null pointer.
    • destroy throws if calls on the same object are still in progress, instead of freeing the implementation under them. Each ProxyServer would keep a mutex, a count of active calls and a destroyed flag. serverInvoke would increment the count when a request arrives and decrement it when the request's promise settles, so the count covers building the results (which can point into the implementation) and covers methods without a Context, like interruptWait. destroy would not count itself. A refused destroy does not leak: the implementation is freed when the client releases the capability, as it is today.
    • Document the two ways to end an object's life. Dropping the capability is always safe, even with calls in flight: Cap'n Proto keeps the ProxyServer alive until its calls finish, and ~ProxyServerBase then frees the implementation on the async cleanup thread. destroy is for a client that wants the implementation freed synchronously at a known point and knows the object is idle. For example, a mining client discarding a template on a new tip should call interruptWait and drop the template, not call destroy while waitNext may still be returning.

    The libmultiprocess C++ client is unaffected, since it only sends destroy from the proxy destructor, after its own calls on that proxy have returned.

    Alternatives considered: making destroy wait for running calls (a read/write lock) would tie up a server worker thread for as long as a call like waitNext runs, and having each call hold its own reference to the implementation so destroy can return early adds nothing over dropping the capability, which already behaves that way. The privately reported fix also made destroy fail while calls are in progress.

    Reproduction

    Both cases reproduced on 2026-09-26 in mptest against Bitcoin Core master (bitcoin/bitcoin@bfdcd9797cd1a1345bdaf7ee7ef48f94028262da, which carries libmultiprocess v13 in its subtree), the code being the same as on master. The test adds a makeCallback [@25](/bitcoin-core-multiprocess/contributor/25/) () -> (result :FooCallback) method whose returned object is owned only by its ProxyServer, like a BlockTemplate, and sends raw Cap'n Proto requests on the client's capability, as a pycapnp or capnp-rust client could.

    • destroy then call on the same capability: SIGSEGV on the server worker thread; with -DSANITIZERS=address,undefined, proxy.h:289:16: runtime error: member call on null pointer of type 'mp::test::FooCallback'.
    • A hook holds a call inside the implementation while destroy is sent on a second server thread from makeThread. destroy returns while the call is still running; when the call is released, ASan reports heap-use-after-free ... in mp::test::OwnedCallback::call(int), freed by thread ... in ProxyServerBase<...>::invokeDestroy(). Without a sanitizer the call returned -258435454 instead of 102.

    Both happen on every run (the hook forces the interleaving in the second). A test patch is ready and can be attached here or opened as a PR. Details: https://ipcbugs.ofsky.org/bugs/mp-2026-call-after-destroy-null-impl/#reproduction and https://ipcbugs.ofsky.org/bugs/mp-2026-destroy-during-call-race/#reproduction

    Reported privately in an independent security review, 2026-08-13, and publicly in bitcoin/bitcoin#36502 (2026-10-11, found with fuzzamoto), which hits the same null dereference at proxy.h:289 after BlockTemplate.destroy followed by getBlock. The functional test in that report would work as a Bitcoin Core regression test.

    Links

  2. ryanofsky added the label bug on Oct 15, 2026
  3. ryanofsky added the label hardening on Oct 15, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/libmultiprocess. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-28 08:30 UTC

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