Go library + runner binary for running OCI container images as microVMs via libkrun.
Two-process model: pure-Go library spawns a CGO runner subprocess. Module: github.com/stacklok/go-microvm.
task build-dev # Build runner (requires libkrun-devel, Linux)
task build-dev-darwin # Build runner (macOS, requires Homebrew libkrun, signs entitlements)
task build-runner # Build runner + libs using builder container (no system libkrun needed)
task fetch-runtime # Download pre-built runtime from GitHub Release
task fetch-firmware # Download pre-built firmware from GitHub Release
task builder-image-build # Build the builder container image locally
task test # go test -v -race ./...
task test-nocgo # go test excluding CGO packages (used by CI)
task build-nocgo # Verify compilation of pure Go packages
task lint # golangci-lint run ./...
task lint-fix # Auto-fix lint issues
task fmt # go fmt + goimports
task verify # fmt + lint + test (CI pipeline)
task tidy # go mod tidy
task package-runtime # Package runtime tarball for release
task package-firmware # Package firmware tarball for release
task clean # Remove bin/, dist/, and coverage filesRun a single test: go test -v -race -run TestName ./path/to/package
macOS dev setup: brew tap slp/krun && brew install libkrun libkrunfw (see docs/MACOS.md for details)
Entry point: microvm.go:Run() orchestrates the full pipeline (preflight, pull, hooks, config, net, spawn, post-boot). Config via functional options in options.go. Returns a *VM handle (vm.go).
CGO boundary: Only krun/ and runner/cmd/go-microvm-runner/ use CGO. Everything else is pure Go. The runner binary is sacrificial -- krun_start_enter() never returns, so it runs in a detached subprocess.
Key subsystems: hypervisor/ (Backend abstraction + libkrun impl), image/ (OCI pull + cache), runner/ (subprocess spawning), net/ (Provider interface + firewall + hosted mode + egress policy + topology constants), guest/ (guest-side boot orchestration, hardening, SSH server), hooks/ (RootFS hook factories for key injection, file injection), extract/ (binary bundle caching), preflight/ (platform checks via build tags), ssh/ (keygen + client), state/ (flock-based JSON persistence), internal/ (pathutil, procutil).
- CGO boundary is strict: Only
krun/andrunner/cmd/go-microvm-runner/use CGO. Every other package MUST stayCGO_ENABLED=0. Never importkrunfrom a non-CGO package. - Runner config is duplicated:
runner.Configinrunner/config.goand a duplicateConfigstruct inrunner/cmd/go-microvm-runner/main.go. When adding a field, update BOTH structs with the same JSON tag, then handle it inrunVM(). krun_start_enter()never returns: It callsexit()when the guest shuts down. That's why we need the two-process model -- the runner process is sacrificial.- Platform build tags: Preflight checks, resource checks, and some net code use
//go:build linuxor//go:build darwin. Each platform goes in a separate file. macOS preflight checks verifykern.hv_supportsysctl and usehw.memsize/syscall.Statfsfor resources. - Entitlements required on macOS:
assets/entitlements.plisthas three entitlements:com.apple.security.hypervisor,com.apple.security.cs.disable-library-validation, andcom.apple.security.cs.allow-dyld-environment-variables(needed because the hypervisor entitlement activates hardened runtime, which strips DYLD_* vars). Thetask build-dev-darwincommand signs automatically. - CGO Homebrew paths:
krun/context.goCGO directives include-L/opt/homebrew/liband-L/usr/local/libfor macOS. The linker ignores nonexistent paths. - Tests excluding CGO packages: When CGO isn't available, exclude krun:
CGO_ENABLED=0 go test $(go list ./... | grep -v krun | grep -v go-microvm-runner) - Functional options pattern: All public config uses
With*constructors applying to unexportedconfigstruct viaoptionFunc. Follow the existing pattern inoptions.goexactly. - Backend abstraction:
WithRunnerPath,WithLibDir, andWithSpawnerare NOT on the top-levelmicrovmpackage. They live inhypervisor/libkrunas backend-specific options. Usemicrovm.WithBackend(libkrun.NewBackend(libkrun.WithRunnerPath(...))). Similarly,VM.PID()is gone; useVM.ID()(returns string).
- SPDX headers required: Every
.goand.yamlfile needs// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.and// SPDX-License-Identifier: Apache-2.0(use#for YAML). - Use
log/slogexclusively -- nofmt.Printlnorlog.Printfin library code. - Wrap errors with
fmt.Errorf("context: %w", err)forming readable chains. - Prefer table-driven tests. Test files go alongside the code they test.
- When adding a new package, include a
doc.gowith SPDX header and ensure it doesn't importkrun.
- Imperative mood, capitalize, no trailing period, limit subject to 50 chars
- IMPORTANT: Never use
git add -A. Stage specific files only.
After any code change:
task fmt && task lint # Format and lint
task test # Full test suite with race detectorAfter modifying CGO-free packages only:
CGO_ENABLED=0 go vet $(go list ./... | grep -v krun | grep -v go-microvm-runner)When tests fail, fix the implementation, not the tests.
Read these on demand when working on related subsystems:
docs/ARCHITECTURE.md-- Deep technical architecture, two-process model, extension pointsdocs/SECURITY.md-- Trust boundaries, guest escape analysis, hardeningdocs/NETWORKING.md-- Networking modes (runner-side, hosted), firewall, wire protocoldocs/MACOS.md-- macOS support, code signing, Hypervisor.frameworkdocs/TROUBLESHOOTING.md-- Common issues, log files, resource limits