mirage-hello #
A minimal MirageOS unikernel that boots on macOS / Apple
silicon under Solo5's hvt tender, backed by
Apple's Hypervisor.framework instead of Linux/KVM.
| ___|
__| _ \ | _ \ __ \
\__ \ ( | | ( | ) |
____/\___/ _|\___/____/
Solo5: Bindings version v0.12.0
Solo5: Memory map: 64 MB addressable:
Solo5: reserved @ (0x0 - 0xfffff)
Solo5: text @ (0x100000 - 0x24ffff)
Solo5: rodata @ (0x250000 - 0x28ffff)
Solo5: data @ (0x290000 - 0x55ffff)
Solo5: heap >= 0x560000 < stack < 0x4000000
2026-08-08T00:32:14-00:00: [INFO] [application] hello from MirageOS on Apple silicon (4)
2026-08-08T00:32:14-00:00: [INFO] [application] hello from MirageOS on Apple silicon (3)
2026-08-08T00:32:15-00:00: [INFO] [application] hello from MirageOS on Apple silicon (2)
2026-08-08T00:32:15-00:00: [INFO] [application] hello from MirageOS on Apple silicon (1)
Solo5: solo5_exit(0) called
The whole unikernel is the two files in this repo: config.ml
describes the device graph (nothing but a job here) and
unikernel.ml logs four times, half a second apart.
Why the forks #
Upstream Solo5 and ocaml-solo5 build only on Linux and the BSDs. Running this needs two forks:
- tsirysndr/solo5 — adds an
hvtbackend on Apple's Hypervisor.framework, plus the portability work to build the tender on a Mach-O host. - tsirysndr/ocaml-solo5 — makes the OCaml→Solo5 cross-compiler build on a macOS host.
Both are pinned from source below; neither is on opam yet.
Requirements #
- Apple silicon (M-series). There is no x86_64 Hypervisor.framework backend.
- macOS 11 or later.
- Xcode command line tools:
xcode-select --install - opam:
brew install opam
Step by step #
1. Install the toolchain dependencies #
Apple ships no ELF linker or objcopy, and no bare-metal aarch64 compiler runtime. Homebrew supplies all three:
brew install lld llvm aarch64-elf-gcc
llvm is keg-only, so put it on PATH for the rest of these steps:
export PATH="$(brew --prefix llvm)/bin:$PATH"
2. Clear inherited C flags #
If your shell exports CPPFLAGS or CFLAGS — Homebrew's openjdk sets
CPPFLAGS, for instance — they leak into ocamlfind ocamlopt, which rejects
them and fails the build on clock_stubs.o:
unset CPPFLAGS CFLAGS LDFLAGS
3. Create an opam switch #
ocaml-solo5 wants OCaml 5.4.1 or 5.5.0:
opam switch create solo5-mirage ocaml-base-compiler.5.4.1
eval $(opam env --switch=solo5-mirage --set-switch)
4. Pin the forks #
git clone -b hvf-macos-aarch64 https://github.com/tsirysndr/solo5.git
git clone -b macos-hvf-aarch64 https://github.com/tsirysndr/ocaml-solo5.git
opam pin add solo5 ./solo5 --kind=path -y
opam pin add ocaml-solo5 ./ocaml-solo5 --kind=path -y
Building ocaml-solo5 compiles a full OCaml cross-compiler; expect it to take
a while.
Note — a path pin copies the tree without
.git, so Solo5 cannot derive its version. If the pin fails withversion.h.distrib: Not found, run./scripts/gen_version_h.sh include/version.h.distribinside the clone once.
5. Install mirage #
opam install mirage
6. Build the unikernel #
mirage configure -t hvt
make
This produces dist/hello.hvt, a statically linked aarch64 ELF binary. It is
not a macOS executable — it only runs inside the tender.
7. Run it #
solo5-hvt --mem=64 dist/hello.hvt
Troubleshooting #
hv_vm_create() failed: 0xfae94001 — the tender lost its code signature.
Hypervisor.framework refuses to create a VM without the
com.apple.security.hypervisor entitlement, and entitlements only count as part
of a signature, so anything that rewrites the binary (strip, some installers)
drops it. Re-sign from the Solo5 checkout:
codesign --force --sign - \
--entitlements tenders/hvt/solo5-hvt.entitlements \
$(which solo5-hvt)
ocamlopt: unknown option '-I/opt/homebrew/...' — see step 2.
llvm-objcopy: command not found — see step 1; llvm is keg-only and not
linked into /opt/homebrew/bin.
configure.sh: ERROR: ld.lld is not LLVM LLD — lld is missing from
PATH, or an older Solo5 checkout is being used whose version check does not
recognise Homebrew's banner.
Known limitations #
These come from the Solo5 macOS backend, not from this example:
- The tender does not drop privileges. macOS has no equivalent of
seccomp,pledge(2)orcapsicum(4), so the sandboxing Solo5 applies on other hosts is absent here. --net:only accepts the@<fd>form. macOS has no TAP device, so frames have to be shuttled by a helper such asvmnet-helperorpasstover a socket passed to the tender. Named interfaces returnENOTSUP.- There is no
solo5-hvt-debug; the gdb and dumpcore backends are unported.
Going further #
config.ml is where a real unikernel grows. Adding a device — a block store, a
network stack, a console — means adding it to the job's signature there and
re-running mirage configure. See the
MirageOS docs and
mirage-skeleton for worked
examples.