Skip to content

Commit 6584e0c

Browse files
SashaMITcursoragent
andcommitted
feat(vz-backend): Phase 2 Day 4 — codesign helper, vm-debug boot, first-boot attempt
Ship the local-dev codesign helper that bestows `com.apple.security.virtualization` on a `cargo build` `elastos` binary so Apple's `VZVirtualMachineConfiguration.validateWithError` accepts it, unblocking the Day 3 lifecycle wiring. Add `elastos vm-debug boot --rootfs … --kernel …` so a Mac dev can drive the Day 3 path end-to-end (load → start → wait for Ctrl-C or guest-stop → stop) and stream guest kernel printk to stdout via the existing `vm_console` tracing target. New files: - scripts/dev/sign-elastos-vz/sign.sh — idempotent macOS-only ad-hoc signer (`codesign --force --sign - --entitlements …`). Refuses to run off macOS, verifies the entitlement landed before exiting 0, supports `--verify-only` for inspection. - scripts/dev/sign-elastos-vz/vz.entitlements.plist — minimal entitlements plist granting one key: `com.apple.security.virtualization`. Single source of truth. - scripts/dev/sign-elastos-vz/README.md — one-page operator guide: why, how, what NOT to expect (no notarization, no developer cert). - elastos/crates/elastos-server/src/vm_debug_cmd.rs — clap `VmDebugCommand::Boot` handler. Validates inputs (rootfs + kernel exist, memory >= 64 MiB, vcpus >= 1), stages a tempdir capsule layout (hard-link rootfs in, fall back to copy), synthesises a minimal `CapsuleManifest` with the Vz-mandated boot args, calls `VzProvider::load`/`start`, races `tokio::signal::ctrl_c()` against polled `provider.status` so the CLI exits cleanly whether the user Ctrl-Cs or the guest halts itself. macOS-only impl; Linux fall-through returns a typed "see docs/MAC.md" error. - docs/vz-backend/PHASE_2_DAY_4_NOTES.md — verbatim outcome log of the first-boot attempt on this dev machine. Touched files: - elastos/crates/elastos-server/src/main.rs — `Commands::VmDebug(VmDebugCommand)` added; dispatch arm hands off to `vm_debug_cmd::run`. The `EnvFilter` is widened by one additive directive (`vm_console=info`) so guest-kernel printk from BOTH the crosvm and Vz backends is visible without forcing operators to set `RUST_LOG`. `RUST_LOG` still overrides. - docs/MAC.md — appends the "First boot on Apple Silicon" operator recipe and the kernel-artifact note. End-to-end attempt on macOS 26.4.1 / Apple Silicon (full output in PHASE_2_DAY_4_NOTES.md): 1. cargo build, sign, --verify-only — all clean. 2. `vm-debug boot` with stand-in artifacts reached `provider.load` and `provider.start` cleanly. Apple's Vz framework returned its expected "Internal Virtualization error. The virtual machine failed to start." because the stand-in kernel is 18 bytes. **Day 4 contract met: the wiring is end-to-end correct on a signed binary; only a real arm64 Linux Image + rootfs is missing for actual guest boot. That artifact procurement is Day 5.** 3. The Day 3 `ENTITLEMENT_HINT` message did NOT appear, which is the regression target — it should appear only on unsigned binaries. Apple Silicon's hard signing requirement means we can't easily run an unsigned binary on this host to verify the negative case, but the pure-string Day 3 unit tests (`format_validate_error_embeds_day_4_hint_…`) cover the contract deterministically. Tests added (5; all pass locally): - vm_debug_cmd::tests::validate_boot_inputs_rejects_missing_rootfs - vm_debug_cmd::tests::validate_boot_inputs_rejects_missing_kernel - vm_debug_cmd::tests::validate_boot_inputs_rejects_tiny_memory - vm_debug_cmd::tests::validate_boot_inputs_rejects_zero_vcpus - vm_debug_cmd::tests::build_staging_layout_creates_capsule_dir_with_entrypoint_rootfs Plus one cfg-gated Linux-only test (`vm_debug_boot_on_non_macos_returns_typed_error_pointing_at_docs`) which CI will exercise. Gates re-run locally: - cargo build --workspace - cargo test -p elastos-vz (69 green) - cargo test -p elastos-server --bin elastos vm_debug (5 green) - cargo clippy -p elastos-vz -p elastos-server --all-targets -- -D warnings - cargo fmt --check - scripts/check-linux-untouched.sh bcf5a0a (gate OK) - scripts/dev/mac-vz-feature-check probe (still exit 0; validate line still SKIPs because the probe is its own unsigned binary) Pre-existing failures in `elastos-server::setup::tests::*` (3 tests around vmlinux platform-entry lookup for darwin-arm64) are unrelated to Day 4 — verified by running them on the prior commit (2994515) where they also fail. They belong to the upstream darwin-runtime setup work, not the Vz backend, and are outside this commit's scope. Day 4 does NOT include: - VZVirtualMachineDelegate (Day 5+) - shipping a vmlinux artifact in the repo (kernel procurement is Day 5) - production notarization / distribution signing (Phase 6) - touching the four Linux-untouched protected crates Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2994515 commit 6584e0c

7 files changed

Lines changed: 867 additions & 1 deletion

File tree

docs/MAC.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,57 @@ trade-off.
140140
Linux host) and connect to it. This mirrors how Docker Desktop and
141141
the Kubernetes control plane handle the Mac case.
142142

143+
## First boot on Apple Silicon (Phase 2 Day 4+)
144+
145+
Once the Vz backend lands you'll be able to boot a Linux guest
146+
end-to-end against your own kernel + rootfs. The wiring is in
147+
place as of Phase 2 Day 4; the only remaining gap is "a real
148+
arm64 Linux kernel image that Apple's `VZLinuxBootLoader`
149+
accepts" — see [`vz-backend/PHASE_2_DAY_4_NOTES.md`](vz-backend/PHASE_2_DAY_4_NOTES.md)
150+
for the verified outcome of the Day 4 attempt.
151+
152+
The operator recipe, once kernel artifacts are available:
153+
154+
```bash
155+
# 1. Build the runtime binary.
156+
cargo build -p elastos-server
157+
158+
# 2. Sign it with com.apple.security.virtualization so Apple's
159+
# VZVirtualMachineConfiguration.validateWithError accepts the
160+
# config. The script is idempotent — re-run after every
161+
# `cargo build`.
162+
scripts/dev/sign-elastos-vz/sign.sh
163+
164+
# 3. Boot the guest. The kernel must be a raw arm64 Linux Image
165+
# (NOT a bzImage). The rootfs will be exposed as /dev/vda
166+
# inside the guest. Guest kernel printk streams via the
167+
# `vm_console` tracing target — visible by default.
168+
target/debug/elastos vm-debug boot \
169+
--rootfs /path/to/rootfs.img \
170+
--kernel /path/to/Image \
171+
--memory-mb 256 \
172+
--vcpus 1
173+
174+
# Press Ctrl-C to stop. The VM also stops itself if the guest
175+
# shuts down.
176+
```
177+
178+
If step 2 is skipped you'll see a single operator-friendly
179+
error string when `vm-debug boot` calls `provider.load`:
180+
181+
> `vz validate (vm_id='…'): missing com.apple.security.virtualization entitlement — sign the binary with scripts/dev/sign-elastos-vz/ (Phase 2 Day 4) or see docs/MAC.md. Apple error: …`
182+
183+
If the kernel artifact isn't a Vz-compatible arm64 Image, Apple
184+
returns `Internal Virtualization error. The virtual machine
185+
failed to start.` from `provider.start`. That's the signal you
186+
need a real kernel — Day 5 (and Phase 3 in `PLAN.md`) covers
187+
how to get one.
188+
143189
## Cross-references
144190

145191
- The plan: [`docs/vz-backend/PLAN.md`](vz-backend/PLAN.md)
192+
- Day 4 outcome log: [`docs/vz-backend/PHASE_2_DAY_4_NOTES.md`](vz-backend/PHASE_2_DAY_4_NOTES.md)
193+
- Codesign helper: [`scripts/dev/sign-elastos-vz/README.md`](../scripts/dev/sign-elastos-vz/README.md)
146194
- The principles this plan obeys: [`PRINCIPLES.md`](../PRINCIPLES.md)
147195
(#10, #11, #12 in particular)
148196
- The runtime's support boundary: [`state.md`](../state.md)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Phase 2 Day 4 — outcome log
2+
3+
Operator-facing summary of what Day 4 actually achieved on real
4+
hardware. Single source of truth for "what works, what doesn't,
5+
what's next" so Day 5 can pick up without ambiguity. Companion
6+
to [`PLAN.md`](PLAN.md) (high-level phasing) and
7+
[`PHASE_0_SCOPE.md`](PHASE_0_SCOPE.md) (audit / risk register).
8+
9+
## What this commit ships
10+
11+
| Deliverable | Status |
12+
|---|---|
13+
| `scripts/dev/sign-elastos-vz/sign.sh` + `vz.entitlements.plist` + `README.md` | Done — ad-hoc signs the local `elastos` binary with `com.apple.security.virtualization`, idempotent, refuses to run off macOS |
14+
| `elastos vm-debug boot --rootfs … --kernel …` CLI subcommand | Done — `Commands::VmDebug` registered everywhere, real implementation gated to macOS, non-macOS returns typed "see docs/MAC.md" error |
15+
| `vm_console` tracing target opted into the default env filter | Done — single additive `add_directive("vm_console=info"…)` in `elastos-server/src/main.rs`; `RUST_LOG` still overrides |
16+
| End-to-end first boot attempt on this dev machine | **Attempted; outcome documented below** |
17+
18+
## End-to-end attempt result
19+
20+
**Host:** macOS 26.4.1 / aarch64 (Apple Silicon).
21+
22+
**Procedure executed:**
23+
1. `cargo build -p elastos-server`
24+
2. `scripts/dev/sign-elastos-vz/sign.sh elastos/target/debug/elastos`
25+
3. `scripts/dev/sign-elastos-vz/sign.sh --verify-only elastos/target/debug/elastos`
26+
confirmed `com.apple.security.virtualization = true`.
27+
4. Created a stand-in 1 MiB zero-filled "rootfs" + an 18-byte
28+
"kernel" stand-in (`/tmp/fake-rootfs.img`, `/tmp/fake-kernel.bin`).
29+
5. `elastos/target/debug/elastos vm-debug boot --rootfs /tmp/fake-rootfs.img --kernel /tmp/fake-kernel.bin --memory-mb 128`
30+
31+
**Verbatim output:**
32+
33+
```text
34+
vm-debug boot: loading capsule
35+
2026-05-22T06:42:40.529620Z INFO elastos_vz::provider: Loaded MicroVM
36+
capsule 'vm-debug-boot' with ID microvm-9c5055ad-cebe-4cbf-a49c-ad69945b0e63
37+
vm-debug boot: capsule loaded (microvm-9c5055ad-cebe-4cbf-a49c-ad69945b0e63); starting…
38+
Error: vm-debug boot: provider.start: Compute error:
39+
vz start (vm_id='0d7f43ac-a550-454d-ade0-b58a73f7bb74'):
40+
Internal Virtualization error. The virtual machine failed to start.
41+
```
42+
43+
**Interpretation (line by line):**
44+
45+
1. `vm-debug boot: loading capsule` — the CLI dispatch reached
46+
`provider.load`. The staging-dir layout + manifest synth
47+
worked end-to-end.
48+
2. `Loaded MicroVM capsule … ID microvm-…``VzProvider::load`
49+
returned `Ok(CapsuleHandle)`. This implies all of:
50+
- `ffi::builder::BuiltMachine::from_vm_config` accepted the
51+
inputs (rootfs + kernel paths exist, sizing valid).
52+
- `VZVirtualMachineConfiguration.validateWithError`
53+
**succeeded** — the entitlement is real, the per-device
54+
config the Day 2 builder produced is acceptable to Apple
55+
for the host's macOS version, and the Day 1 probe's
56+
extrapolation held.
57+
- `VZVirtualMachine::initWithConfiguration:queue:` succeeded
58+
— the per-provider GCD queue and the SendableVm wrapper
59+
are correctly wired.
60+
3. `vz start (vm_id='…'): Internal Virtualization error. The
61+
virtual machine failed to start.` — this is Apple's response
62+
when it tries to actually boot the kernel image and finds
63+
it isn't a valid Linux Image. **Expected** for an 18-byte
64+
stand-in.
65+
66+
**Conclusion.** The Phase 1–3 lifecycle wiring (validate →
67+
init → start → completion-handler block → Tokio oneshot →
68+
typed Rust error) is end-to-end functional on a signed Apple
69+
Silicon binary. The only thing Day 4 cannot verify is the
70+
actual guest boot, because we did not procure a real arm64
71+
Linux Image + matching rootfs.
72+
73+
## What this rules out
74+
75+
- ❌ Missing entitlement (would surface as the Day 3
76+
`ENTITLEMENT_HINT` message; we don't see it).
77+
- ❌ Config-validation rejection (would surface from
78+
`validateWithError` with a specific configuration error;
79+
we don't see it).
80+
- ❌ GCD queue / dispatch wiring (the start call reached Apple
81+
and Apple's framework reported back through our completion
82+
handler).
83+
- ❌ FFI builder problem (the BuiltMachine assembled cleanly
84+
for a config with both a kernel file and a rootfs file).
85+
86+
## What's left for Day 5
87+
88+
1. **Procure a Vz-compatible arm64 Linux kernel + minimal
89+
rootfs.** Candidates ranked by effort:
90+
- Apple's developer sample code under
91+
`https://developer.apple.com/documentation/virtualization/running_linux_in_a_virtual_machine`
92+
ships a known-working pair.
93+
- Lima's published kernel + initramfs releases at
94+
`https://github.com/lima-vm/alpine-lima/releases` are
95+
reliable for arm64.
96+
- Build from upstream: `arch/arm64/configs/defconfig` plus
97+
`CONFIG_VIRTIO_BLK`, `CONFIG_VIRTIO_NET`, `CONFIG_HVC_DRIVER`,
98+
`CONFIG_VIRTIO_CONSOLE` — see Phase 0 audit §C "kernel
99+
config" for the full list.
100+
2. **First-boot acceptance test.** Run `vm-debug boot` against
101+
the real artifacts; observe `vm_console` lines like
102+
`[ 0.000000] Linux version …` reach stdout via the tracing
103+
forwarder; clean Ctrl-C stop; verify no leftover state in
104+
the tempdir.
105+
3. **VZVirtualMachineDelegate.** Once boot works, surface
106+
guest-stop reasons and crash details via the delegate
107+
protocol (Phase 0 §D pitfall #9; Day 3 deferred this).
108+
109+
## Reproducing this attempt locally
110+
111+
```bash
112+
# Sign the binary.
113+
cargo build -p elastos-server
114+
scripts/dev/sign-elastos-vz/sign.sh
115+
116+
# Build stand-in artifacts so the "infrastructure works"
117+
# assertion is reproducible without a real kernel.
118+
dd if=/dev/zero of=/tmp/fake-rootfs.img bs=1M count=1
119+
echo "fake-kernel-bytes" > /tmp/fake-kernel.bin
120+
121+
# Drive the lifecycle. Expected exit 1 with Apple's "Internal
122+
# Virtualization error" once we reach `provider.start`.
123+
target/debug/elastos vm-debug boot \
124+
--rootfs /tmp/fake-rootfs.img \
125+
--kernel /tmp/fake-kernel.bin \
126+
--memory-mb 128
127+
```
128+
129+
## Anchors
130+
131+
- [`PLAN.md`](PLAN.md) — Phase 2 Day 4 in the phasing table
132+
- [`PHASE_0_SCOPE.md`](PHASE_0_SCOPE.md) §B, §D pitfalls #9, #10
133+
- [`scripts/dev/sign-elastos-vz/README.md`](../../scripts/dev/sign-elastos-vz/README.md)
134+
- [`docs/MAC.md`](../MAC.md) — operator recipe
135+
- `elastos-vz/src/ffi/lifecycle.rs::ENTITLEMENT_HINT` — the
136+
string that does **not** appear in the Day 4 attempt above,
137+
confirming the codesign path works

elastos/crates/elastos-server/src/main.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod share_cmd;
2121
mod shares_cmd;
2222
mod site_cmd;
2323
mod trust_cmd;
24+
mod vm_debug_cmd;
2425
mod webspace_cmd;
2526

2627
use clap::{Parser, Subcommand};
@@ -491,6 +492,12 @@ enum Commands {
491492
#[arg(long)]
492493
rollback_to: Option<String>,
493494
},
495+
496+
/// Developer entry point: drive the Apple Silicon Vz backend
497+
/// directly. macOS only — see `docs/MAC.md` and
498+
/// `docs/vz-backend/PLAN.md` (Phase 2 Day 4).
499+
#[command(name = "vm-debug", subcommand)]
500+
VmDebug(vm_debug_cmd::VmDebugCommand),
494501
}
495502

496503
#[derive(Subcommand)]
@@ -1025,11 +1032,19 @@ async fn main() -> anyhow::Result<()> {
10251032
let _ = rustls::crypto::ring::default_provider().install_default();
10261033

10271034
// Initialize logging — uses ConditionalStderr so interactive VMs can suppress output.
1035+
//
1036+
// The extra `vm_console=info` directive opts the guest-kernel
1037+
// console target into the default filter so both the Linux
1038+
// (crosvm) and macOS (Vz) backends' `tracing::info!(target =
1039+
// "vm_console", …)` events are visible without forcing
1040+
// operators to set `RUST_LOG` by hand. `EnvFilter::from_default_env`
1041+
// still wins, so a user-provided `RUST_LOG` can override.
10281042
tracing_subscriber::fmt()
10291043
.with_writer(ConditionalStderr)
10301044
.with_env_filter(
10311045
tracing_subscriber::EnvFilter::from_default_env()
1032-
.add_directive("elastos=info".parse().expect("valid tracing directive")),
1046+
.add_directive("elastos=info".parse().expect("valid tracing directive"))
1047+
.add_directive("vm_console=info".parse().expect("valid tracing directive")),
10331048
)
10341049
.init();
10351050

@@ -1078,6 +1093,14 @@ async fn main() -> anyhow::Result<()> {
10781093
return capsule_publish_cmd::run_publish(path).await;
10791094
}
10801095

1096+
Commands::VmDebug(vm_debug) => {
1097+
// Phase 2 Day 4 (docs/vz-backend/PLAN.md). The
1098+
// subcommand itself decides whether the host is
1099+
// capable; here we just hand off so the typed error
1100+
// surface lives in one place.
1101+
return vm_debug_cmd::run(vm_debug).await;
1102+
}
1103+
10811104
Commands::Share {
10821105
path,
10831106
channel,

0 commit comments

Comments
 (0)