Sin musl, PID 1 panic con "error while loading shared libraries: libgcc_s.so.1" porque el initramfs no incluye libgcc/glibc/ld-linux. Solución estándar: target x86_64-unknown-linux-musl produce un ELF totalmente estático. Cambios en scripts/build-arje-initrd.sh: - ARJE_TARGET=x86_64-unknown-linux-musl por default (override con env). - Chequeo del target instalado antes de buildear; mensaje accionable con los comandos exactos (rustup target add..., apt install musl-tools, etc.) si falta. - Sanity check con `file`: aborta si ente-zero quedó dinámico. - Sanity check para busybox: aborta si el BUSYBOX_BIN apunta a un binario dinámico (la otra causa #1 de panic). - BIN_DIR ahora apunta a target/$TARGET/release/. Docs (docs/arje-boot.md): - §2a explica el porqué de musl. - §2b lista requisitos del host (rustup target, musl-tools, busybox-static). - §7 sección nueva de troubleshooting con el síntoma exacto del libgcc_s panic + 3 escenarios comunes más. - Checklist pre-deploy actualizado con el chequeo de "statically linked". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
14 KiB
Booteando arje — initramfs, QEMU y bare metal
arje es el init absorbido por brahman: ente-zero corre como PID 1, lee
la Tarjeta Semilla (/ente/seed.card.json), monta /proc /sys /dev /sys/fs/cgroup, y encarna recursivamente cada Card declarada en
genesis vía ente-incarnate::Incarnator (clone(2) + namespaces + cgroup v2).
Este documento describe el ciclo completo:
- Layout del initramfs.
- Build del initrd (
scripts/build-arje-initrd.sh). - Boot en QEMU (
scripts/run-arje-qemu.sh). - Boot en bare metal / VM persistente (entrada de GRUB).
- Tarjeta Semilla — qué pone, qué valida, cómo customizar.
- Debugging: tracing, sockets de introspección, snapshot/restore.
1. Layout del initramfs
El initrd es un CPIO + gzip (formato newc, estándar Linux). Su raíz:
/init wrapper sh, ejecuta /sbin/ente-zero
/sbin/ente-zero PID 1 (musl-static por default)
/usr/sbin/ente-echo
/usr/sbin/ente-policy-provider
/usr/sbin/ente-*-compat 13 shims D-Bus (logind, hostnamed, …)
/ente/seed.card.json Tarjeta Semilla canónica
/bin/{sh,ls,mount,…} busybox-static
/etc, /dev, /proc, /sys, /run mountpoints vacíos
/sys/fs/cgroup mountpoint cgroup v2
Path canónico de la semilla en prod: /ente/seed.card.json (ver
crates/core/ente-zero/src/seed.rs). En dev (no PID 1):
./seed.card.json en el cwd, o se sintetiza una mínima.
2. Build del initrd
2a. Por qué musl-static
ente-zero corre como PID 1: si tiene cualquier dependencia dinámica
(libgcc_s.so.1, libc.so.6, ld-linux-x86-64.so.2) que no está
presente en el initramfs, el kernel paniquea con
error while loading shared libraries: libgcc_s.so.1.
Solución estándar: compilar contra el target x86_64-unknown-linux-musl,
que produce un ELF totalmente estático (no necesita libc ni
intérprete en runtime). Es el default del script.
2b. Requisitos del host
rust toolchaincon el target musl:rustup target add x86_64-unknown-linux-muslmusl-gccpara crates conbuild.rsC (sled, blake3, etc.):apt install musl-tools # Debian/Ubuntu apk add musl-dev gcc # Alpine pacman -S musl # Archcpio,gzippara empaquetar.busybox-staticpara el userspace mínimo dentro del initramfs:apt install busybox-static # Debian/Ubuntu # Alpine ya trae busybox built-in
2c. Invocación
# Default: musl-static, semilla de prod, sale en out/arje.initrd.cpio.gz
scripts/build-arje-initrd.sh
# Customizar seed/salida:
scripts/build-arje-initrd.sh seeds/arje-minimal.card.json out/min.cpio.gz
# busybox-static no está en $PATH:
BUSYBOX_BIN=/path/to/busybox scripts/build-arje-initrd.sh
# Vendorear binarios extra estáticos:
EXTRA_BINS="/usr/local/bin/strace-static" scripts/build-arje-initrd.sh
# (NO RECOMENDADO) build glibc dinámico — kernel panic asegurado salvo que
# vendorees libgcc_s + libc + ld-linux a mano:
ARJE_TARGET=x86_64-unknown-linux-gnu scripts/build-arje-initrd.sh
2d. Qué hace el script
- Verifica que el target musl esté instalado; si no, da el comando exacto para instalarlo.
cargo build --release --target x86_64-unknown-linux-muslparaente-zero+ los 14 shims compat.- Sanity-check con
file: aborta si algún binario quedó dinámico. - Valida la seed con
seeds/validate.sh(parse +Card::validate). - Verifica que
busyboxsea estático (también aborta si es dinámico). - Stage en un tmpdir → copia binarios + crea mountpoints + escribe
/init. - Empaqueta
find . | cpio -o -H newc | gzip -9.
Tamaño típico (musl): ~10-15 MB descomprimido, 4-6 MB el .cpio.gz.
3. Boot en QEMU
Requisito: un kernel Linux + qemu-system-x86_64.
3a. Con kernel del host
# Default: usa /boot/vmlinuz-$(uname -r)
scripts/run-arje-qemu.sh
# Con kernel explícito:
scripts/run-arje-qemu.sh out/arje.initrd.cpio.gz /boot/vmlinuz-6.6.0
El script invoca:
qemu-system-x86_64
-accel kvm (si /dev/kvm está, fallback tcg)
-m 1024 -smp 2
-kernel <vmlinuz>
-initrd out/arje.initrd.cpio.gz
-append "rdinit=/init console=ttyS0,115200 panic=10"
-nographic -serial mon:stdio
-no-reboot
rdinit=/init le dice al kernel que el primer programa a ejecutar es
nuestro /init, no /sbin/init. console=ttyS0,115200 redirige el log
del kernel + nuestro tracing a la serial → stdio del host. Salida con
Ctrl-A X.
3b. Sin kernel del host
Bajar un kernel mínimo (ej. Ubuntu cloud kernel):
wget -O /tmp/vmlinuz \
https://cloud-images.ubuntu.com/jammy/current/unpacked/jammy-server-cloudimg-amd64-vmlinuz-generic
scripts/run-arje-qemu.sh out/arje.initrd.cpio.gz /tmp/vmlinuz
O usar el kernel de Alpine (más liviano, ~10 MB):
wget -O /tmp/vmlinuz \
https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/netboot/vmlinuz-lts
3c. Override del cmdline
KERNEL_CMD="ente.log=debug" se concatena al cmdline. Cosas útiles:
| flag | efecto |
|---|---|
panic=10 |
reboot 10 s tras kernel panic (debug) |
loglevel=7 |
log del kernel hasta debug |
quiet |
silencia banner kernel |
RUST_LOG=trace |
(no se interpreta; usar env en /init) |
init=/sbin/ente-zero |
salta /init, ejecuta directo (no recomendado) |
ente-zero lee RUST_LOG y BRAHMAN_* del env. Para setearlas, editar
/init antes de empaquetar, o agregar -fw_cfg name=opt/foo,... a qemu.
3d. Smoke test esperado
Con seeds/arje-prod.card.json:
ente-zero despierta como PID 1
Tarjeta Semilla cargada y validada path=/ente/seed.card.json
bus interno escuchando path=/run/ente-bus.sock
brahman handshake escuchando (Unix) socket=/run/brahman-init.sock
brahman admin escuchando socket=/run/brahman-admin.sock
instanciando genesis seed=arje.seed.prod count=16
Ente compat-logind encarnado pid=...
Ente compat-hostnamed encarnado pid=...
...
arje# ← shell en tty
Con seeds/arje-minimal.card.json: solo el shell.
4. Boot en bare metal / VM persistente
El mismo .cpio.gz sirve como initramfs estándar de Linux. Entrada
GRUB típica (/etc/grub.d/40_custom + update-grub):
menuentry "arje" {
linux /boot/vmlinuz-6.6.0 rdinit=/init console=tty1 panic=10
initrd /boot/arje.initrd.cpio.gz
}
Copiar a /boot/:
sudo cp out/arje.initrd.cpio.gz /boot/arje.initrd.cpio.gz
sudo update-grub
Para una VM (Proxmox, libvirt, etc.) basta con apuntar el "Direct Kernel
Boot" al vmlinuz y al initrd. En Proxmox: editar /etc/pve/qemu-server/<vmid>.conf:
args: -kernel /boot/vmlinuz-6.6.0 -initrd /boot/arje.initrd.cpio.gz \
-append "rdinit=/init console=ttyS0,115200 panic=10"
serial0: socket
4a. Sin pivot_root (initramfs es el rootfs final)
El initrd actual no hace pivot_root — se queda como rootfs. Esto es
intencional: arje no asume nada del disco. Para persistencia, las Cards
hijas deben montar el FS de disco a demanda (ej. genesis: [{ label: "mount-data", payload: Native("/bin/mount", ["/dev/sda1", "/mnt"]), … }]).
Cuando necesites pivot_root a un FS real (instalación full-disk), agregar
un Ente que haga switch_root antes de instanciar el resto — pendiente
de implementar como Capability::SwitchRoot.
5. Tarjeta Semilla — detalles
5a. Seeds estándar
| seed | uso |
|---|---|
seeds/arje-minimal.card.json |
PID 1 + 1 shell /bin/sh. Smoke test para QEMU. |
seeds/arje-prod.card.json |
Constelación completa: 14 compat shims + getty. |
5b. Validación
seeds/validate.sh seeds/arje-prod.card.json
El script carga la Card vía ente_brain::load_card_file() (que llama a
brahman_card::Card::validate()) y verifica que ente-zero la encarne
hasta instanciando genesis.
5c. Customizar
Estructura de un genesis-child:
{
"schema_version": 1,
"id": "<ULID Crockford base32 — no I L O U>",
"label": "mi-servicio",
"provides": ["Journal"],
"requires": [],
"permissions": {
"networking": "loopback",
"filesystem": "read-write",
"ipc": { "allow": ["wit-v1"] },
"processes": false
},
"soma": {
"namespaces": { "mount": true, "pid": true, "net": false, ...},
"rlimits": { "mem_bytes": 268435456 },
"cgroup": { "path": "ente.slice/mi-servicio", "cpu_weight": 100 }
},
"payload": { "Native": { "exec": "/usr/sbin/mi-bin", "argv": [], "envp": [] } },
"supervision": { "Restart": { "initial": 100, "max": 30000 } },
"lifecycle": "daemon",
"priority": "normal",
"flow": { "input": [], "output": [] },
"genesis": []
}
Validación clave (brahman_card::Card::validate):
schema_version == 1.labelno vacío, ≤ 256 bytes.- ULID válido (Crockford base32, sin
I L O U). provides ∩ requires == ∅.Payload::Native.execno vacío.Payload::Wasm.module_sha256no todo ceros.- rlimits:
mem_bytes > 0 && < 1 TiB,nproc ∈ [1, 65535],nofile ∈ [1, 1M]. cgroup.cpu_weight,io_weight ∈ [1, 10000].flow.inputyflow.outputcon nombres únicos.- Recursivo sobre
genesis.
6. Debugging
6a. Tracing
ente-zero usa tracing-subscriber. Default: ente_zero=debug,info.
Override con RUST_LOG:
# En el initrd: editar /init antes de empaquetar
echo 'export RUST_LOG="trace"' >> wrapper
6b. Sockets de introspección (Unix, dentro del initrd)
| socket | servicio |
|---|---|
/run/ente-bus.sock |
bus interno (postcard, BusRequest::Invoke/...) |
/run/brahman-init.sock |
handshake brahman |
/run/brahman-admin.sock |
snapshots de sesiones + matches |
/run/ente-brain.sock |
introspección del cerebro |
Conectar desde un Ente hijo: socat - UNIX-CONNECT:/run/brahman-admin.sock.
(El initrd debe tener socat o equivalente; agregalo con EXTRA_BINS.)
6c. Snapshot / restore
ente-zero --checkpoint /ente/checkpoint.json # escribe al cerrar
ente-zero --restore /ente/checkpoint.json # reconstruye al boot
Snapshot adjunto del cerebro: /ente/checkpoint.brain.json.
6d. Metrics
ente-zero --metrics-addr 127.0.0.1:9911
Endpoint Prometheus desde dentro de la VM. Para exponerlo al host bajo
QEMU, agregar -netdev user,hostfwd=tcp::9911-:9911 -device virtio-net,netdev=….
6e. Modo DEV en host (sin PID 1)
ente-zero detecta si su PID != 1 y entra en DEV MODE: no monta
kernel surface, no se vuelve subreaper, sale tras 4 s. Útil para
iterar Cards en el host:
mkdir /tmp/arje-test && cp seeds/arje-minimal.card.json /tmp/arje-test/seed.card.json
cd /tmp/arje-test && target/release/ente-zero
7. Troubleshooting
7a. error while loading shared libraries: libgcc_s.so.1 al boot
Causa: el /sbin/ente-zero del initrd se compiló contra glibc
(x86_64-unknown-linux-gnu) y depende dinámicamente de libgcc_s.so.1,
libc.so.6, ld-linux-x86-64.so.2. Esas libs no están en el initramfs,
el kernel mata PID 1 → panic.
Fix: usar el target musl (default del script desde mayo 2026):
rustup target add x86_64-unknown-linux-musl
apt install musl-tools # Debian/Ubuntu — para crates con build.rs C
scripts/build-arje-initrd.sh # ya usa musl por default
Verificar manualmente que el binario quedó estático:
file target/x86_64-unknown-linux-musl/release/ente-zero
# → "ELF 64-bit LSB executable, ..., statically linked, ..."
Si decidís quedarte en glibc dinámico, hay que vendorear libgcc_s.so.1,
libc.so.6 y ld-linux-x86-64.so.2 (más cualquier dep de tracing y
tokio) bajo /lib64/ y /lib/x86_64-linux-gnu/ del initrd. No
recomendado.
7b. Kernel panic: Kernel panic - not syncing: Attempted to kill init!
Causa: ente-zero salió con error antes de bind del bus, o /init
crashed antes de exec. PID 1 muriendo = panic kernel.
Fix: agregar panic=10 al cmdline (ya está en el script) para
auto-reboot tras 10 s y ver el error. Bajo QEMU, -no-reboot lo
convierte en exit limpio. Capturar el log:
HEADLESS=1 scripts/run-arje-qemu.sh 2>&1 | tee /tmp/arje-boot.log
Buscar la línea anterior a Attempted to kill init.
7c. seed inválida al correr build-arje-initrd.sh
Causa: el JSON tiene un ULID con caracteres prohibidos (I L O U
están excluidos de Crockford base32), un Payload::Native.exec
vacío, o un campo del schema rechazado por brahman_card::Card::validate.
Fix: correr el validador directo para ver el error completo:
seeds/validate.sh seeds/mi-seed.card.json
# Si falla, las primeras 40 líneas del log van a stderr.
7d. cannot find -lcrypt o cannot find -lpam al compilar musl
Causa: algún crate del workspace pulled in una dep C que no encontró su lib bajo musl.
Fix: para los crates de arje no debería pasar (revisamos las deps).
Si ocurre con un crate nuevo, suele resolverse con RUSTFLAGS="-C target-feature=+crt-static" o aislando el feature problemático.
Apéndice — checklist pre-deploy
rustup target add x86_64-unknown-linux-muslinstalado en el host build.musl-tools(Debian/Ubuntu) o equivalente disponible.busybox-staticdisponible para vendorear userspace.cargo build --release --target x86_64-unknown-linux-musl -p ente-zerocompila sin warnings.file target/x86_64-unknown-linux-musl/release/ente-zeroreporta "statically linked".seeds/validate.sh seeds/arje-prod.card.json→ OK.scripts/build-arje-initrd.shproduceout/arje.initrd.cpio.gz.scripts/run-arje-qemu.sharranca y muestraTarjeta Semilla cargada y validada+instanciando genesis count=16(o el count que toque).- Si vas a bare metal: tener un kernel
vmlinuzrescue (ej. Alpine netboot) en/boot/por si arje no levanta. - Para VMs Proxmox/libvirt: serial console habilitada para ver el arranque sin display.