Skip to content

Commit 9251de6

Browse files
SashaMITcursoragent
andcommitted
feat(vz-backend): Phase 2 Day 5 — initramfs + first real boot
Adds VZLinuxBootLoader.setInitialRamdiskURL: support so distro kernels (every Ubuntu/Debian/Alpine arm64 cloud image we know of) can reach userspace, threads --initramfs through `elastos vm-debug boot`, and ships scripts/dev/fetch-vz-kernel.sh — an idempotent macOS-only fetcher pinned to Ubuntu's archived release-20260515 with SHA-256 baked in, that downloads, gunzip's the kernel to a raw arm64 Image, and converts the qcow2 disk to raw via qemu-img. Verified end-to-end on Apple Silicon: the runtime now boots a real Linux 5.15 kernel, mounts EXT4 from the disk image, launches systemd, and prints "Welcome to Ubuntu 22.04.5 LTS!" through the vm_console tracing target — see docs/vz-backend/PHASE_2_DAY_5_NOTES.md for the verbatim console excerpts. Tests: +6 new (3 boot-loader ramdisk wiring, 1 builder threading, 2 vm-debug --initramfs validation); 72 total elastos-vz + vm_debug_cmd tests pass. No new failures elsewhere — the three setup::tests::* failures inherited from Pre-Work remain unchanged (and unrelated; tracked in Day 4 + Day 5 outcome logs). Linux-untouched gate (vs bcf5a0a) stays green. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 90ded6e commit 9251de6

9 files changed

Lines changed: 863 additions & 39 deletions

File tree

docs/MAC.md

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -140,57 +140,89 @@ 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+)
143+
## First boot on Apple Silicon (Phase 2 Day 5)
144144

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.
145+
As of Phase 2 Day 5 you can boot a real Linux guest end-to-end.
146+
The control plane (codesign → load → validate → start → console
147+
forwarder → state polling) is fully wired; the missing piece
148+
from Day 4 — "a real arm64 kernel + initramfs that Vz accepts"
149+
— is now a one-shot `fetch-vz-kernel.sh` invocation against
150+
Ubuntu's archived cloud images. The Day 5 outcome log
151+
([`vz-backend/PHASE_2_DAY_5_NOTES.md`](vz-backend/PHASE_2_DAY_5_NOTES.md))
152+
records the verbatim guest console output.
151153

152-
The operator recipe, once kernel artifacts are available:
154+
Operator recipe:
153155

154156
```bash
155157
# 1. Build the runtime binary.
156158
cargo build -p elastos-server
157159

158160
# 2. Sign it with com.apple.security.virtualization so Apple's
159161
# VZVirtualMachineConfiguration.validateWithError accepts the
160-
# config. The script is idempotent — re-run after every
161-
# `cargo build`.
162+
# config. Re-run after every `cargo build`.
162163
scripts/dev/sign-elastos-vz/sign.sh
163164

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.
165+
# 3. Fetch a Vz-compatible kernel + initramfs + rootfs.
166+
# Downloads Ubuntu 22.04 arm64 cloud-image artifacts to
167+
# ~/.local/share/elastos/vz-bin/, verifies their SHA-256
168+
# against checksums baked into the script, gunzips the
169+
# kernel to a raw Linux Image, and converts the qcow2
170+
# disk to raw via `qemu-img` (install via `brew install qemu`
171+
# if missing). Idempotent.
172+
scripts/dev/fetch-vz-kernel.sh
173+
174+
# 4. Boot the guest. Guest kernel printk streams via the
175+
# `vm_console` tracing target — visible by default at
176+
# `info` level. Ubuntu's rootfs lives on /dev/vda1, not
177+
# the whole disk, so we override the default --boot-args.
168178
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.
179+
--rootfs ~/.local/share/elastos/vz-bin/rootfs.img \
180+
--kernel ~/.local/share/elastos/vz-bin/Image \
181+
--initramfs ~/.local/share/elastos/vz-bin/initramfs.img \
182+
--memory-mb 1024 \
183+
--boot-args 'console=hvc0 root=/dev/vda1 rw'
184+
185+
# Press Ctrl-C to stop. The VM also stops itself if the
186+
# guest reaches an end state (panic, shutdown).
176187
```
177188

178-
If step 2 is skipped you'll see a single operator-friendly
179-
error string when `vm-debug boot` calls `provider.load`:
189+
You can swap in your own kernel + rootfs at any time — the
190+
fetch script is a known-working starting point, not a hard
191+
dependency. Anything that satisfies Vz's contract works
192+
(arm64 raw Linux Image, raw disk image, optional initramfs
193+
mmap-able by Vz).
194+
195+
### Common error shapes
196+
197+
If you skip the codesign step (#2 above) you'll see this
198+
error when `vm-debug boot` calls `provider.load`:
180199

181200
> `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: …`
182201
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.
202+
If the kernel artifact isn't a Vz-compatible arm64 Image,
203+
Apple returns the same opaque message from `provider.start`:
204+
205+
> `Internal Virtualization error. The virtual machine failed to start.`
206+
207+
That's the signal that the kernel format is wrong — either
208+
it's a bzImage (x86), a compressed vmlinuz that wasn't
209+
decompressed, or a kernel built without arm64 Image format
210+
support. Re-run `fetch-vz-kernel.sh` to get a known-good
211+
artifact, or check that your own kernel's first 0x44 bytes
212+
contain the `ARMd…PE\0\0` magic at offset 0x38/0x40.
213+
214+
If you pass `--initramfs <path>` but the file is missing,
215+
validation errors before any Vz call:
216+
217+
> `boot loader: initramfs file does not exist at /path/to/initramfs`
188218
189219
## Cross-references
190220

191221
- The plan: [`docs/vz-backend/PLAN.md`](vz-backend/PLAN.md)
192222
- Day 4 outcome log: [`docs/vz-backend/PHASE_2_DAY_4_NOTES.md`](vz-backend/PHASE_2_DAY_4_NOTES.md)
223+
- Day 5 outcome log: [`docs/vz-backend/PHASE_2_DAY_5_NOTES.md`](vz-backend/PHASE_2_DAY_5_NOTES.md)
193224
- Codesign helper: [`scripts/dev/sign-elastos-vz/README.md`](../scripts/dev/sign-elastos-vz/README.md)
225+
- Kernel fetcher: [`scripts/dev/fetch-vz-kernel.sh`](../scripts/dev/fetch-vz-kernel.sh)
194226
- The principles this plan obeys: [`PRINCIPLES.md`](../PRINCIPLES.md)
195227
(#10, #11, #12 in particular)
196228
- The runtime's support boundary: [`state.md`](../state.md)
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Phase 2 Day 5 — first real boot
2+
3+
Day 4 closed with every byte of the Vz lifecycle wired and a
4+
`vm-debug boot` that reached Apple's `startWithCompletionHandler:`
5+
before failing with the opaque *"Internal Virtualization error.
6+
The virtual machine failed to start."* The Day 4 outcome log
7+
([`PHASE_2_DAY_4_NOTES.md`](PHASE_2_DAY_4_NOTES.md)) attributed
8+
that failure exclusively to "kernel artifact incompatibility";
9+
Day 5 confirms that diagnosis was correct by replacing the
10+
synthetic kernel + rootfs with real artifacts and observing
11+
a full Ubuntu boot to systemd.
12+
13+
## TL;DR
14+
15+
> **Apple Silicon now boots a real Linux guest under
16+
> `elastos-vz`.** Kernel reaches userspace, EXT4 mounts, systemd
17+
> initialises, Ubuntu 22.04 announces itself, all kernel printk
18+
> output streams through the `vm_console` tracing target. The
19+
> guest pauses where every cloud-init-driven Linux VM pauses
20+
> when nobody hands it metadata (`Wait for cloud-init`), which
21+
> is a userspace contract, not a virtualisation defect.
22+
23+
## Day 5 deliverables
24+
25+
| File | Change |
26+
|---|---|
27+
| [`elastos-vz/src/config.rs`](../../elastos/crates/elastos-vz/src/config.rs) | `VmConfig.initramfs_path: Option<PathBuf>` + `with_initramfs_path` builder. `VzConfig` gains the same field as a provider-wide default. |
28+
| [`elastos-vz/src/ffi/boot_loader.rs`](../../elastos/crates/elastos-vz/src/ffi/boot_loader.rs) | `build_boot_loader` accepts `Option<&Path>` and calls `VZLinuxBootLoader.setInitialRamdiskURL:` when present. Three new tests assert: (a) `None` leaves the ramdisk URL `nil`, (b) `Some(present)` round-trips through Apple's NSURL copy, (c) `Some(missing)` returns a typed not-found error matching the existing kernel-not-found shape. |
29+
| [`elastos-vz/src/ffi/builder.rs`](../../elastos/crates/elastos-vz/src/ffi/builder.rs) | Plumbs `vm.initramfs_path` to `build_boot_loader`. One new threading-correctness test walks `BuiltMachine → bootLoader → downcast<VZLinuxBootLoader> → initialRamdiskURL` to confirm the field reaches Apple, not just the Rust intermediate. |
30+
| [`elastos-vz/src/provider.rs`](../../elastos/crates/elastos-vz/src/provider.rs) | `VzProvider::load` copies the provider-wide initramfs default onto each `VmConfig` when the per-VM field is `None`. The `is_none()` precondition keeps the door open for a future per-VM override. |
31+
| [`elastos-server/src/vm_debug_cmd.rs`](../../elastos/crates/elastos-server/src/vm_debug_cmd.rs) | `--initramfs <path>` clap flag; canonicalised + validated; threaded into `VzConfig::with_initramfs_path`. Two new tests cover validation. |
32+
| [`scripts/dev/fetch-vz-kernel.sh`](../../scripts/dev/fetch-vz-kernel.sh) | Idempotent macOS-only fetcher. Downloads Ubuntu 22.04 arm64 cloud-image artifacts (kernel + initrd + qcow2 disk) from an archived `release-YYYYMMDD/` path, verifies SHA-256 against checksums baked into the script, gunzips the kernel to a raw arm64 Image, and converts the disk from qcow2 to raw via `qemu-img`. `--verify-only` re-checksums without downloading; `--force` clobbers. |
33+
| [`scripts/dev/sign-elastos-vz/sign.sh`](../../scripts/dev/sign-elastos-vz/sign.sh) | Default binary path corrected from `target/debug/elastos``elastos/target/debug/elastos` so the documented recipe (`sign.sh` with no args) works. Day 4 introduced this bug; Day 5 fixes it as a one-line polish since the new MAC.md recipe relies on the default. |
34+
| [`docs/MAC.md`](../MAC.md) | "First boot on Apple Silicon" section updated for the new four-step recipe: `cargo build → sign.sh → fetch-vz-kernel.sh → vm-debug boot`. Common error shapes documented. |
35+
36+
## What we did
37+
38+
1. Added `setInitialRamdiskURL:` support to `ffi/boot_loader.rs`,
39+
threaded it through `ffi/builder.rs`, exposed it on `VmConfig`
40+
and `VzConfig`. Verified end-to-end with a threading test that
41+
downcasts the `VZBootLoader` Apple hands back to a
42+
`VZLinuxBootLoader` and reads the stored URL.
43+
2. Added the `--initramfs <path>` flag to `elastos vm-debug boot`,
44+
with the same `does-the-file-exist` discipline already applied
45+
to `--rootfs` and `--kernel`.
46+
3. Wrote `scripts/dev/fetch-vz-kernel.sh` pinned to Ubuntu's
47+
archived `release-20260515` for jammy 22.04 (immutable release
48+
path, perfect for baked-in checksums). Verified all three
49+
SHA-256 hashes against Ubuntu's own `SHA256SUMS` files. The
50+
script does the kernel decompression and qcow2→raw conversion
51+
inline so the operator gets one command to run.
52+
4. Built the binary, signed it with the codesign helper, fetched
53+
the artifacts, ran `elastos vm-debug boot` against them.
54+
55+
## First-boot evidence (verbatim excerpts)
56+
57+
`vm-debug boot --rootfs … --kernel … --initramfs … --memory-mb 1024 --boot-args 'console=hvc0 root=/dev/vda1 rw'`:
58+
59+
```
60+
vm-debug boot: loading capsule
61+
2026-05-22T07:29:38.701071Z INFO elastos_vz::provider: Loaded MicroVM capsule 'vm-debug-boot' with ID microvm-71355ae7-…
62+
vm-debug boot: capsule loaded (microvm-71355ae7-…); starting…
63+
vm-debug boot: guest started. Press Ctrl-C to stop. Guest kernel console streams via tracing target `vm_console`.
64+
```
65+
66+
Kernel boot (first lines from the guest's `/dev/hvc0`, forwarded
67+
through our `ConsoleForwarder` to the `vm_console` tracing
68+
target):
69+
70+
```
71+
[ 0.140416] cacheinfo: Unable to detect cache hierarchy for CPU 0
72+
[ 0.140951] loop: module loaded
73+
[ 0.141149] tun: Universal TUN/TAP device driver, 1.6
74+
[ 0.141288] ehci-pci: EHCI PCI platform driver
75+
[ 0.142044] NET: Registered PF_INET6 protocol family
76+
77+
[ 0.146484] Loaded X.509 cert 'Canonical Ltd. Secure Boot Signing (Ubuntu Core 2019): c1d57b8f…'
78+
```
79+
80+
Rootfs mounted from the disk image:
81+
82+
```
83+
[ 2.513350] EXT4-fs (vda1): mounted filesystem with ordered data mode. Opts: (null). Quota mode: none.
84+
```
85+
86+
Userspace alive, Ubuntu detects the host correctly:
87+
88+
```
89+
[ 2.597310] systemd[1]: systemd 249.11-0ubuntu3.20 running in system mode …
90+
[ 2.597422] systemd[1]: Detected virtualization vm-other.
91+
[ 2.597453] systemd[1]: Detected architecture arm64.
92+
Welcome to Ubuntu 22.04.5 LTS!
93+
[ 2.597893] systemd[1]: Hostname set to <ubuntu>.
94+
[ 2.672244] systemd[1]: Queued start job for default target Graphical Interface.
95+
```
96+
97+
The guest then enters cloud-init's `Wait for cloud-init Network
98+
to be Configured` job, which would normally hang for the full
99+
default timeout (~5 min) and then proceed to a login prompt
100+
that no one is reading. We sent SIGINT after ~2 min of confirmed
101+
userspace activity; `provider.stop` cleanly transitioned the VM
102+
through Apple's Stopping → Stopped sequence.
103+
104+
## What this proves
105+
106+
Every Day 1–5 piece of work is operational end-to-end:
107+
108+
| Component | Status |
109+
|---|---|
110+
| `is_supported()` returning `true` on Apple Silicon | ✅ Day 1 |
111+
| `VzProvider::new` and `init` | ✅ Day 1 |
112+
| FFI builder assembling a complete `VZVirtualMachineConfiguration` | ✅ Day 2 |
113+
| Per-provider serial GCD queue | ✅ Day 2 |
114+
| Persistent `VZGenericMachineIdentifier` under the state dir | ✅ Day 2 |
115+
| `VZVirtualMachineConfiguration.validateWithError:` passing | ✅ Day 3 |
116+
| GCD-bridged async lifecycle (`start` / `stop`) | ✅ Day 3 |
117+
| Console pipe + `vm_console` tracing forwarder | ✅ Day 3 |
118+
| `com.apple.security.virtualization` entitlement | ✅ Day 4 |
119+
| `elastos vm-debug boot` CLI + capsule staging | ✅ Day 4 |
120+
| `setInitialRamdiskURL:` for distro kernels | ✅ Day 5 |
121+
| Real arm64 Linux kernel + initramfs + rootfs | ✅ Day 5 |
122+
| Guest reaches userspace (EXT4 mount + systemd up) | ✅ Day 5 |
123+
124+
The Linux kernel's identification of the hypervisor as
125+
`vm-other` confirms it sees Apple's hypercall surface but
126+
not the QEMU / KVM markers it would otherwise advertise. That
127+
is the correct Vz answer.
128+
129+
## What Day 5 deliberately did NOT do
130+
131+
These were called out as out-of-scope in the Day 5 prompt and
132+
remain that way:
133+
134+
1. **`VZVirtualMachineDelegate`** for richer guest-stop
135+
diagnostics. The polling-on-`current_state` + console
136+
forwarder + Apple's completion-handler-on-start trio is
137+
sufficient for this milestone. The delegate will land in
138+
Day 6 or Phase 3 when we need it (e.g. surfacing
139+
guest-side panic strings without parsing the console
140+
stream).
141+
2. **More than one kernel source variant.** The fetch script
142+
pins Ubuntu jammy `release-20260515`. If Ubuntu ever
143+
rotates the release path or stops the archive, Day 6 swaps
144+
in a different known-good source (Debian, Alpine, NixOS,
145+
or a hand-built Yocto image). The wiring (`--initramfs`,
146+
the SHA-verify loop) is artifact-agnostic.
147+
3. **Cloud-init bypass.** The guest's pause on
148+
`Wait for cloud-init Network to be Configured` is the
149+
expected behaviour for a cloud image booted without
150+
metadata. Phase 3 of the plan (capsule integration)
151+
produces capsule rootfs images that don't ship cloud-init
152+
at all, so this stops being relevant. The Day 5 attempt
153+
was about proving Vz boots Linux, not about productionising
154+
an Ubuntu image.
155+
4. **Pre-existing `elastos-server::setup::tests::*` failures.**
156+
Three tests in `setup.rs` continue to fail with the
157+
"no platform entry for darwin-arm64" message inherited
158+
from Pre-Work. They are unrelated to the Vz backend
159+
(their fix lives inside the `setup` module, which manages
160+
`components.json`) and were explicitly excluded from
161+
Phase 2's scope. Day 5 commits introduce zero new test
162+
failures.
163+
164+
## Reproducible local recipe
165+
166+
```bash
167+
# From the repo root.
168+
169+
# 1. Build.
170+
( cd elastos && cargo build -p elastos-server --bin elastos )
171+
172+
# 2. Sign.
173+
scripts/dev/sign-elastos-vz/sign.sh
174+
175+
# 3. Fetch artifacts (~700 MB download, ~2.2 GB after qcow2→raw).
176+
# Requires `qemu-img` (brew install qemu).
177+
scripts/dev/fetch-vz-kernel.sh
178+
179+
# 4. Boot.
180+
elastos/target/debug/elastos vm-debug boot \
181+
--rootfs ~/.local/share/elastos/vz-bin/rootfs.img \
182+
--kernel ~/.local/share/elastos/vz-bin/Image \
183+
--initramfs ~/.local/share/elastos/vz-bin/initramfs.img \
184+
--memory-mb 1024 \
185+
--boot-args 'console=hvc0 root=/dev/vda1 rw'
186+
187+
# Wait for `EXT4-fs (vda1): mounted filesystem` + the systemd
188+
# banner. Press Ctrl-C once the cloud-init wait kicks in.
189+
```
190+
191+
## Day 6 handoff
192+
193+
With first boot in hand, the next milestones — in the order
194+
the plan calls them out — are:
195+
196+
1. **Per-VM initramfs override path** so the supervisor can
197+
plumb per-capsule artifacts later in Phase 3 without
198+
touching the provider-wide default.
199+
2. **`VZVirtualMachineDelegate`** for guest-side fault
200+
surfacing (panic strings, vCPU traps, balloon events).
201+
The delegate will give us a structured analogue to Apple's
202+
opaque "Internal Virtualization error" — useful for the
203+
hardening phase.
204+
3. **Phase 3 (virtio plumbing) entrypoint:** wire the Carrier
205+
bridge through the multi-port `VZVirtioConsoleDeviceConfiguration`
206+
slot the builder already reserves at `/dev/hvc1`, replace
207+
the placeholder NSPipe attachment with a real socketpair,
208+
and connect to the supervisor's Carrier endpoint.
209+
210+
## Cross-references
211+
212+
- The plan: [`PLAN.md`](PLAN.md)
213+
- Day 0 / context: [`PHASE_0_SCOPE.md`](PHASE_0_SCOPE.md)
214+
- Day 4 outcome log: [`PHASE_2_DAY_4_NOTES.md`](PHASE_2_DAY_4_NOTES.md)
215+
- Operator recipe: [`../MAC.md`](../MAC.md) ("First boot on Apple Silicon")
216+
- Codesign helper: [`../../scripts/dev/sign-elastos-vz/README.md`](../../scripts/dev/sign-elastos-vz/README.md)
217+
- Kernel fetcher: [`../../scripts/dev/fetch-vz-kernel.sh`](../../scripts/dev/fetch-vz-kernel.sh)

0 commit comments

Comments
 (0)