One-command development sandboxes on Firecracker microVMs. https://git.thaloco.com/thaloco/banger/
Find a file
Thales Maciel 43982a4ae3
Phase B-1: ownership fixup via debugfs pass
imagepull.Flatten now captures per-file uid/gid/mode/type from the
tar headers as it walks layers, returning a Metadata map alongside
the extracted tree. Whiteouts correctly drop the victim's metadata.
The returned Metadata feeds the new imagepull.ApplyOwnership, which
pipes a batched `set_inode_field` script to `debugfs -w -f -`.

Why: mkfs.ext4 -d copies the runner's on-disk uids verbatim, so
without this pass setuid binaries become setuid-nonroot and sshd
refuses to start on the resulting image. With the pass, a pulled
debian:bookworm has /usr/bin/sudo with uid=0 + setuid bit surviving
intact.

imagepull.BuildExt4 signature unchanged; ownership is applied as a
separate step by the daemon orchestrator between BuildExt4 and
StageBootArtifacts, keeping each helper focused. The seam
(d.pullAndFlatten) now returns (Metadata, error) for test stubs to
feed synthetic metadata.

StdinRunner is a new duck-typed extension next to CommandRunner;
the real system.Runner implements RunStdin, test mocks don't need
to unless they exercise stdin. Prevents every existing mock from
growing a new method.

Tests:
 - TestFlattenCapturesHeaderMetadata: setuid bit + mode survive the
   tar-header walk
 - TestApplyOwnershipRewritesUidGidMode: real debugfs round-trip —
   create ext4 with runner's uid, apply synthetic metadata setting
   uid=0 + setuid mode, verify via `debugfs -R stat` that the
   inode now has uid=0 and mode 04755
 - TestBuildOwnershipScriptDeterministic: sorted, well-formed
   sif script output

Debugfs and mkfs.ext4 tests skip if the binaries aren't on PATH.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 18:04:22 -03:00
cmd Stamp shared build metadata into banger binaries 2026-03-22 17:14:06 -03:00
docs Phase 4: OCI import docs 2026-04-16 17:37:07 -03:00
examples Rename experimental Void image to void 2026-04-01 20:15:28 -03:00
internal Phase B-1: ownership fixup via debugfs pass 2026-04-16 18:04:22 -03:00
scripts Phase 5: kernel catalog publish flow + docs 2026-04-16 15:56:56 -03:00
.gitignore Phase 5: kernel catalog publish flow + docs 2026-04-16 15:56:56 -03:00
AGENTS.md Phase 4: OCI import docs 2026-04-16 17:37:07 -03:00
go.mod Phase 1: imagepull package — pull, flatten, ext4 2026-04-16 17:22:13 -03:00
go.sum Phase 1: imagepull package — pull, flatten, ext4 2026-04-16 17:22:13 -03:00
LICENSE Add LICENSE, update .gitignore, add security note to README 2026-04-14 16:54:33 -03:00
Makefile Add lint targets, fix gofmt drift, broaden Makefile build inputs 2026-04-16 16:49:17 -03:00
README.md Phase 4: OCI import docs 2026-04-16 17:37:07 -03:00

banger

banger manages Firecracker development VMs with a local daemon, managed image artifacts, and an experimental localhost web UI.

Requirements

  • Linux with /dev/kvm
  • sudo
  • Firecracker installed on PATH, or firecracker_bin set in config
  • The usual host tools checked by ./build/bin/banger doctor

banger now owns complete managed image sets. A managed image includes:

  • rootfs
  • optional work-seed
  • kernel
  • optional initrd
  • optional modules

There is no runtime bundle anymore.

Build

make build

This writes:

  • ./build/bin/banger
  • ./build/bin/bangerd
  • ./build/bin/banger-vsock-agent

Install

make install

That installs:

  • banger
  • bangerd
  • the banger-vsock-agent companion helper under ../lib/banger/

Config

Config lives at ~/.config/banger/config.toml.

Supported keys:

  • log_level
  • web_listen_addr
  • firecracker_bin
  • ssh_key_path
  • default_image_name
  • auto_stop_stale_after
  • stats_poll_interval
  • metrics_poll_interval
  • bridge_name
  • bridge_ip
  • cidr
  • tap_pool_size
  • default_dns

If ssh_key_path is unset, banger creates and uses:

  • ~/.config/banger/ssh/id_ed25519

default_image_name now only means “use this registered image when vm create omits --image”. The daemon does not auto-register images from host paths.

Core Workflow

Check the host:

./build/bin/banger doctor

Register an existing host-side image stack:

./build/bin/banger image register \
  --name base \
  --rootfs /abs/path/rootfs.ext4 \
  --kernel /abs/path/vmlinux \
  --initrd /abs/path/initrd.img \
  --modules /abs/path/modules

Or pull a pre-built kernel from the catalog and reference it by name:

./build/bin/banger kernel list --available
./build/bin/banger kernel pull void-6.12
./build/bin/banger image register \
  --name base \
  --rootfs /abs/path/rootfs.ext4 \
  --kernel-ref void-6.12

See docs/kernel-catalog.md for catalog maintenance.

Or pull a rootfs directly from any OCI registry (Docker Hub, GHCR, …):

./build/bin/banger image pull docker.io/library/debian:bookworm \
  --kernel-ref void-6.12

image pull downloads the image, flattens its layers into an ext4 rootfs, and registers it as a managed banger image. Experimental — see docs/oci-import.md for current limitations (notably: file-ownership caveat means pulled images are a base for image build, not yet directly bootable).

Build a managed image from an existing registered image:

./build/bin/banger image build \
  --name devbox \
  --from-image base \
  --docker

Promote an unmanaged image into daemon-owned managed artifacts:

./build/bin/banger image promote base

Create and use a VM:

./build/bin/banger vm create --image devbox --name testbox
./build/bin/banger vm ssh testbox
./build/bin/banger vm stop testbox

vm create stays synchronous by default, but on a TTY it now shows live progress until the VM is fully ready.

Start a repo-backed VM session:

./build/bin/banger vm run
./build/bin/banger vm run ../some-repo --branch feature/alpine --from HEAD

vm run resolves the enclosing git repository, creates a VM, copies a git checkout plus current tracked and untracked non-ignored files into /root/repo, starts a best-effort guest tooling bootstrap that only uses mise, prints next-step commands, and exits. It does not auto-attach opencode anymore. The bootstrap runs asynchronously and logs its output inside the guest.

After vm run, use one of:

./build/bin/banger vm ssh <vm-name>
opencode attach http://<vm-name>.vm:4096 --dir /root/repo
./build/bin/banger vm acp <vm-name>
./build/bin/banger vm ssh <vm-name> -- "cd /root/repo && claude"
./build/bin/banger vm ssh <vm-name> -- "cd /root/repo && pi"

For ACP-aware host tools, ./build/bin/banger vm acp <vm-name> bridges stdio to guest opencode acp over SSH. It uses /root/repo when that checkout exists, otherwise /root, and --cwd lets you override the guest working directory explicitly.

If you want reusable orchestration primitives instead of the vm run convenience flow, use the daemon-backed workspace and session commands directly:

./build/bin/banger vm workspace prepare <vm-name>
./build/bin/banger vm workspace prepare <vm-name> ../other-repo --guest-path /root/repo --readonly
./build/bin/banger vm session start <vm-name> --name planner --cwd /root/repo --stdin-mode pipe -- pi --mode rpc --no-session
./build/bin/banger vm session list <vm-name>
./build/bin/banger vm session attach <vm-name> planner
./build/bin/banger vm session logs <vm-name> planner --stream stderr
./build/bin/banger vm session stop <vm-name> planner

vm workspace prepare materializes a local git checkout into a running VM. The default guest path is /root/repo and the default mode is a shallow metadata copy plus tracked and untracked non-ignored overlay. Repositories with git submodules must use --mode full_copy; the metadata-based modes still reject them.

vm session start creates a daemon-managed long-lived guest command. The daemon preflights that the requested guest cwd exists and that the main command, plus any repeated --require-command entries, exist in guest PATH before launch. Use --stdin-mode pipe when you need live attach; otherwise use the default detached mode and inspect sessions with list, show, logs, stop, and kill.

vm session attach is currently exclusive and same-host only. The daemon exposes a local Unix socket bridge using stdio_mux_v1, so only one active attach is allowed at a time. Pipe-mode sessions keep enough guest-side state for the daemon to rebuild that bridge after a daemon restart.

Web UI (experimental)

bangerd serves an experimental local web UI by default at:

  • http://127.0.0.1:7777

The UI is convenient for local observability but is not a stable or supported interface. Its endpoints, layout, and behaviour may change without notice, and it has not been hardened for anything beyond single-user localhost use. Do not expose the listen address to a shared network.

See the effective URL with:

./build/bin/banger daemon status

Disable it with:

web_listen_addr = ""

Guest Services

Provisioned glibc-backed images include:

  • banger-vsock-agent
  • guest networking bootstrap
  • mise
  • opencode
  • claude
  • pi
  • a default guest opencode service on 0.0.0.0:4096

Alpine currently remains opencode-only.

If these host auth files exist, banger syncs them into the guest on VM start:

  • ~/.local/share/opencode/auth.json -> /root/.local/share/opencode/auth.json
  • ~/.claude/.credentials.json -> /root/.claude/.credentials.json
  • ~/.pi/agent/auth.json -> /root/.pi/agent/auth.json

Changes on the host take effect after the VM is restarted. Session/history directories are not copied.

From the host:

./build/bin/banger vm ports testbox
opencode attach http://<guest-ip>:4096

Manual Helpers

The shell helpers are now explicit manual workflows under ./build/manual.

Rebuild a Debian-style manual rootfs:

make rootfs ARGS='--base-rootfs /abs/path/rootfs.ext4 --kernel /abs/path/vmlinux --initrd /abs/path/initrd.img --modules /abs/path/modules'

The output lands in:

  • ./build/manual/rootfs-docker.ext4
  • ./build/manual/rootfs-docker.work-seed.ext4

Experimental Void Flow

Stage a Void kernel:

make void-kernel

Build the experimental Void rootfs:

make rootfs-void

Register it:

make void-register

That flow uses:

  • ./build/manual/void-kernel/
  • ./build/manual/rootfs-void.ext4
  • ./build/manual/rootfs-void.work-seed.ext4

Experimental Alpine Flow

Stage an Alpine virt kernel:

make alpine-kernel

Build the experimental Alpine rootfs:

make rootfs-alpine

Register it:

make alpine-register

Create a VM from it:

./build/bin/banger vm create --image alpine --name alpine-dev

That flow uses:

  • ./build/manual/alpine-kernel/
  • ./build/manual/rootfs-alpine.ext4
  • ./build/manual/rootfs-alpine.work-seed.ext4

The experimental Alpine flow stages a pinned Alpine release by default. Override that pin with ALPINE_RELEASE=... when running the make alpine-kernel and make rootfs-alpine helpers if you need a different patch release.

Alpine support currently applies to the explicit register-and-run flow above. The generic banger image build --from-image ... path remains Debian/systemd- oriented and should not be treated as an Alpine image builder.

Security

Guest VMs are single-user development sandboxes, not multi-tenant servers. Every provisioned image is configured with:

PermitRootLogin yes
StrictModes no

This is intentional. The host SSH key is the only authentication mechanism, no password auth is enabled, and VMs are reachable only through the host bridge network (172.16.0.0/24 by default). Do not expose the bridge interface or the VM guest IPs to an untrusted network.

Notes

  • Firecracker is resolved from PATH by default.
  • Managed image delete removes the daemon-owned artifact dir.
  • The companion vsock helper is internal to the install/build layout, not a user-configured runtime path.