Skip to content

core: parkIfInFrame's double-claim guard is unreachable from a backend now that every dispatch goes through one Completion #648

Description

@Yaraslaut

Found while doing #571, filed rather than folded in.

The finding

detail::parkIfInFrame's double-claim arm (include/morph/core/bridge.hpp) is
no longer reachable from any backend:

inline bool parkIfInFrame(AsyncDispatchHandoff& handoff, bool succeeded, ::morph::exec::detail::ModelId modelId,
                          std::exception_ptr failure) {
    bool inFrame = false;
    {
        std::scoped_lock const guard{handoff.mtx};
        if (handoff.fired) {
            // A backend is contractually allowed exactly one callback per dispatch;
            // swallow a second one rather than reporting twice.
            return true;
        }

Before #571, Bridge handed a backend two raw std::functions
(onRegistered, onError) and a backend that violated "exactly one callback"
by calling one of them twice reached this arm. That is what
tests/test_async_registration.cpp's DoubleFiringBackend was written to
exercise ("attachHandlerAsync reports exactly once even when the backend fires
its callback twice inline").

After #571 every dispatch site has the shape

completion.then([...](ModelId newId){ if (parkIfInFrame(*handoff, true, newId, nullptr)) return; ... })
          .onError([...](const std::exception_ptr& f){ if (parkIfInFrame(*handoff, false, {}, f)) return; ... });

over one Completion. morph::async::detail::CompletionState settles once
and once only — Promise::resolve/reject are documented no-ops on an
already-settled state — so exactly one of the two lambdas ever runs, exactly
once. handoff.fired is therefore always false on entry.

Five call sites, all the same shape: attachHandlerAsync, ensureBoundAsync,
assignHandlerPrimary, registerHandlerImpl and rebindThroughSurface.

A second, smaller instance of the same class sits next to it: the
try { ... } catch (...) around the dispatch in attachHandlerAsync and
ensureBoundAsync. IBackend::bindModel's default wraps bindModelBlocking
in its own try/catch and rejects rather than throws, and no in-tree override
throws out of the dispatch call, so only an out-of-tree override can reach
those two catch blocks. In-tree they are reachable only from
tests/test_async_registration.cpp's ThrowingDispatchBackend.

Verification status

Established by reading the code, on laneLADDER-batch-570-571 at
085b401b (#571's commit), not by a coverage run. What is measured is only
that the suite still passes with the twins gone:

tests/morph_tests            22931 assertions in 1556 test cases  (1 failed as expected)
tests/qt/morph_qt_tests        578 assertions in   79 test cases
tests/net/morph_net_tests     1112 assertions in  191 test cases

I did not run scripts/coverage.sh, so I have not confirmed by measurement
that the arm now shows zero hits — which is the evidence that would settle this
either way. No Emscripten toolchain was available either, so nothing about a
WASM configuration is covered.

DoubleFiringBackend was kept and migrated: it now settles the same promise
twice from inside bindModel, so the test still pins the observable contract
(exactly one onDone). What it no longer pins is this guard, because
CompletionState absorbs the second settle first. That substitution is
recorded in the double's own comment and in docs/spec/core/backend.md's
migration-status section, so this issue is not the only record of it.

Why it is worth a ticket rather than a shrug

This repository's branch-coverage gate (scripts/check_branch_coverage.py plus
scripts/branch_partial_allowlist.json) exists to stop exactly this drifting
into an unexplained partial arm that somebody later allowlists with a guessed
reason. Either the arm should be deleted, or it should carry an allowlist entry
whose reason says "unreachable from a backend since morph#571; retained because
parkIfInFrame is also called from the dispatching frame" — and that sentence
should be written by whoever checks it, not inferred from a red gate.

Note that deleting it is not obviously right: parkIfInFrame is a free
function in detail, and the invariant that makes the arm dead is a property of
every current caller, not of the function. A sixth call site that does not go
through a single Completion would resurrect the need.

What would change the verdict

  • A coverage run showing the arm taken — then it is reachable by a route I
    did not find, and this should be closed as invalid with that route named.
  • A new AsyncDispatchHandoff caller that can settle the handoff twice — then
    the guard is live again and this should be closed.
  • A decision to delete the guard, with DoubleFiringBackend either deleted or
    repurposed — then this closes as done.

Not in scope of #571

#571 is a deletion of four IBackend verbs. Deciding whether a now-defensive
guard in Bridge should be deleted, allowlisted or left alone is a separate
judgement about Bridge's internals, and folding it into the removal would
hide it inside a 1000-line diff.

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