better/compute

Instance runtimes

The runtimes bcd runs VPS and sandbox instances on, what each one does for disks, NICs, GPUs, volumes, snapshots, consoles and seeds, how placement and admission apply, and how they run under bcd helper.

A VPS host's bcd node runs the instances its NodeSpec's instances section lists through a runtime: a driver for one hypervisor or container engine behind a small Go interface. The instances converger decides what should exist and in which state; the runtime makes it so. This page covers the runtimes themselves. What the converger does around them (the desired set, user data, admission, network rules, snapshot transfers) is on instances, and the control plane's side is on VPS instances.

No VMM has booted under bcd on real KVM

libvirt-kvm (362317c) and cloud-hypervisor (2598f0a) are written, reviewed and tested against fake tools only: a fake virsh and qemu-img, a fake cloud-hypervisor REST API, fake systemd-run, ip and seed tools. As of round 6 (.claude/coordination.md, 2026-10-04) no virtual machine has booted under bcd on a real KVM host: the development machine has /dev/kvm in WSL but no hypervisor installed, and installing one needs the user's approval. The end-to-end tests (tests/e2e/bcd-vps.test.ts, bcd-placement.test.ts) run a fakehw build's file-backed fake runtime through the real control plane. Treat everything below about real VMs as the code's intent, checked by unit tests, not as production behaviour.

Which runtimes a host runs

There is no configuration key. A host build of bcd node runs each built-in runtime whose tool is on its PATH when it starts (newRuntimes in cmd/bcd/hardware_host.go), then adds bcx plugin runtimes:

RuntimeTurned on byFiles underId (InstanceSpec.runtime)
libvirt-kvmvirsh on the PATHdata_dir/libvirtlibvirt-kvm
cloud-hypervisorcloud-hypervisor on the PATHdata_dir/cloud-hypervisorcloud-hypervisor
A bcx plugin runtimeA hosted driver plugin whose manifest lists the kind runtime, with an id no built-in runtime hasThe plugin's ownThe driver id
The fake runtimeA fakehw build (go build -tags fakehw), alwaysdata_dir/fakehw-runtime.jsonlibvirt-kvm

Only node agents run runtimes; bcd site and bcd ramdisk run none. Every runtime the node runs is advertised in Hello.drivers with kind runtime, which the gateway turns into the node label bc.dev/driver.<runtime>. vps places a hosted instance only on hosts that carry its runtime's label (0fca413). If a spec names a runtime this host lacks anyway, that instance fails with runtime <id> is not installed on this host. A host with no runtime at all fails a non-empty instances section terminally with this host runs no instance runtime, and bcd doctor warns ("neither virsh nor cloud-hypervisor: this host runs no VPS instances").

All volumes share one store, data_dir/volumes, whatever the runtime (see volumes).

The Runtime interface

Every runtime implements runtime.Runtime (daemon/internal/runtime/runtime.go):

MethodDoes
Name()The driver id
List(ctx)Every domain the runtime holds, managed or not, with its state, resources, addresses, MACs, GPUs and open volumes
Ensure(ctx, spec, userData, restore)Defines the domain for a spec, or updates an existing one (resources, NICs, GPUs). It never starts it. userData is the NoCloud user data for a new domain; restore is a downloaded, verified snapshot a new domain boots from
SetState(ctx, id, state)Starts, stops, pauses or resumes a managed domain (running, stopped, paused)
Snapshot(ctx, id, includeMemory, cpuModel, w)Writes a snapshot tar of a managed domain to w, and answers the CPU model a memory state depends on
Destroy(ctx, id)Removes a managed domain with its disks and taps

Optional interfaces add what only some runtimes do: Console (a serial console), Rebind (the identity rebind disk), VolumeGrower (tell a running guest a volume grew), the balloon's size and usage readers, SelfAdmitting (a runtime that checks capacity itself, so bcd's admission leaves it alone) and NormalPagesRestore (memory restores run on normal pages).

Two rules hold for every runtime:

  • Ownership. A domain is Managed only when bcd created it. Every other domain is listed so the control plane can report the leak, and is never defined over, started, stopped or destroyed.
  • Tap names. NIC i of instance id lives on the host interface runtime.TapName(id, i): bc, the first 10 hex digits of the SHA-256 of the instance id, n, then the index (it fits IFNAMSIZ). bcd's nftables rules match taps by name, so they exist before the tap does.

libvirt-kvm

runtime.Libvirt drives virsh -c qemu:///system and qemu-img. Each domain it defines carries bcd's metadata (namespace https://better-compute.dev/libvirt/1: allocation, image digest and format, addresses, the snapshot it was restored from), which is how bcd tells its own domains apart.

ConcernWhat libvirt-kvm does
DiskA qcow2 overlay instances/<id>/disk.qcow2 on a base image downloaded once over https, verified against its SHA-256, checked as a tenant image and kept as images/<sha256>.<format>. An iso image boots from a cdrom onto a blank qcow2 disk
Growqemu-img resize while stopped, virsh blockresize while running; a smaller size fails the instance
NICsvirtio NICs on the spec's bridges, on taps named TapName
CPUhost-passthrough, so a memory snapshot restores only on the same CPU model
Statesstart (--paused for a paused start), suspend, resume. A stop is virsh shutdown, then virsh destroy after 60 seconds (ShutdownTimeout). A start that fails with "Cannot allocate memory" is a capacity failure
Deletedestroy and undefine, then the instance directory is removed
ConsoleThe pty libvirt gives the domain's serial port

Running as root, bcd adds o+x (search, never read) to each directory from data_dir down to a domain's disks, seed, volumes and base image, so QEMU's own user (libvirt-qemu or qemu) reaches them while listing stays closed (f7b8805). Under the helper the model is different (below).

fakehw builds replace the runtime with a file-backed fake under the same id. It writes the domain XML bcd would define on a real host to data_dir/fakehw-plans/<id>.xml (or <id>.error when the spec would be refused) through runtime.RenderLibvirt, so tests can assert pinning, NUMA binding, huge pages, the balloon, volumes and GPU addresses that the fake cannot run.

cloud-hypervisor

runtime.CloudHypervisor runs one cloud-hypervisor VMM per instance in a systemd transient unit bcd-ch-<id>.service in bcd-instances.slice (started with systemd-run --collect, so a VM outlives bcd), and drives it over its REST API on instances/<id>/api.sock. cloud-hypervisor keeps no registry, so instances/<id>/vm.json is bcd's record: the allocation, image and base, the VMM's uid, addresses, networks, MACs, GPUs, placement, volumes, the VM configuration, and what was handed to the running VMM (VFIO groups, bound PCI addresses, volumes). A directory without that record, marked better-compute/cloud-hypervisor/1, is not bcd's.

ConcernWhat cloud-hypervisor does
FirmwareBoots /usr/share/cloud-hypervisor/CLOUDHV.fd; bcd doctor warns when it is missing
DiskThe same verified and checked qcow2 overlays as libvirt-kvm (it reuses that image code over its own directory), image_type stated for each disk, backing_files only for bcd's own overlay. An iso image is refused
NICsPersistent taps named TapName, created and bridged by bcd before the VMM starts (933c51b). Since b12c217 bcd opens each tap and hands its file descriptor to the VMM (vm.add-net, or net_fds on a restore), so the VMM never names a tap
IdentityEach VMM runs as its instance's own uid from 0x7FFD0000 up, recorded in vm.json, with no capabilities and no network (see the cloud-hypervisor unit)
Growqemu-img resize while stopped, vm.resize-disk while running
Statesvm.create and vm.boot (then vm.pause for a paused start), vm.pause, vm.resume. A stop is the ACPI power button for up to 60 seconds, then vm.shutdown, vmm.shutdown and systemctl stop, and fails unless the unit is then inactive or failed
DeleteStops the VM first, so a running VMM's directory is never removed, then removes its taps and directory
ConsoleThe serial port's unix socket, instances/<id>/serial.sock

It needs systemd 247 or later, which bcd doctor checks, and a cloud-hypervisor whose API takes NIC file descriptors.

bcx plugin runtimes

A bcx driver plugin whose manifest lists the kind runtime becomes a runtime on bcd node (agent.PluginRuntime, walkthrough 4). It speaks the TypeScript RuntimeDriver contract over Invoke, with positional JSON arguments:

OperationArgumentsAnswer
listnone[{ id, state, addresses, message? }]
applycore's InstanceSpec with its desiredState{ id, state, addresses, message? }
removethe instance idnull

Ensure only records the spec; SetState sends apply with the desired state, so the plugin both defines and starts. A new instance's user data and a restore (as a local file:// URL of the verified snapshot) go with its first apply only. The idempotency key is instance:<id>:apply:<state> or instance:<id>:remove. Everything a runtime plugin lists counts as bcd's.

What bcd passes a plugin, and what it does not:

  • Passed (b8a1801): each NIC's tap name, gateway and nameservers, the network policy, sshFromHostOnly, identityRebindRef, the placement, and each volume's path and serial in attach order.
  • Enforced by bcd. When the tap rules guard an instance (a PRIVATE or NONE policy, or SSH from the host only) and a running plugin instance has no interface under one of its tap names, bcd stops and fails it (runtime <id> did not create taps …: its network rules cannot apply).
  • Not supported. Snapshots (Snapshot answers unsupported, so no plugin snapshot exists to restore from, although a restore's verified file would be passed on); the identity rebind and identity_rebound_ref; consoles.

Plugins need root to start, so an unprivileged node under the helper hosts none (see bcd helper).

The fake runtime

runtime.Fake keeps its domains in a JSON file, so they outlive the daemon process. A new boot id (a host reboot) stops every domain, as it would stop real VMs. It honors optional vCPU and memory limits itself (AdmitsItself, so bcd's host admission skips it), refuses a disk that shrinks, refuses a snapshot from another runtime and a memory snapshot from another CPU model, refuses a memory snapshot beside a read-write volume, echoes on its "console", and acts as a compliant guest for the identity rebind. The TypeScript FakeHypervisor follows the same rules, which is what better-compute dev --sim and the simulator run.

Passthrough GPUs

An instance's gpu_device_ids are GPU UUIDs. Both built-in runtimes find each GPU's PCI address from the inventory's bus id (a GPU already on vfio-pci is found by its remembered address), and an instance whose GPU has none fails.

Steplibvirt-kvmcloud-hypervisor
BindingManaged PCI <hostdev>: libvirt rebinds the device itselfbcd binds every endpoint of the GPU's IOMMU group to vfio-pci before the start (driver_override, unbind, drivers_probe), the GPU's audio function included; bridges stay on pcieport (be4de2a). It checks the devices moved, so a host without vfio-pci loaded fails the start
Unbind boundAn unbind waits at most 30 seconds (UnbindTimeout); past that the start fails rather than block every section
Persistence modeOff for the domain's GPUs before a start, on again after a destroy (5bb9f22)Off before a GPU leaves nvidia, on again once it is back (f25abbe)
Giving backlibvirt reattaches on destroyA stop gives the groups back to root; a destroy hands the devices back to their host driver, even when the spec dropped a GPU meanwhile

Persistence mode matters because nvidia-persistenced (which the nvidia-driver host unit keeps running) holds every GPU open, and the driver's unbind waits until the GPU is closed. Both runtimes refuse a memory snapshot of an instance with passthrough GPUs, since the GPU's state lives in the device (an instance with passthrough GPUs cannot be snapshotted with its memory; snapshot its disk only). The host's IOMMU and vfio-pci come from the iommu and kernel-modules units, and bcd doctor warns when vfio-pci is missing on a host with an active IOMMU.

NoCloud seeds

User data never travels in the spec. The converger fetches it with a SecretRequest bound to the allocation, and the runtime writes it into a NoCloud seed, built with genisoimage (-volid cidata -joliet -rock), else xorriso -as mkisofs with the same options:

FileContent
user-dataThe fetched user data, or #cloud-config with an empty object when there is none
meta-datainstance-id: <id> and local-hostname: <id>
network-configOnly when a NIC has leased addresses, a gateway or nameservers (0791fc9): cloud-init network config version 2, each NIC matched by MAC, its CIDR addresses static with DHCP off, a default route through its gateway, and its nameservers. A NIC without addresses keeps dhcp4: true. A NIC without a valid MAC beside static NICs fails the seed, since cloud-init would leave it unconfigured

libvirt-kvm attaches seed.iso as a read-only cdrom; cloud-hypervisor attaches the same file as a read-only raw disk. A restored instance keeps the seed in its snapshot, attached as raw and never probed. bcd doctor fails a VPS host without either genisoimage or xorriso.

Placement

InstanceSpec.placement (from a flavor's placement, see flavors) is checked before anything is created (runtime.PlacementOf), and a placement the host cannot apply fails the instance:

FieldRule
cpusA kernel cpulist with at least as many CPUs as vCPUs. With exactly one CPU per vCPU, vCPU i is pinned to the i-th CPU; otherwise the vCPUs float within the set. A range spans at most 4096 CPUs
memsA cpulist of NUMA nodes the memory is bound to
hugepage_size_bytes0, 2 MiB or 1 GiB, and the memory must be a whole number of pages
balloonA virtio balloon, dropped when huge pages back the memory
Placementlibvirt-kvmcloud-hypervisor
CPUsvcpu cpuset (floating) or cputune vcpupin (pinned)cpus.affinity, and AllowedCPUs on the VMM's unit
NUMAnumatune in strict modeAllowedMemoryNodes on the VMM's unit
Huge pagesLocked memoryBacking huge pagesmemory.hugepages; the unit's MemoryMax drops to the VMM's overhead
BalloonAn explicit memballoon (none unless asked)A balloon with free page reporting

A restore keeps bcd's pinning, never the snapshot's. A cloud-hypervisor memory restore runs on normal pages whatever the placement says, because its saved configuration keeps no huge pages; admission charges it that way (RestoresOnNormalPages). The balloon controller that resizes balloons under host memory pressure is on memory ballooning. bcd advertises instance.placement in Hello on hosts with a runtime, and vps sends placements only there.

Admission

Before a runtime starts a domain that is not running (a resume from PAUSED is not a start), bcd checks that the host can give it its memory (0dd122a), counting what is committed rather than used: running and paused managed domains on normal pages with their VMM overhead (1/32 of the memory plus 256 MiB), bound holds' memory limits, and starts already admitted in the same pass, against MemTotal less the huge page pool and 1 GiB of headroom. An instance with huge pages is checked against free pages of that size on its NUMA nodes and host-wide (aeaa982). A start that does not fit stays stopped with ResourcesExhausted: … in its status, is retried on the next pass, and is not an ERROR. The rules and what vps does with the refusal are on instances.

A runtime that reports ErrNoCapacity, or a libvirt-kvm start that fails for lack of memory, is a capacity failure of that instance. A runtime that admits itself (the fake) is left alone, and a host whose /proc/meminfo cannot be read starts without the check, with a warning.

Volumes

Persistent volumes are sparse raw files in data_dir/volumes/<id>.raw, shared by every runtime on the host and attached by serial (/dev/disk/by-id/virtio-<serial> in the guest), vdb onward after the root disk:

RuntimeAttached as
libvirt-kvmRaw virtio disks, read-only when asked
cloud-hypervisorRaw disks with their serial, bound into the VMM's unit; a read-write one handed to the VMM's user while it runs, a read-only one readable by that user through an ACL entry
A pluginPath and serial in its spec

Growth is applied live through VolumeGrower (virsh blockresize, vm.resize-disk; b5415a2). bcd tracks which domains have which volumes open and never lets a second writer in, applies stops and deletes before starts, and never destroys a volume a running VM has open. Both built-in runtimes refuse a memory snapshot beside a read-write volume (ErrMemorySnapshotWithVolumes). The store's rules, space accounting and reports are on persistent volumes.

Snapshots and restores

instance.snapshot asks the runtime for a tar written to a staging file under data_dir/snapshots, which bcd hashes and uploads; restore_from downloads and verifies a snapshot before a new instance boots from it. The transfers are on instances; the runtimes' formats are:

RuntimeTar contentsRestore
libvirt-kvmmeta.json (runtime, CPU model, includeMemory), the overlay flattened onto its base (qemu-img convert), the seed, and with memory virsh save's state. The domain then carries on with virsh restore --xml of bcd's own domain.xml (81f9c21). A disk-only snapshot of a running domain pauses it for the copyUnpacks into the new instance's directory, checks the disk and seed, and starts with virsh restore and the new domain's XML
cloud-hypervisormeta.json, the flattened overlay, the seed, and with memory cloud-hypervisor's own snapshot of the paused VM under ch/ (config.json, state.json, memory-ranges); a running VM is paused and resumedThe saved config.json must have exactly the instance's vCPUs and memory; bcd rebuilds it keeping only CPU, memory and platform keys and supplies payload, disks, NICs, serial, console, rng and devices itself, then vm.restore and vm.resume

A snapshot names its runtime in meta.json, and the other runtime refuses it. A memory snapshot restores only on the same CPU model. A memory restore's guest gets its own SSH identity through the identity rebind (ca08d5c), which both built-in runtimes implement with a hot-added vdz disk.

Serial consoles

Instance console tunnels (f8273ef) reach a running, managed instance's serial port, never the host's shell. libvirt-kvm opens the pty libvirt gave the domain's serial port; cloud-hypervisor dials instances/<id>/serial.sock; the fake echoes what it is sent, prefixed with console: . Under the helper, root opens the libvirt pty and passes the node its descriptor (libvirt.console). Plugin runtimes have no console.

Under bcd helper

On an unprivileged bcd node ([helper] set), forHelper (cmd/bcd/agent.go) wires each built-in runtime to bcd helper:

RuntimeGoes through the helperStays with the node
libvirt-kvmEvery privileged virsh verb, as libvirt.* (ed3c7d2): define, the state changes, blockresize, setmem, the rebind disk's attach and detach, save, restore and console; root validates each definition default-deny and acts only on bcd's domainsReads over libvirt's read-only socket (virsh -r for list, domstate, dumpxml, domstats; any other verb is refused), its files under data_dir/libvirt
cloud-hypervisorTaps, VFIO groups and the VMM unit, as ch.* (d86fcbe, 7fe74a1); root builds the unit from the instance record it checksEvery file, shared with the VMM's uid through POSIX ACL entries (the unit's UMask becomes 0007), and the VMM's API socket

libvirt then runs with dynamic_ownership = 0, and QEMU's user is in the node's group, so instance disks are 0660 (read-only ones 0640) and the directories on the way get group search instead of others' search. Without [helper] instance_bridges, root defines no domain with a NIC and creates no tap. What root checks for each operation is on bcd helper and in the overview's libvirt through the helper and cloud-hypervisor through the helper.

What the tests cover

TestsAgainstCovers
internal/runtime libvirt_test.goA fake virsh and qemu-img (writing real qcow2 headers since b12c217)Domain XML and metadata, states, grow and shrink, snapshot and clone with memory, a CPU mismatch, destroy, a mismatched image digest, base image and overlay checks
internal/runtime cloudhypervisor_test.goA fake VMM API and fake systemd-run, qemu-img, ip and seed toolsDefine, states, memory snapshot and restore into a clone, the rebuilt restore configuration, taps before the VMM, the unit's uid and confinement, NIC fds, VFIO groups handed back, a rewritten overlay, a VMM that survives its stop, the rebind
internal/runtime placement_test.go, netconfig_test.go, vfio_test.go, volumes_test.go, balloon_test.goFakes and temporary filesPlacement parsing, network-config rendering, VFIO binding against a fake sysfs, the volume store, the balloon
internal/runtime hypervisor_test.go, ch_privileged_linux_test.go and the helper's testsFake helper sockets and toolsBoth runtimes under the helper
tests/e2e/bcd-vps.test.ts, bcd-placement.test.tsA fakehw bcd's fake runtime and the real control planeCreate, start, delete, placement plans and the balloon under simulated pressure

None of these runs a real VMM. Round 6 (2026-10-04) brought bcd onto a real host with an RTX 5090 in WSL, but that host has no hypervisor installed, so no instance has booted on real KVM.

Was this page helpful?

Better Compute is created by Sean Filimon

Last updated

On this page