A deeper-dive companion to the top-level README. Read that first for what runPHI is, how to build it, and the user-facing workflow. This document covers the internals: what each crate does, how the OCI lifecycle is dispatched, how the forwarding-to-runc decision is made, and where the on-disk state lives.
flowchart TB
%% Upper orchestration layer
subgraph upper["Container orchestration layer"]
direction LR
kubelet
dockerd
containerd
CRIO["CRI-O"]
end
%% runphi binary
subgraph runphi_bin["runphi binary (one backend baked in at build time)"]
direction TB
subgraph runphi_crate["runphi crate (hypervisor-independent)"]
main["main: OCI dispatch"]
forwarding["forwarding decision<br/>(annotation / /boot/config.json /<br/>/run/runPHI/<id>/)"]
end
oci["liboci_cli<br/>(OCI command parsing)"]
f2b["frontend_to_backend<br/>(FrontendConfig, ImageConfig)"]
logging["logging<br/>(log file + TickSource trait)"]
subgraph active_backend["backend_jailhouse OR backend_xen (Cargo feature selects one at build time)"]
direction TB
backend_api["startguest / stopguest /<br/>createguest / destroyguest /<br/>cleanup / storeinfo"]
configGenerator["configGenerator<br/>(builds cell/domU config)"]
tick_source["TickSource impl<br/>(/dev/mem MMIO | /dev/arm_timer)"]
end
end
%% Below runphi
runc["runc_vanilla<br/>(/usr/local/sbin/runc_vanilla)"]
hypervisor["Jailhouse cell | Xen domU"]
%% Connections
kubelet --> containerd
kubelet --> CRIO
dockerd --> containerd
containerd -- "exec runc" --> main
CRIO --> main
main --> oci
main --> forwarding
main --> backend_api
main --> logging
backend_api --> f2b
backend_api --> configGenerator
logging -- "installs at startup" --> tick_source
forwarding -- "standard containers<br/>(no /boot/config.json)" --> runc
backend_api -- "manages" --> hypervisor
Walking the diagram:
- One binary, one backend.
runphiis a single binary; which hypervisor backend is compiled in is decided by Cargo features (jailhousedefault, or--features xen).runphi --versionprints which one. There is no runtime backend switch. - Hypervisor-independent core. Everything in the
runphi,logging,liboci_cli, andfrontend_to_backendcrates is the same regardless of backend. The active backend is re-exported at therunphicrate root asbackend, so the rest of the codebase callsbackend::createguest(...)without knowing which one it is. - Forwarding fork. Before runPHI takes ownership of a container,
forwardingdecides whether the OCI call should be handled here or forwarded to vanilla runc. Decision rules in order: (1) explicit OCI annotationorg.runphi.runtime, (2) atcreate, presence of/boot/config.jsonin the rootfs, (3) for later commands, presence of/run/runPHI/<id>/. - TickSource boundary. The
logging::timermodule is hypervisor-independent, but its tick source is not. Each backend implements a tinyTickSourcetrait (one method,read_ticks() -> u64) and the backend'stimer::install()is called once frommain. After that,timer::capture()works the same way everywhere.
| Crate | Hypervisor-aware? | Purpose |
|---|---|---|
runphi |
no | Binary. Parses OCI args, decides forwarding, dispatches to the backend, owns main(). |
liboci_cli |
no | OCI command-line parsing. Subset of the OCI runtime spec. |
frontend_to_backend |
no | Shared data structures (FrontendConfig, ImageConfig) used as the API between runphi and any backend. Parses /boot/config.json from the container rootfs. |
logging |
no | Log file writer; timer module with the TickSource trait and u64 tick API. |
backend_jailhouse |
yes | Jailhouse backend: drives jailhouse cell {create,load,start}, MMIO mmap of /dev/mem for the timer. |
backend_xen |
yes | Xen backend: drives xl create/unpause/destroy, reads /dev/arm_timer for the timer. |
The two backend crates expose the same seven items (startguest, stopguest, destroyguest, cleanup, createguest, storeinfo, plus the configGenerator module). Adding a new backend means producing the same surface. See rust_runphi/README.md § Adding a new hypervisor backend for the step-by-step recipe.
containerd (or CRI-O, or any OCI-compliant runtime client) execs /usr/bin/runc for every container lifecycle event. Because the switch_to_runphi.sh script replaced the distro's runc with runPHI, containerd is actually invoking us. The dispatch in runphi/src/main.rs looks like this for each OCI command:
create → read bundle config.json → forwarding::decide_create
→ (forward to runc_vanilla and exit) OR
(mkdir /run/runPHI/<id>/ → frontend::commands::create
→ backend::createguest → backend::storeinfo)
start → forwarding::decide_existing (checks /run/runPHI/<id>/)
→ forward OR frontend::commands::start → backend::startguest
kill → same forwarding check → backend::stopguest + backend::destroyguest
delete → same forwarding check → backend::stopguest + backend::destroyguest + backend::cleanup
state → same forwarding check → read bundle/pidfile/rootfs from /run/runPHI/<id>/
createguest is the heavy step. On Jailhouse it calls jailhouse cell create with the cell config produced by configGenerator, then jailhouse cell load for the inmate binary. On Xen it builds a domU config and calls xl create -p (paused), with startguest later running xl unpause.
| Path | Created by | Contents |
|---|---|---|
/usr/share/runPHI/log.txt |
logging::init_logger |
All runPHI log lines and (optionally) timer phase markers. |
/run/runPHI/<id>/ |
runphi/src/main.rs on create |
Per-container working dir. Its presence is the source of truth for "this is a runPHI-managed container" in subsequent OCI commands. |
/run/runPHI/<id>/bundle |
backend::storeinfo |
Absolute path to the OCI bundle. |
/run/runPHI/<id>/pidfile |
backend::storeinfo |
Path to the container's pidfile (the init process). |
/run/runPHI/<id>/rootfs |
backend::storeinfo |
Absolute path to the mounted rootfs. |
/run/runPHI/<id>/config<id>.conf |
backend configGenerator |
Hypervisor-specific config: the Jailhouse .c cell file or the Xen domU config, depending on backend. |
<rootfs>/boot/config.json |
container image (input, not produced by us) | Boot parameters for the partitioned cell: which kernel/inmate to load, OS variant, devices, memory layout, etc. Parsed by frontend_to_backend::ImageConfig::get_from_file. |
/usr/local/sbin/runc_vanilla |
switch_to_runphi.sh |
Backup of the distro's original runc, executed when runPHI forwards a non-runPHI container. |
A runPHI-managed container ships a /boot/config.json in its rootfs. Its
presence is the marker that makes forwarding keep the container instead of
forwarding it to runc, and frontend_to_backend::ImageConfig::get_from_file
parses it. The os_var field selects the kind of guest the Xen backend builds.
Bare-metal inmate (Zephyr or any single binary):
{ "os_var": "zephyr", "inmate": "/boot/hello.bin", "net": "no" }The backend emits a minimal domU config: kernel = "<rootfs>/boot/hello.bin",
the cpu/memory stanzas, and (on ARM) gic_version="v2".
Linux domU (kernel + initramfs):
{ "os_var": "linux", "inmate": "/boot/Image", "ramdisk": "/boot/rootfs.cpio.gz", "net": "no" }The container must ship a kernel Image and a rootfs.cpio.gz initramfs under
/boot. inmate is the kernel, ramdisk is the initramfs that becomes the
guest's root filesystem (root runs from RAM — no virtual disk, no host LVM). For
this the backend additionally emits, on top of the bare-metal stanza:
type = "pvh"— only on x86 (xlon x86 needs an explicit PVH boot type; ARM has a single guest type and omits it).ramdisk = "<rootfs>/boot/rootfs.cpio.gz".extra = "console=hvc0"— kernel command line; the console is routed to the Xen PV console soxl console <id>shows the boot.
On x86, gic_version is never emitted (it is an ARM GIC concept that xl
rejects on x86). A bare-metal container's generated config is unchanged on ARM.
Besides the initramfs default, a Linux guest can boot from a virtual disk
attached as xvda (the backend then appends root=/dev/xvda to extra).
Selected by disk_type in /boot/config.json (handled by
backend_xen/src/configGenerator/disk.rs):
disk_type |
Root filesystem | Fields | Lifecycle |
|---|---|---|---|
(absent / "") |
initramfs (ramdisk), in RAM |
ramdisk |
nothing to provision or clean |
"file" |
raw ext4 image shipped in the container, used in place from the mounted rootfs | disk_image (path in rootfs) |
none — guest writes land in the container's writable layer and are discarded with the container |
"lvm" |
host logical volume populated at create with a clone of the container rootfs (the docker image becomes the VM root) |
disk_size (MB, optional — defaults to rootfs size +30% +64 MB) |
create: lvcreate → mkfs.ext4 → copy rootfs; destroy: lvremove. State tracked in /run/runPHI/<id>/disk |
{ "os_var": "linux", "inmate": "/boot/Image", "disk_type": "file", "disk_image": "/boot/rootfs.img" }
{ "os_var": "linux", "inmate": "/boot/Image", "disk_type": "lvm", "disk_size": 1024 }Notes:
- With a disk root,
ramdiskis optional: if present the initramfs switch-roots viaroot=; if absent the kernel must havexen-blkfrontbuilt in to mount/dev/xvdadirectly. - LVM host prerequisites: a volume group (default name
test-vg, overridable by writing the VG name into/usr/share/runPHI/xen_lvm_vg) andmkfs.ext4(e2fsprogs). Free space in the VG is checked at config generation. - Example boot configs:
target/demo_containers/linux/config.json(initramfs),config.file-disk.json,config.lvm.json.
- OCI dispatch: rust_runphi/crates/runphi/src/main.rs
- Forwarding decision: rust_runphi/crates/runphi/src/forwarding.rs
- Frontend command handlers: rust_runphi/crates/runphi/src/frontend/commands.rs
- Shared backend API types: rust_runphi/crates/frontend_to_backend/src/lib.rs
- Timer trait: rust_runphi/crates/logging/src/timer.rs
- Jailhouse backend: rust_runphi/crates/backend_jailhouse/src/lib.rs
- Xen backend: rust_runphi/crates/backend_xen/src/lib.rs
An earlier version of the codebase carried a runphi/src/unused/ tree
with three groups of dead scaffolding never wired into the build:
workload/— wasmtime / wasmer / wasmedge integration sketches for running WASM workloads as the in-container payload.commands/— per-OCI-command stubs (create,start,kill, ...) in the youki/runc-style of one file per command, before the current consolidated dispatch inrunphi/src/main.rs.observability.rs,rootpath.rs— standalone helper modules.
These files were removed to keep the active source tree small. They are recoverable from git history if anyone wants to revive the WASM workload work or use the per-command layout as a starting point for new OCI command implementations.