Harden hypervisor process liveness checks - #363
Open
yummybomb wants to merge 43 commits into
Open
Conversation
This was referenced Aug 6, 2026
yummybomb
marked this pull request as ready for review
August 6, 2026 19:20
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 6, 2026 19:26
f9e6580 to
3c8b92f
Compare
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 6, 2026 19:40
3c8b92f to
915f0ef
Compare
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 7, 2026 14:02
915f0ef to
a187984
Compare
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
2 times, most recently
from
August 7, 2026 20:52
76b9f42 to
78fc483
Compare
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 8, 2026 01:05
78fc483 to
f8fbe79
Compare
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 9, 2026 07:52
ea4603c to
f17f33e
Compare
This was referenced Aug 9, 2026
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 10, 2026 15:37
9c1fbaf to
1a1ff46
Compare
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 10, 2026 18:26
7dbe8e7 to
be6c8ff
Compare
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 10, 2026 18:34
be6c8ff to
fdd7b9a
Compare
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 10, 2026 18:58
fdd7b9a to
bba964c
Compare
resolveRuntimeHypervisorPID discarded the confirmed flag from ResolveProcessPID, so a process matched only by its command line could receive the boot-scoped PID/start-time identity token. Later destructive paths short-circuit on that token without re-confirming socket ownership, elevating an unconfirmed match to a trusted owner. Record the full identity only for the direct child we spawned or a confirmed socket owner; a command-line-only match stores the bare PID so stop/delete must confirm ownership through the socket before acting on it. Restore reuses the same helper instead of minting a second token.
resolveLiveHypervisorPID used the recorded boot ID only as a positive signal. When the stored boot ID differed from the current host boot and the instance socket was gone, a live process wearing the recycled PID made the resolver fail closed, so stop/delete aborted forever on an instance whose hypervisor provably cannot be running. A boot-scoped identity from a different host boot cannot identify a live hypervisor on this boot — HypervisorProcessIdentityExists already treats it as dead. Zero the stored PID before socket resolution so teardown proceeds while the unrelated PID holder is left untouched.
When legacy metadata carries a live stored PID but no boot-scoped identity, resolveLiveHypervisorPID failed closed on ErrNoOwningProcess, wedging stop and delete forever once the PID was recycled. That error means both the socket-listener scan and the full command-line scan found nothing, and a live hypervisor always holds its control-socket listener - the same signal already treated as dead when the stored PID no longer exists. Return dead instead of erroring so pre-upgrade instances stay deletable after PID reuse. Also document that HypervisorProcessExists fails open by design.
resolveRuntimeHypervisorPID minted a boot-scoped identity token for a fallback PID that ProcessExists had just disproven, stamping the current boot ID (and, if the PID were recycled mid-call, a live start time) onto a process that is not the hypervisor. Record the bare PID with a cleared token instead, matching the command-line-only branch.
shutdownHypervisor SIGKILLed the raw stored PID and removed the control socket without the ownership checks the other kill paths enforce, so a recycled PID could be signaled and the fail-closed stop fallback was undermined. Resolve the live owner first: kill the resolved PID only, skip the kill when the recorded hypervisor is provably gone, and fail closed before any teardown when ownership cannot be confirmed, keeping the socket as evidence for the hardened kill path.
When the control client cannot be built but the resolved socket owner is alive, shutdownHypervisor force kills the owner instead of reporting a completed shutdown and unlinking the socket over a running VMM. A command-line-only match that is already dead now classifies as provable death rather than failing closed: the socket-owner scan found nothing and no live process matches the command line, the same conclusion as ErrNoOwningProcess. The classification moves into classifyResolvedHypervisorOwner so that race is directly testable.
refreshHypervisorPID ran resolveLiveHypervisorPID on every hydration of every running instance, so each list/get paid socket resolution — worst case a full /proc scan — for instances mid-transition. Hydration is read-only and its answer never authorizes teardown: stop, delete, standby, and the vGPU release guards all re-resolve identity before acting. Restore the cheap contract: trust a live stored PID, resolve the socket only when it is dead, and mint the identity token only for a confirmed owner. Durable token backfill for legacy metadata stays with BackfillHypervisorProcessIdentities at startup, which persists it; hydration's in-memory upgrade never did.
Embed HypervisorProcessIdentity in StoredMetadata anonymously so the persisted JSON keys are unchanged, replace the nine hand-cleared (PID, StartTime, BootID) sites with Set/SetUnconfirmed/Clear methods, and pass the struct to resolveLiveHypervisorPID instead of three positional fields.
killProcessAndWait replaces the kill blocks in killHypervisor, forceKillHypervisorProcess, and forceKillHypervisorPID. All callers now get the process-group escalation standby already had, and the SIGKILL wait used by stop and delete is a named constant.
Count backfilled, skipped, and failed instances and emit one info line so a wholesale backfill failure is visible without debug logging.
A hypervisor that survives SIGKILL is stuck in uninterruptible sleep and no amount of waiting unsticks it, so the long wait only slowed down stop and delete. killProcessAndWait still escalates to the process group and grants a 2s grace period after the initial wait.
Delete no longer fails when the hypervisor cannot be confirmed dead or instance data removal fails. Instead the instance is marked pending-delete: it disappears from list, get, and name lookups (freeing its name for reuse), the delete lifecycle event fires, and the API call returns success. A background finalizer retries the remaining teardown until the hypervisor is provably gone, including a pass at startup where the boot-scoped process identity settles the stuck case immediately after a host reboot. Admission capacity stays held until finalization since the stuck hypervisor may still pin its memory and devices, and TAP GC preserves pending-delete TAPs for the same reason. A new hypeman_instances_pending_delete_total gauge exposes wedged teardowns.
This reverts commit 2559a7d.
HypervisorProcessIdentityExists and HypervisorProcessExists have no callers in this change; the vGPU work that consumes them introduces them itself. killProcessAndWait always waited 2s regardless of which of the two identically-valued constants callers passed, so the parameter goes away.
The resolver disproof branches (boot mismatch, start-time mismatch) were each tested through killHypervisor with subprocess scaffolding; test them at the resolver level instead and keep one wrapper-level no-op test. Fold the classify fail-closed/provable-death cases into one table test and drop the kill-level duplicate. Replace the three backfill tests that only exercised the needsHypervisorIdentityBackfill predicate with a direct table test.
Both resolved the live owner and called killProcessAndWait; the only differences were an early return already handled inside resolveLiveHypervisorPID and a trailing socket remove that is a no-op on stop's path (shutdownHypervisor already removed it).
standbyInstance logged and continued when shutdownHypervisor failed, then released the TAP and cleared the process identity — written when that error meant only "graceful API failed after we killed the raw PID". With fail-closed ownership checks the error can now mean nothing was killed, so continuing would release the network under a live paused VMM and erase the only identity that could ever find it again. Standby now resumes the VM and returns the error; the snapshot on disk is harmless and a retried standby redoes it. shutdownHypervisor's trailing graceful-API error is demoted to a warning since it only fires after the process is confirmed gone, giving the function a single contract: error means the hypervisor may still be running.
yummybomb
force-pushed
the
hypeship/hypervisor-liveness
branch
from
August 14, 2026 20:43
aafd431 to
aba44c2
Compare
shutdownHypervisor unlinked the control socket via a defer on every return and again before waiting for the process to exit. On the paths where the kill fails and standby resumes the VM, that left a live hypervisor with no socket file, so a later graceful standby or stop could not connect and fell straight into the force-kill path. Unlink the socket only once the hypervisor is provably gone, matching the contract killHypervisor already follows on delete and stop.
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.
Summary
Layer 1 of the vendor VFIO vGPU stack (
generalize-vgpu-device← this ←vendor-vfio-backend←vendor-vfio-vgpu). Pure hypervisor-process hardening with no vGPU-specific code; reviewable in isolation.The upper layers guard vGPU release decisions on "is this instance's hypervisor still alive", so the liveness answer has to be trustworthy first:
ProcessExists— one exported, EPERM-aware, zombie-filtering definition instead of scattered barekill(pid, 0)probes. EPERM means the process exists but cannot be signaled; treating it as dead would be wrong.Wait4returns ECHILD immediately and the kill loop finished before the process had exited. Poll for actual exit in that case.Failure contract
When the hypervisor can be confirmed dead or killed, delete completes synchronously (worst case ~2s graceful + SIGKILL waits). When ownership is genuinely unprovable — a live command-line-only socket match, or a process surviving SIGKILL — stop/delete return an error (API 500) with metadata retained and the restart policy blocked, so a retried delete is safe and converges once the ambiguity clears (debug process exits, host reboots). An earlier revision handled this with a background delete finalizer; it was removed in favor of the synchronous error after review — the ambiguous cases are rare, self-resolving, and better surfaced loudly than retried silently.
Testing
go build ./...,go vetcleango test -race ./lib/instances/targeted suites pass (TestCreateInstanceWithNetworkrequires image pulls + iptables and fails in this environment on the unmodified base as well;TestStandbyAndRestore/TestForkCloudHypervisorFromRunningNetworkboot real VMs and cannot run in this sandbox — CI covers them)Note
High Risk
Changes core stop/delete/standby and hypervisor kill semantics on Linux; incorrect ownership logic could leave VMs running or signal wrong processes, though ambiguous cases fail closed with retained metadata.
Overview
Hardens hypervisor liveness so stop, delete, and standby only tear down a VMM when ownership is provable, avoiding mistaken kills of recycled PIDs and wedged lifecycle operations.
Introduces
HypervisorProcessIdentity(PID plus boot-scoped start time and boot ID) embedded in instance metadata with unchanged JSON keys. Confirmed socket owners get a full identity token; command-line-only or dead fallbacks store an unconfirmed PID only.resolveLiveHypervisorPIDdrives destructive paths: trust matching tokens, require confirmed/procsocket ownership, treat empty socket scans or stale boot tokens as provably dead, and fail closed on live ambiguous command-line matches.Linux socket resolution now returns a
confirmedflag, prefers listeners over accepted connections, handles multiple owners, and addsResolveProcessPIDForOwnerfor expected-owner fast paths.ProcessExistsis unified (EPERM-aware, zombie-filtering);killProcessAndWaitreplaces ad-hoc SIGKILL loops and waits for non-child hypervisors after hypeman restart.Behavioral tightening: delete and standby abort (retain metadata) when kill/shutdown cannot confirm the hypervisor is gone; graceful stop waits on the socket owner, not the stored PID; shutdown defers unlinking the control socket until the process is confirmed dead. Startup runs
BackfillHypervisorProcessIdentitiesfor legacy instances missing identity tokens.Reviewed by Cursor Bugbot for commit f9abc52. Bugbot is set up for automated code reviews on this repo. Configure here.