|
| 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