Files
ecr/SPEC.md
T
vhaudiquet 2c47a5c662
CI / Check (push) Successful in 1m7s
CI / Format (push) Successful in 14s
CI / Clippy (push) Successful in 1m8s
CI / Test (push) Successful in 1m24s
docs: update QEMU mode docs for =PATH syntax and uncompressed initramfs
2026-09-20 22:37:33 +02:00

10 KiB

ecr - implementation specification

Synopsis

ecr [OPTIONS] <DISTRO[:VERSION]> -- [COMMAND]...

CLI Interface

Positional Arguments

  • <distro> (required): Distribution name or OCI image reference
  • <version> (optional): Distribution version/codename

Options

Flag Default Description
-a, --arch <arch> host arch Target architecture
--bind <path> cwd Directory to overlay-mount (can be specified multiple times)
--bind-rw <path> none Read-write bind mount at /mnt/<basename> (can be specified multiple times, overrides --bind for same path)
--no-cache false Download fresh tarball, ignore cache
--no-bind false Skip mounting any directory
--kernel[=PATH] none Boot with QEMU system emulation; downloads the default Alpine linux-virt kernel when no =PATH is given
-m, --memory <size> 2G Memory size for QEMU VM (only used with --kernel)
-v, --verbose false Print diagnostic messages
-h, --help - Show help
-V, --version - Show version

File Layout

Cache Directory

~/.cache/ecr/
├── ubuntu-noble-amd64.tar.gz
├── alpine-latest-x86_64.tar.gz
├── debian-bookworm-amd64.tar.gz
└── ...

No metadata files. Tarballs are downloaded once and never redownloaded. Users can delete files manually or use --no-cache to fetch fresh.

Config File

~/.config/ecr.yaml:

dns:
  - 1.1.1.1

Distro Sources

Direct Tarball Downloads

Distro Version Format Source
Ubuntu noble, jammy, mantic or 26.04, 25.10, 22.04, latest, lts cdimage.ubuntu.com
Alpine 3.20, 3.19, latest, edge dl-cdn.alpinelinux.org

Docker Hub (OCI Registry)

All other distributions use Docker Hub images via OCI registry API:

Distro Image Reference
Debian library/debian
Arch library/archlinux
Fedora library/fedora
Gentoo gentoo/stage3
Custom <image>[:tag] or <registry>/<image>[:tag]

Custom Image References

Users can specify any OCI-compatible image:

ecr debian:bookworm -- ./build.sh
ecr gentoo/stage3 -- emerge --sync
ecr gcr.io/my-project/my-image:v1.0 -- /app/test

Architecture Mapping

ecr Ubuntu Alpine Docker Hub
amd64 amd64 x86_64 amd64
arm64 arm64 aarch64 arm64
armhf armhf armv7 arm/v7
riscv64 riscv64 riscv64 riscv64
ppc64el ppc64el ppc64le ppc64le
s390x s390x s390x s390x

OCI Image Download

For Docker Hub images:

  1. Get anonymous bearer token from https://auth.docker.io/token
  2. Query manifest list: GET https://registry.hub.docker.com/v2/<repo>/manifests/<tag>
  3. Select manifest matching target architecture
  4. Download layer blobs with authentication
  5. Extract layers to rootfs

If architecture is not available in manifest list, error with available architectures:

Error: No manifest found for architecture 'riscv64'. Available: amd64, arm64, ppc64le, s390x

Execution Flow

  1. Parse CLI arguments
  2. Resolve distro/version/arch to image source
  3. Check cache for existing tarball
  4. If not cached, download tarball (direct or OCI)
  5. Create temp directory for extraction
  6. Extract tarball to temp directory
  7. Create namespaces: user, pid, mount, uts
  8. Set up mounts: /proc, /sys (ro), /dev, /dev/pts
  9. Write /etc/resolv.conf with DNS servers
  10. Set up overlay mounts for bind paths
  11. Set up read-write bind mounts
  12. Set environment variables
  13. Exec shell or command in chroot
  14. On exit, clean up temp directory

Namespace Setup

Namespaces (Always Created)

  • user: Map current user to root (UID 0) inside
  • pid: Isolated process tree
  • mount: Private mounts for chroot setup
  • uts: Hostname set to ecr-<distro>-<random>

Network

Host network namespace (no isolation).

User Namespace Mapping

uid_map: 0 <current_uid> 1
gid_map: 0 <current_gid> 1

This makes the user appear as root inside the chroot while remaining unprivileged on the host.

Mounts Inside Chroot

Path Type Options
/proc proc defaults
/sys sysfs ro,nosuid,nodev,noexec
/dev devtmpfs nosuid
/dev/pts devpts nosuid,noexec
/root/ overlay lowerdir=<bind_path>, upperdir=, workdir=
/mnt/ bind rw (for --bind-rw)
/etc/resolv.conf file written with DNS

QEMU Integration

Foreign Architecture Detection

If --arch differs from host architecture, QEMU is required.

binfmt_misc Check

Before entering chroot, verify binfmt_misc is registered for target architecture by checking /proc/sys/fs/binfmt_misc/qemu-<arch>.

If not registered, error with message:

Error: binfmt_misc not registered for riscv64

Install QEMU user emulation:
  Ubuntu/Debian: sudo apt install qemu-user-static
  Arch: sudo pacman -S qemu-user-static-binfmt
  Alpine: sudo apk add qemu-user-static

QEMU Binary

No action required. Modern qemu-user-static packages register binfmt_misc with the F (fix binary) flag, loading the interpreter into kernel memory. The kernel handles foreign binary execution transparently.

QEMU System Emulation Mode

When --kernel is specified, ecr switches from namespace/chroot mode to QEMU system emulation. The extracted rootfs is converted to an uncompressed CPIO initramfs and booted with the provided kernel.

Usage

The kernel path uses = syntax (--kernel=PATH); --kernel without a value downloads the default kernel. Without =, a following path would be parsed as the DISTRO argument.

ecr --kernel ubuntu:noble
ecr --kernel=/boot/vmlinuz ubuntu:noble
ecr --kernel=/boot/vmlinuz --memory 4G alpine
ecr --kernel=/boot/vmlinuz debian -- /bin/sh -c "echo hello"

Execution Flow

  1. Download/cache rootfs tarball (same as namespace mode)
  2. Extract tarball to temporary directory
  3. Create uncompressed CPIO initramfs from rootfs (streamed to disk), including essential device nodes (/dev/ttyS0, /dev/null, /dev/tty) and an /init script that mounts proc/sys/dev, sets the hostname, execs the requested command argv verbatim (each argv element base64-encoded in the cmdline as ECR_ARGV), and powers off on exit
  4. Launch QEMU with:
    • -kernel <path> - provided (or downloaded) kernel
    • -initrd initramfs.cpio - rootfs as initramfs
    • -append "console=ttyS0 [quiet] ECR_SHELL=... [ECR_ARGV=...] ECR_HOSTNAME=..." - kernel command line (quiet unless -v)
    • -m <memory> - memory size (default 2G)
    • -display none -serial mon:stdio - console on stdio
    • -netdev user,id=net0 -device virtio-net-pci,netdev=net0 - network NIC
    • -enable-kvm -cpu host - when the host supports KVM and the target matches the host architecture
  5. Wait for QEMU to exit (init powers the VM off when the command/shell exits; -no-reboot makes QEMU terminate)
  6. Cleanup temporary files

Initramfs Creation

The rootfs directory is converted to an uncompressed CPIO archive (newc format) using the cpio crate, streamed entry by entry so large rootfs images never need to fit in memory. Hard links are preserved: the first occurrence of a (device, inode) pair carries the data with a synthetic inode, subsequent occurrences are zero-size entries sharing that inode, which the kernel's initramfs loader turns into real hard links.

Architecture Support

ecr Arch QEMU System Binary
amd64/x86_64 qemu-system-x86_64
arm64/aarch64 qemu-system-aarch64
armhf/armv7 qemu-system-arm
riscv64 qemu-system-riscv64
ppc64el qemu-system-ppc64
s390x qemu-system-s390x

Requirements

  • QEMU system emulator installed (qemu-system-<arch>)
  • Kernel with required drivers (serial console, virtio-net for network)

Differences from Namespace Mode

Feature Namespace Mode QEMU Mode
Isolation User namespace Full VM
Performance Near-native Emulated (KVM-accelerated when available)
Root access No Yes (inside the VM)
Foreign arch binfmt_misc required Built-in emulation
Bind mounts Overlay/bind Not supported (flags are ignored with a warning)
Network Host network User-mode NIC (not configured inside the guest)

File Handling

Overlay Mount (Default)

By default, the current working directory is mounted as an overlay filesystem at /root/<basename> inside the chroot, where <basename> is the name of the current directory.

Overlay configuration:

  • lowerdir: the source directory (read-only)
  • upperdir: temp directory for modifications
  • workdir: temp directory required by overlayfs

Changes made inside the chroot are written to upperdir and discarded on exit. The host directory is never modified.

Multiple --bind paths can be specified, each creates an overlay at /root/<basename>.

Example:

$ cd ~/projects/myapp
$ ecr ubuntu:noble -- make build
# ~/projects/myapp mounted at /root/myapp
# Build artifacts written to overlay, discarded on exit

Read-Write Bind Mount

--bind-rw <path> creates a true read-write bind mount at /mnt/<basename>. This modifies the host filesystem directly. Use with caution.

Multiple --bind-rw paths can be specified. If a path is specified in both --bind and --bind-rw, the read-write mount takes precedence.

If no path is specified, defaults to current working directory.

No Mount

--no-bind skips mounting any directory.

DNS

Default DNS server is 1.1.1.1. Configured via /etc/resolv.conf in chroot:

nameserver 1.1.1.1

Override with config file (~/.config/ecr.yaml):

dns:
  - 8.8.8.8
  - 8.8.4.4

Environment Variables

Default environment inside chroot:

  • HOME=/root
  • USER=root
  • SHELL=/bin/bash (or /bin/sh if bash unavailable)
  • TERM=
  • PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Host environment is not inherited.

Signal Handling

Forward SIGINT, SIGTERM, SIGHUP, SIGQUIT to child process. Wait for child to exit before cleanup.

Security Requirements

User Namespace Required

ecr requires unprivileged user namespaces. If unavailable (sysctl kernel.unprivileged_userns_clone=0 or AppArmor restrictions), error with:

Error: User namespaces not available

Enable with:
  sysctl -w kernel.unprivileged_userns_clone=1

Or check AppArmor profile restrictions.