Repository navigation
macOS guests 3/6: Darwin guest service, readiness and exec - #500
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8f6dd1c. Configure here.
b2a5fae to
a54ec4a
Compare
|
Real native Darwin GuestService2222 QA passed in an isolated cold-booted OCI guest using the current shared binary, user-launched at UID501 only. Host shared client verified Root daemon was not installed: guest noninteractive sudo is unavailable and no privileged provisioning was performed. System image capability remained false. These results prove native transport/user-level exec/files/cancellation and shutdown admission, not normal API system-agent installation/readiness or successful graceful root shutdown. Those remain gates. Original guest/API/storage untouched; isolated QA VM ended stopped. |
a1ed358 to
243ce9a
Compare
|
Simplification update at 243ce9a: common command setup/process-group cancellation and bounded wait/drain/final-status handling for both TTY modes. Permanent tests cover pre-cancelled starts and failed output sends cancelling/reaping commands. Full agent/client race suites passed 3 runs; combined post-restack focused suites also passed 3 runs. Base changed to feat/macos-oci-images so the image rollback fix and bundle consolidation are inherited once rather than duplicated. Root provisioning remains undone. All PRs remain draft. This is source-level/synthetic validation, not a new live boot or production build proof. Default Codex independent review was attempted but is still blocked by authentication (HTTP401). Published atomically after checking reviewer heads with explicit per-ref force-with-lease; prior heads preserved locally. |
|
Root QA follow-up pushed as ae3685d; desktop restacked atomically with explicit leases. Manually installed shared system-role agent survived isolated normal-API cold boots. Repeated race-instrumented authenticated API tests verified UID0 exec (both TTY modes), root-owned0600 file copy round trips, and descendant cancellation. Live testing reproduced and fixed Darwin readiness using the nonexistent /bin/true; /usr/bin/true now probes successfully and normal GET persists GuestAgentReadyAt without a Linux workload marker. Fixed premature shutdown fallback after a lost Darwin RPC reply: configured grace period is retained while still requiring owned VMM exit; Linux policy is unchanged. Final normal API stop accepted the RPC and confirmed VMM exit/storage closure without forced fallback. Corrected stale opt-in test calls left after runtime simplification. Focused instances/API/agent/client tests pass race,count3; default independent autoreview remains401-blocked. Manual QA provisioning is not automatic image installation or end-to-end build/publication proof. PR remains draft. |
c336b30 to
1cae99b
Compare
1cae99b to
24fad86
Compare
24fad86 to
2df3f56
Compare
2df3f56 to
ebf6c6e
Compare
ebf6c6e to
fbfd9d2
Compare
39e247a to
f472950
Compare
85e8d2f to
cf64b2e
Compare
cf64b2e to
c0da4ad
Compare
a8ff002 to
7a480d4
Compare
7a480d4 to
3a0d7a5
Compare
3a0d7a5 to
77b12e8
Compare
Shared GuestService on host-CID vsock 2222, root system agent, agent readiness probe, agent-gated exec, copy and stat, and a macOS stop floor. Darwin exec runs its own bounded lifecycle in exec_lifecycle.go. Linux exec, boot markers, stop and the guest client are unchanged.
The guest display was fixed at 1280x800 at 80 ppi. Use 3024x1964 at 160 ppi, which is 1512x982 points at 2x. Changes the display only on the macOS guest path; the Linux path is untouched.
The group watcher stopped when cmd.Wait returned. On a TTY, Wait returns as soon as the shell exits, so a timeout during the output drain never killed a background child that held the terminal open. The watcher now runs until the exec lifecycle finishes. The exit status was bounded only after cancellation. A client that stopped reading could hold the handler with no cancellation at all. The status send now has its own deadline, counted from the send. A timed-out exec whose status cannot be sent still reports the cancellation error. Tests: a TTY timeout during the drain kills the background child (failed before this change), and a stalled client cannot block the exit status.
Readiness probes, the readiness state and shutdown checked SkipGuestAgent alone. They now use GuestAgentEnabled, which also requires the image to declare the agent. For Linux guests the two are equal. macOS instances created before the boot.raw name keep their disk at overlay.raw. Start and VM-process lookup use overlay.raw when boot.raw is absent. New instances still use boot.raw. The guest-operation helper's comment no longer claims to be the only rule.
The direct exec path and the macOS lifecycle each built the command, started the PTY, ran the stdin and resize pump, and computed the exit status. Those now live in exec_common.go. The policy stays in each path: the direct path buffers stdout and waits without a bound, and the lifecycle streams output and bounds its drain. A deadline now reports 124 on the direct path too, which matches its existing comment. Before, a timed-out direct exec reported the signal exit code (-1).
Probes run concurrently with other callers on the same guest, so a failed probe evicted the pooled connection and cancelled their RPCs. KeepPooledConn only stopped that eviction. ProbeExec dials a connection for each attempt and closes it. The pooled connection is never touched by a probe. The readiness probe uses ProbeExec, and KeepPooledConn is removed.
Instances created before this change kept their disk at overlay.raw. macOS is experimental and breaking changes there are acceptable, so there is no fallback. New instances and existing ones both use boot.raw.
A kill after the leader was reaped could reach a reused process group id. Go's cmd.Wait reaps the leader, and the lifecycle needed that to read the pipes. The leader now waits for its exit without being reaped: waitid with WNOWAIT on Linux, and kqueue NOTE_EXIT on Darwin. Darwin checked on a macOS guest: the exit event fires before the reap, the group is still signalable, and a watch on an already-exited child still fires. The lifecycle reads its own pipes, drains within the bound, and reaps last. processGroup allows a kill only before reap. cmd.Cancel is now a no-op, so the group watcher is the only killer. Tests: the reuse guard now uses processGroup. A pipe-drain timeout test covers the drain on every platform. The TTY drain test is skipped on macOS, which revokes the terminal when the shell exits.
sjmiller609
left a comment
There was a problem hiding this comment.
One nit here
linux regression: lib/system/guest_agent/exec.go:130,196 — slow output delivery can replace an already-completed command’s exit code with 124.
maybe if you want, consider adding RPC in the guest to report some versioning information, since in this scenario we have the guest agent being packaged with the image, not injected in initrd like it is in the case of linux. that addresses concerns like the below
agent compatibility: lib/images/types.go:39–41, lib/instances/query.go:443–460 — guest_agent: true plus executing true doesn’t establish compatibility. add protocol/capability negotiation and document image-based upgrades/rollback without breaking older linux agents.
startup timeout: lib/instances/query.go:299–339 — a missing/broken agent leaves managed macOS guests initializing indefinitely. add an overall deadline, retain an actionable failure reason, stop the VM, and preserve disks/logs. keep linux behavior unchanged.
The shared exit-code helper reported 124 whenever the exec deadline had passed when the status was computed. On the direct (Linux) path that happens after the buffered output is sent, so a command that exited in time could report 124 if delivering its output outlasted the deadline. A timed-out command also changed from its signal status (-1) to 124. The direct path now reports the process status, as before the macOS work. The macOS lifecycle keeps reporting 124 on a deadline.
The macOS agent ships inside its image, so it can be older or newer than the host. A new GetAgentInfo RPC reports the agent's protocol version, build version and optional capabilities. The host calls it when a macOS agent first answers and records the reply on the instance. An agent that predates the RPC answers Unimplemented and is recorded as protocol 0. An agent newer than the host's protocol is refused, so the guest stays Initializing with that reason. Linux guests run the agent embedded in hypeman's initrd, so the host does not call GetAgentInfo for them.
An image that declares the guest agent but ships a missing, broken or incompatible one left the guest Initializing indefinitely. On macOS hosts a controller now stops a guest that has been Initializing for 10 minutes, after one last probe under the instance lock. The reason, with the last probe failure, is recorded in the exit message. Disks and logs are kept, start clears the message, and the restart policy applies as for any other stop. Linux guests are not checked.
|
@sjmiller609 thanks, all three addressed:
|

Summary
Third of six macOS guest PRs. Gives Darwin guests a system guest agent (a root LaunchDaemon serving the existing
GuestServiceon vsock 2222). It also changes how macOS instances report readiness, run exec, stop, and boot, and it makes the exec exit path wait for the process group leader without reaping it.Stack position: base is
main, which now includes #498 and #499. Commits on top ofmain:0b1dae2(the guest agent, readiness, and exec), then the remediation commits from review.guest_agent: truein its platform config. Images without it keep the old behavior.Initializinguntil a readiness probe succeeds, thenRunning, like Linux guests. Without the agent,Runningstill means "VMM running".ebfa583).e2090c3). Only the macOS lifecycle reports 124 on a deadline.23a38e6): a newGetAgentInfoRPC reports the agent's protocol version, build version and optional capabilities. The macOS readiness probe records it on the instance. An agent that predates it is protocol 0; an agent with a newer protocol than the host is refused. The host does not call it for Linux guests.e85a8c7): on macOS hosts, a guest whose declared agent has not answered after 10 minutes inInitializingis stopped. The reason (with the last probe failure) goes inexit_message; disks and logs are kept and the restart policy applies.Existing and new flows
Readiness (macOS only; Linux paths are unchanged):
Probes do not use the shared pooled connection. Before, a probe that failed evicted the pool for everyone else.
KeepPooledConnis removed.Exec exit path (macOS lifecycle; the Linux direct path is separate on purpose):
Shared between both paths (
exec_common.go): command setup, PTY start, the stdin and resize pump, and exit-status rules. The policy stays separate: the Linux path buffers output and waits without a bound, and the macOS lifecycle streams and bounds its drain.Stop:
Capability checks: readiness, probes, and shutdown use
GuestAgentEnabled(), which also requires the macOS image declaration. Linux is unchanged, since thereGuestAgentEnabled()equals!SkipGuestAgent.Boot disk: a macOS instance's disk is always
boot.raw. An instance created onmainbefore this PR kept its disk atoverlay.raw, and it will not start after this change (see Caveats).Change size
Totals from GitHub for this PR against
main: 60 files, +2,399 / −192.Approximate meaningful implementation: about 650 lines. This is an estimate, not a Git count. Excluded: tests, docs, moved code (about 80 lines moved from
exec.gointoexec_common.go), mechanical call-site updates, and generated code (none).vsock_darwin.go,vsock_darwin_nocgo.go,ready_darwin.go,network_darwin.go,shutdown_darwin.go,shutdown_policy.go,main.goexec_lifecycle.goexec_group.go,exec_reap_linux.go,exec_reap_darwin.goexec_common.golib/guest/client.goquery.go,guest_os.go,macos.go,stop.go,create.gocmd/api/api/exec.go,cp.go,guest_unsupported.gocmd/vz-shim/macos_arm64.goBehavior changes
Initializinguntil the agent answers, so it reportsRunninglater than before. MacOS images without the agent are unchanged.Initializingafter 10 minutes is stopped instead of waiting forever.boot.rawonly. MacOS instances created before this PR keep their disk atoverlay.rawand will not start.Rollout
main, so it can merge as soon as review is done.Testing
TestDirectExecReportsProcessExitCode: a direct exec that exits in time keeps its code when output delivery outlasts the deadline, and a timeout reports -1 (TTY and non-TTY). Fails on the previous head.TestGetAgentInfo,TestGuestServiceReportsAgentInfo,TestMacOSAgentInfoRecordedAtReadiness: current, legacy and too-new agents, and an RPC failure.TestMacOSAgentStartupDeadlineStopsGuest,TestMacOSAgentStartupOverdueRules: an overdue guest is stopped with its reason; one within the deadline is left alone.main: the same 51 failures on both (KVM, root networking,mkfs.erofs, Docker Hub).go test ./lib/system/guest_agent/ -count=1 -race— ok on Linux (14 s).-count=3 -raceon Linux — ok (40 s), includingTestExecTimeoutDuringDrainKillsBackgroundChild,TestExecTimeoutDuringPipeDrainKillsBackgroundChild,TestSendExitCodeBoundedWhenClientStopsReading, andTestCancellationAfterWaitDoesNotKillGroup.go test ./lib/guest/ -count=1 -race— ok, includingTestProbeExecLeavesPooledConnectionInPlace.GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go test -c ./lib/system/guest_agent/and run in the guest:NOTE_EXITevent fired before the reap, the group was still signalable,kill(-pgid)reached a background child, and a watch on an already-exited child still fired.go test ./lib/instances/with a targeted filter (guest, readiness, running, macOS, stop, health, VM storage path) — 77 pass, 7 skip, 3 fail. The 3 failures are Firecracker and Cloud Hypervisor fork and stop tests that needmkfs.erofs, which is not installed on the review VM. They are environmental.go veton./lib/instances/,./lib/guest/, and./lib/system/guest_agent/— clean, using placeholder embed files that were removed afterwards.gofmt -lon the changed Go files — clean.go build ./..., the full./lib/instances/and./cmd/api/...suites (embedded hypervisor binaries are missing from this checkout), and the cgo vz shim on Darwin. The display change has not been booted on a VM.Caveats / Not included
overlay.raw) do not start after it. No migration or fallback.Review focus
lib/system/guest_agent/exec_lifecycle.go(finishExec,executeLifecycleNoTTY) andexec_group.go: the reap ordering and the kill guard.lib/system/guest_agent/exec_reap_linux.goandexec_reap_darwin.go: the non-reaping wait.lib/guest/client.go(ProbeExec) andlib/instances/query.go: probes and the readiness state.Stack
Each PR is one commit on the one below it.