Skip to content

Upstream findings — hypeman

Defects found in hypeman while building the nap-005 backend, recorded here so they can be reported upstream and so Barista's own test results stay interpretable: three of them required a local workaround, and a reader six months from now needs to know which measurements were taken on a patched substrate.

Versions: macOS 0.16.1 (arm64), Linux 0.17.0 (arm64), API 0.3.0.


1. The linux/arm64 release embeds x86-64 guest binaries — guests cannot boot

Severity: blocking. No instance can start on linux/arm64.

Symptom

The guest kernel boots and then panics immediately:

Run /init as init process
/init.bin: line 11: syntax error: unterminated quoted string
Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000200
CPU: 1 UID: 0 PID: 1 Comm: busybox

The shell-syntax error is a red herring worth explaining, because it is what makes this look like a quoting bug in generated config. /init is a /bin/sh wrapper ending in exec /init.bin "$@". init.bin is an x86-64 ELF on an aarch64 kernel, so execve returns ENOEXEC; busybox sh then falls back to interpreting the file as a shell script, and line 11 of the ELF's bytes contains an unbalanced quote. The reported error is a property of the binary's bytes, not of any script.

Cause

lib/system/initrd.go builds the initrd from two embedded binaries, and both embed directives name a single, non-arch-qualified path:

// lib/system/guest_agent_binary.go
//go:embed guest_agent/guest-agent

// lib/system/init_binary.go
//go:embed init/init

Whatever the build placed at those paths is embedded into every host build. The release pipeline evidently populates them with amd64 binaries, so the linux/arm64 hypeman-api carries an x86-64 init and an x86-64 guest-agent. Verified directly:

artifact initrd built by macOS 0.16.1 initrd built by Linux 0.17.0
/init.bin ELF aarch64 ✓ ELF x86-64
/usr/local/bin/guest-agent ELF aarch64 ✓ ELF x86-64

The two are separate failures in sequence: fixing only init.bin gets the guest to chroot and then fails at failed to start guest-agent: fork/exec /opt/hypeman/guest-agent: exec format error.

Not a nested-virtualisation problem, though it presents as one: it reproduces on hypeman run busybox with no other software involved, and identically on both Linux hypervisors (cloud-hypervisor and firecracker) — which is itself the tell, since two independent VMMs failing the same way points at the payload.

Other embedded assets are handled correctly per-arch (caddy and firecracker land under system/binaries/<name>/<version>/aarch64/), so this is specific to the two go:embeded guest binaries.

Suggested fix

Arch-qualify the embeds the way the downloaded binaries already are — e.g. //go:embed guest_agent/guest-agent_$GOARCH selected by build tag, or embed both and choose at initrd-build time. A cheap regression test: assert the ELF e_machine of both binaries matches runtime.GOARCH before writing the initrd, which would have turned this kernel panic into a startup error naming its cause.

Workaround used by Barista

The correct aarch64 binaries were taken from the macOS 0.16.1 initrd and grafted into the Linux 0.17.0 initrd at /var/lib/hypeman/system/initrd/aarch64/latest/initrd (extract cpio, replace init.bin and usr/local/bin/guest-agent, repack as newc). hypeman does not rebuild the initrd when its .hash sidecar is unchanged, so the patch survives a daemon restart.

Consequence for Barista's measurements, and the reason this file exists: every Linux number Barista records was taken against a substrate patched this way, pairing hypeman-api 0.17.0 with 0.16.1 guest binaries. The vsock protocol between them proved compatible in practice (the agent reaches HYPEMAN-AGENT-READY and the entrypoint runs), but it is a version skew, and any surprising result on Linux should suspect it first. On linux/amd64 the shipped binaries are the right arch, so a normal deployment does not need this and does not carry the skew.


2. Guest network is unreachable from the host on macOS (upstream #358)

Severity: blocking for host↔guest transport on macOS. Open upstream.

Guests are addressed on 10.100.0.0/16, but on macOS the vz bridge is 192.168.64.1/24 and no host interface carries the guest subnet, so route get 10.100.x.y resolves via the physical LAN and packets leave the machine. Every host→guest path fails the same way, including the /ingresses Caddy reverse proxy, which returns 502 with route, listener and DNS all verified healthy.

Linux is unaffected and the contrast is the proof:

macOS / vz Linux
host interface on 10.100/16 none vmbr0: 10.100.0.1/16
route to a guest address via the physical LAN dev vmbr0 src 10.100.0.1

So the /ingresses transport design is sound; it is the macOS platform binding that is broken.


3. Ingress DNS collides with systemd-resolved on :5353

Severity: minor, but silent. On Ubuntu 24.04, hypeman's ingress DNS binds :5353, which systemd-resolved already holds. Barista's Lima config moves it to 5354 (.tools/nap-linux.yaml).


4. A missing mkfs.erofs fails images with the cause only in the journal

Severity: minor, poor diagnostics. Without erofs-utils installed, every image reaches status: failed and the API reports nothing actionable — the real cause appears only in the daemon's journal. Barista's node preflight now checks for mkfs.erofs by name, alongside the existing caddy and mkfs.ext4 checks.


5. POST /instances answers a bare 500 internal_error for a bad hypervisor

Severity: minor, poor diagnostics. Asking for vz on Linux fails with an unexplained 500; the real message — no VM starter for hypervisor type: vz — is only in the journal. Relevant to anyone driving the API programmatically, which is the whole of Barista's use.


6. network.egress is schema-validated and unenforced — and unknown fields are accepted

Severity: high, and silent by construction. Measured 2026-08-08 on hypeman-api 0.16.1 against the pinned contract 0.3.0 (nap-014 task 4.2). A VM created with network.egress.enabled: true and enforcement.mode: http_https_only opens a direct TCP connection to 1.1.1.1:443 exactly like an unmediated twin. Repeated with the stronger mode: all, which the contract describes as rejecting direct non-mediated TCP egress: both 443 and 53 stayed open.

Barista was removed from the picture before this was filed — the instance was created straight at the substrate API, so the mapping is not the fault. GET /instances/{id} never echoes an egress object back, and the daemon's "allocated network" log line mentions no egress handling.

The second half is what makes the first undetectable: the API returns 201 for a request carrying an invented field (network.totally_not_a_real_field). A client therefore cannot distinguish "the policy was applied" from "the policy was discarded" by any response it receives. An accepted create is no evidence at all.

This is the worst degradation shape a sandbox platform has: the caller believes untrusted code cannot reach the internet, and it can. Barista's node reports egress_control: false and refuses mediated specs rather than passing them through, and its acceptance test is written as a tripwire asserting today's behaviour — so the day this is fixed upstream, that test fails and says so.


7. No vsock transport for a third-party guest agent

Severity: design gap, not a defect. vsock does not occur anywhere in openapi.yaml at 0.3.0, and Instance.network carries enabled, name, ip and mac — no vsock field, port or CID. So a guest agent that is not hypeman's own can be reached only over the instance's IP on the shared default network.

hypeman itself runs a vsock channel for its own agent (docs/adr-001-substrate-evaluation.md §2 — bidirectional-streaming Exec over vsock, disabled with --skip-guest-agent), so the transport exists in the implementation and is simply not exposed.

The consequence for anything building on hypeman: since network.name is always "default", one instance's agent port is reachable by every sibling VM on the host, and the only available defence is in-band authentication. Barista has built per-instance mutual TLS for exactly this (barista-021). An exposed vsock endpoint would retire that mechanism entirely — a channel with no network identity to spoof needs no certificate to pin.


Substrate state on the nap-linux dev VM

Not a defect, but recorded here for the same reason the rest of this file exists: a measurement is only interpretable if you know what the substrate was doing.

nap-005 task 5.5 set hypervisor.firecracker_snapshot_memory_backend: uffd in the VM's /etc/hypeman/config.yaml to measure the lazy-restore path. Reverted on 2026-08-08; the VM is back on the stock file backend and hypeman-uffd@0.1.6.service is stopped. The uffd variant is kept at /etc/hypeman/config.yaml.uffd-nap005 if the sweep needs repeating.

Only the uffd rows of the dirty-memory sweep (docs/BRD.md §6) were taken with that setting live; every other Linux number on this VM is on the file default.

Reverted rather than left on because the setting bought 5–15% on resume — partly inside run-to-run noise — while one of the six UFFD runs killed the firecracker VMM (fc.sock: connection refused) with no cause established. A non-default pager left active would make every later failure on this VM start its diagnosis with "was it the pager?", which is exactly the interpretability tax this file exists to avoid.