Commits
Ptime.Span.of_int_s 3_153_600_000 does not fit a 31-bit int: on the
wasi arm the folded literal truncated to ~2001, an expired-at-creation
certificate, with a wasm_of_ocaml integer-overflow warning pointing at
it. The same 100 years as Ptime.Span.v (36500, 0L) is exact on every
backend.
The slicing-by-8 engine is now written once over Optint.t, whose
representation optint picks at run time from the actual word size
(Immediate64 dispatch on Sys.word_size): immediate int on 64-bit,
boxed Int32 elsewhere. Every intermediate stays within 32 bits of its
own accord, so the same code is exact on native, bytecode, js_of_ocaml
and wasm. The slice_native and slice_portable backends, the
default_implementation selection, and the narrow-int refusal guard all
disappear; there is nothing left to mis-select.
Benchmarked (bench/bench.ml, arm64, CRC-32C): release profile
1530-1632 MB/s for the Optint engine vs 1344-1606 MB/s for the raw-int
slice_native across 64B-64KB - parity at small sizes, +14% at 256B and
up (the 32-bit register invariant makes the top-byte mask redundant,
which the old loop paid for); dev profile +11% at 4KB. flambda folds
the dispatch; there is no profile where the Optint engine loses.
Consumers no longer select a backend: drop nox-crc.slice_portable from
ocaml-slirp/examples/web, from the mirage wasi rule (mirage/lib/
rules.ml), and from the 57 regenerated cross-build arms in
unikernels/bin/gen. The identical per-backend test suites merge into
test/slice. The wasi arms still print optint's own dead-code
integer-overflow warnings (its immediate arm never runs on a 32-bit
word); those predate this change.
Makes the committed test/js spec test pass: a plain nox-crc link
computes the catalogue check values exactly under node.
Opt local network fixtures into loopback egress now that the native slirp backend preserves the default SSRF denylist. Refresh the TCP compute probes and tm-downlink table transcript to match their current programs.
The server assumed one multi-queue device, where the transport steers a
flow by destination port and so each shard must listen on a port
congruent to its index. Several single-queue devices behave differently:
each link is its own L2 network, so no steering is needed and every
shard serves the same port on its own link. Tell the two apart by the
netifs' MACs -- one device's queues share its address, separate devices
do not -- and pick the port scheme accordingly.
Add the eio/eio_main/eio_posix pin-depends templates the other unikernels
carry, so a standalone opam build of unikernel-columnar and
unikernel-compress pins the same eio the monorepo does.
mrg injects parquet.compress_backend_portable into a cross arm only when
the unikernel links parquet, so a parquet unikernel builds for solo5,
unikraft, xen and wasi -- none of which carry libzstd or libz -- while
keeping the fast C reader stubs. Add storage/columnar: it writes a 500-row
table to Parquet with Snappy and LZ4 pages and reads it back through the
typed row codec, running identically as a native binary and a 31-bit wasi
guest (the negative scores exercise the reader's INT32 sign-extension).
Regenerate the cross arms. This also propagates nox-crc.slice_portable into
every wasi arm, which the generator emits but the committed arms had been
missing.
Round-trips a compressible chunk through both pure-OCaml block compressors and
reports each ratio. It builds and runs natively, as a Solo5 unikernel, and as a
31-bit wasi (wasm) guest -- the target that makes the LZ4/Snappy Int32 match and
hash matter: both compile warning-free there and produce byte-identical output
on 64-bit. Gives the two codecs a real wasm-reachable consumer.
The tcp-server and smp demo workloads run a glibc LCG whose multiplier
1103515245 overflows a 31-bit int; these unikernels also build for the wasi
(wasm) target. Bind the multiplier (split into 16-bit halves) once outside the
allocation-free inner loop: loop-invariant, so the 64-bit multiply is unchanged.
unikernel-smp's wasi build compiles through wasm_of_ocaml; without the
declaration the compiler bump dropped the tool from the switch and the
default build alias failed.
A mirage-eio xen unikernel with two netfronts bridged by the in-guest
loopback: a TCP echo server on netif "0" and a client on netif "1" that
connects across the bridge, sends a probe, and checks the echo -- two
full TCP stacks reaching each other with no dom0. The skeleton the iperf
and sql+ssh unikernels are app swaps on.
Replace tcp-server's catch-all int parse with int_of_string_opt so the
package carries no catch-all handler.
A dom0less domU with two netfronts: an echo server on netif 0 and a client on netif 1, each a full TCP stack, that reach each other over the in-guest loopback (loopback.ml floods each frame to the other vif). It is the skeleton the iperf and sql+ssh unikernels swap their apps onto, and the first live netfront TX/RX path in a boot shape -- the four existing shapes exercise block, xenstore, and static shm, not a served network round-trip.
The cram and setup predated the sharded rewrite. Drive with --cores 1 (one
stack over one queue) and assert the per-shard transfer lines the unikernel
now prints.
The iperf-server unikernel moves onto sharded_netif: one share-nothing
TCP stack per receive queue per worker domain, so the benchmark
exercises the multiqueue path end to end.
Tcp_eio.Net.sharded_over_netif runs N independent stacks, one per pool worker
domain: the boot domain reads the device and routes each frame to its owner
shard by the symmetric four-tuple hash (Tcp.Shard.route), so a flow and its
connection state stay on one core for its lifetime, and each shard's egress
leaves through its own queue drained by its own boot fiber -- no shared transmit
lock. A session that spans several connections (iperf3: control + data streams)
passes a route to steer by destination port instead.
Move Slirp.tcp_route (frame -> four-tuple) and is_arp_frame into Tcp_eio.Net:
the generic frame-routing both the slirp edge and the guest driver need, not
slirp-specific. slirp_eio uses the moved versions.
The iperf3 server shards across cores: each shard serves its own port and a
whole test stays on one core. On a single-queue native netif the aggregate caps
at ~10 Gbit/s (the one device reader is the ceiling); a multi-queue netif lets
it scale past that.
The echo pair proved the stack; it could not show whether the domains
pool buys anything over TCP. The server now offloads a CPU-bound LCG
chain per connection to the pool (accept and socket I/O stay on the
boot domain, where the from-scratch stack lives), and the client fires
the same requests sequentially then all at once: the ratio is the
multicore speedup as seen from outside, checksum-verified both ways.
Both ring halves live in the same guest; naming domain 0 only worked
when the harness booted it as dom0.
The cross-only guest is built by nothing on the host, so it rotted
unseen: the client API became a pure value threading its request ids
(b190b792) and the link lost its load address when a missing
TEXT_START became a loud error (2e9bef81). Thread the client through
the requests, link at GUEST_RAM0_BASE, and generate the startup with
the shared solo5 shim.
mrg registry output after the rules.ml change: every xen arm now
generates its xstartup.c with the shared solo5 startup instead of the
static-argv shim.
The wasi arm links bytecode through wasm_of_ocaml, so its link runs
ocamlc, which rejects the release profile's -use-lto (an ocamlopt
flag). With that flag restored to the workspace env, every cross build
of a unikernel failed at the wasi executable -- including
[dune build -x solo5], which builds the default context's arms too.
The cross arms are immune for a reason worth stating: each states its
own (link_flags ...), which replaces the env's rather than extending
it. The wasi arm had none, so it inherited. Give it an empty one.
Regenerates all 54 committed arms; the gen rules diff them against a
fresh walk, so they fail until regenerated.
The example moved to an Eio.Executor_pool demo but its cram test and README
still described the Domain.spawn Fibonacci demo it replaced, so the committed
test failed and the README claimed unikraft schedules domains cooperatively on
the boot core -- the opposite of what the backend now does.
Each job now computes an LCG chain seeded by its position and the unikernel
reports the sum, which is fixed by the arithmetic alone, so the test pins down
that every job ran exactly once and computed the right chain rather than only
that the run printed a timing. The parallel total is also compared against the
same total computed sequentially. --jobs and --iterations size the workload, so
the machine check stays fast and deterministic while the defaults still
saturate the pool for long enough to measure.
Being a directory in unikernels/bin rather than under a category, smp was
invisible to `mrg registry`, which walks category/name -- so its cross-build
arms were never regenerated or diffed and had gone stale. Under simple/ the
registry regenerates them, which is what picks up the cmdliner dependency the
boot arguments need.
The bad fixture only exercised the missing-.mli path, so the branch
that inspects an existing interface's suite type had no test at all --
the branch just taught to skip an unresolved typedtree. Skipping and
going silent are one edit apart with nothing to tell them apart.
bad_type/ is good/ with an internal leaked into the interface: built,
so the type resolves, and still reported.
Each domain calls R.irq_selftest once at boot, demonstrating the GICv3
interrupt subsystem in a dom0less domU. Boot-verified: both domains report the
SGI handler FIRED.
The demo boots two dom0less domUs that increment a counter in one
xen,static-shm page, signalling over the static event channel -- proof of a
real cross-domain data plane (the count reaches 2x each domain's rounds, so
both wrote the same host page) with no xenstore, no dom0, no grants.
Boot-verified against a real Xen/ARM hypervisor.
With only a couple of waves' worth of jobs, the parallel run drains
before it fills and its speedup falls well short of the worker count.
Submit enough jobs to keep every worker busy across the whole run, and
enlarge the minor heap up front so the boot's submit-time allocation
does not trigger collections that pause the workers mid-run.
The xen link script leaves TEXT_START undefined so a missing link address
is a loud error; rules.ml passes -Wl,--defsym,TEXT_START=0x40000000, but the
committed per-unikernel arms predate that and omitted it, so every generic
xen unikernel failed to link (symbol not found: TEXT_START) and the gen-diff
test was red. Regenerated via mrg dune. Boot-verified: hello-world links,
loads at 0x40000000, and prints under a dom0less Xen boot.
eio resolves only from the local opam overlay, so a fresh switch picks
up the published version instead of the commit unikernel-smp is built
against. Declare the pin already used tree-wide.
CONFIG_STATIC_MEMORY sits behind UNSUPPORTED in mainline Xen, so static partitioning is a vendor-BSP / functional-safety feature, not a mainline-supported one. Say that plainly, and point at the normal PV domU as the production path on a standard Xen.
The two-domain demo README shows the working mrg run -t xen --domains 2 flow and the Xen STATIC_MEMORY config it needs. self-boot's README no longer claims mrg run -t xen boots it -- it links at the dom0 load address, so its run path moves to a xen.dom0 target; it points at the two-domain demo for the dom0less domU path.
With solo5-abi=xen now defaulting to the domU load address, the two-domain demo links as the deployable unikernel.xen (0x40000000) and runs via mrg run -t xen --domains 2. self-boot, a dom0 selftest, overrides to Xen's dom0 load address (0x60000000) with --defsym.
Two mirage-eio unikernels boot as sibling domUs (d1, d2) and ping-pong over a static interdomain event channel Xen pre-binds from the host device tree. Each end yields the vCPU in SCHEDOP_poll until the peer's EVTCHNOP_send wakes it, then notifies back: event-driven, no busy-poll, no dom0, no xenstore. The channel is symmetric, so both domains run one image and need no role.
self-boot's scheduler phase requests all three of the Mirage_eio timing and
entropy devices and reads each through its Eio interface over the linked Xen
backend: the wall clock (RTC-backed real epoch), the monotonic clock
(CNTVCT_EL0), and the rng (Fortuna). Confirms the DSL device surface, not just
the runtime primitives, works on xen.
The runtime now anchors clock_wall to the platform RTC, so wall time has a
real epoch on a hardware dom0 (TLS validation needs it). self-boot checks both
the runtime clock_wall and the Mirage_eio DSL clock device (Eio.Time.now over
the linked Xen backend) read a post-2017 epoch, not time since boot.
The device-tree phase now reads each virtio-mmio transport's register
block over R.mmio_read32: transport 0's magic 'virt' proves the 32-bit
register accessor reaches the device, and a populated slot's nonzero DeviceID
(1 = net, 2 = block) is reported. With a virtio-net-device attached this finds
the net transport, confirming the discovery path a virtio-net frontend needs.
A bootable mirage-eio unikernel that drives every Xen/ARM PV layer live
and reports one check per step -- the hypercall runtime and clocks, a live
interdomain event channel, the hypervisor-mapped grant table, netfront and
blkfront end to end, the Mirage_eio.run scheduler path, and the boot device
tree. It runs through mrg (mrg run -t xen unikernels/bin/xen/self-boot),
alongside the other bootable unikernels, rather than from the library tree.
The device-tree phase reads the guest's own platform layout (RAM base and
size, the 32 virtio-mmio transports and their GIC interrupts) from the FDT Xen
passes in x0, parsed through Xen.Devicetree, and cross-checks against the
hypervisor's own log: x0 = 0x68000000 (Xen's 'Loading d0 DTB to 0x68000000'),
RAM base 0x60000000 512 MiB (Xen's BANK[0]). Boots 22/22 under a real Xen/ARM
hypervisor in QEMU.
The example comment claimed unikraft places domains on separate cores on an SMP guest. That is false: Unikraft boots multiple cores but has no SMP thread scheduler (upstream issue #587), so Domain.spawn creates real domains that time-slice on the boot core. Fix the comment and note the reason in the README.
The smp multidomain example registered the package in dune-project but
its dune-generated .opam was left untracked; commit it so the package
resolves like its siblings.
The Xen runtime's wait now takes an absolute deadline; the store-ring
block callback waits until the port fires, which is deadline 0 (no timeout).
A device-less unikernel that spawns two extra OCaml domains with Domain.spawn, runs a CPU-bound Fibonacci on each and the main domain, and joins them. It boots on the mirage-eio scheduler alone and demonstrates that the OCaml 5 multicore runtime brings up more than one domain under a unikernel; the cram test checks the fixed join results so it stays deterministic across targets and host core counts.
The XenStore-over-a-real-ring guest was buried under
mirage/lib/eio/xen-runtime/xs-boot with a private run.sh. Move it to
unikernels/bin/xen/xenstore as a proper unikernel producing
unikernel.xen, so it boots with the standard flow --
XEN=~/git/xen/xen/xen mrg run -t xen unikernels/bin/xen/xenstore
-- instead of a hand-rolled script. It still links only the hypercall
runtime and nox-xen, not the full device library.
unikernel-crunch depends on eio directly, so monopam lint wants the pin
recorded in its opam template like the other unikernel packages.
Embed a small directory tree with eio-fs-crunch and rebuild it in memory at boot without declaring a block device. Cover the native output and empty manifest in Cram, register the cross-target build rules, and include the generated demo.
Wait for the SSH server description the guest currently emits before starting the client, and report startup failure instead of spinning forever.
Synchronize the SSH server cram setup on the description line the guest now emits, and fail with its log instead of looping forever if startup exits.
The TCP table contains the held client and the current query connection; the prior query is pruned once it reaches CLOSED, so the subsequent table summary remains at two live rows.
Start the SSH, SQL, and gRPC clients in readiness order so their ephemeral ports are deterministic. After SQL closes, assert that CLOSED has been pruned from the live TCP table.
Wait for the appliance startup marker, which follows frame-socket acquisition, before running the LAN peer. Surface the firewall log if the guest exits during startup.
Synchronize setup on the guest lease log before treating the published echo port as ready, and fail with the guest log if the process exits during startup.
Match the committed generated Dune registry with the current Mirage generator output.
The backend-neutral device description should not live under Eio or expose a Solo5-shaped ABI. Model runtime resources as optional singleton devices and pass normalized runtime metadata to applications, allowing each backend to project the capabilities it needs.
The slicing-by-8 engine is now written once over Optint.t, whose
representation optint picks at run time from the actual word size
(Immediate64 dispatch on Sys.word_size): immediate int on 64-bit,
boxed Int32 elsewhere. Every intermediate stays within 32 bits of its
own accord, so the same code is exact on native, bytecode, js_of_ocaml
and wasm. The slice_native and slice_portable backends, the
default_implementation selection, and the narrow-int refusal guard all
disappear; there is nothing left to mis-select.
Benchmarked (bench/bench.ml, arm64, CRC-32C): release profile
1530-1632 MB/s for the Optint engine vs 1344-1606 MB/s for the raw-int
slice_native across 64B-64KB - parity at small sizes, +14% at 256B and
up (the 32-bit register invariant makes the top-byte mask redundant,
which the old loop paid for); dev profile +11% at 4KB. flambda folds
the dispatch; there is no profile where the Optint engine loses.
Consumers no longer select a backend: drop nox-crc.slice_portable from
ocaml-slirp/examples/web, from the mirage wasi rule (mirage/lib/
rules.ml), and from the 57 regenerated cross-build arms in
unikernels/bin/gen. The identical per-backend test suites merge into
test/slice. The wasi arms still print optint's own dead-code
integer-overflow warnings (its immediate arm never runs on a 32-bit
word); those predate this change.
Makes the committed test/js spec test pass: a plain nox-crc link
computes the catalogue check values exactly under node.
The server assumed one multi-queue device, where the transport steers a
flow by destination port and so each shard must listen on a port
congruent to its index. Several single-queue devices behave differently:
each link is its own L2 network, so no steering is needed and every
shard serves the same port on its own link. Tell the two apart by the
netifs' MACs -- one device's queues share its address, separate devices
do not -- and pick the port scheme accordingly.
mrg injects parquet.compress_backend_portable into a cross arm only when
the unikernel links parquet, so a parquet unikernel builds for solo5,
unikraft, xen and wasi -- none of which carry libzstd or libz -- while
keeping the fast C reader stubs. Add storage/columnar: it writes a 500-row
table to Parquet with Snappy and LZ4 pages and reads it back through the
typed row codec, running identically as a native binary and a 31-bit wasi
guest (the negative scores exercise the reader's INT32 sign-extension).
Regenerate the cross arms. This also propagates nox-crc.slice_portable into
every wasi arm, which the generator emits but the committed arms had been
missing.
Round-trips a compressible chunk through both pure-OCaml block compressors and
reports each ratio. It builds and runs natively, as a Solo5 unikernel, and as a
31-bit wasi (wasm) guest -- the target that makes the LZ4/Snappy Int32 match and
hash matter: both compile warning-free there and produce byte-identical output
on 64-bit. Gives the two codecs a real wasm-reachable consumer.
The tcp-server and smp demo workloads run a glibc LCG whose multiplier
1103515245 overflows a 31-bit int; these unikernels also build for the wasi
(wasm) target. Bind the multiplier (split into 16-bit halves) once outside the
allocation-free inner loop: loop-invariant, so the 64-bit multiply is unchanged.
A mirage-eio xen unikernel with two netfronts bridged by the in-guest
loopback: a TCP echo server on netif "0" and a client on netif "1" that
connects across the bridge, sends a probe, and checks the echo -- two
full TCP stacks reaching each other with no dom0. The skeleton the iperf
and sql+ssh unikernels are app swaps on.
Replace tcp-server's catch-all int parse with int_of_string_opt so the
package carries no catch-all handler.
A dom0less domU with two netfronts: an echo server on netif 0 and a client on netif 1, each a full TCP stack, that reach each other over the in-guest loopback (loopback.ml floods each frame to the other vif). It is the skeleton the iperf and sql+ssh unikernels swap their apps onto, and the first live netfront TX/RX path in a boot shape -- the four existing shapes exercise block, xenstore, and static shm, not a served network round-trip.
Tcp_eio.Net.sharded_over_netif runs N independent stacks, one per pool worker
domain: the boot domain reads the device and routes each frame to its owner
shard by the symmetric four-tuple hash (Tcp.Shard.route), so a flow and its
connection state stay on one core for its lifetime, and each shard's egress
leaves through its own queue drained by its own boot fiber -- no shared transmit
lock. A session that spans several connections (iperf3: control + data streams)
passes a route to steer by destination port instead.
Move Slirp.tcp_route (frame -> four-tuple) and is_arp_frame into Tcp_eio.Net:
the generic frame-routing both the slirp edge and the guest driver need, not
slirp-specific. slirp_eio uses the moved versions.
The iperf3 server shards across cores: each shard serves its own port and a
whole test stays on one core. On a single-queue native netif the aggregate caps
at ~10 Gbit/s (the one device reader is the ceiling); a multi-queue netif lets
it scale past that.
The echo pair proved the stack; it could not show whether the domains
pool buys anything over TCP. The server now offloads a CPU-bound LCG
chain per connection to the pool (accept and socket I/O stay on the
boot domain, where the from-scratch stack lives), and the client fires
the same requests sequentially then all at once: the ratio is the
multicore speedup as seen from outside, checksum-verified both ways.
The cross-only guest is built by nothing on the host, so it rotted
unseen: the client API became a pure value threading its request ids
(b190b792) and the link lost its load address when a missing
TEXT_START became a loud error (2e9bef81). Thread the client through
the requests, link at GUEST_RAM0_BASE, and generate the startup with
the shared solo5 shim.
The wasi arm links bytecode through wasm_of_ocaml, so its link runs
ocamlc, which rejects the release profile's -use-lto (an ocamlopt
flag). With that flag restored to the workspace env, every cross build
of a unikernel failed at the wasi executable -- including
[dune build -x solo5], which builds the default context's arms too.
The cross arms are immune for a reason worth stating: each states its
own (link_flags ...), which replaces the env's rather than extending
it. The wasi arm had none, so it inherited. Give it an empty one.
Regenerates all 54 committed arms; the gen rules diff them against a
fresh walk, so they fail until regenerated.
The example moved to an Eio.Executor_pool demo but its cram test and README
still described the Domain.spawn Fibonacci demo it replaced, so the committed
test failed and the README claimed unikraft schedules domains cooperatively on
the boot core -- the opposite of what the backend now does.
Each job now computes an LCG chain seeded by its position and the unikernel
reports the sum, which is fixed by the arithmetic alone, so the test pins down
that every job ran exactly once and computed the right chain rather than only
that the run printed a timing. The parallel total is also compared against the
same total computed sequentially. --jobs and --iterations size the workload, so
the machine check stays fast and deterministic while the defaults still
saturate the pool for long enough to measure.
Being a directory in unikernels/bin rather than under a category, smp was
invisible to `mrg registry`, which walks category/name -- so its cross-build
arms were never regenerated or diffed and had gone stale. Under simple/ the
registry regenerates them, which is what picks up the cmdliner dependency the
boot arguments need.
The bad fixture only exercised the missing-.mli path, so the branch
that inspects an existing interface's suite type had no test at all --
the branch just taught to skip an unresolved typedtree. Skipping and
going silent are one edit apart with nothing to tell them apart.
bad_type/ is good/ with an internal leaked into the interface: built,
so the type resolves, and still reported.
The demo boots two dom0less domUs that increment a counter in one
xen,static-shm page, signalling over the static event channel -- proof of a
real cross-domain data plane (the count reaches 2x each domain's rounds, so
both wrote the same host page) with no xenstore, no dom0, no grants.
Boot-verified against a real Xen/ARM hypervisor.
With only a couple of waves' worth of jobs, the parallel run drains
before it fills and its speedup falls well short of the worker count.
Submit enough jobs to keep every worker busy across the whole run, and
enlarge the minor heap up front so the boot's submit-time allocation
does not trigger collections that pause the workers mid-run.
The xen link script leaves TEXT_START undefined so a missing link address
is a loud error; rules.ml passes -Wl,--defsym,TEXT_START=0x40000000, but the
committed per-unikernel arms predate that and omitted it, so every generic
xen unikernel failed to link (symbol not found: TEXT_START) and the gen-diff
test was red. Regenerated via mrg dune. Boot-verified: hello-world links,
loads at 0x40000000, and prints under a dom0less Xen boot.
The two-domain demo README shows the working mrg run -t xen --domains 2 flow and the Xen STATIC_MEMORY config it needs. self-boot's README no longer claims mrg run -t xen boots it -- it links at the dom0 load address, so its run path moves to a xen.dom0 target; it points at the two-domain demo for the dom0less domU path.
Two mirage-eio unikernels boot as sibling domUs (d1, d2) and ping-pong over a static interdomain event channel Xen pre-binds from the host device tree. Each end yields the vCPU in SCHEDOP_poll until the peer's EVTCHNOP_send wakes it, then notifies back: event-driven, no busy-poll, no dom0, no xenstore. The channel is symmetric, so both domains run one image and need no role.
self-boot's scheduler phase requests all three of the Mirage_eio timing and
entropy devices and reads each through its Eio interface over the linked Xen
backend: the wall clock (RTC-backed real epoch), the monotonic clock
(CNTVCT_EL0), and the rng (Fortuna). Confirms the DSL device surface, not just
the runtime primitives, works on xen.
The device-tree phase now reads each virtio-mmio transport's register
block over R.mmio_read32: transport 0's magic 'virt' proves the 32-bit
register accessor reaches the device, and a populated slot's nonzero DeviceID
(1 = net, 2 = block) is reported. With a virtio-net-device attached this finds
the net transport, confirming the discovery path a virtio-net frontend needs.
A bootable mirage-eio unikernel that drives every Xen/ARM PV layer live
and reports one check per step -- the hypercall runtime and clocks, a live
interdomain event channel, the hypervisor-mapped grant table, netfront and
blkfront end to end, the Mirage_eio.run scheduler path, and the boot device
tree. It runs through mrg (mrg run -t xen unikernels/bin/xen/self-boot),
alongside the other bootable unikernels, rather than from the library tree.
The device-tree phase reads the guest's own platform layout (RAM base and
size, the 32 virtio-mmio transports and their GIC interrupts) from the FDT Xen
passes in x0, parsed through Xen.Devicetree, and cross-checks against the
hypervisor's own log: x0 = 0x68000000 (Xen's 'Loading d0 DTB to 0x68000000'),
RAM base 0x60000000 512 MiB (Xen's BANK[0]). Boots 22/22 under a real Xen/ARM
hypervisor in QEMU.
The example comment claimed unikraft places domains on separate cores on an SMP guest. That is false: Unikraft boots multiple cores but has no SMP thread scheduler (upstream issue #587), so Domain.spawn creates real domains that time-slice on the boot core. Fix the comment and note the reason in the README.
A device-less unikernel that spawns two extra OCaml domains with Domain.spawn, runs a CPU-bound Fibonacci on each and the main domain, and joins them. It boots on the mirage-eio scheduler alone and demonstrates that the OCaml 5 multicore runtime brings up more than one domain under a unikernel; the cram test checks the fixed join results so it stays deterministic across targets and host core counts.
The XenStore-over-a-real-ring guest was buried under
mirage/lib/eio/xen-runtime/xs-boot with a private run.sh. Move it to
unikernels/bin/xen/xenstore as a proper unikernel producing
unikernel.xen, so it boots with the standard flow --
XEN=~/git/xen/xen/xen mrg run -t xen unikernels/bin/xen/xenstore
-- instead of a hand-rolled script. It still links only the hypercall
runtime and nox-xen, not the full device library.