Solo5 hvt ABI and MFT1/ABI1 note format as wire codecs
10 kB
354 lines
1#!/bin/sh
2# End-to-end boot harness for solo5-kvm under *real* KVM on an Apple-silicon Mac.
3#
4# There is no /dev/kvm on macOS, but Apple's Virtualization.framework can expose
5# nested virtualisation to a Linux guest (M3+, macOS 15+). So this:
6#
7# 1. builds, in a Linux/arm64 container, the solo5-kvm tender and the small
8# aarch64 hvt guests, and packs them into an initramfs with a busybox init;
9# 2. boots a linuxkit kernel + that initramfs in a VM via `kvm-vz` (nested
10# virt on), where the guest kernel is itself a KVM host;
11# 3. the init runs each guest through solo5-kvm on the guest's own /dev/kvm and
12# prints a marker the host greps for.
13#
14# It is the solo5-hvf boot harness (test/boot/run.sh) closed over the KVM
15# backend: byte-identical guests, asserted the same way, but on real KVM.
16#
17# Needs Docker (for the Linux build) and `kvm-vz` (the ocaml-vz launcher). SKIPs
18# cleanly when either is missing or the host lacks nested virt.
19#
20# Usage: run_kvm_vz.sh [PATH/TO/kvm-vz] (defaults to dune's built kvm-vz)
21
22set -eu
23
24root=$(cd "$(dirname "$0")/../../.." && pwd) # monorepo root
25work=$(mktemp -d)
26trap 'rm -rf "$work"' EXIT
27skip() { echo "SKIP: $1"; exit 0; }
28
29command -v docker >/dev/null 2>&1 || skip "no docker"
30docker info >/dev/null 2>&1 || skip "no docker daemon"
31
32# A linuxkit kernel (6.6.13): an arm64 Image that boots from an external
33# initramfs and has arm64 KVM built in. (Docker Desktop's own kernel is built for
34# a disk root and silently ignores an initramfs, so it cannot be used here.)
35# Resolve the linuxkit OCI image from uniboot's cache if warm, else pull it; the
36# kernel ships gzipped, so decompress to a raw Image.
37kver=linuxkit/kernel:6.6.13
38kernel="$work/kernel"
39cached=$(find "$HOME/.cache/uniboot/checkout/linuxkit" -path '*rootfs/kernel' 2>/dev/null | head -1)
40if [ -n "$cached" ]; then
41 gunzip -c "$cached" >"$kernel" 2>/dev/null || cp "$cached" "$kernel"
42else
43 echo "solo5-kvm-vz: pulling $kver ..."
44 docker pull --platform linux/arm64 "$kver" >/dev/null 2>&1 || skip "cannot pull $kver"
45 cid=$(docker create --platform linux/arm64 "$kver" 2>/dev/null) || skip "cannot create $kver"
46 docker cp "$cid:/kernel" "$work/kernel.gz" >/dev/null 2>&1 \
47 || { docker rm "$cid" >/dev/null 2>&1; skip "no /kernel in $kver"; }
48 docker rm "$cid" >/dev/null 2>&1
49 gunzip -c "$work/kernel.gz" >"$kernel" 2>/dev/null || cp "$work/kernel.gz" "$kernel"
50fi
51[ -s "$kernel" ] || skip "kernel resolution failed"
52
53# kvm-vz: argument, or dune's codesigned build.
54kvmvz=${1:-}
55if [ -z "$kvmvz" ]; then
56 kvmvz="$root/_build/default/ocaml-vz/bin/kvm-vz-signed"
57 [ -x "$kvmvz" ] || (cd "$root" && dune build ocaml-vz/bin/kvm-vz-signed) \
58 || skip "cannot build kvm-vz"
59fi
60[ -x "$kvmvz" ] || skip "no kvm-vz at $kvmvz"
61
62image="mono-linux-test:ocaml-5.4"
63docker image inspect "$image" >/dev/null 2>&1 \
64 || skip "base image $image missing (run ./docker-test.sh once to build it)"
65
66# ---- The in-VM init: run each guest on the guest's real /dev/kvm. -------------
67# Built into the initramfs as /init. Markers: HAVE-DEVKVM, per-guest OK/FAIL, and
68# ALL-PASS, which the host asserts.
69cat >"$work/init" <<'INIT'
70#!/bin/busybox sh
71busybox mount -t devtmpfs dev /dev 2>/dev/null
72busybox mount -t proc proc /proc 2>/dev/null
73busybox mount -t sysfs sys /sys 2>/dev/null
74export PATH=/bin
75[ -e /dev/kvm ] || busybox mknod /dev/kvm c 10 232 2>/dev/null
76echo "KVMVZ-INIT-START"
77if [ -e /dev/kvm ]; then echo "KVMVZ-HAVE-DEVKVM"; else echo "KVMVZ-NO-DEVKVM"; fi
78pass=0; fail=0
79# A guest that hangs inside KVM_RUN (runaway, W^X fault loop) is stopped the way
80# upstream Solo5 stops one -- by a signal. A background killer sends SIGTERM after
81# a deadline (no timeout(1) applet in this initramfs), which the tender's handler
82# turns into exit 2; fast guests finish first and the killer is reaped.
83run() { # run NAME EXPECT
84 name=$1; want=$2
85 /solo5-kvm --mem=2 "/guests/$name.hvt" >/g.out 2>&1 &
86 gpid=$!
87 ( busybox sleep 2; busybox kill -TERM "$gpid" 2>/dev/null ) &
88 kpid=$!
89 wait "$gpid"; rc=$?
90 busybox kill "$kpid" 2>/dev/null
91 out=$(busybox cat /g.out)
92 if [ "$rc" = "$want" ]; then
93 echo " OK $name (exit $rc) ${out:+| $out}"; pass=$((pass+1))
94 else
95 echo " FAIL $name: exit $rc (want $want) | $out"; fail=$((fail+1))
96 fi
97}
98run halt 42
99run hello 0
100run oob 2
101run badhc 2
102run runaway 2
103run wx 2
104run data_noexec 2
105run virtio_magic 0
106run poll 0
107echo "=== seccomp audit (denied syscalls, if any) ==="
108dmesg 2>/dev/null | grep -iE "seccomp|syscall=" | tail -12
109echo "KVMVZ-RESULT pass=$pass fail=$fail"
110[ "$fail" = 0 ] && echo "KVMVZ-ALL-PASS"
111busybox poweroff -f
112INIT
113
114# ---- Build the Linux artifacts + initramfs in the container. -----------------
115# Native aarch64 in the container, so plain gcc/as assemble the guests (no cross
116# toolchain) and the solo5-kvm tender builds from source. The guest .S bodies are
117# the same as test/boot/run_kvm.sh; kept compact here (the hand-assembled set,
118# without the cross-cc real-unikernel and slirp cases).
119build='
120set -eu
121B=/tmp/dune-build
122dune build ocaml-solo5/bin/main_kvm.exe
123out=/tmp/kvmvz-out
124rm -rf $out && mkdir -p $out/root/bin $out/root/guests $out/root/dev \
125 $out/root/proc $out/root/sys
126
127# busybox + a shell and applets.
128cp /bin/busybox.static $out/root/bin/busybox 2>/dev/null || cp "$(command -v busybox)" $out/root/bin/busybox
129$out/root/bin/busybox --install -s $out/root/bin
130
131# the tender + its dynamic loader/libs (musl).
132cp $B/default/ocaml-solo5/bin/main_kvm.exe $out/root/solo5-kvm
133for lib in $(ldd $out/root/solo5-kvm 2>/dev/null | awk "/=>/{print \$3} /ld-musl/{print \$1}" | sort -u); do
134 [ -f "$lib" ] && { mkdir -p "$out/root$(dirname "$lib")"; cp "$lib" "$out/root$lib"; }
135done
136
137# the init.
138cp /src-out-init $out/root/init
139chmod +x $out/root/init $out/root/solo5-kvm
140
141# Solo5 ABI + manifest notes shared by every guest (.balign: .align is 2^N on ARM).
142notes() {
143cat <<EOF
144.section .note.solo5.abi, "a"
145.balign 4
146.long 6
147.long 16
148.long 0x31494241
149.asciz "Solo5"
150.balign 4
151.long 1
152.long 2
153.long 0
154.long 0
155.section .note.solo5.manifest, "a"
156.balign 4
157.long 6
158.long 12
159.long 0x3154464d
160.asciz "Solo5"
161.balign 4
162.long 0
163.long 1
164.long 0
165.text
166.balign 4
167.global _start
168_start:
169EOF
170}
171cat >/tmp/guest.ld <<EOF
172ENTRY(_start)
173PHDRS { text PT_LOAD FLAGS(5); data PT_LOAD FLAGS(6); note PT_NOTE FLAGS(4); }
174SECTIONS {
175 . = 0x100000;
176 .text : { *(.text) } :text
177 . = ALIGN(0x4000);
178 .data : { *(.data) } :data
179 .note : { *(.note.solo5.abi) *(.note.solo5.manifest) } :note
180}
181EOF
182build_guest() { { notes; cat; } >/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; }
183
184build_guest halt <<EOF
185 movz x1, #0x40
186 movk x1, #0x1, lsl #32
187 adr x0, hc
188 str w0, [x1]
1891: b 1b
190.balign 8
191hc: .quad 0
192 .word 42
193 .word 0
194EOF
195build_guest hello <<EOF
196 adr x2, msg
197 adr x3, ps
198 str x2, [x3]
199 movz x1, #0x10
200 movk x1, #0x1, lsl #32
201 mov w0, w3
202 str w0, [x1]
203 movz x1, #0x40
204 movk x1, #0x1, lsl #32
205 adr x0, hs
206 str w0, [x1]
2071: b 1b
208msg: .ascii "hello\n"
209.set mlen, . - msg
210.data
211.balign 8
212ps: .quad 0
213 .quad mlen
214hs: .quad 0
215 .word 0
216 .word 0
217EOF
218build_guest oob <<EOF
219 movz x1, #0x10
220 movk x1, #0x1, lsl #32
221 movz w0, #0xffff
222 movk w0, #0xffff, lsl #16
223 str w0, [x1]
2241: b 1b
225EOF
226build_guest badhc <<EOF
227 movz x1, #0x100
228 movk x1, #0x1, lsl #32
229 movz w0, #0
230 str w0, [x1]
2311: b 1b
232EOF
233build_guest runaway <<EOF
234 udf #0
235EOF
236build_guest wx <<EOF
237 adr x2, _start
238 str w2, [x2]
239 movz x1, #0x40
240 movk x1, #0x1, lsl #32
241 adr x0, hs
242 str w0, [x1]
2431: b 1b
244.data
245.balign 8
246hs: .quad 0
247 .word 0
248 .word 0
249EOF
250build_guest data_noexec <<EOF
251 adr x0, data_exec
252 br x0
253.data
254.balign 4
255data_exec:
256 movz x1, #0x40
257 movk x1, #0x1, lsl #32
258 adr x0, data_hs
259 str w0, [x1]
2601: b 1b
261.balign 8
262data_hs: .quad 0
263 .word 0
264 .word 0
265EOF
266build_guest virtio_magic <<EOF
267 movz x1, #0x4000, lsl #16
268 movk x1, #0x1, lsl #32
269 ldr w0, [x1]
270 movz w2, #0x6976
271 movk w2, #0x7472, lsl #16
272 cmp w0, w2
273 b.ne fail
274 movz x1, #0x40
275 movk x1, #0x1, lsl #32
276 adr x0, hs_ok
277 str w0, [x1]
2781: b 1b
279fail:
280 movz x1, #0x40
281 movk x1, #0x1, lsl #32
282 adr x0, hs_fail
283 str w0, [x1]
2842: b 2b
285.data
286.balign 8
287hs_ok: .quad 0
288 .word 0
289 .word 0
290hs_fail: .quad 0
291 .word 1
292 .word 0
293EOF
294
295# A guest with no net device that issues POLL with a 1 ms deadline, then halts 0.
296# It exercises the no-device POLL wait, which on Linux is epoll_pwait under the
297# seccomp sandbox; a clean exit 0 proves the epoll path holds. POLL is hypercall
298# 3 (0x18); the struct is IN timeout_nsecs, OUT ready_set + ret, so it lives in
299# writable .data.
300build_guest poll <<EOF
301 adr x0, pp
302 movz x1, #0x18
303 movk x1, #0x1, lsl #32
304 str w0, [x1]
305 movz x1, #0x40
306 movk x1, #0x1, lsl #32
307 adr x0, hs
308 str w0, [x1]
3091: b 1b
310.data
311.balign 8
312pp: .quad 1000000
313 .quad 0
314 .word 0
315 .word 0
316hs: .quad 0
317 .word 0
318 .word 0
319EOF
320
321# Pack the initramfs.
322( cd $out/root && find . | busybox cpio -o -H newc 2>/dev/null | gzip -9 ) >$out/initramfs.cpio.gz
323echo "initramfs: $(wc -c <$out/initramfs.cpio.gz) bytes"
324cp $out/initramfs.cpio.gz /out-host/initramfs.cpio.gz
325'
326
327echo "solo5-kvm-vz: building Linux artifacts + initramfs in $image ..."
328docker run --rm --privileged \
329 -v "$root":/src \
330 -v /src/_opam \
331 -v "$work":/out-host \
332 -v "$work/init":/src-out-init:ro \
333 -w /src \
334 -e DUNE_BUILD_DIR=/tmp/dune-build \
335 "$image" \
336 bash -ceu "$build"
337
338[ -f "$work/initramfs.cpio.gz" ] || { echo "FAIL: initramfs not built"; exit 1; }
339
340# ---- Boot it under nested KVM and assert the markers. ------------------------
341echo "solo5-kvm-vz: booting under kvm-vz (nested virt) ..."
342"$kvmvz" --timeout 120 --cmdline "console=hvc0" \
343 --initrd "$work/initramfs.cpio.gz" "$kernel" 2>&1 | tee "$work/console.log" || true
344
345echo "---"
346if grep -q KVMVZ-ALL-PASS "$work/console.log"; then
347 echo "solo5-kvm-vz: ALL PASS (guests ran on real /dev/kvm)"
348 exit 0
349else
350 grep -q KVMVZ-NO-DEVKVM "$work/console.log" \
351 && echo "solo5-kvm-vz: guest kernel has no /dev/kvm (kernel lacks CONFIG_KVM)"
352 echo "solo5-kvm-vz: FAILED (no ALL-PASS marker)"
353 exit 1
354fi