feat: reconnect-ready QEMU transports and VM.attach() for host crash recovery#89
Open
ShravanSunder wants to merge 2 commits into
Open
feat: reconnect-ready QEMU transports and VM.attach() for host crash recovery#89ShravanSunder wants to merge 2 commits into
ShravanSunder wants to merge 2 commits into
Conversation
…ash recovery
Add support for re-owning a running QEMU VM after a host process crash
via deterministic transport identity and a new VM.attach() API.
Changes:
- Derive all QEMU transport socket paths from a stable runtime ID under
a per-VM runtime directory (/tmp/gondolin-runtime/{id}/)
- Add reconnect-ms to QEMU chardev/netdev args when QEMU >= 9.2
- Persist runtime metadata (runtime.json) after VM start for discovery
- Add VM.attach({ id, ...options }) that validates metadata, verifies
the orphaned QEMU is alive, and rebuilds host-side listeners
- SandboxController attach mode: skip spawn, verify PID liveness,
terminate attached process on close with SIGTERM/SIGKILL escalation
- Shared isProcessAlive/signalProcess utilities with proper ESRCH/EPERM
discrimination
- Guest sandboxd: non-blocking open for virtio ports with errdefer fd
cleanup to avoid blocking on dead ports during reconnect window
- VMAttachOptions uses Pick<VMOptions> to exclude inapplicable fields
- VMConstructionOptions as discriminated union for type-safe attach/create
Closes earendil-works#88
Avoids ambiguity with Node's built-in process module.
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.
Problem
When the host process dies unexpectedly, QEMU keeps running (reparented to PID 1), but Gondolin has no way to re-own that live VM from a new host process. The guest-side
sandboxddaemon survives and waits for reconnection, and the host-sideVirtioBridgecan recreate its listeners — but transport socket paths are random UUIDs that die with the process, and there is no metadata to discover the orphaned QEMU. Downstream systems are forced to treat any host crash as full VM loss, even when the guest is still alive.#88
Solution
Add deterministic transport identity and a
VM.attach()API so a new host process can recover ownership of an already-running VM.How it works
1. Deterministic runtime directory
All QEMU transport sockets are now placed under a stable per-VM directory:
Previously each socket used a random UUID suffix (
gondolin-virtio-{random}.sock). Now they derive from the VM's stableid, so a new process can reconstruct the exact same paths.2. Reconnect-ready QEMU sockets
When QEMU >= 9.2 is detected (via
--versionparsing), chardev and netdev socket args includereconnect-ms=5000. This tells QEMU to retry the socket connection every 5 seconds after a disconnect, instead of giving up permanently. Detection is automatic and falls back gracefully on older QEMU versions.3. Runtime metadata
After VM start, a
runtime.jsonfile is written to the runtime directory containing the VM id, QEMU PID, binary path, creation timestamp, and reconnect capability. This is the discovery mechanism forVM.attach(). The file is removed onVM.close().4.
VM.attach({ id, ...options })New static method that:
ESRCH= dead,EPERM= alive but different user)SandboxServerin attach mode —SandboxControllerskips spawn, transitions directly to "running"5. Attach-mode process lifecycle
SandboxControllerwith anattachedPid:start()verifies the PID is alive before reporting "running"close()sends SIGTERM, polls for exit, escalates to SIGKILL after 3s, hard timeout at 10sisProcessAlive/signalProcessutilities)6. Guest-side non-blocking open
sandboxd'stryOpenRpcPath()now opens virtio ports withO_NONBLOCK(then clears it viafcntl), witherrdeferto prevent fd leaks. This avoids blocking indefinitely on a dead virtio port during the reconnect window.What
VM.attach()does NOT dogondolin attachCLI session-IPC behaviorreconnect-ms(< 9.2)Type design
VMAttachOptionsusesPick<VMOptions, ...>to expose only attach-relevant fields (excludesmemory,cpus,autoStart,rootfswhich are fixed by the running VM)VMConstructionOptionsis a discriminated union (mode: "create"|mode: "attach") preventing invalid field combinationsVmRuntimeMetadatavalidates all fields includingqemuPid > 0and wrapsJSON.parsewith contextual error messagesTesting
Automated unit tests (
pnpm --dir host test)These run without hardware virtualization and are part of the standard test suite:
test/qemu-arch-mismatch.test.ts— option resolutionparseQemuVersion— valid versions (9.2, 10.2), malformed inputtest/sandbox-controller.test.ts— attach-mode controller behaviorbuildQemuArgs— reconnect-ms appended to all chardevs and netdevtest/vm-internals.test.ts— VM.attach and metadata lifecycleVM.attachrejects missing runtime metadataVM.attachrejects corrupt runtime metadataVM.attachrejects non-reconnect-capable runtimesVM.attachrejects stale PIDs (ESRCH)VM.attachreuses runtime metadata id and attached QEMU pidLive reconnect validation (
pnpm --dir host test:attach-reconnect)This is a separate validation script that requires real QEMU >= 9.2 with hardware virtualization. It is not part of the standard
pnpm testsuite — it must be run manually on a machine that can boot VMs. The script (scripts/validate-vm-attach-reconnect.ts) with its helper (test/helpers/vm-attach-helper.ts) exercises a real crash/reattach flow:vm.idto stdout, then keeps the VM aliveVM.attach({ id })from a new process/tmp/reconnect-marker) still existskill -0)vm.exec(...)worksvm.close()shuts the orphaned QEMU down cleanlyLatest reconnect validation output:
Build and existing test suite
All validation steps were run sequentially:
make build→ exit 0 ✅make check→ exit 0 ✅make test→ exit 0 ✅pnpm --dir host test— 464 pass, 4 fail (pre-existing: require hardware virtualization), 0 regressionspnpm --dir host test:attach-reconnect→ exit 0 ✅Closes #88
🤖 Generated with Claude Code