Concepts
Runtimes
On this page
Brig does not boot a sandbox. A runtime does: hull on macOS, nerdctl on Linux. Under hull, vz, hvi and qemu are hypervisor backends, called backends below.
Platform comparison #
| macOS | Linux | |
|---|---|---|
| Runtime Brig drives | hull |
nerdctl |
| Projects you take on | Brig and hull | Brig, plus three projects it does not own: nerdctl, containerd and urunc |
| What boots the microVM | A hull backend: hvi, vz or qemu |
The containerd shim io.containerd.urunc.v2 (urunc) |
| Binary Brig looks for on PATH | hull |
nerdctl, then docker |
| Isolated network | A gateway per sandbox, on the hvi backend |
A network per sandbox, made with nerdctl network create |
| Egress policy | enforced on the hvi backend |
cannot enforce |
| tmpfs mounts | Mounted in the running sandbox with exec -u root |
Passed at create time with --tmpfs |
| Boot bundle | Fetched by hull | Carried by the runtime bundle. oras fetches it on a host without that bundle. |
| Where the version pin lives | The hull cask in brig-sh/homebrew-brig |
RUNTIME_VERSION in install.sh |
On Linux, install.sh installs nerdctl, containerd and urunc from the runtime bundle described in Install. The nerdctl and containerd in that bundle are upstream releases. For its urunc, see Runtime requirements.
Components #
The licences were read from each repository's LICENSE file on 2026-08-26, and not checked again after that date.
| Component | What it does | Where it comes from | Licence |
|---|---|---|---|
brig, brigd |
Resolves the profile, the guest home and the credentials, then drives the runtime | The Brig repository | Apache-2.0 |
hull |
CLI that pulls an OCI image and boots it as a microVM on Apple Silicon | brig-sh/hull | Apache-2.0 |
vz-runner |
Swift helper hull launches for its vz backend. It is what talks to Virtualization.framework. |
Same repository as hull, installed beside it | Apache-2.0 |
hvi |
Separate microVM monitor for hull's hvi backend. It talks to Hypervisor.framework directly. |
brig-sh/hvi-vmm, a git submodule of hull, installed beside hull | Apache-2.0 |
nerdctl |
Docker-compatible CLI for containerd. It is the binary Brig drives on Linux. | containerd/nerdctl | Apache-2.0 |
containerd |
Daemon underneath nerdctl. It holds the image store and hands each container to a shim. | containerd/containerd | Apache-2.0 |
urunc |
The containerd shim io.containerd.urunc.v2. It boots the container as a microVM, not as a process. |
urunc-dev/urunc, built by the Linux runtime bundle from the feat/unchanged_containers-exec-fixes branch |
Apache-2.0 |
cosign |
Verifies the signature on a guest image before it boots. Optional. Without it, verification degrades to a warning. | sigstore/cosign | Apache-2.0 |
oras |
Pulls the boot bundle on Linux for a genericBoot profile. Optional otherwise. |
oras-project/oras | Apache-2.0 |
| Boot bundle | The kernel, container-initrd and the in-guest agent. They let Brig exec into an image built as an ordinary container. Published as an OCI artifact at ghcr.io/nofireai/hull-assets, one tag per guest platform. |
Fetched by hull on macOS. On Linux the runtime bundle carries its own kernel and initrd, and oras fetches this one only on a host without that bundle. | Unconfirmed. It is signed with keyless cosign, but the repository that builds it is not public and states no licence. |
nerdctl, containerd and urunc predate Brig. Brig passes them only flags that any other caller can pass.
Architecture shows how hull, hvi and the network gateway fit together on macOS.
macOS backends #
Six of the eight shipped profiles set hypervisor: hvi, network: isolated and genericBoot: true (internal/profile/specs). Each new sandbox of those profiles uses hvi and gets a separate network. This path needs the hvi binary beside hull, a working gateway, and the boot bundle.
hull accepts three values for its --hypervisor flag. Brig always passes the flag. The value is BRIG_HYPERVISOR if that is set, and otherwise the backend the profile names.
hvi |
vz |
qemu |
|
|---|---|---|---|
| Talks to | Hypervisor.framework directly, through the hvi microVM monitor |
Virtualization.framework, through the vz-runner helper |
Hypervisor.framework, through QEMU's -accel hvf |
| Chosen when | The profile or BRIG_HYPERVISOR names it |
The profile or BRIG_HYPERVISOR names it, or neither names a backend |
The profile or BRIG_HYPERVISOR names it |
| Shipped profiles | Six of eight, with network: isolated |
Two, with network: shared |
None |
| Network gateway | Yes. The only backend that runs one. | No | No |
--network isolated |
Runs | Refused | Refused |
| Attached egress policy | Runs | Refused | Refused |
Graphical console (kind: gui profile) |
Refused | Runs. The only backend that can show one. | Refused |
hull does not ship QEMU. Install it with brew install qemu. hull's backends page says what else each backend needs.
Note
brig doctor'sHypervisor.framework availableline does not report on these three backends. It is the pass string of onekern.hv_supportsysctl read, which shows only whether this Mac can back a microVM. It never gates the exit status.
Other backends and custom profiles
BRIG_HYPERVISOR=vz with BRIG_NETWORK=shared moves the six hvi profiles to the vz backend. It overrides the isolation that vz cannot provide.
A custom profile with no network choice falls back to shared on vz or qemu. The NETWORK row in brig info names the reason. An explicit isolated is still refused.
The boundary #
Brig decides the following:
- which image, and whether its signature verified (
internal/verify) - which host directory is the guest home
- which credentials are resolved, which are denied for billing safety, and which are handed in by name and not by value
- the sandbox name, memory, CPU count, network mode, pull policy, root filesystem type, hypervisor backend and shared directories
- on macOS, the kernel and initrd paths for an image that carries no kernel, and the gateway address each sandbox takes
The runtime decides the following:
- how the image is pulled and stored
- how the sandbox is configured and booted
- how a command gets into the guest
- what a stopped instance means
Brig links no runtime code. It runs each runtime, cosign and oras as a subprocess. Brig has three direct Go dependencies:
| Dependency | Used for |
|---|---|
sigs.k8s.io/yaml |
The profiles |
golang.org/x/sys |
The terminal and process calls |
github.com/godbus/dbus/v5 |
The Linux secret store |
Runtime commands #
From internal/runtime/hull.go, unless another file is named:
hull --version # does this hull boot a digest?
hull assets pull # HULL_BOOT_ASSETS=<dir> in the environment
hull assets dir
hull ps
hull ps -a # falls back to `hull ps` if -a is refused
hull run --detach --name <name>
--hypervisor <vz|hvi|qemu> --net <shared|none> # none is --network offline
--pull <missing|always|never> --mem <MB> --cpus <n>
[--rootfs-type <block|virtiofs|9pfs>]
[--annotation com.urunc.unikernel.bootKernel=<path>]
[--annotation com.urunc.unikernel.bootInitrd=<path>]
[--gateway-sock <path> --gateway-cidr <cidr>]
[--shared-dir <host>:<guest>[:ro]]...
[--gui [--gui-title <title>]]
[--env <NAME>|<NAME>=<value>]... <image>
hull exec [-t] [--cwd <dir>] [-u <user>] [--env <NAME>|<NAME>=<value>]... <name> -- <cmd>...
hull logs [--follow] [--tail <n>] <name>
hull stop <name>
hull rm <name>
hull network-gateway --help # does this hull enforce a policy? (--egress-default)
hull network-gateway --socket <path> --qemu-socket <path>.qemu
--subnet 198.18.0.0/24 --gateway-ip 198.18.0.1 # internal/runtime/gateway.go
hull network-gateway --socket <path> --qemu-socket <path>.qemu
--subnet <a /30 of its own> --gateway-ip <first address on it>
[--egress-default <allow|deny>] # an isolated sandbox, or one
[--egress-allow <rule>]... [--egress-deny <rule>]... # carrying a policyEvery exec goes through hull exec. The reachability probe, the captured read, the credential written over stdin, and the terminal handover all build the same argv (execArgs). The handover replaces the Brig process with hull, so the guest gets a real terminal.
brig logs <ref> runs hull logs <name>. If a sandbox does not come up, Brig's error output also names that command.
Brig picks the sandbox subnet from 198.18.0.0/15, the range RFC 2544 reserves for network benchmarking. The public internet never routes that range and almost nothing claims it, so a collision with a network you need is unlikely. The other ranges are crowded:
| Range | Used by |
|---|---|
| 10.0.0.0/8 | Corporate VPNs and cloud VPCs |
| 172.16.0.0/12 | Docker |
| 192.168.0.0/16 | Home routers and vmnet on macOS |
| 100.64.0.0/10 | Tailscale |
| 198.19.0.0/16 | OrbStack. Brig leaves this sibling range alone. |
From internal/runtime/nerdctl.go:
nerdctl image inspect --format {{index .RepoDigests 0}} <ref> # the digest to pin
nerdctl ps --filter name=^<name>$ --format {{.Names}}
nerdctl ps -a --format {{.Names}}\t{{.Status}}
nerdctl network ls --format {{.Name}}
nerdctl network create <name> # --network isolated: a network per sandbox
nerdctl network rm <name> # with the sandbox, and by rm --all
nerdctl run --detach --name <name>
--runtime io.containerd.urunc.v2 # BRIG_CONTAINERD_RUNTIME overrides
--memory <MB>m --cpus <n>
[--pull <missing|always|never>]
[--network none | --network <name>] # offline, or isolated
[--annotation com.urunc.unikernel.bootKernel=<path>]
[--annotation com.urunc.unikernel.bootInitrd=<path>]
[--annotation com.urunc.unikernel.hypervisor=cloud-hypervisor]
[-v <host>:<guest>[:ro]]... [--tmpfs <path>:<options>]...
[-e <NAME>|<NAME>=<value>]... <image> sleep infinity
nerdctl exec -i [-t] [-w <dir>] [-u <user>] [-e <NAME>|<NAME>=<value>]... <name> <cmd>...
nerdctl logs [--follow] [--tail <n>] <name>
nerdctl stop <name>
nerdctl rm <name>The container runs sleep infinity. A container exits when its command exits, and the sandbox must outlive the exec that used it.
brig logs <ref> runs nerdctl logs <name>.
Verification and fetch commands #
Brig also runs two commands that are not aimed at a runtime:
oras pull ghcr.io/nofireai/hull-assets:<os>-<arch> --output <dir>
# internal/runtime/bootfetch.go
cosign verify --certificate-identity-regexp <identity>
--certificate-oidc-issuer <issuer> <image> # internal/verify/verify.goCommand environment #
Every runtime command carries HULL_TELEMETRY_PRODUCT=brig and HULL_TELEMETRY_SUPPRESS=1 in its environment. Brig lifts the suppression only for the operations a user asked for, so one Brig command counts once. DO_NOT_TRACK and HULL_TELEMETRY_DISABLED pass through untouched and win.
Only the bare variable name of a forwarded value goes in argv, so nothing readable in ps carries a secret.
| Value | How it reaches the runtime |
|---|---|
| A forwarded value | In the environment, with a bare --env NAME (-e NAME for nerdctl) in argv |
The guest's HOME, PATH, TMPDIR and XDG_* |
On the command line as --env NAME=value (-e NAME=value for nerdctl), because the runtime reads those names for itself |
An ordinary value with BRIG_ENV_ARGV=1 |
On the command line. Use this for a runtime build that cannot take a bare --env NAME. |
A value Brig resolved on your behalf, with BRIG_ENV_ARGV=1 |
Stays off the command line |
See Security model.
Runtime discovery #
Brig finds the runtime in this order (internal/runtime/runtime.go):
BRIG_RUNTIMEnames the runtime,hullornerdctl. Brig refuses any other value by name. If the variable is unset, the runtime ishullon macOS andnerdctleverywhere else.BRIG_RUNTIME_BINis the executable to run. Brig takes a path as it stands, and looks up a bare name on PATH. Brig reports a missing or non-executable one against the variable before anything runs.- A profile's
runtimeBindoes the same thing without a variable per shell. It loses toBRIG_RUNTIME_BIN. Brig expands a leading~. Brig reports a missing or non-executable path against the profile that named it. - Otherwise PATH:
hullfor the hull runtime, andnerdctlthendockerfor the other one.
brig info <ref> prints the result, as runtime hull (/opt/homebrew/bin/hull). brig doctor reports the runtime's version beside the rest of the host.
The docker fallback on Linux
If PATH has no nerdctl, Brig takes docker and says so in one line: brig on stderr, brigd in the response's warnings.
To choose docker and remove that line, name docker in BRIG_RUNTIME_BIN, or its full path in runtimeBin. runtimeBin does no PATH lookup, so a bare docker there is refused as missing.
For the limits of docker, see Runtime requirements.
Capability checks #
Brig asks the runtime four questions:
| Question | Command | Asked of |
|---|---|---|
| Where do your boot assets live? | hull assets dir |
hull |
| Which sandboxes exist, and in what state? | ps |
Either runtime |
| Can you boot a digest reference? | hull --version |
hull |
| Can your gateway enforce a policy? | hull network-gateway --help |
hull |
hull --version is the only version Brig reads. A current hull release boots a digest reference from its store, so Brig pins the image it verified. An unreadable answer counts as pinning.
Before Brig boots a sandbox that carries a policy, it reads network-gateway --help for one word, --egress-default. A sandbox with no policy runs no probe. See Policy enforcement.
With an old build, Brig degrades one feature at a time:
| Runtime build | What Brig does |
|---|---|
A hull without assets dir |
Falls back to ~/.hull/assets |
A runtime without ps -a |
Falls back to the plain listing |
| A hull that cannot pin a digest | Boots the tag and says so |
| A gateway that cannot enforce a policy | Refuses a run that carries a policy. This is the one refusal. |
| A hull too old for a flag Brig passes | Fails at that flag, with hull's own message |
Runtime requirements #
hull must accept the verbs and flags listed in Runtime commands, and three more things.
| Requirement | Why Brig needs it |
|---|---|
A bare --env NAME, which takes the value from hull's own environment |
Without it the only way to forward a credential is BRIG_ENV_ARGV=1, which puts values where ps can read them. |
exec -u root |
Brig uses it to mount a tmpfs inside a running sandbox (internal/wrap/secretfiles.go). |
network-gateway, for the hvi backend |
That backend has no built-in egress, so Brig starts a gateway. |
Brig starts one gateway for each isolated sandbox. Sandboxes that request shared use one shared gateway. Brig assigns the addresses on those networks.
Warning Guests on one shared gateway reach each other. See Security model for the answer per backend.
Gateways started by older versions
A gateway started by an older Brig has no API socket, so it cannot publish a port. The next boot replaces it when no sandbox is on it and no other boot is starting on it.
nerdctl must do the following:
- carry
--annotationthrough to the shim - take
--runtime - honour
-v,--tmpfsand a bare-e NAME
urunc must read com.urunc.unikernel.bootKernel and com.urunc.unikernel.bootInitrd from the container's OCI spec and boot the image with them. It is the same pair hull takes on its command line. Brig also passes com.urunc.unikernel.hypervisor=cloud-hypervisor on every genericBoot run, so urunc must find a cloud-hypervisor binary.
The runtime bundle builds its urunc from the feat/unchanged_containers-exec-fixes branch of urunc-dev/urunc, which implements the pair. It builds its container-initrd from the same commit.
containerd must be running with the urunc shim installed.
docker, your own urunc, and runc
docker is accepted in place of nerdctl and works for an image that carries its own kernel. docker does not pass annotations to the runtime, so Brig refuses a genericBoot profile on it. Without the annotations, the sandbox has no kernel.
Your own urunc. No urunc release reads the annotation pair, v0.8.0 included. A release ignores both annotations and looks for a urunc.json in the image. A stock image has none, so the sandbox never becomes ready. brig doctor still reports the runtime and the boot assets as ok. A host that brings its own urunc (BRIG_INSTALL_RUNTIME=0) also needs a build from the feat/unchanged_containers-exec-fixes branch.
runc. BRIG_CONTAINERD_RUNTIME=runc asks for a plain container, which shares the host kernel. That is the weaker boundary. The envelope's ISOLATION row says which one a run got. Security model says what the weaker one costs.
Policy enforcement #
A run path is a runtime with one backend. Before a run that carries an egress policy, Brig asks whether the run path enforces the policy. The answer comes from one table, shown in Policies.
| Run path | Answer | Probe |
|---|---|---|
hull on hvi |
enforced |
network-gateway --help must exit zero within 30 seconds and list --egress-default |
hull on vz or qemu |
cannot enforce |
None |
| nerdctl or docker, on any shim | cannot enforce |
None |
| hull on a backend the table does not name | unknown |
Brig boots a policy-bound run only on enforced. The refusal names the property, the runtime and the backend.
On hvi, the probe can give two other answers. Brig refuses the run on both.
| Probe result | Answer |
|---|---|
The gateway does not take --egress-default. It drops the rules. |
cannot enforce |
| The probe fails: the binary does not run, it exits non-zero, or it gives no answer within 30 seconds | unknown |
Versions and pins #
Brig's source pins no version of hull, nerdctl, containerd or urunc, and verifies no digest of any of them. The pin is on the install path.
The hull cask in brig-sh/homebrew-brig names one release tarball and its sha256. Brig's cask depends on that cask, so brew install --cask brig gets the build the tap names. Casks/hull.rb shows what an install gives you.
Before hull writes the boot bundle, it verifies the bundle's signature with cosign against the publishing workflow. It records the digest it verified.
The pin is RUNTIME_VERSION in install.sh, which names one release of the runtime bundle. If cosign is available, install.sh verifies the signature on that release's checksums.txt before it runs the bundle's installer. The bundle's pins.env records the urunc commit it was built from, and brig-ctl version prints it.
The runtime bundle does not use the boot bundle. Its launcher points BRIG_BOOT_ASSETS at the kernel and initrd the bundle carries. Its release signs a record of their digests, and the bundle's installer keeps that record beside them.
Hosts without the runtime bundle
The boot bundle arrives through oras, which verifies no signature itself. To pin a version or point at a mirror, set BRIG_BOOT_ASSETS_REF.
After an oras fetch by digest, Brig writes a provenance.json of the shape hull writes. A later run can then tell an older bundle from a changed file.
Boot bundle digests #
On both platforms Brig compares the kernel and initrd with a list of digests before it boots them. The list comes from the verified bundle, or from the signed record in the Linux runtime bundle. See Security model.
Brig fetches the boot bundle by the digest whose signature it verified, not by the tag. hull gets repo@sha256:... as HULL_BOOT_ASSETS_REF, and oras pulls the same. A tag that moves between the verification and the fetch cannot deliver an unverified bundle.
| Setting | Behaviour |
|---|---|
BRIG_BOOT_ASSETS_REF |
Pins a version of ghcr.io/nofireai/hull-assets the same way |
HULL_BOOT_ASSETS_ALLOW_FOREIGN |
hull refuses a reference in any other repository unless this is set. Brig does not set it. |
No verified digest and BRIG_BOOT_ASSETS_REF unset |
Brig drops any HULL_BOOT_ASSETS_REF from the environment it hands hull |
Swaps #
These swaps need no code:
| Swap | How |
|---|---|
| A different build of the same runtime | BRIG_RUNTIME_BIN, or runtimeBin in a profile |
| docker in place of nerdctl | It is already in the PATH search, with the genericBoot limitation in Runtime requirements |
| A different containerd shim | BRIG_CONTAINERD_RUNTIME |
| A different hypervisor backend under hull | BRIG_HYPERVISOR, or hypervisor: in a profile |
Any shim that reads the two boot annotations can replace urunc with no change in Brig. The envelope's ISOLATION row names the shim. If Brig cannot place a shim other than urunc's, the row calls the boundary unknown and not a microVM.
A third runtime needs a code change. BRIG_RUNTIME accepts only hull and nerdctl. Make two changes in internal/runtime/runtime.go:
- Implement the
Runtimeinterface. - Add a case to
DetectFor.
The interface lists kind, binary, running, list, run, probe, output, feed, replace, stop, remove, and logs hint. Brig needs nothing else from a runtime.
A replacement for hull must boot an OCI image as a microVM and exec into it. Guest homes, credentials and profiles do not change.
Shared components #
| Ownership | Components |
|---|---|
| Shared with other projects | containerd, nerdctl, urunc, cosign and oras. None of them depends on Brig. |
| Shared between Brig and hull | The boot bundle, the on-disk layout it lands in, and the two annotation names. A machine that has run either runtime has already seeded the other. |
| Built for this stack | hull, vz-runner and hvi. hull is a general microVM runtime and does not depend on Brig. |
| Brig's alone | Profiles, the secret store and its provenance records, the credential forwarding rules, the billing denylist, the guest home contract, image verification policy and brigd |
Hull from source #
This section applies only if you work on hull, and only on macOS.
The released hull is signed, notarized and stapled, so brew install --cask brig needs none of the steps below. A build of Brig from source needs none of them either. Brig holds no entitlement and drives the hull it finds.
A from-source hull cannot boot a microVM unless it is signed with an Apple identity. Two binaries need an entitlement:
| Binary | Talks to | Entitlement it needs |
|---|---|---|
vz-runner |
Virtualization.framework | com.apple.security.virtualization |
hvi |
Hypervisor.framework | com.apple.security.hypervisor |
macOS honours an entitlement only on a binary signed with a real Apple identity: an Apple Development or Developer ID Application certificate.
An ad-hoc signature (codesign --sign -) gets the entitlement honoured only with AMFI disabled. To disable AMFI, you must disable SIP and set a boot argument.
A copy of an entitled binary loses its signature. Re-sign the binary after every build and every copy.
If you give hull's make macos a CODESIGN_IDENTITY, it builds and signs all three binaries. hull's README documents the entitlement plists. The shipped Brig profiles ask for the hvi backend, so a from-source build needs a signed hvi binary as well as a signed vz-runner.