Skip to content

fix(desktop): clear registered Runtime Host residue before update handoff - #3348

Open
liugddx wants to merge 4 commits into
apache:mainfrom
liugddx:fix/update-handoff-runtime-host-drain
Open

fix(desktop): clear registered Runtime Host residue before update handoff#3348
liugddx wants to merge 4 commits into
apache:mainfrom
liugddx:fix/update-handoff-runtime-host-drain

Conversation

@liugddx

@liugddx liugddx commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the update-drain gap behind #3340: a Runtime Host election winner the desktop manager never adopted — a late winner, or a survivor from a previous app generation — keeps running from the installation directory quitAndInstall is about to replace. Windows delivers no cross-process SIGTERM, electron-builder's NSIS upgrade quits when it cannot clear such a process, and the update silently never applies. CI run 32382283646 captured the orphan's full command line (execution-candidate-main.js --generation 0.1.11, alive 106s after creation, well past its parent's quit), untouched files and registration, and the retained pre-upgrade backup.

Mechanism

The existing tracked drain is kept as-is: installUpdateprepareForUpdatehost.upgrade.prepare wire verb → kernel drain → waitForHostExit(pid). After it, the manager now reads the root-scoped host registration (registration.json, the authoritative live-host record election winners write) through a boot-injected capability closure (resolveExistingStorageRootControlDirectory + readHostRegistration, newly exported from @maka/runtime-host/client), and:

  • an explicitly ephemeral registration naming a live pid that is not the one just drained is terminated outright and awaited — its executable image is about to be replaced and its committed state is crash-safe by the platform's recovery evidence, so termination is strictly better than a doomed installer handoff;
  • a registration that is service-mode, unlabeled (predates lifecycleMode, could be a deployment-owned service host — destructive polarity requires the explicit label), dead, unreadable, or absent leaves the sweep inert, with the installer's own app-running handling as the remaining line;
  • ESRCH on the kill is tolerated (the idle ephemeral residue can self-terminate between the liveness probe and the kill); EPERM stays fatal;
  • a residue that survives termination rejects, surfacing install_failed instead of handing off to an installer that cannot succeed.

Normal-quit host survival (session persistence) is untouched: the sweep runs only on the update path.

Stated boundaries and accepted residual risk

  • pid reuse (accepted, explicit): a host that crashes leaves a stale registration; if Windows reuses that pid before the next election overwrites the file and the user updates in that window, the sweep would terminate an unrelated same-user process. The platform's identity-checking alternative (endpoint handshake + expectedHostEpoch probe before the kill) is noted as a follow-up hardening; the trigger requires a crashed registered host, pid reuse, and an update in the same window, and no lane can deterministically exercise it. Flagged by the local review gate and accepted here rather than silently.
  • If the tracked local target never became ready, prepareForUpdate throws before the sweep (pre-existing behavior); a winner elected after the registration read is inherently uncovered (millisecond window, installer handling remains).
  • The orphan-genesis question ("why did a candidate outlive its 37s connection timeout") resolved to by-design ephemeral-host lifetime (idle grace after an accepted connection), not a defect — recorded in windows: auto-update silently fails when a Runtime Host process survives the app quit (installer cannot clear it) #3340.

Verification

  • L2 (local): @maka/runtime-host + @maka/desktop build and typecheck clean; biome clean; runtime-host-desktop-manager suite 19 pass / 0 fail, including new pins: kill-before-await ordering for the orphan, inert sweep for service/unlabeled/dead/unregistered registrations (each case constructed so removing its specific guard turns it red), unkillable residue → rejection, unreadable registration → degrade to installer handling. An independent review gate ran before push; its three P2s (ESRCH tolerance, destructive polarity on unlabeled registrations, red-signal coverage for the service guard) are fixed in this commit, and its pid-reuse finding is the accepted risk above.
  • L3 (CI): the Windows release-windows-check autoupdate gate exercises the real handoff; the orphan race it removes was hitting roughly one in three recent runs.

Closes #3340. Relates to #3279 (a stale endpoint owner is a plausible shared root) and #2142 Phase 4 (process-drain evidence).

liugddx and others added 2 commits August 21, 2026 00:30
…doff

The update drain covered only the tracked connection: installUpdate ->
prepareForUpdate -> prepareHostUpgrade drains the host the manager is
connected to, but an election winner it never adopted - a late winner,
or a survivor from a previous app generation - keeps running from the
installation directory the updater is about to replace. Windows has no
cross-process SIGTERM, the NSIS upgrade quits when it cannot clear such
a process, and the update silently never applies (apache#3340; run
32382283646 captured the orphan's command line, the untouched files and
registration, and the retained pre-upgrade backup).

After the tracked drain, read the root-scoped host registration (the
authoritative live-host record election winners write) through a
boot-injected capability closure; a live registered ephemeral host that
is not the drained pid is terminated outright - its image is about to
be replaced and its committed state is crash-safe by the platform's
recovery evidence - and awaited. A residue that survives termination
rejects, surfacing install_failed instead of a doomed installer
handoff. Normal-quit host survival (session persistence) is untouched:
the sweep runs only on the update path.

Unit tests pin the sweep: orphan terminated and awaited, drained-pid
and service-mode and dead and unregistered residue left alone, an
unkillable residue rejects, an unreadable registration degrades to the
installer's own handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
Run 32392391944 failed at the packaged renderer smoke with the
30-second CDP deadline and no app stderr - the exact flake family
apache#3327 fixes; this branch is based on main, which predates those
harness repairs.

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Codex review — blocking finding on exact head 27adb845a288a080a49a156ad7898ee321991f3b.

P1: prove process identity before terminating the registered PID. In RuntimeHostDesktopManagerImpl.#drainRegisteredHostResidue, the registration may be stale after a Host crash. The current guards prove only that the old registration said ephemeral and that some process now occupies its PID; process.kill(pid) then terminates that process. If Windows has reused the PID before the update, this kills an unrelated same-user process. The PR description acknowledges exactly this execution path, so it is not merely theoretical.

The smallest sound fix is to authenticate the live endpoint immediately before termination using the registration's expected hostEpoch and root identity (the existing connection handshake already validates those values), and fail safe when identity cannot be proved. Add a regression case where a stale registration names a live reused PID and assert that killHost is not called. A liveness probe alone cannot close this gap.

Required conclusions:

  1. Optimal for the actual problem: no; the post-drain sweep addresses the orphan, but its destructive identity boundary is incomplete.
  2. Production code to delete: none identified.
  3. Tests to delete/replace: none; add the stale-registration/PID-reuse case above.
  4. Deeper refactor: no; reuse the existing authenticated Host identity check at this boundary.
  5. Ready to merge: no.
  6. Residual risk/gaps: unrelated-process termination during update; independent human review is also still required.

This affects process safety and update behavior, so it is a protected-area change requiring independent human review under CONTRIBUTING.md after the blocker is fixed.

liugddx and others added 2 commits August 21, 2026 08:04
Codex review blocker on apache#3348: the sweep proved only that the old
registration said ephemeral and that some process now occupies its pid;
after a host crash leaves a stale registration and Windows reuses the
pid, that terminates an unrelated same-user process.

The sweep now requires a positive identity proof before the kill,
through the mechanism the review pointed at: a boot-injected probe
connects through the published control plane only
(connectExistingRuntimeHost - no filesystem writes), the existing
handshake validates root identity, composition and Host Epoch, and the
probe additionally requires the reported epoch to equal the one in the
registration the sweep read. A dead endpoint, refused handshake, moved
epoch, or probe error all refuse termination - a reused pid cannot
answer on the dead host's control plane, so the stale-registration case
degrades to the installer's own app-running handling. The default
without wiring refuses everything.

Regressions: reused-pid stale registration is never killed nor awaited;
a probe failure refuses termination without failing the update; the
kill path pins verify-then-kill-then-await ordering; the guard-polarity
cases inject a proving identity so each stays protected by its own
specific guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated-by: Claude Fable 5
@liugddx

liugddx commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

@jackwener Addressed at head 3bae05d78 with a stricter fail-closed boundary.

After implementing the suggested authenticated handshake, I re-reviewed the destructive boundary and found that handshake -> close connection -> process.kill(pid) still leaves a PID-reuse TOCTOU window: the authenticated Host can exit after the handshake and before the PID kill. Closing that correctly would require a process-handle-bound identity/termination primitive or a protocol self-shutdown operation, both outside this PR's narrow scope.

So this revision removes the untracked registration/PID residue sweep entirely. Update preparation now drains only the Host authenticated by the manager's existing connection and waits for that tracked PID to exit; any untracked process residue remains fail-closed at the installer's app-running gate. The sweep-only boot wiring, injectable kill/liveness surface, and its tests were removed as well (net 2 insertions, 200 deletions from the reviewed head).

Local verification:

  • @maka/runtime-host build
  • @maka/desktop main build
  • runtime-host desktop manager tests: 14/14
  • Biome formatting/check and git diff --check

This avoids terminating any process from stale registration evidence without introducing the broader process-handle/protocol architecture deferred from this slice. Independent human security review is still required before merge.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

windows: auto-update silently fails when a Runtime Host process survives the app quit (installer cannot clear it)

2 participants