Follow-up to #3557, which is already merged (7f68af7655e5bbc74e70f531dea1acfca2e93dc9). This was recorded as a non-blocking [P3] during review and deliberately not treated as a merge hold; opening it so it does not get lost.
The gap
In the PID-known retirement path, packages/cli/src/runtime-host-service-manager.ts:380-417, the old Host is prepared and then backend.stop() is called without holding a State Root fence. Only the service.state === 'starting' branch assigns a rootFence.
If the old process exits abnormally in that window and systemd starts a successor, backend.stop() can stop the successor while the command then reports retirement using the old epoch and PID. The caller is told the host it named was retired; what actually happened is that a different, newer host was stopped.
The post-stop checks do not close this. verifyRuntimeHostRootReleased(root) and the stopped.active/state/pid inspection prove that no writer holds the root at that moment — they cannot prove an identity-continuous handoff, because the identity being asked about may already have been replaced.
Why the obvious fix is not enough
Re-reading identity after stop is too late: by then the successor has already been stopped and any writes it made in the window are unrecoverable. The correct shape is to hold the fence across the stop, or to model retirement as an explicit transition that fails when the observed identity is not the one that was prepared.
Acceptance criteria
- A crash → restart interleaving test: prepare against a known PID, kill that process abnormally inside the window, let a successor come up, and assert the command does not stop the successor and does not report retirement of the old epoch/PID as success.
- The State Root fence is held across
backend.stop() on the PID-known path, not only on the starting path — or retirement is expressed as an explicit transition that rejects an identity mismatch.
- The post-stop root-vacancy check is documented for what it actually proves (no current writer) rather than as evidence of a continuous handoff.
Severity
P3 as rated: the window is narrow, there is no concurrent writer, and no direct data corruption was demonstrated. It is a correctness gap in what the command claims happened, which is why it is worth fixing rather than shipping as understood behaviour.
Provenance
Found during #3557 review, with the fix direction corrected in a review addendum (5002040661) after the first proposed fix — a post-stop identity re-read — was shown to be too late. A second reviewer independently reproduced the same window afterwards and labelled it corroboration rather than an independent finding.
Follow-up to #3557, which is already merged (
7f68af7655e5bbc74e70f531dea1acfca2e93dc9). This was recorded as a non-blocking [P3] during review and deliberately not treated as a merge hold; opening it so it does not get lost.The gap
In the PID-known retirement path,
packages/cli/src/runtime-host-service-manager.ts:380-417, the old Host is prepared and thenbackend.stop()is called without holding a State Root fence. Only theservice.state === 'starting'branch assigns arootFence.If the old process exits abnormally in that window and systemd starts a successor,
backend.stop()can stop the successor while the command then reports retirement using the old epoch and PID. The caller is told the host it named was retired; what actually happened is that a different, newer host was stopped.The post-stop checks do not close this.
verifyRuntimeHostRootReleased(root)and thestopped.active/state/pidinspection prove that no writer holds the root at that moment — they cannot prove an identity-continuous handoff, because the identity being asked about may already have been replaced.Why the obvious fix is not enough
Re-reading identity after
stopis too late: by then the successor has already been stopped and any writes it made in the window are unrecoverable. The correct shape is to hold the fence across the stop, or to model retirement as an explicit transition that fails when the observed identity is not the one that was prepared.Acceptance criteria
backend.stop()on the PID-known path, not only on thestartingpath — or retirement is expressed as an explicit transition that rejects an identity mismatch.Severity
P3 as rated: the window is narrow, there is no concurrent writer, and no direct data corruption was demonstrated. It is a correctness gap in what the command claims happened, which is why it is worth fixing rather than shipping as understood behaviour.
Provenance
Found during #3557 review, with the fix direction corrected in a review addendum (
5002040661) after the first proposed fix — a post-stop identity re-read — was shown to be too late. A second reviewer independently reproduced the same window afterwards and labelled it corroboration rather than an independent finding.