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

Aperta
#378 0 commenti 1 reazione 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@xyzconstant ci sta già lavorando.

Dal 25/10/2026.

  • #382 di @xyzconstant — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
40/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
cpp

Direzione di ricerca

Start from include/mp/proxy-io.h (the ProxyServerBase::invokeDestroy around lines 694-700) and include/mp/proxy-types.h ServerDestroy near line 576, then follow ProxyMethodTraits::invoke in include/mp/proxy.h near line 291 where m_impl.get() is dereferenced unchecked. The path to change is serverInvoke in mp/proxy-io.cpp: give each ProxyServer a mutex, an active-call count incremented on request arrival and decremented when the promise settles, and a destroyed flag. Done looks like: a method after destroy raises "method called after destroy", destroy refuses while calls are in flight, and the behavior is documented in doc/design.md; the mptest makeCallback case in the issue is the reproduction.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug hardening

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

Origin

Both have been there since the libmultiprocess initial commit (bitcoin-core/libmultiprocess@06e1045baba0, 2019). Its invokeDestroy() already deleted the implementation and nulled the pointer without a lock, method dispatch already called through it without a check, and calls already ran on per-connection worker threads, so a destroy could run during one. m_impl is a std::shared_ptr now, and its reset() in invokeDestroy() behaves the same way. The bugs became reachable from clients other than the libmultiprocess C++ client when Bitcoin Core added -ipcbind (bitcoin/bitcoin#30509, 2024) and gave BlockTemplate a destroy method (bitcoin/bitcoin#31288, 2024, in v29.0). waitNext (bitcoin/bitcoin#31283, 2025, first released in v30.0) widened the destroy-during-call window from a short call to the client's whole timeout. Release binaries include IPC from v30.0.

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 () -> (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

Lingua principale
C++
Stelle
62
Fork
58
Merge medio
6g 11h
PR unite (30g)
7

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di bitcoin-core/libmultiprocess

Tutte le issue di bitcoin-core/libmultiprocess

Issue simili

Altre issue su C++

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.