Replies: 24 comments 28 replies
|
Hello @Anamika1608, please take a look on the above plan and let us know if everything is ok. |
|
hi, yeah. plan looks good to me. |
|
Hi @cmainas my only machine right now is an Apple Silicon Mac (M4). apple's hypervisor doesn't expose nested virtualization, so i can't get /dev/kvm inside a local Linux VM, which rules out Firecracker and Cloud Hypervisor locally, since both need KVM to start. QEMU still runs under TCG without KVM, and its QMP control socket works in that mode too. could you confirm a few things:
thanks! |
|
@Anamika1608 the link to the kata-containers runtime-rs FC refactor I mentioned: https://github.com/kata-containers/kata-containers/tree/fix/runtime-rs-net-fc |
|
i have a working socket PoC. in urunc, firecracker.go now launches firecracker --api-sock instead of --no-api --config-file, and i added an HTTP-over-Unix client (firecrackerClient) that does waitForSocket -> PUT /machine-config, /boot-source, /drives/ -> PUT /actions {InstanceStart}. the socket is a per-container pathname socket. an integration test drives a real Firecracker over the api-sock with this client, and a real guest boots, the serial shows InstanceStart returning 204 and the kernel coming up (Linux version 6.1.174). i'm running it on an Apple-Silicon Lima VM with nested virtualization, so it's real KVM. there are also unit tests against a fake API server that need no KVM. here is the committed code - Anamika1608@d601cbe also here is the doc - https://docs.google.com/document/d/1lPLB9skvf7s7l8uB9Nz-YK7Zvy0W9NYe61dSxjkVe2w/edit?usp=sharing two questions:
|
|
so, i have successfully connected to vmm via socket from outside of urunc, and ran the urunc as the containerd. it passed. i did sent the vmm info curn request and it did succeed.
i have also added my whole POC process in this doc with the theory that we needed for phase 1 - https://docs.google.com/document/d/1lPLB9skvf7s7l8uB9Nz-YK7Zvy0W9NYe61dSxjkVe2w/edit?usp=sharing check once? and let me know if this completes our phase 1. thanks! |
PHASE 1: THEORETICAL BACKGROUND + POCScope of this sectionPhase 1 covers the fundamental theory: Linux namespaces, IPC via Unix sockets, and how urunc manages these today. I analyzed the urunc source and verified the behavior of control sockets through manual testing. Every detail here is backed by the Linux man pages, urunc codebase, or official documentation. Namespaces and socket placementurunc isolates unikernels by placing the VMM inside specific Linux namespaces. Because a control socket is needed for runtime configuration, namespace boundaries dictate where this socket must live. If the runtime cannot cross the namespace wall, it cannot talk to the VMM. This makes namespace theory the primary constraint for our design. Namespace OverviewNamespaces isolate global system resources so they appear private to a process group. Per
The API uses three main syscalls: For this project, the network namespace (where the VMM sits) and the mount namespace (which controls file visibility) are critical. Note: the IPC namespace does not isolate Unix domain sockets. Unix Socket Address TypesUnix domain sockets provide local IPC using three address formats defined in
Pathname sockets require specific filesystem permissions, while abstract sockets bypass standard file permissions entirely. Impact of Namespaces on IPCTwo key facts determine how we communicate across namespaces: Fact 1: Abstract sockets are trapped. Fact 2: Pathname sockets are filesystem-bound. Visibility depends on the mount namespace, not the network namespace. If the host can see the file, it can connect (pending permissions). I verified this with a demo: an abstract socket listener in a netns was invisible to the host (Connection refused), but a pathname socket in Socket Location ConstraintsTo allow the host-side runtime to talk to the jailed VMM:
Since urunc can enter the container's namespace using Current urunc Namespace UsageThe existing urunc flow already handles complex isolation:
urunc Internal IPCurunc already utilizes pathname sockets for its The VMM Boot GapCurrently, urunc disables control channels before Industry Comparisons
Impact on PoC DesignThe theory led to a single design choice: a pathname socket per container. The PoC swaps References
SETTING UP THE ENVIRONMENT FOR FIRECRACKER VMM AND IF KVM EXISTS1) create & start the nested-virt VMlimactl start --name=fc-vm --vm-type=vz --set='.nestedVirtualization=true' template://defaultLima prints the VM config (CPUs/memory/disk) and asks you to confirm, choose "Proceed with the current configuration." Flags recap: It's done when you get your shell prompt back with a line like 2) is /dev/kvm there?limactl shell fc-vm -- sh -c 'uname -m; ls -l /dev/kvm'Success = an architecture line (aarch64) followed by a device line like INSTALLING FIRECRACKER1) step into your Linux boxlimactl shell fc-vm2) install firecrackercd ~
ARCH="$(uname -m)"
VERSION="v1.7.0"
curl -L https://github.com/firecracker-microvm/firecracker/releases/download/${VERSION}/firecracker-${VERSION}-${ARCH}.tgz | tar -xz
sudo mv release-${VERSION}-${ARCH}/firecracker-${VERSION}-${ARCH} /usr/local/bin/firecracker
rm -rf release-${VERSION}-${ARCH}
firecracker --versionFirecracker v1.7.0 installed and it ran cleanly. DOWNLOAD GUEST KERNEL + DISK IMAGERun these inside the VM. First a working directory and the prep tools: sudo apt-get update && sudo apt-get install -y squashfs-tools jq e2fsprogs
mkdir -p ~/fc && cd ~/fcDownload the guest kernel (vmlinux), it auto-discovers the latest aarch64 CI kernel: ARCH="$(uname -m)"
S3="https://s3.amazonaws.com/spec.ccfc.min"
CI_ARTIFACTS_PREFIX=$(curl -fsSL "$S3?list-type=2&prefix=firecracker-ci/&delimiter=/" \
| grep -oP "(?<=<Prefix>)firecracker-ci/[0-9]{8}-[^/]+/(?=</Prefix>)" \
| sort | tail -1)
latest_kernel_key=$(curl -fsSL "$S3?list-type=2&prefix=${CI_ARTIFACTS_PREFIX}${ARCH}/vmlinux-" \
| grep -oP "(?<=<Key>)(${CI_ARTIFACTS_PREFIX}${ARCH}/vmlinux-[0-9]+\.[0-9]+\.[0-9]{1,3})(?=</Key>)" \
| sort -V | tail -1)
wget "$S3/${latest_kernel_key}"Download + build the root filesystem (Ubuntu, converted to a writable ext4 with an SSH key baked in for later login): latest_ubuntu_key=$(curl -fsSL "$S3?list-type=2&prefix=${CI_ARTIFACTS_PREFIX}${ARCH}/ubuntu-" \
| grep -oP "(?<=<Key>)(${CI_ARTIFACTS_PREFIX}${ARCH}/ubuntu-[0-9]+\.[0-9]+\.squashfs)(?=</Key>)" \
| sort -V | tail -1)
ubuntu_version=$(basename $latest_ubuntu_key .squashfs | grep -oE '[0-9]+\.[0-9]+')
wget -O ubuntu-$ubuntu_version.squashfs.upstream "$S3/$latest_ubuntu_key"
unsquashfs ubuntu-$ubuntu_version.squashfs.upstream
ssh-keygen -f id_rsa -N ""
cp -v id_rsa.pub squashfs-root/root/.ssh/authorized_keys
mv -v id_rsa ./ubuntu-$ubuntu_version.id_rsa
sudo chown -R root:root squashfs-root
truncate -s 1G ubuntu-$ubuntu_version.ext4
sudo mkfs.ext4 -d squashfs-root -F ubuntu-$ubuntu_version.ext4then run below inside fc directory ls -lh ~/fcCONNECTING WITH FIRECRACKER VMM VIA SOCKETTerminal 1 — start Firecrackerwe already have one shell in the VM. In it: cd ~/fc
API_SOCKET="/tmp/firecracker.socket"
sudo rm -f $API_SOCKET
sudo /usr/local/bin/firecracker --api-sock "${API_SOCKET}"It prints a couple of lines and then sits there doing nothing. That's exactly right, it just built an empty VM and is waiting for instructions on the socket. Leave it running. (We use sudo so it can open Terminal 2 — open a second shell into the VM and drive itOn your Mac, open a new terminal tab and run: limactl shell fc-vm
cd ~/fcThen paste this whole block, each curl configures the empty VM over the socket: API_SOCKET="/tmp/firecracker.socket"
# 1) tell it which KERNEL to boot + kernel command line
KERNEL="./$(ls vmlinux* | tail -1)"
sudo curl -X PUT --unix-socket "${API_SOCKET}" \
--data "{\"kernel_image_path\": \"${KERNEL}\", \"boot_args\": \"console=ttyS0 reboot=k panic=1 pci=off\"}" \
"http://localhost/boot-source"
# 2) give it the DISK (rootfs)
ROOTFS="./$(ls *.ext4 | tail -1)"
sudo curl -X PUT --unix-socket "${API_SOCKET}" \
--data "{\"drive_id\": \"rootfs\", \"path_on_host\": \"${ROOTFS}\", \"is_root_device\": true, \"is_read_only\": false}" \
"http://localhost/drives/rootfs"
# 3) CPUs + memory
sudo curl -X PUT --unix-socket "${API_SOCKET}" \
--data "{\"vcpu_count\": 2, \"mem_size_mib\": 1024}" \
"http://localhost/machine-config"
# 4) POWER ON
sudo curl -X PUT --unix-socket "${API_SOCKET}" \
--data "{\"action_type\": \"InstanceStart\"}" \
"http://localhost/actions"IMPLEMENTATION OF ABOVE
ABSTRACT AND PATHNAME SOCKETSetupsudo apt-get install -y socat
sudo ip netns add demo # create a new netns called "demo"
ip netns list # confirm it existsDemo A: an ABSTRACT socket is trapped in the netns# Listener bound to an ABSTRACT socket, INSIDE the demo netns (background):
sudo ip netns exec demo socat ABSTRACT-LISTEN:mysock,fork SYSTEM:'echo HELLO-FROM-NETNS' &
sleep 1
echo "=== try to reach it from the HOST netns (expect FAIL) ==="
socat -T2 - ABSTRACT-CONNECT:mysock ; echo "exit code = $?"
echo "=== try to reach it from INSIDE the demo netns (expect HELLO) ==="
sudo ip netns exec demo socat -T2 - ABSTRACT-CONNECT:mysock ; echo "exit code = $?"From the host: a "Connection refused" error, exit code = 1 -> The abstract socket simply doesn't exist in the host's network namespace. Demo B: a PATHNAME socket crosses the netns wallsudo pkill -f ABSTRACT-LISTEN # stop the previous listener
sudo rm -f /tmp/path.sock
# Listener bound to a PATHNAME socket (a file in /tmp), still INSIDE demo netns:
sudo ip netns exec demo socat UNIX-LISTEN:/tmp/path.sock,fork SYSTEM:'echo HELLO-VIA-PATH' &
sleep 1
echo "=== the socket FILE is visible from the host, even though the listener is in demo ==="
ls -l /tmp/path.sock
echo "=== try to reach it from the HOST netns (expect HELLO this time) ==="
socat -T2 - UNIX-CONNECT:/tmp/path.sock ; echo "exit code = $?"The pathname socket reaches across the network namespace. That file is visible from the host, even though the listener is inside demo. But the connection was blocked by permissions, not by the namespace, because "connecting to a stream socket object requires write permission on that socket." (I ran the client as user anamika, no sudo) One-time setup – make the Mac <-> VM share writable# 1) stop the VM
limactl stop fc-vm
# 2) make the home share writable + fast (virtiofs suits your vz VM)
limactl edit fc-vm -y \
--set '.mountType = "virtiofs"' \
--set '.mounts[0].writable = true'
# 3) start it back up
limactl start fc-vm
# 4) verify it's now writable from inside the VM
limactl shell fc-vm -- sh -c 'touch /Users/anamika/open-source/urunc-dev/urunc/.wtest && echo WRITABLE && rm -f /Users/anamika/open-source/urunc-dev/urunc/.wtest'Step 4 should print WRITABLE. If it does not print WRITABLE then it is possible the response is stale, run this: limactl shell fc-vm -- sudo mount -o remount,rw /Users/anamika
limactl shell fc-vm -- sh -c 'findmnt -no OPTIONS /Users/anamika; touch /Users/anamika/open-source/urunc-dev/urunc/.wtest 2>&1 && echo WRITABLE && rm -f /Users/anamika/open-source/urunc-dev/urunc/.wtest || echo STILL-READONLY'TO RUN THE URUNC CODE IN VMStep A – make your branch (on the Mac)cd /Users/anamika/open-source/urunc-dev/urunc
git checkout -b poc/fc-api-sock
git branch # confirm: * poc/fc-api-sockStep B – install Go + build deps (inside the VM)limactl shell fc-vmThen, inside the VM: # build prerequisites (gcc for cgo, libseccomp for urunc's seccomp bits)
sudo apt-get update && sudo apt-get install -y build-essential pkg-config libseccomp-dev git
# Go 1.25.4 (urunc's pinned version), arm64
curl -LO https://go.dev/dl/go1.25.4.linux-arm64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.25.4.linux-arm64.tar.gz
rm go1.25.4.linux-arm64.tar.gz
# put Go on your PATH
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc && source ~/.bashrc
go versionExpect: Step C – baseline build of the unchanged branch (inside the VM)This compiles urunc as-is, to prove Go + deps + the writable mount all work before you change anything: cd /Users/anamika/open-source/urunc-dev/urunc
go build ./...NOW MAKE THE FIRST COMMIT CHANGES LOCALLY - poc/fc-api-sockRUNNING THE INTEGRATION TESTin the vm: cd /Users/anamika/open-source/urunc-dev/urunc
sudo env \
FC_KERNEL="$(ls $HOME/fc/vmlinux* | tail -1)" \
FC_ROOTFS="$(ls $HOME/fc/*.ext4 | tail -1)" \
/usr/local/go/bin/go test -tags integration -v \
-run TestFirecrackerSocketBoot_Integration ./pkg/unikontainers/hypervisors/Firecracker now launches with a control socket ( TESTING THE REAL URUNC TO TALK TO VMM VIA SOCKETRUN THE CONTAINERDRun in the VM: # 1) create a default containerd config
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
# 2) install + start the containerd systemd service (matching v2.3.2)
sudo wget -qO /etc/systemd/system/containerd.service \
https://raw.githubusercontent.com/containerd/containerd/v2.3.2/containerd.service
sudo systemctl daemon-reload
sudo systemctl enable --now containerd
# 3) verify
echo "active: $(systemctl is-active containerd)"
sudo ctr version | grep -A1 Server
sudo ctr plugin ls | grep "snapshotter.v1" | awk '{print $2, $NF}'Build + install urunc from your branchThis builds urunc + containerd-shim-urunc-v2 from your poc/fc-api-sock branch (so the --api-sock change is baked in) and installs them. In the VM: cd /Users/anamika/open-source/urunc-dev/urunc
# confirm you're on your PoC branch
git branch --show-current
# build, then install the binaries to /usr/local/bin
make
sudo make install
# verify
which urunc containerd-shim-urunc-v2
urunc --version 2>&1 | head -3Register urunc as a containerd runtimeThis adds a small runtime block to containerd's config (using overlayfs, since we skip devmapper), then restarts. In the VM: sudo tee -a /etc/containerd/config.toml >/dev/null <<'EOF'
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.urunc]
runtime_type = "io.containerd.urunc.v2"
container_annotations = ["com.urunc.unikernel.*"]
pod_annotations = ["com.urunc.unikernel.*"]
snapshotter = "overlayfs"
EOF
sudo systemctl restart containerd
echo "active: $(systemctl is-active containerd)"Run a Firecracker container, no guest boot, then reach the socketIMG=harbor.nbfc.io/nubificus/urunc/chttp-firecracker-linux-aarch64:latest
sudo nerdctl rm -f fc-poc 2>/dev/null # clear any earlier attempt
sudo nerdctl pull "$IMG"
sudo nerdctl run -d --name fc-poc --runtime io.containerd.urunc.v2 "$IMG"; echo "run exit: $?"
sleep 2
echo "=== containers ==="
sudo nerdctl ps -a
echo "=== firecracker process (look for --api-sock) ==="
pgrep -af firecracker || echo "no firecracker process"5) Connect to that socket from the host (outside urunc) and query the VMMRun this in the VM: NOTE - the FCPID and NAME is from this demo example run, to get your run FCPID and NAME, run this: FCPID=30868
NAME=f89234e874aacddf21efb5e5a78c0b63ecfead0fc579d16e78c1b6ffe5e90741.sock
echo "=== the VMM socket, seen from the host (outside urunc) ==="
sudo ls -l /proc/$FCPID/root/tmp/$NAME
echo "=== connect to the socket and ask the VMM about itself ==="
sudo curl -s --unix-socket /proc/$FCPID/root/tmp/$NAME http://localhost/ ; echoWhat you should get back is Firecracker's instance info, something like: {"id":"anonymous-instance","state":"Not started","vmm_version":"1.7.0","app_name":"Firecracker"}DIAGRAM SHOWING CONNECTION
|
|
Hello @Anamika1608 , thank you for the report and the PR. Just to make communication easier, let;s name the two approaches we have as:
A few notes from the sync about the next steps:
Please let us know if you need any help with the parts in |
|
Hello @Anamika1608 , a few notes from the previous sync:
|
|
hi @cmainas, update on the api-based approach after restructuring it the way you described in the last sync. what i changedpreviously i had misunderstood the parallelization: i was overlapping urunc's own steps with each other (rootfs prep alongside network setup) and still sending the whole vmm configuration in one burst at the very end. now it follows the staged design you outlined:
one implementation detail worth mentioning: the api client now holds a single persistent connection to the socket, established at spawn time. this is because results, 20 runs per modesame methodology for both: timer starts when
it is a dead heat, 2 ms apart on average, with fully overlapping ranges and no failures either side. i want to correct my earlier report explicitly: the "api is about 15% faster" number i shared from 5 runs does not survive a larger sample. that gap came from a single 2279 ms config-file outlier inflating a small sample. why i think the overlap does not show upof the roughly 1250 ms total, about a second is the guest kernel booting and the app becoming ready, which nothing on urunc's side can touch. the host-side work that can actually be overlapped (tap creation, rootfs prep) is in the tens of ms, and that is roughly the same size as the extra cost of driving the boot over the socket instead of one file read (i measured about 29 ms standalone earlier). so the two effects roughly cancel. one overlap i have not tried yetthere is still some unused headroom in the current code. the block device list becomes available right after rootfs prep finishes, which can be before network setup finishes, since the two run concurrently. but right now i send i can implement that split, but i want to be honest that i do not expect it to change the comparison meaningfully. rootfs prep is fast, so the extra overlap window is small, and the total is dominated by guest boot either way. it would be a correctness-of-design improvement more than a performance one. where that leaves the decisionwhat the measurements say so far is that api-based costs nothing in startup time, but also gains nothing, at least for this workload (one image, one unikernel type, linux guest, no block rootfs). its actual value is the live control socket to the running vmm, which is what graceful shutdown and later snapshots need and which questions
|
|
i tried reducing the socket polling time and measured the effect in isolation (a small standalone go app that times only up to state Running, so guest boot does not dominate), 30 runs each:
firecracker's socket is actually ready in about 0.1ms (the busy-loop number), so most of the 10ms wait was just polling-granularity waste. 1ms recovers almost all of it (about 11ms off the average), and the busy loop only saves ~1ms more. i committed the 1ms interval (not the busy loop), since 1ms captures nearly all the benefit while a busy loop would spin the cpu until the socket is ready. to be clear about the scope: this does not make the actual container boot faster. the table above is from the isolated test. in the real run (nerdctl run until the guest answers on http), the total is about 1 second, and almost all of that is the guest kernel booting, which this change does not touch. here are the real end-to-end numbers (nerdctl run until the guest returns http 200), 20 runs each, all in the same session so they are directly comparable.
-- separate PR: configurable control socket for the monitors i also raised the new pr (#841) for the socket path configuration for all three vmms, please do address the comment on this pr as well, there only. |
end-to-end timing script (config-file vs api-based)this is the script i used to measure the end-to-end boot time of the two boot modes: it times from how to run it
[monitors.firecracker]
default_memory_mb = 256
default_vcpus = 1
boot_mode = "api" # or "config-file"then
notes for adapting it
the script#!/bin/bash
set -u
IMG="harbor.nbfc.io/nubificus/urunc/chttp-firecracker-linux-aarch64:latest"
RUNS=20
NAME="fc-timing-test"
declare -a TIMES
for i in $(seq 1 $RUNS); do
sudo nerdctl rm -f "$NAME" >/dev/null 2>&1
T0=$(date +%s%N)
sudo nerdctl run -d --name "$NAME" --runtime io.containerd.urunc.v2 "$IMG" >/dev/null
IP=""
for _ in $(seq 1 500); do
IP=$(sudo nerdctl logs "$NAME" 2>&1 | grep -oP "ipaddr=\K[0-9.]+" | head -1)
[ -n "$IP" ] && break
sleep 0.02
done
if [ -z "$IP" ]; then
echo "RUN $i: FAILED to find guest IP"
sudo nerdctl rm -f "$NAME" >/dev/null 2>&1
continue
fi
READY=0
for _ in $(seq 1 1000); do
CODE=$(curl -s -m 1 -o /dev/null -w "%{http_code}" "http://$IP:80/" 2>/dev/null)
if [ "$CODE" = "200" ]; then
READY=1
break
fi
sleep 0.02
done
T1=$(date +%s%N)
if [ "$READY" -eq 1 ]; then
MS=$(( (T1 - T0) / 1000000 ))
echo "RUN $i: $MS ms (guest IP $IP)"
TIMES+=("$MS")
else
echo "RUN $i: FAILED, guest never responded"
fi
sudo nerdctl rm -f "$NAME" >/dev/null 2>&1
done
echo "---"
if [ "${#TIMES[@]}" -gt 0 ]; then
MIN=${TIMES[0]}; MAX=${TIMES[0]}; SUM=0
for t in "${TIMES[@]}"; do
(( t < MIN )) && MIN=$t
(( t > MAX )) && MAX=$t
SUM=$(( SUM + t ))
done
AVG=$(( SUM / ${#TIMES[@]} ))
printf "%s\n" "${TIMES[@]}" | sort -n > /tmp/times-sorted.txt
N=${#TIMES[@]}
if (( N % 2 == 1 )); then
MEDIAN=$(sed -n "$(( (N+1)/2 ))p" /tmp/times-sorted.txt)
else
M1=$(sed -n "$(( N/2 ))p" /tmp/times-sorted.txt)
M2=$(sed -n "$(( N/2+1 ))p" /tmp/times-sorted.txt)
MEDIAN=$(( (M1 + M2) / 2 ))
fi
echo "successful runs: ${#TIMES[@]}/${RUNS}"
echo "min: ${MIN} ms, max: ${MAX} ms, avg: ${AVG} ms, median: ${MEDIAN} ms"
else
echo "no successful runs"
fi |
|
Hello @Anamika1608 , here are a few notes form yesterday's sync:
|
|
hi @cmainas, i have splitted the work across 3 prs -
two questions: 1. custom in config-file mode firecracker is exec'd after one of the fix: spawn firecracker already chrooted ( 2. api-driven boot for qemu / cloud-hypervisor. right now both just expose the socket for post-boot control. cloud-hypervisor's REST api can drive the whole boot over the socket like firecracker's api mode (same parallelization potential); qemu can only trigger the boot over QMP ( |
|
hi @cmainas, update on all three PRs, following your notes on confinement and using the api-driven boot for the other monitors. firecracker #809
qemu #841
cloud hypervisor #847
i am also writing up the VMM lifecycle design document next and will share it when it is ready. |
|
i built and tested the urunc side of graceful shutdown ("half 1"), following the monitor-native direction from @pmoust's last comment. this is the part that sends the shutdown event; teaching urunit to react to it is the separate next step. here is what i did, how i tested it, and two things i want your call on. PR - #869 ( i reached the PR limit, so thats why i've opened it as draft) approachit builds on the config-based control socket (#850). in that mode there is no supervising urunc process, the monitor is the container's init process, so the natural place to press the button is the
it is fire-and-return: urunc does not wait and does not add its own timeout. the container manager already escalates to SIGKILL after its grace period, so there is no reason for urunc to be a second timeout authority. any failure, an unsupported monitor, the feature being off, or any signal other than SIGTERM falls back to the exact kill behaviour we have today. the design decision i want your call onthe feature is an opt-in flag ( Firecracker realityFirecracker only has testingbuilt and ran an 8-case live matrix on aarch64. but the main thing: urunit does not react to these events yet, so no test shows a guest actually cleaning up and shutting down. what the tests prove is that urunc presses the button correctly and returns, and that the fallback is safe. the clearest evidence is the stop latency:
that gap is the button being pressed. other cases: a custom a separate bug i want to flagi also want to pick up @pmoust's finding about urunit and Cloud Hypervisor: urunit exits the VM with i have not reproduced this myself yet (the Cloud Hypervisor guest does not fully boot on my aarch64 setup, so the app-exit path is not something i can exercise here), so i am flagging it as @pmoust's observation rather than something i verified. this is independent of graceful shutdown (it is the normal app-exits path, not the stop-from-outside path). i would like to open it as its own issue rather than fold it in here, since the fix is in urunit and has its own x86-vs-aarch64 considerations. is that ok? questions
|
|
Hello @Anamika1608 , some TODOs from today's meeting:
Please let me know if I forgot something. |
|
hi @cmainas, thank you for providing me the image. the whole qemu flows work end to end. i did test with the image - urunc #869 sent system_powerdown -> the driver kernel delivered KEY_POWER -> our urunit nubificus/urunit#15 read it and SIGTERM'd the app -> the app exited cleanly -> urunit unmounted and stopped the VM. but still the firecracker and cloud hypervisor are not tested, since i am on aarch64 env. |
|
hi @cmainas, i tested snapshot and restore with firecracker and qemu. both work in urunc, live on aarch64. every command goes over the control socket from #850, reached at FirecrackerFirecracker has a clean snapshot API. Snapshot: Restore: copy both files out, stop the container, create the tap QEMUQEMU has no single snapshot call, so i migrate to a file and restore with Snapshot: Restore: the new qemu needs the same command line as the original. urunc builds that itself, so this is easy for the real feature; for the test i rebuilt it by hand (kernel and initrd copied out of the monitor rootfs, The mechanism is proven. The rest is integration:
|
|
Hello @Anamika1608 , a few notes from today's sync:
Please let me know if I forgot anything. |
|
hi @cmainas, so i researched about the containerd support, we can't build this end to end right now. containerd supports the checkpoint but the restore is still not supported in low level.
|
VMM lifecycle in
|
| Monitor | Values of boot_mode |
Default value | Control socket | What the socket does at boot |
|---|---|---|---|---|
| Firecracker | any value other than config-file selects api mode; config-file selects exec-replacement mode |
api |
HTTP over --api-sock <path> |
full configuration: staged PUT requests, then InstanceStart |
| QEMU | api selects api mode; any other value, including unset, selects exec-replacement mode |
unset (exec-replacement mode) | QMP over -qmp unix:<path>,server,nowait |
starts the guest only, with cont. No configuration over QMP |
| Cloud Hypervisor | api selects api mode; any other value, including unset, selects exec-replacement mode |
unset (exec-replacement mode) | HTTP REST over --api-socket path=<path> |
full configuration: one vm.create, then vm.boot |
| Solo5-hvt | none | not applicable | none | not applicable |
| Solo5-spt | none | not applicable | none | not applicable |
The default inverts between Firecracker and the other two monitors. On Firecracker, an unset boot_mode selects api mode. On QEMU and Cloud Hypervisor, an unset boot_mode selects exec-replacement mode, and only the literal value api turns it on. Each branch's own docs/configuration.md lists the same defaults. QEMU is the one monitor whose api mode drives only the guest's start, because QMP cannot configure a machine: QEMU needs its full configuration on the command line before it starts. Firecracker and Cloud Hypervisor instead start with a short command line in api mode and take their configuration over the socket.
In api mode, urunc refuses two things up front rather than degrading them. QEMU and Cloud Hypervisor both return "boot_mode=api does not support the virtiofs shared filesystem yet" before any spawn, for a virtiofs shared filesystem. Cloud Hypervisor's payload builder also errors, rather than silently dropping data, when a unikernel supplies raw arguments the REST schema cannot express.
Solo5-hvt and Solo5-spt have no control socket and no api mode; urunc always reaches them through syscall.Exec. Solo5-hvt is the one monitor whose PreExec does real work: with seccomp enabled, it applies a seccomp-bpf allowlist filter and sets NoNewPrivs immediately before the exec. Solo5-spt's PreExec does nothing. The code also names a sixth monitor type, Hedge, which nobody can use: its Ok() always returns an error, so building a Hedge monitor always fails.
The api boot sequence
The sequence begins where the walkthrough left off: Exec() finishes the rootfs preparation, calls changeRoot, creates the control socket directory, spawns the monitor inside the monitor rootfs, and connects to the control socket (retrying every 1ms for up to 5 seconds).
Configuration is where the three monitors differ most. Firecracker receives its configuration as separate PUT requests, in this order: /machine-config (CPU count and memory size); /network-interfaces/<ifaceID>, only when a tap device exists; /drives/<driveID>, one request per block device; /boot-source (kernel, boot arguments, initrd); and /vsock, only when a vsock path is set. Cloud Hypervisor receives its entire configuration, including fixed settings, in one vm.create request. QEMU receives nothing at this stage, because its configuration already went out on the command line.
Exec() then runs setupUser and the StartContainer hooks, skips BuildExecCmd because the monitor is already running and configured, and sends StartSuccess. urunc start wakes at that point, sets the container state to running, and runs the Poststart hooks.
Only then does urunc start the guest. vmm.PreExec runs first and does nothing on Firecracker, QEMU or Cloud Hypervisor. The last statement before Supervise() is the trigger that starts the guest: InstanceStart for Firecracker, sent through /actions; cont for QEMU, sent through the QMP connection; and vm.boot for Cloud Hypervisor, sent through the REST API.
The recorded test runs confirm this order. QEMU's log shows, in order: starting as a paused supervised child, preparing to start the vmm (just before the start-success message), then sending QMP cont. The guest wrote its first line to the serial console about 27.7ms after nerdctl run -d returned. Cloud Hypervisor's log shows, in order: starting as a supervised child, sending vm.create, preparing to start the vmm, then sending vm.boot.
Api boot sequence
The api boot sequence: possible approaches
The sequence above is one approach, and the one urunc uses today. This section records what the monitor API itself allows: the order of the socket calls, when they can be sent, and where the start trigger must go. Every claim below was tested live on aarch64, except where it says otherwise.
- The order of the configuration requests is free (Firecracker, tested live). urunc sends machine-config, network-interfaces, drives, boot-source, then vsock, today. A fresh Firecracker also accepted boot-source first, then machine-config, then network-interfaces, then
InstanceStart, and the guest booted normally. The test image had no extra drives and no vsock, so those two requests were not reordered live. - Configuration can be staged over time (Firecracker, tested live). urunc sent machine-config, network-interfaces and boot-source with a two-second gap between each. Firecracker returned 204 for all three, and the guest did not boot during this time. It booted only after
InstanceStart. - All configuration must be sent before the start trigger (Firecracker, tested live).
InstanceStartsent to a fresh, unconfigured Firecracker returned HTTP 400: "Cannot start microvm without kernel configuration." Sent last, after full configuration, the guest booted only at that point. - QEMU sends no configuration over the socket (current urunc behavior). urunc sets the QEMU machine on the command line, so the only boot-time socket call is the start trigger. We verified separately, during the CRIU restore test, that QMP
contresumes a paused QEMU guest, so the trigger call works over the socket. - Cloud Hypervisor is not tested here. Its REST API takes the whole VM in one
vm.createrequest, then avm.boot. This is documented behavior and urunc's current branch, but the Cloud Hypervisor guest does not boot on this aarch64 setup, so this claim is not verified. - Where the start trigger goes is fixed by the OCI contract, not a free choice. The OCI runtime contract says the guest starts on the
startcommand and not before. The trigger sits at the start handshake, afterSendMessage(StartSuccess)and afterPreExecreturns. All three api-mode branches place it there today.
Stop and delete
urunc kill reads the signal the caller named, or SIGTERM if none, and sends it (unix.Kill(pid, signal)) to the process ID in state.json. Which process receives it depends on the process model: the monitor's ID in exec-replacement mode, the urunc supervisor's ID in api mode. The supervisor forwards SIGTERM and SIGINT to the monitor child. It cannot forward SIGKILL, because no process can catch SIGKILL.
urunc delete --force calls unikontainer.Kill() first, which joins the container's network namespace (a missing namespace is not an error), calls vmm.Stop(pid), and cleans up leftover tap devices. vmm.Stop is the same function for every monitor type: it sends SIGKILL, treats "no such process" as success, then polls every 100ms for up to 2 seconds to confirm the process is gone.
When the monitor child exits on its own, Supervise() computes an exit code from cmd.Wait(): 0 for no error, the child's own code for a normal exit error, or 1 for any other error. One case is surprising: when the child dies from a signal, ExitCode() returns -1, and os.Exit truncates that to 255.
The recorded test runs show both outcomes. A nerdctl stop on an api-mode Firecracker container took 60ms, reported Exited (255), and left no Firecracker process behind: the -1 path, where the child died from the forwarded signal. QEMU stopped in 58 and 90ms across two runs, and Cloud Hypervisor in 52ms, both with exit code 0, because each monitor handles the signal itself. The exec-replacement baselines look the same: QEMU in 79ms and Cloud Hypervisor in 55ms, both exit code 0.
Unikontainer.Delete() refuses to run while the container is still running. Otherwise it removes files: the whole monitor rootfs directory, if urunc created one, or else a fixed list of paths (/lib, /lib64, /usr, /proc, /dev, /tmp, and the monitor binary) under the rootfs prefix, leaving the bundle directory itself in place. It then removes the container's base directory, and urunc delete runs the Poststop hooks.
Design decisions and rationale
Confinement depends on order, not on a setting. urunc always creates the socket directory after changeRoot, and always starts the monitor after that. Only UsesControlSocket gates the step; the boot mode does not. The scenarios under "The control socket" show this holds for default, custom and invalid socket_path values alike.
Api mode is the default on Firecracker, and an opt-in on QEMU and Cloud Hypervisor. On the two opt-in monitors, api mode refuses what it cannot express rather than degrading it: both reject virtiofs before any spawn, and Cloud Hypervisor's payload builder returns an error rather than silently dropping unrepresentable arguments.
The start trigger comes after the OCI start handshake, not before it. On all three branches, urunc sends the start-the-guest call strictly after SendMessage(StartSuccess) and after PreExec returns. The recorded QEMU and Cloud Hypervisor test runs show exactly that order.
The deferred <session>.Kill() covers the window between spawn and hand-off. An error in the middle of configuration leaves no monitor child behind.
Measurements show no boot-time difference between the two modes on Firecracker. The recorded test runs hold two 20-run comparisons, dataset A and B, both timing a container from nerdctl run until the guest answered HTTP 200, on binaries predating the confinement change.
| Dataset | config-file average | config-file median | api average | api median |
|---|---|---|---|---|
| A, 20 runs per mode | 1258 ms | 1276 ms | 1260 ms | 1261 ms |
| B, 20 runs per mode, 1ms poll | 1310 ms | 1335 ms | 1316 ms | 1341 ms |
In dataset A, api mode is 2ms slower on average and 15ms faster on the median. In dataset B, api mode is 6ms slower on average. The sign of the difference flips between datasets, and between average and median inside dataset A: the two modes take the same time within measurement noise. Dataset A's spread is 1111-1376ms for api mode and 1082-1354ms for config-file mode. Dataset B also measured a 10ms poll variant (1334ms average) and a busy loop (1329ms average), and a 5-run api sample taken after the confinement change stayed in the same band, at 1189ms average. Both sets of recorded test runs attribute the flatness to the guest boot, which is about 1s of the roughly 1.25s total.
QEMU's api set of 5 runs averaged 919ms; its exec-replacement set of 3 runs averaged 934ms. The 15ms (1.6%) difference sits inside the run-to-run spread. The Cloud Hypervisor tests never reached a running guest, so they produced no end-to-end timing.
urunc polls the control socket every 1ms, not the previous 10ms, while waiting for the monitor to accept a connection. A standalone benchmark of 30 runs per interval measured: 10ms poll interval, 9.0ms average socket wait, 22.9ms average total; 1ms poll interval, 1.1ms average socket wait, 11.5ms average total; busy loop with no sleep, 0.1ms average socket wait, 10.3ms average total. Most of the original 10ms wait was poll granularity, not real waiting time. urunc uses the 1ms interval rather than the busy loop, because 1ms captures nearly all of the gain without spinning the CPU.
What the api path adds is the live socket. After boot it answers state queries from the host through /proc/<pid>/root/..., in the subset of scenarios noted under "The control socket".
The supervised monitor receives the caller's own standard input, output and error streams, set explicitly. This closed a gap where an api-mode child's standard input defaulted to /dev/null, so input piped into nerdctl run -i never reached the guest's shell.
Known limitations
- urunc does not forward SIGKILL. A SIGKILL sent to the supervising urunc process kills it at once. The monitor child keeps running, on Firecracker, QEMU and Cloud Hypervisor alike, because
Supervise()handles SIGTERM and SIGINT only. No process can catch SIGKILL. Supervise()'s exit-code mapping produces 255, rather than the usual 143, for a monitor that dies from a forwarded SIGTERM. This is live-observed for Firecracker only. QEMU and Cloud Hypervisor handle SIGTERM themselves and exit 0.- Api boot mode rejects the virtiofs shared filesystem up front, on both QEMU and Cloud Hypervisor.
- Vsock has no live boot coverage in any of the three recorded test runs. Neither of Firecracker's two images sets
vsock.uds_path, so unit tests and code review are its only coverage. Cloud Hypervisor's report notes the same gap for its own two images. QEMU's report has no vsock scenario at all. - In the test registry, no Cloud Hypervisor image boots a working guest on the tested hardware (64-bit ARM,
aarch64), in either boot mode. A live Cloud Hypervisor guest boot through api mode is therefore unverified. - The Cloud Hypervisor failure looks the same in both boot modes: Cloud Hypervisor detects the block device as a raw image, then rejects the guest kernel's first write to sector 0. The virtual machine resets for roughly 1 to 2.4 seconds, then goes quiet with no guest console output. The image asks for a serial console named
console=ttyS0; Cloud Hypervisor'saarch64machine instead offers PL011, the Arm serial device model, whose console isttyAMA0. That mismatch is consistent with the silence. The report reads the difference in reset-loop duration as run-to-run variance of one upstream failure. - Cloud Hypervisor boot timing has no end-to-end measurement, because the guest never finishes booting. The recorded test runs give three signals instead: spawn,
vm.createandvm.bootall land within the same wall-clock second; the monitor's first device-manager event is about 5ms after process start; and the guest's first block request is at about 126ms. - The Cloud Hypervisor command-line path reports 281018368 bytes of guest memory, where urunc computed 268435456 bytes. The difference comes from
BytesToStringMB, the function that turns a byte count into a megabyte string for the command line: it rounds, and Cloud Hypervisor then reads the rounded value. Only the api path sends the exact byte count. - Hedge is present in the code but nobody can use it. It implements only three of the monitor interface's methods for real (
UsesKVM,SupportsSharedfs,Path). Every other method, includingOk(), returns an error, so building a Hedge monitor always fails.
Closing pointer
In api mode the control socket stays open for the life of the container, and the host can reach it at /proc/<pid>/root/<socket path>. Any later feature that acts on a running guest, such as a pause or a snapshot, would use this same socket.
Appendix: differences between the three branches
For code that none of the three pull requests touches, this document uses main. The branches disagree with each other on the following points.
- The default boot behavior inverts between the monitors, through
boot_mode. Firecracker treats every value exceptconfig-fileas api mode, so an unsetboot_modeboots it over the control socket. QEMU and Cloud Hypervisor treat every value exceptapias command-line mode, so both need an explicit opt-in. UsesControlSocketreports whether a monitor uses a control socket. Each branch's copy answers true only for the monitor that branch adds. The converged behavior is the union of the three copies.- Only QEMU's api mode limits the control socket to starting the guest. Firecracker and Cloud Hypervisor also send the guest's configuration over the socket.
- Firecracker's copy of
Exec()differs from the other two in three ways: it retries the rootfs chooser, rather than erroring, when the monitor root-directory annotation is empty; it sets up the network in the background in api mode, rather than waiting for it to finish; and it runs one rootfs preparation step twice, rather than once. - Three more differences do not change behavior: Firecracker creates the socket directory in a named helper, where QEMU and Cloud Hypervisor write the same code inline; the
boot_modeandsocket_pathdoc comments intypes.gocome in two wordings, Firecracker's differing from the other two, which are byte-identical to each other; and Firecracker's guest configuration step (ConfigureGuest) no longer prefixes guest file paths with the monitor's own root directory, matching what the exec-replacement path always sent, a fix with no counterpart on the other two monitors. - Two facts hold for one monitor only. Firecracker is the only monitor whose supervisor exited with code 255 in a recorded run. Cloud Hypervisor is the only monitor whose socket has a companion
.sock.lockfile.
Appendix: terms
| Term | Meaning in this document |
|---|---|
| monitor | the program that creates and runs the virtual machine: Firecracker, QEMU, Cloud Hypervisor, Solo5-hvt or Solo5-spt. |
| VMM | virtual machine monitor. Also the name of urunc's monitor interface, which every monitor type implements. |
| guest | the software that runs inside the virtual machine, under the monitor's control. |
| unikernel | a guest operating system that urunc builds and configures for one container, through one common interface. |
| OCI | Open Container Initiative. Its specification defines the container bundle, the runtime commands and the lifecycle hooks used here. |
| shim | the program containerd starts for every container to run the urunc commands (containerd-shim-urunc-v2). |
| reexec process | the second urunc process, started by urunc create from the same binary. It waits, then launches the monitor. |
| OCI init process | the container's one main process. urunc records its process ID in state.json; every later command acts on that ID. |
state.json |
the container's state file. urunc saves it at each step, with the container's status and process ID. |
| supervisor | the urunc process that stays alive in api mode after it starts the monitor as its child. |
| exec-replacement mode | the process model where the monitor replaces the urunc process and becomes the OCI init process. |
| api mode | the process model where urunc stays alive as a supervisor and the monitor runs as its child. |
boot_mode |
the per-monitor setting that selects between the two process models. |
Exec() |
the urunc function, run inside the reexec process, that prepares the machine and launches the monitor. |
| control socket | a Unix socket the monitor opens in api mode, used to configure the guest, start it, or both. |
socket_path |
the optional per-monitor setting that gives the control socket a custom path. Without it, urunc defaults to /tmp. |
| monitor rootfs | the monitor's private root directory (MonRootfs), holding only the files the monitor needs. |
changeRoot |
the urunc function that makes the monitor rootfs the process's new root, just before the monitor starts. |
| pivot | the step inside changeRoot that swaps the root directory and detaches the old one (pivot_root). |
| tap device | a virtual network interface on the host, through which the guest's network traffic flows. |
| initrd | initial RAM disk, a small filesystem image loaded into memory with the guest kernel. |
| QMP | the QEMU Machine Protocol, QEMU's own control socket language, in JSON. |




Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Mentorship plan: Improve lifecycle management of sandbox monitors
This discussion outlines the proposed plan for the CNCF mentorship project "Improve lifecycle management of sandbox monitors”.
The plan is structured into three phases. Each phase has clear goals and specific outcomes, along with suggested tasks and sub-tasks to help guide the work. The listed sub-tasks are meant as guidance and reference points, they are not strict requirements. The exact order of tasks within each phase can be adjusted as long as the main outcomes are achieved.
Phase 1
The goal of Phase 1 is to build the necessary background and get familiar with IPC over sockets in linux namespaces. This phase is expected to last up to 3 weeks (08/06/2026 – 29/06/2026).
Tentative tasks and sub-tasks
Outcome
Deadline: Completed no later than 28/06/2026 (AoE).
Description: A report with the:
Phase 2
The goal of Phase 2 is to extend the quick PoC above to a proper solution which will be integrated in urunc. This phase is expected to last up to 4 weeks (29/06/2026 – 27/07/2026). During this time, the focus will shift to a proper design and implementation of a solution for all monitors
Tentative tasks and sub-tasks
Outcomes
Deadline: Completed no later than 27/07/2026 (AoE).
Description: The following items:
Phase 3
The goal of Phase 3 is to utilize the VMM API for adding support for CRIU. This phase is expected to last up to 4 weeks (27/07/2026 – 31/08/2026). During this time, the focus will shift to utilizing the monitor's API for creating and restoring snapshots and integrating this workflow with CRIU.
Tentative tasks and sub-tasks
Outcomes
Deadline: Completed no later than 31/08/2026 (AoE).
Description: The following items:
All reactions