commit 5cb0428876ce82bab6ee4c46e27096e2bfddebe0 Author: Valentin Haudiquet Date: Mon Jun 8 18:40:31 2026 +0200 initial commit: SPEC.md, implementation spec diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..a561fc5 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,252 @@ +# ecr - implementation specification + +## Synopsis + +``` +ecr [:] [-a ] [options] [-- command [args...]] +``` + +## CLI Interface + +### Positional Arguments + +- `` (required): Distribution name +- `` (optional): Distribution version/codename + +### Options + +| Flag | Default | Description | +|------|---------|-------------| +| `-a, --arch ` | host arch | Target architecture | +| `--bind [path]` | cwd | Directory to overlay-mount | +| `--bind-rw [path]` | none | Read-write bind mount at `/root/` (bypasses overlay) | +| `--no-cache` | false | Download fresh tarball, ignore cache | +| `-h, --help` | - | Show help | +| `-V, --version` | - | Show version | + +## File Layout + +### Cache Directory + +``` +~/.cache/ecr/ +├── ubuntu-noble-amd64.tar.gz +├── debian-bookworm-arm64.tar.gz +├── arch-latest-riscv64.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`: + +```yaml +dns: + - 1.1.1.1 +cache_dir: ~/.cache/ecr +``` + +## Distro Sources + +| Distro | Version Format | URL Pattern | +|--------|----------------|-------------| +| Ubuntu | noble, jammy, mantic or 26.04, 25.10, 22.04, latest, lts | https://cdimage.ubuntu.com/ubuntu-base/bionic/daily/current/bionic-base-amd64.tar.gz | +| Debian | bookworm, bullseye, sid, latest | https://cloud.debian.org/images/cloud/trixie/latest/debian-13-nocloud-amd64.tar.xz | +| Arch | latest | https://geo.mirror.pkgbuild.com/iso/latest/archlinux-bootstrap-x86_64.tar.zst | +| Alpine | 3.20, 3.19, latest, edge | https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-3.23.0-x86_64.tar.gz | +| Fedora | 40, 41, rawhide, latest | Extract from `registry.fedoraproject.org/fedora:` | + +### Architecture Mapping + +| ecr | Ubuntu | Debian | Arch | Alpine | Fedora | +|-----|--------|--------|------|--------|--------| +| amd64 | amd64 | amd64 | x86_64 | x86_64 | x86_64 | +| arm64 | arm64 | arm64 | aarch64 | aarch64 | aarch64 | +| armhf | armhf | armhf | - | armv7 | - | +| riscv64 | riscv64 | riscv64 | riscv64 | riscv64 | riscv64 | +| ppc64el | ppc64el | ppc64el | - | ppc64le | ppc64le | +| s390x | s390x | s390x | - | s390x | s390x | + +### Fedora Container Extraction + +For Fedora, download the container image layer and extract: + +1. Query manifest: `GET https://registry.fedoraproject.org/v2/fedora/manifests/` +2. Parse manifest, get layer digest +3. Download layer blob +4. Extract tarball + +## Execution Flow + +1. Parse CLI arguments and config file +2. Resolve distro/version/arch to tarball URL +3. Check cache for existing tarball +4. If not cached, download tarball +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 mount for workspace directory +11. Set environment variables +12. Exec shell or command in chroot +13. 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--` + +### Network + +Host network namespace (no isolation). + +### User Namespace Mapping + +``` +uid_map: 0 1 +gid_map: 0 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=, upperdir=, workdir= | +| /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-`. + +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. + +## File Handling + +### Overlay Mount (Default) + +By default, the current working directory is mounted as an overlay filesystem at `/root/` inside the chroot, where `` 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. + +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 ` bypasses overlay and creates a true read-write bind mount at `/mnt/`. This modifies the host filesystem directly. Use with caution. + +### 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 `--dns` flag or config file: + +```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. +``` + +### No Root Fallback + +There is no fallback to running as real root. The tool is designed for unprivileged use. + +## Implementation + +### Language + +Rust. + +### Dependencies + +- `nix`: Unix syscall bindings (clone, unshare, mount, chroot, namespaces) +- `serde` + `serde_yaml`: config parsing +- `reqwest`: HTTP downloads +- `tar`: tarball extraction +- `xz2`: xz decompression +- `zstd`: zstd decompression +- `flate2`: gzip decompression +- `clap`: CLI parsing +- `signal-hook`: signal handling +- `tempfile`: temp directories \ No newline at end of file