#!/bin/sh # End-to-end boot harness for solo5-kvm under *real* KVM on an Apple-silicon Mac. # # There is no /dev/kvm on macOS, but Apple's Virtualization.framework can expose # nested virtualisation to a Linux guest (M3+, macOS 15+). So this: # # 1. builds, in a Linux/arm64 container, the solo5-kvm tender and the small # aarch64 hvt guests, and packs them into an initramfs with a busybox init; # 2. boots a linuxkit kernel + that initramfs in a VM via `kvm-vz` (nested # virt on), where the guest kernel is itself a KVM host; # 3. the init runs each guest through solo5-kvm on the guest's own /dev/kvm and # prints a marker the host greps for. # # It is the solo5-hvf boot harness (test/boot/run.sh) closed over the KVM # backend: byte-identical guests, asserted the same way, but on real KVM. # # Needs Docker (for the Linux build) and `kvm-vz` (the ocaml-vz launcher). SKIPs # cleanly when either is missing or the host lacks nested virt. # # Usage: run_kvm_vz.sh [PATH/TO/kvm-vz] (defaults to dune's built kvm-vz) set -eu root=$(cd "$(dirname "$0")/../../.." && pwd) # monorepo root work=$(mktemp -d) trap 'rm -rf "$work"' EXIT skip() { echo "SKIP: $1"; exit 0; } command -v docker >/dev/null 2>&1 || skip "no docker" docker info >/dev/null 2>&1 || skip "no docker daemon" # A linuxkit kernel (6.6.13): an arm64 Image that boots from an external # initramfs and has arm64 KVM built in. (Docker Desktop's own kernel is built for # a disk root and silently ignores an initramfs, so it cannot be used here.) # Resolve the linuxkit OCI image from uniboot's cache if warm, else pull it; the # kernel ships gzipped, so decompress to a raw Image. kver=linuxkit/kernel:6.6.13 kernel="$work/kernel" cached=$(find "$HOME/.cache/uniboot/checkout/linuxkit" -path '*rootfs/kernel' 2>/dev/null | head -1) if [ -n "$cached" ]; then gunzip -c "$cached" >"$kernel" 2>/dev/null || cp "$cached" "$kernel" else echo "solo5-kvm-vz: pulling $kver ..." docker pull --platform linux/arm64 "$kver" >/dev/null 2>&1 || skip "cannot pull $kver" cid=$(docker create --platform linux/arm64 "$kver" 2>/dev/null) || skip "cannot create $kver" docker cp "$cid:/kernel" "$work/kernel.gz" >/dev/null 2>&1 \ || { docker rm "$cid" >/dev/null 2>&1; skip "no /kernel in $kver"; } docker rm "$cid" >/dev/null 2>&1 gunzip -c "$work/kernel.gz" >"$kernel" 2>/dev/null || cp "$work/kernel.gz" "$kernel" fi [ -s "$kernel" ] || skip "kernel resolution failed" # kvm-vz: argument, or dune's codesigned build. kvmvz=${1:-} if [ -z "$kvmvz" ]; then kvmvz="$root/_build/default/ocaml-vz/bin/kvm-vz-signed" [ -x "$kvmvz" ] || (cd "$root" && dune build ocaml-vz/bin/kvm-vz-signed) \ || skip "cannot build kvm-vz" fi [ -x "$kvmvz" ] || skip "no kvm-vz at $kvmvz" image="mono-linux-test:ocaml-5.4" docker image inspect "$image" >/dev/null 2>&1 \ || skip "base image $image missing (run ./docker-test.sh once to build it)" # ---- The in-VM init: run each guest on the guest's real /dev/kvm. ------------- # Built into the initramfs as /init. Markers: HAVE-DEVKVM, per-guest OK/FAIL, and # ALL-PASS, which the host asserts. cat >"$work/init" <<'INIT' #!/bin/busybox sh busybox mount -t devtmpfs dev /dev 2>/dev/null busybox mount -t proc proc /proc 2>/dev/null busybox mount -t sysfs sys /sys 2>/dev/null export PATH=/bin [ -e /dev/kvm ] || busybox mknod /dev/kvm c 10 232 2>/dev/null echo "KVMVZ-INIT-START" if [ -e /dev/kvm ]; then echo "KVMVZ-HAVE-DEVKVM"; else echo "KVMVZ-NO-DEVKVM"; fi pass=0; fail=0 # A guest that hangs inside KVM_RUN (runaway, W^X fault loop) is stopped the way # upstream Solo5 stops one -- by a signal. A background killer sends SIGTERM after # a deadline (no timeout(1) applet in this initramfs), which the tender's handler # turns into exit 2; fast guests finish first and the killer is reaped. run() { # run NAME EXPECT name=$1; want=$2 /solo5-kvm --mem=2 "/guests/$name.hvt" >/g.out 2>&1 & gpid=$! ( busybox sleep 2; busybox kill -TERM "$gpid" 2>/dev/null ) & kpid=$! wait "$gpid"; rc=$? busybox kill "$kpid" 2>/dev/null out=$(busybox cat /g.out) if [ "$rc" = "$want" ]; then echo " OK $name (exit $rc) ${out:+| $out}"; pass=$((pass+1)) else echo " FAIL $name: exit $rc (want $want) | $out"; fail=$((fail+1)) fi } run halt 42 run hello 0 run oob 2 run badhc 2 run runaway 2 run wx 2 run data_noexec 2 run virtio_magic 0 run poll 0 echo "=== seccomp audit (denied syscalls, if any) ===" dmesg 2>/dev/null | grep -iE "seccomp|syscall=" | tail -12 echo "KVMVZ-RESULT pass=$pass fail=$fail" [ "$fail" = 0 ] && echo "KVMVZ-ALL-PASS" busybox poweroff -f INIT # ---- Build the Linux artifacts + initramfs in the container. ----------------- # Native aarch64 in the container, so plain gcc/as assemble the guests (no cross # toolchain) and the solo5-kvm tender builds from source. The guest .S bodies are # the same as test/boot/run_kvm.sh; kept compact here (the hand-assembled set, # without the cross-cc real-unikernel and slirp cases). build=' set -eu B=/tmp/dune-build dune build ocaml-solo5/bin/main_kvm.exe out=/tmp/kvmvz-out rm -rf $out && mkdir -p $out/root/bin $out/root/guests $out/root/dev \ $out/root/proc $out/root/sys # busybox + a shell and applets. cp /bin/busybox.static $out/root/bin/busybox 2>/dev/null || cp "$(command -v busybox)" $out/root/bin/busybox $out/root/bin/busybox --install -s $out/root/bin # the tender + its dynamic loader/libs (musl). cp $B/default/ocaml-solo5/bin/main_kvm.exe $out/root/solo5-kvm for lib in $(ldd $out/root/solo5-kvm 2>/dev/null | awk "/=>/{print \$3} /ld-musl/{print \$1}" | sort -u); do [ -f "$lib" ] && { mkdir -p "$out/root$(dirname "$lib")"; cp "$lib" "$out/root$lib"; } done # the init. cp /src-out-init $out/root/init chmod +x $out/root/init $out/root/solo5-kvm # Solo5 ABI + manifest notes shared by every guest (.balign: .align is 2^N on ARM). notes() { cat </tmp/guest.ld </tmp/$1.S; as -o /tmp/$1.o /tmp/$1.S; ld -T /tmp/guest.ld --build-id=none -o $out/root/guests/$1.hvt /tmp/$1.o; } build_guest halt </dev/null | gzip -9 ) >$out/initramfs.cpio.gz echo "initramfs: $(wc -c <$out/initramfs.cpio.gz) bytes" cp $out/initramfs.cpio.gz /out-host/initramfs.cpio.gz ' echo "solo5-kvm-vz: building Linux artifacts + initramfs in $image ..." docker run --rm --privileged \ -v "$root":/src \ -v /src/_opam \ -v "$work":/out-host \ -v "$work/init":/src-out-init:ro \ -w /src \ -e DUNE_BUILD_DIR=/tmp/dune-build \ "$image" \ bash -ceu "$build" [ -f "$work/initramfs.cpio.gz" ] || { echo "FAIL: initramfs not built"; exit 1; } # ---- Boot it under nested KVM and assert the markers. ------------------------ echo "solo5-kvm-vz: booting under kvm-vz (nested virt) ..." "$kvmvz" --timeout 120 --cmdline "console=hvc0" \ --initrd "$work/initramfs.cpio.gz" "$kernel" 2>&1 | tee "$work/console.log" || true echo "---" if grep -q KVMVZ-ALL-PASS "$work/console.log"; then echo "solo5-kvm-vz: ALL PASS (guests ran on real /dev/kvm)" exit 0 else grep -q KVMVZ-NO-DEVKVM "$work/console.log" \ && echo "solo5-kvm-vz: guest kernel has no /dev/kvm (kernel lacks CONFIG_KVM)" echo "solo5-kvm-vz: FAILED (no ALL-PASS marker)" exit 1 fi