Skip to content

bridge: installReconnectHandler's liveness-check-then-touch-this window (morph#486's shape), with no safe mechanical fix #489

Description

@Yaraslaut

Summary

Bridge gates five of its own callbacks on CallbackToken::active() and then
touches this. That is the same check-then-act shape as morph#486, which was a
use-after-free — and #486's fix (PR #488) deliberately covers only
~BridgeHandler, because that is the one site where a documented guarantee was
being broken. The other sites are untouched and are recorded here rather than
folded into that PR.

Verification status

Inferred from reading the code. Not reproduced, not measured. I did not
construct a case that lands in any of these windows, and I do not know their
rates. What is measured is that the identical shape one function away
(~BridgeHandlerBridge::deregisterHandler) faulted 26 times in 200
unsanitized runs — see #486 and PR #488. That is evidence about the shape, not
about these five call sites.

Revision: include/morph/core/bridge.hpp at origin/master fdbfd65, line
numbers as of that revision.

The sites

Each does alive.active() / weakLiveness.active() and then dereferences
this. Between the two, another thread can complete ~Bridge.

  1. executeVia's .then continuation (bridge.hpp:1495-1520). Reads
    bool const bridgeAlive = alive.active(); then this->_pendingCalls,
    onResult(...) (which for a result-keyed action reaches back into the bridge
    through a captured raw pointer), hasSubscribers() and publishResult(...).
    Runs on whichever thread the backend resolved on.
  2. executeVia's .onError continuation (bridge.hpp:1555-1561). Same
    check, then this->_pendingCalls.fetch_sub(...).
  3. registerHandlerImpl's onRegistered callback (bridge.hpp:1629-1646).
    Checks weakLiveness.active(), then takes _mtx and calls loadBackend()
    both members of a possibly-destroyed Bridge.
  4. installReconnectHandler's handler (bridge.hpp:1740-1775). Checks the
    token, then std::scoped_lock{_mtx, _attachMtx} and iterates _handlers.
    Explicitly documented as running "on the backend's transport thread".
  5. switchBackend's and the whenBound paths were not audited beyond the
    above; treat the list as "at least these", not "exactly these".

The comments at sites 1 and 2 already reason carefully about whether the
check is needed and get that part right — the schedulerRef capture next to
them is a correct, pin-based solution to the same problem for the
TimeoutScheduler. What none of them addresses is that the check and the use
are two separate steps.

Why PR #488 did not extend the gate to these

detail::BridgeLifetime (added by #488) would close every one of these
mechanically. It was not applied here because it makes ~Bridge block, and
each site needs its own deadlock argument before that is safe:

  • Site 4 is the clear counter-example. QtWebSocketBackend::registerModel
    blocks the calling thread on a nested QEventLoop, and the reconnect handler
    calls registerModelWithContext. Holding a gate across that, while ~Bridge
    on the Qt thread waits for it, is a deadlock by construction.
  • Sites 1 and 2 call publishResult, which reaches subscription sinks. Blocking
    ~Bridge behind consumer-supplied code is a different risk from blocking it
    behind deregisterModel, which is provably non-blocking on every shipped
    backend.

So the honest position is: the gate is the right primitive, but "apply it
everywhere" is not a safe mechanical edit, and the alternative for these sites
may be pinning (as schedulerRef already does) rather than gating.

What would change the verdict

  • Close it if someone shows these windows are unreachable — e.g. that every
    path into them is already ordered behind something that outlives ~Bridge.
  • Raise it if any of them is reproduced. Site 4 is the most promising to
    attack: a QtWebSocketBackend reconnect firing on the transport thread while
    the owner destroys the Bridge.

Related

🤖 Filed with Claude Code

https://claude.ai/code/session_01C6uB9zxdSG8qp3VNAAyFvF

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreSubsystem: corebugSomething isn't workingtriage: validWell-framed; implement as written

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions