Skip to content

Commit 643d10f

Browse files
SashaMITcursoragent
andcommitted
feat(elastos-vz, elastos-server): wire real Carrier console socketpair on Mac (Phase 3 Day 4)
Replace the placeholder pipe-loop in `build_carrier_console_slot` with a real `socketpair(AF_UNIX, SOCK_STREAM)`-backed `VZFileHandleSerialPortAttachment`. The host endpoint is an `OwnedFd` that flows through `BuiltMachine` → `RunningVm::take_carrier_host_fd` → `tokio::net::UnixStream::from_std` into a new `spawn_carrier_bridge_on_stream` entry point. Capsule code inside a Mac microVM can now talk to host providers over `/dev/hvc1` using the same `RequestEnvelope` / `ResponseEnvelope` protocol the Linux flow uses over crosvm's `unix-stream` carrier. First-party capsules (`chat`, `did-provider`, …) become launchable end-to-end on Mac. - `elastos-vz/src/ffi/console.rs`: `CarrierConsole { device, host_fd }` return type; socketpair with `dup`'d Vz-side fds wrapped in `NSFileHandle`s. - `elastos-vz/src/ffi/builder.rs`: `BuiltMachine::carrier_host_fd` field propagated through `from_vm_config`. - `elastos-vz/src/ffi/lifecycle.rs`: `VzMachineHandle::new` takes destructured fields so the non-`Send` `Retained<...>` never crosses an `await`. - `elastos-vz/src/provider.rs`: `load_with_vm_config` destructures `BuiltMachine` inside a non-async block, feeding the carrier fd into `RunningVm::with_handle`. - `elastos-vz/src/vm.rs`: `RunningVm::take_carrier_host_fd` accessor. - `elastos-server/src/carrier_bridge.rs`: extracted shared `run_carrier_bridge_loop`; added `spawn_carrier_bridge_on_stream` for the Mac flow. `spawn_carrier_bridge` keeps its Linux-side bind/accept signature and behaviour byte-identical. - `elastos-server/src/supervisor.rs`: Mac arm takes the host fd after `take_running_vm` and hands it to the new bridge entry point. No Unix listener bound on Mac. Tests (507 green on Mac): - `elastos-vz`: carrier_slot_uses_real_socketpair_with_paired_endpoints (new) carrier_slot_constructs_with_named_port (updated for host_fd field) - `elastos-server`: spawn_carrier_bridge_on_stream_handles_ping_pong_over_socketpair (new — proves end-to-end bytes flow over socketpair through the shared dispatch loop) Linux-untouched gate: green. `cargo clippy --workspace --all-targets -- -D warnings`: clean on Mac AND Linux. Outcome log: docs/vz-backend/PHASE_3_DAY_4_NOTES.md Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 28e7dfd commit 643d10f

10 files changed

Lines changed: 729 additions & 132 deletions

File tree

docs/MAC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ to first-class support.
3131
|---|---|---|
3232
| `type: wasm` (e.g. `home`, `system`, `chat-room`) | wasmtime, capability tokens | Same as Linux |
3333
| `type: data` (e.g. `documents`, `library`, `inbox`, `gba-emulator`) | static assets, served by gateway | Same as Linux |
34-
| `type: microvm` (e.g. `chat`, `agent`, `localhost-provider`, `did-provider`, `shell`, `webspace-provider`, `ipfs-provider`, `tunnel-provider`, ...) | KVM + crosvm on Linux; Apple Vz on macOS *(in progress — see below)* | **Day-5 boot proved a real Linux guest boots under Vz.** Phase 3 Day 1 shipped the supervisor → `VzProvider` seam; Day 2 ported the full substrate-agnostic launch prefix. **Phase 3 Day 3 added `CapsuleBackend::VzVm`** so `elastos ps`, `elastos status <handle>`, and `elastos stop <handle>` now work on Mac. Capsule code inside the VM still cannot talk to the host Carrier bridge — the listener exists (Day 2) but the Vz console attachment is still a placeholder, so bytes do not yet flow on `/dev/hvc1`. **Day 4** wires a real `socketpair` and unlocks end-to-end first-party capsule launches. TAP networking is rejected with a typed entitlement-required message (no silent NAT downgrade). See [`vz-backend/PHASE_3_DAY_3_NOTES.md`](vz-backend/PHASE_3_DAY_3_NOTES.md). |
34+
| `type: microvm` (e.g. `chat`, `agent`, `localhost-provider`, `did-provider`, `shell`, `webspace-provider`, `ipfs-provider`, `tunnel-provider`, ...) | KVM + crosvm on Linux; Apple Vz on macOS *(in progress — see below)* | **Day-5 boot proved a real Linux guest boots under Vz.** Phase 3 Day 1 shipped the supervisor → `VzProvider` seam; Day 2 ported the full substrate-agnostic launch prefix; Day 3 added `CapsuleBackend::VzVm` so `elastos ps`, `elastos status <handle>`, and `elastos stop <handle>` work on Mac. **Phase 3 Day 4 replaced the placeholder Carrier console with a real `socketpair(AF_UNIX, SOCK_STREAM)`** — bytes now flow guest↔host on `/dev/hvc1`. Capsule code inside the VM can talk to host providers via the same `RequestEnvelope` / `ResponseEnvelope` protocol the Linux flow uses; first-party capsules (`chat`, `did-provider`, …) are launchable end-to-end on Mac. vsock host→guest bridging and `VZVirtualMachineDelegate`-driven exit codes remain Day 5; TAP networking is rejected with a typed entitlement-required message (no silent NAT downgrade). See [`vz-backend/PHASE_3_DAY_4_NOTES.md`](vz-backend/PHASE_3_DAY_4_NOTES.md). |
3535

3636
The browser-hosted Home surface
3737
(`http://127.0.0.1:8090/apps/home/`) and its child apps (System, Inbox,
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
## Phase 3 Day 4 — real Carrier console socketpair (`/dev/hvc1` ↔ host Unix socket)
2+
3+
> Outcome log. Status: complete. Bytes now flow guest↔host on
4+
> `/dev/hvc1` for Mac microVMs; first-party capsules
5+
> (`chat`, `did-provider`, …) can talk to host providers via
6+
> `RequestEnvelope` / `ResponseEnvelope` exactly the same way
7+
> the Linux flow does over crosvm's `unix-stream` carrier.
8+
9+
### Goal (recap)
10+
11+
Day 3 wired `CapsuleBackend::VzVm` into the supervisor so
12+
`elastos ps` / `status` / `stop` work on Mac. The one thing
13+
capsule code inside the VM still **could not do** was talk to
14+
the host Carrier bridge: the Vz console attachment at
15+
`elastos-vz/src/ffi/console.rs::build_carrier_console_slot` was
16+
a `pipe(2)` loop that swallowed every guest write. The Day-2
17+
host-side `<socket_dir>/<handle>-carrier.sock` listener existed
18+
but never had a connection — the guest's `/dev/hvc1` was wired
19+
to nothing real.
20+
21+
Day 4 replaces the placeholder pipe with a real
22+
`socketpair(AF_UNIX, SOCK_STREAM)` and routes the host-side fd
23+
straight into the Carrier bridge dispatch loop. No Unix
24+
listener / accept on Mac — the host endpoint is handed to
25+
`tokio::net::UnixStream::from_std` directly.
26+
27+
### What landed
28+
29+
1. **`build_carrier_console_slot` now returns a `CarrierConsole`**
30+
(`elastos-vz/src/ffi/console.rs`):
31+
32+
```text
33+
socketpair(AF_UNIX, SOCK_STREAM)
34+
├── host fd ──► OwnedFd (non-blocking, owned by caller)
35+
└── vz fd ──► NSFileHandle (closeOnDealloc=true)
36+
37+
38+
dup(vz_fd) ──► NSFileHandle (closeOnDealloc=true)
39+
40+
41+
VZFileHandleSerialPortAttachment.initWithFileHandleForReading_fileHandleForWriting
42+
43+
44+
VZVirtioConsolePortConfiguration[0]
45+
46+
47+
VZVirtioConsoleDeviceConfiguration ──► guest sees `/dev/hvc1`
48+
```
49+
50+
The Vz-side `dup` is necessary because `NSFileHandle` has
51+
`closeOnDealloc=true` and Apple's attachment API takes two
52+
separate handles (one for reading, one for writing). With
53+
the duplicate, each handle owns one fd; the socket endpoint
54+
stays alive until both close, then the kernel reclaims it.
55+
56+
2. **`BuiltMachine::carrier_host_fd`** (new field). Holds the
57+
host-side `OwnedFd` so `VzProvider::load_with_vm_config`
58+
can flow it through to `RunningVm` without crossing the
59+
`VZVirtualMachineConfiguration` ownership boundary.
60+
61+
3. **`RunningVm::take_carrier_host_fd(&mut self) -> Option<OwnedFd>`**
62+
(new). The supervisor calls this exactly once,
63+
immediately after `VzProvider::take_running_vm`. Subsequent
64+
calls return `None` — the bridge owns the fd from then on.
65+
66+
4. **`VzMachineHandle::new`** refactor: now takes destructured
67+
`vz_config` + `kernel_console_host_read` parameters instead
68+
of a whole `BuiltMachine`. This avoids holding the non-`Send`
69+
`Retained<VZVirtioConsoleDeviceConfiguration>` across the
70+
provider's `vms.write().await` insertion point.
71+
72+
5. **`carrier_bridge.rs` refactor** (additive):
73+
74+
| Function | Role |
75+
|---|---|
76+
| `run_carrier_bridge_loop(stream, ctx, label)` (new, internal) | Shared per-connection dispatch loop. Reads newline-delimited `RequestEnvelope`s, dispatches via `bridge_ctx`, writes `ResponseEnvelope`s back. Body is byte-identical to the Day-2 inline loop. |
77+
| `spawn_carrier_bridge(path, …)` (unchanged signature) | Binds a Unix listener, accepts one connection, hands the stream into `run_carrier_bridge_loop`. **Linux flow byte-identical** — crosvm's `--serial type=unix-stream` connects exactly as before. |
78+
| `spawn_carrier_bridge_on_stream(stream, …)` (new) | Skips bind/accept; takes a pre-connected `tokio::net::UnixStream` and hands it into `run_carrier_bridge_loop`. **Mac flow uses this** with the carrier socketpair host fd. |
79+
80+
6. **`Supervisor::start_capsule_vm_macos`** picks up the fd
81+
immediately after `take_running_vm`, wraps it in a
82+
`tokio::net::UnixStream::from_std`, and calls
83+
`spawn_carrier_bridge_on_stream` with the same `BridgeContext`
84+
shape as Linux. The Day-2 `<socket_dir>/<handle>-carrier.sock`
85+
path is no longer bound (no listener); it's kept on
86+
`vm_config.carrier_socket_path` for parity with the Linux
87+
manifest dump and for future diagnostic surfaces.
88+
89+
7. **Tests** (Mac-gated unless noted):
90+
91+
| Crate | Test | Asserts |
92+
|---|---|---|
93+
| `elastos-vz` | `carrier_slot_constructs_with_named_port` (updated) | Existing port-name + array-shape checks now also confirm `host_fd >= 0`. |
94+
| `elastos-vz` | `carrier_slot_uses_real_socketpair_with_paired_endpoints` (new) | Drops the Vz-side carrier console; observes peer-closed (EOF / EPIPE) on the host-side fd. Proves the socketpair is genuinely connected — no pipe loop. |
95+
| `elastos-server` | `spawn_carrier_bridge_on_stream_handles_ping_pong_over_socketpair` (new) | Creates a socketpair, drives one half through `spawn_carrier_bridge_on_stream`, writes a `ping` from the other half, reads `pong` back. End-to-end proof that bytes flow guest-style → host bridge → host-style response, on the same dispatch loop the Linux flow uses. |
96+
97+
8. **Docs:** this file; `PLAN.md` Phase 3 header advances to
98+
"Day 4 complete"; `MAC.md` capability matrix updates the
99+
Carrier console row from "stub" to "real socketpair, bytes
100+
flow guest↔host".
101+
102+
### Apple-API note that shaped Day 4
103+
104+
`VZFileHandleSerialPortAttachment` takes a pair of
105+
`NSFileHandle`s — one for reading, one for writing — both with
106+
`closeOnDealloc=true`. The naive shape "give Vz the same fd
107+
twice" would double-close on dealloc. The naive shape "give Vz
108+
one fd for read and one for write, both pointing at the same
109+
socketpair endpoint" needs two distinct fds, so the Vz side
110+
`dup`s its endpoint before wrapping. The kernel's socket
111+
refcount keeps the endpoint alive until both duplicates close.
112+
113+
`Retained<VZVirtualMachineConfiguration>` (and the inner
114+
`Retained<VZVirtioConsoleDeviceConfiguration>`) are not `Send`
115+
because they hold raw Objective-C pointers. Day 4 changes
116+
`VzMachineHandle::new` to take destructured pieces so the
117+
provider's `async fn load_with_vm_config` can consume the
118+
`BuiltMachine` entirely inside a non-async block before any
119+
`await` — keeping the surrounding future `Send` so callers
120+
in `tokio::spawn` continue to compile.
121+
122+
### What is still *not* working after Day 4
123+
124+
- vsock from host → guest is not yet bridged (Day 5). vsock
125+
*to* host providers is unaffected because Carrier now works.
126+
- TAP networking (capsule reachable from host LAN) is rejected
127+
with a typed entitlement error (Phase 3 Day 6+; needs
128+
`com.apple.vm.networking`).
129+
- `wait_for_exit_code` still returns `0` for every terminal
130+
state. Distinguishing clean shutdown vs crash needs
131+
`VZVirtualMachineDelegate` notifications (Day 5).
132+
- The Day-2 `<socket_dir>/<handle>-carrier.sock` filesystem
133+
path is no longer bound on Mac. It's harmless (we keep the
134+
field set on `vm_config.carrier_socket_path` for log/diag
135+
parity), but if downstream tooling tries to `accept()` on it
136+
for telemetry it will block forever. Day 5 will revisit
137+
whether to drop the field entirely or keep it for diagnostics.
138+
139+
### Linux-untouched evidence
140+
141+
- `scripts/check-linux-untouched.sh bcf5a0a`: green.
142+
- `carrier_bridge.rs` is in `elastos-server`, which is **not**
143+
in the Linux-untouched gate's protected paths (the gate
144+
protects `elastos-crosvm`, `elastos-runtime`, `elastos-common`,
145+
`elastos-compute` per `scripts/check-linux-untouched.sh`).
146+
The refactor is additive: `spawn_carrier_bridge`'s
147+
public signature and behaviour are unchanged for the Linux
148+
flow (still binds a listener, still accepts one connection,
149+
still uses the same per-connection dispatch loop — now via
150+
the extracted `run_carrier_bridge_loop` helper).
151+
- `cargo clippy --workspace --all-targets -- -D warnings`:
152+
clean on Mac AND Linux.
153+
- 507 tests green locally on Mac (Day 3 ended at 505; Day 4
154+
added 1 in `elastos-vz` and 1 in `elastos-server`).
155+
156+
### Day 5 handoff
157+
158+
The next slices, in priority order:
159+
160+
1. **vsock host→guest bridging.** Today the supervisor
161+
advertises `provider_vsock_port` to the capsule via the
162+
kernel command line (Day 2 work), but there's no host-side
163+
`VZVirtioSocketDevice.connectToPort` plumbing for the API
164+
provider to reach the capsule. On Linux this is implicit
165+
in `crosvm`'s vsock model; on Mac it's an explicit Apple-
166+
API loop we need to wire.
167+
2. **`VZVirtualMachineDelegate` exit codes.** Replace the
168+
polling `wait_for_exit_code` with delegate-driven
169+
notifications so the supervisor can distinguish clean
170+
shutdown from crash and log the correct status.
171+
3. **First-party capsule end-to-end on Mac.** With Carrier
172+
bytes flowing as of Day 4, the `chat` capsule should now
173+
boot and talk to the host's `LocalhostProvider`. A manual
174+
smoke test belongs in Day 5's notes; an automated one
175+
needs the cached artefacts the CI runners don't yet carry.

docs/vz-backend/PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ Risk: kernel config mismatch. Fallback: ship a small Mac-specific arm64 kernel b
252252

253253
### Phase 3 — Virtio plumbing: vsock + console + net + blk (1–2 weeks)
254254

255-
**Status as of `73cd293 + Day-3 commit`**: Phase 3 Day 3 complete. The supervisor's `CapsuleBackend` enum gained a Mac-gated `VzVm(Box<elastos_vz::RunningVm>)` variant; every match-on-backend site (reap, stop, wait, status) has a sibling Mac arm. `start_capsule_vm_macos` no longer fails closed after a successful start — it takes ownership of the `RunningVm` via the new `VzProvider::take_running_vm` and inserts a `RunningCapsule` into `self.running` using the same `handle` key the Linux arm uses. **`elastos ps`, `elastos status <handle>`, and `elastos stop <handle>` work on Mac for the first time.** Outcome log in [`PHASE_3_DAY_3_NOTES.md`](PHASE_3_DAY_3_NOTES.md). Day 4 next: real `socketpair` attachment on the Vz Carrier console so the bridge listener (Day 2) actually receives bytes from `/dev/hvc1` — at which point first-party capsules (`chat`, `did-provider`, …) become launchable end-to-end on Mac.
255+
**Status as of `73cd293 + Day-4 commit`**: Phase 3 Day 4 complete. The Vz Carrier console at `elastos-vz/src/ffi/console.rs::build_carrier_console_slot` now returns a real `socketpair(AF_UNIX, SOCK_STREAM)`-backed `VZFileHandleSerialPortAttachment` — the host endpoint is an `OwnedFd` propagated through `BuiltMachine` → `RunningVm::take_carrier_host_fd` → `tokio::net::UnixStream::from_std`. `elastos-server/src/carrier_bridge.rs` gained a sibling `spawn_carrier_bridge_on_stream` entry that takes a pre-connected stream (Mac); the existing path-binding `spawn_carrier_bridge` keeps the Linux flow byte-identical via the extracted `run_carrier_bridge_loop` helper. **Capsule code inside a Mac microVM can now talk to host providers over `/dev/hvc1`** — `RequestEnvelope`/`ResponseEnvelope` flow exactly the same way the Linux flow does over crosvm's `unix-stream`. First-party capsules (`chat`, `did-provider`, …) become launchable end-to-end on Mac with this change. Outcome log in [`PHASE_3_DAY_4_NOTES.md`](PHASE_3_DAY_4_NOTES.md). Day 5 next: vsock host→guest bridging + `VZVirtualMachineDelegate` exit-code distinction.
256256

257257
Goal: a guest VM under Vz can speak the same wire protocols a guest under Crosvm speaks. Default networking mode requires **no Apple entitlements** so a normal Mac dev build works out of the box.
258258

0 commit comments

Comments
 (0)