type-context: fix async disconnect race condition found by antithesis #349

pull ryanofsky wants to merge 1 commits into bitcoin-core:master from ryanofsky:pr/tsandis changing 6 files +126 −2
  1. ryanofsky commented at 5:46 PM on August 19, 2026: collaborator

    Fix a race condition reported in #348 where if a disconnect happens during an IPC call that uses a worker thread (an IPC call taking an mp.Context parameter), it can trigger a read-write race detected by TSAN, and also theoretically cause a null pointer dereference (described in the commit message).

    The race condition happens because when RpcCallContext::getResults() is called for the first time, it checks the connection state. So currently if there is a disconnect, when the worker thread calls getResults, this can read connection state at the same time capnproto writes as it processes the disconnect.

    Fix this issue by calling getResults once from the event loop thread before executing the IPC call on the worker thread, so the results message pointer will be cached, and future calls to getResults from the worker thread won't access the connection state or have any race condition.

    This is a one-line fix with many comments and a test.

  2. proxy: fix data race between server request threads and disconnect handling
    ThreadSanitizer reported a data race between a server thread executing an
    async request and the event loop thread handling an abrupt remote disconnect
    (https://github.com/bitcoin-core/libmultiprocess/issues/348): the server
    thread called call_context.getResults(), which reads Cap'n Proto connection
    state, while the event loop thread overwrote that state.
    
    In addition to the general undefined behavior, the race has one interleaving
    with a concrete failure: a server thread can dereference a null pointer and
    crash the process, meaning a client that disconnects mid-call can take down
    the server. RpcConnectionState::disconnect() (capnp/rpc.c++) runs on the event
    loop thread and tears down the connection in two steps, moving the live
    connection out of the RpcConnectionState::connection field (nulling the stored
    pointer) and then flipping the field to its disconnected state. A server
    thread calling getResults() between the two steps passes the is<Connected>()
    check but then dereferences the nulled pointer. The other interleavings are
    harmless: reading the field before both writes builds results into an outgoing
    message that is simply never sent, and reading it after both writes takes the
    normal disconnected code path, which builds results into a message detached
    from the connection. There is no use-after-free, since the objects involved
    stay alive through reference counts and the existing cancellation handshake.
    
    The underlying problem is that connection state may only be accessed on the
    event loop thread, and nothing lets libmultiprocess order server thread
    accesses against the disconnect teardown:
    
    - The teardown happens with no warning. The Connection::onDisconnect promise
      used to clean up after disconnects only fires after capnp has finished
      tearing down the connection and shutting down the stream.
    
    - The in-flight request is not canceled first. With capnp's allowCancellation
      feature off (the default), LocalClient::callInternal (capnp/capability.c++)
      detaches a fork of the call promise, so capnp's teardown does not destroy
      the promise chain that would trigger the CancelMonitor cancellation
      handshake in PassField. Enabling allowCancellation would not help either:
      disconnect() would then cancel in-flight requests as part of its teardown,
      but only after the connection field has already been overwritten, so the
      cancellation handshake still could not order server thread reads against
      those writes, only narrow the window.
    
    So no mutex or flag in libmultiprocess can help; the only options are making
    server threads stop reading connection state, or patching capnp.
    
    Fortunately, only the first getResults() call on a request reads connection
    state, to decide whether to allocate the results struct inside a real outgoing
    message or in a message detached from the connection
    (RpcCallContext::getResults in capnp/rpc.c++). The response it allocates is
    cached, and later getResults() calls return it without reading connection
    state.
    
    So fix the race by initializing the results struct on the event loop thread,
    in the existing loop.sync() call that runs before a request executes. The
    getResults() calls that later run on the server thread just return the cached
    response and never touch connection state. The cost is that if the method
    throws, the preallocated results message is wasted (error returns are built
    separately), the same tradeoff capnp itself makes with its internal "force
    initialization of response" getResults calls.
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
    914dc839f2
  3. DrahtBot commented at 5:47 PM on August 19, 2026: none

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK xyzconstant

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #342 (Allow request cancellation for wrapped C++ methods by xyzconstant)
    • #336 (proxy-io: Reference-count Connection objects by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. xyzconstant commented at 3:01 AM on August 25, 2026: contributor

    Code review ACK 914dc839f279269d11bd575f61dc1c09c5c2008e.

    This one-line change builds an empty Results struct for each context-aware server method during request setup, on the event loop thread before the wrapped method runs on the worker. The first getResults call caches the response body, so subsequent calls from the worker return the cache rather than reading the connection state.

    LGTM.


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-08-26 02:30 UTC

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