Record VM ownership only after the launched process execs - #46
Merged
Conversation
Startup recovery could quarantine a microVM the same agent process had just launched, and vm.Manager.Remove refuses to touch a recovery_pending instance, so a planned update failed identically on every reconcile cycle and applied_revision never advanced. Because service projection required the applied revision to match, one quarantined VM made every service on that node report unknown, including healthy ones. Manager.Start now waits for the launched PID to be running the command line Firework launched for it before recording its identity. systemd reports a transient unit's MainPID at fork, so an earlier inspection either fails or captures systemd's own identity; both persist a running manifest that can never validate again. A launch whose identity cannot be confirmed is killed instead of recorded, and its state directory is removed once the process is proven gone. The same gate now guards the identity recovery records for a starting systemd manifest. Recovery's once-per-process guard is armed even when the VM state directory does not exist, since on a fresh node it appears only after this process creates its first VM, and services already tracked in memory are skipped. A manifest whose identity does not validate is re-proved from the live command line before being quarantined: the instance ID is 128 bits generated once per launch, so a process presenting it with this instance's socket and config paths is provably the described process. A disagreeing process start time is never repaired, because that is the PID-reuse signal the manifest exists to catch. An identity that was never recorded now reports distinctly from one that disagrees, a host boot ID mismatch is classified as a process that did not survive and has its state cleaned rather than quarantined, and a failed launch owning no process no longer blocks every later start. Service projection keeps reporting agent_status_revision_mismatch for a node that has not applied the current revision, but now projects that node's fresh per-service observations instead of stamping every service unknown, so one service that cannot converge no longer erases visibility into the rest of the node. Refs #41 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up on the same defect: guard the launch identity budgets against a zero value so a Manager built outside its constructor cannot collapse the wait to a single pre-exec inspection, count running services from any fresh agent status so the node list does not contradict the service list it is drawn from, and make the test inspector safe for the monitor goroutine that shares it. Refs #41 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A launch whose process exits before it runs Firecracker is now a start failure rather than a VM that started and then died, so the fake binary in this test has to survive its identity confirmation to still be testing the monitor's failure bookkeeping. It passed on darwin, where process inspection is unsupported and the start falls open, and failed on Linux CI. Refs #41 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolve the visibility test conflict with current main and preserve both test suites. Keep fallback service projection limited to nodes that observed the current revision, fail closed on ambiguous failed systemd launches, expose surviving unprovable launches as recovery_pending without tearing down their networking, and clean starting units whose MainPID is proven dead.
Use an explicit exit marker instead of a one-second sleep so Linux race-and-coverage scheduling cannot turn the post-start failure test into a pre-identity launch failure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the VM ownership recovery defects in #41.
Design reference
There is no #41 design doc.
firework-vm-adoption.md(issue #22) governs this code, and this PR brings the implementation back in line with its §5 step 6: "Wait boundedly for the launcher PID, then read its boot ID, start ticks, and executable device/inode identity." The shipped code inspected immediately and discarded the error. This is a divergence fix, not new design.Root cause
systemd-runreports a transient unit'sMainPIDat fork, before the child hasexec'd into Firecracker.Manager.Startinspected that PID immediately, so the recorded identity described either nothing (inspect failed) orsystemditself (inspect succeeded, raced). Both persist arunningmanifest thatvalidateOwnedProcesscan never accept again, so the VM is quarantined intorecovery_pending;Manager.Removerefuses a quarantined instance, the plannedActionUpdatefails identically every 30s,applied_revisionfreezes, and every service on that node projectsunknown.Both variants were confirmed live (2026-07-26 and 2026-08-08). The recorded
process_start_ticksmatched the live values exactly, which proves the right PID was inspected too early rather than the wrong PID resolved — so the fix belongs at when identity is recorded.Changes
Manager.Startwaits for the exec before recording identity. The signal is the command line, not the executable: the launched process must present this instance's unique--idtogether with its socket and config paths. The identity is taken from that same inspection, so the persisted manifest validates by construction. Argv rather than executable equality also keeps the shell-script fake Firecracker insmoke-localworking.A launch whose identity cannot be confirmed is abandoned, not recorded. It is killed (unit-scoped for systemd; an unreaped child for direct), and its state directory is removed only once the process is proven gone — leaving it in place otherwise, per the design's rule that a manifest is deleted only after its process is. A launch that failed before owning any process no longer blocks every later start of that service for the life of the agent process.
internal/vm/recovery.go's starting-manifest path had the identical swallowedInspectand is fixed the same way. It is not in the issue's file list.Recovery's once-per-process guard is armed even when the VM state directory is absent. On a fresh node it appears only after this process creates its first VM, so the single real pass landed in the process that created them. Services already tracked in memory are skipped as well. The guard is deliberately left unarmed on a non-
ErrNotExistread failure, so a transient error retries rather than disabling recovery permanently.A manifest that fails validation is re-proved from the live command line before being quarantined. The instance ID is 128 bits from
crypto/rand, generated once per launch and passed as--id; a process presenting it with this instance's socket and config paths is provably the described process, whatever the recorded identity says. The identity is then repaired and persisted. A disagreeingprocess_start_ticksis never repaired — that is the PID-reuse signal the manifest exists to catch. This is what unsticks nodes already stalled in production: they recover on the next agent restart.Unrecorded and mismatched identity are now distinct errors, per field. An absent field proves nothing about the process, so it stays quarantined; a host boot ID mismatch is now classified as a process that did not survive — the boot ID is host-global and read fresh, so a mismatch proves the recorded PID cannot be the recorded process — and its state is cleaned instead of quarantined.
Projection no longer erases a whole node. A node that has observed but not applied the current revision keeps
agent_status_revision_mismatchon every service placed on it, but its fresh per-service observations are projected instead of stampedunknown. A service whose ownvm_stateis outside the published vocabulary (recovery_pending) still reportsunknown, so the service that actually cannot converge is the one that stands out.nodeSummary'sRunningServicesis counted from the same fresh status so the node list cannot contradict the service list. Service detail keeps the stricter rule: runtime fields still require an applied revision.Acceptance criteria
recovery_pendingclearable through a supported interfaceCriterion 5 is deliberately deferred
Every available mechanism is a bad fit for a bug fix and needs its own design decision:
internal/api/server.go) is GET-only and unauthenticated, so a mutating endpoint is new attack surface;fireworkctltalks to the control plane, and the architecture is pull-based — there is no push channel to agents;firework-agentsubcommand editing the state directory would not clear the running agent's in-memoryrecovery_pending.What this PR does instead is make quarantine much rarer (the race is fixed) and self-healing for the provable case (ownership repair on the next agent restart). The manual procedure — stop the agent, remove
<state_dir>/vms/<service>/, start it — is now documented inDESIGN.md, along with the fact that persistent volumes live outside that directory. Happy to split criterion 5 into its own issue.Tests
systemd's identity until the exec completes — asserts the manifest holds Firecracker's identity, neversystemd's, and validates immediately;Removestill refuses a genuinely quarantined instance, which is unchanged and still covered byTestRecoverQuarantinesPIDReuseAndNeverSignalsIt. What changed is that the raced VMs are no longer quarantined in the first place;unknown, and the node summary agrees.Notes for review
Reconciler.Reconcile. That placement is the underlying reason defect 1 existed. The guard fix satisfies the acceptance criterion; moving the call is a larger refactor and is left out deliberately.systemd-run~1.2s.TestTick_StatusReportsVMProcessFailureWithoutExitedPIDnow has to survive its identity confirmation to still be testing the monitor./proc, so ownership is unprovable there by construction. Only the real inspector on such a platform falls open with a warning, which preserves the local dev loop (make smoke-localpasses); tests and Linux always take the strict path.Validation
make fmt,make test,make lint(incl. staticcheck),make test-race, andmake smoke-localall pass.🤖 Generated with Claude Code