> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
frente C: busybox FUERA de la raiz de confianza — simi en su lugar
Seis ediciones, todas minimas, y el nucleo ENCOGE:
STAGE1_COMPONENTS busybox → simi
ATTEST_PATHS bin/busybox → usr/bin/simi
esqueleto FHS gana `lib`
assemble_rootfs crea /bin/sh -> /usr/bin/simi
assemble_rootfs el loader musl como ENLACE a /toolchain/lib/
REBUILD_DRIVER sus dos ordenes externas, fuera
⚠ LA CLAVE: la superficie de busybox en el stage1 eran TRES cosas, no un
userland. Leyendo REBUILD_DRIVER, antes de `export PATH=/toolchain/...` solo se
usaban /bin/sh (el shebang), /bin/mkdir y /bin/cp. Lo dice el propio driver:
«usa el busybox estatico ANTES de tocar el PATH». Despues, todo sale del
toolchain — verificado: no queda ninguna ruta absoluta a /bin en el guion.
Y las dos ordenes existian solo para dejar el loader musl en /lib. Un ENLACE lo
resuelve: un symlink no exige que su destino exista al crearlo, asi que el
ensamblado NO necesita acceso al toolchain (cero plomeria de firmas), el kernel
resuelve el PT_INTERP siguiendo enlaces, y /toolchain esta montado justo cuando
corren los binarios dinamicos que lo piden.
La alternativa era meter coreutils por mkdir y cp: medido, 109 binarios y
+3,2 MB para usar dos — exactamente lo que la nota de STAGE1_COMPONENTS ya
criticaba de util-linux. Asi el nucleo pasa de 964 K (busybox) a 530 K (simi).
⚠ Y una guarda que puse al revisar: el bucle de «adelgazar busybox» retira de
/bin todo nombre que el userland provea en /usr/bin — y /bin/sh vive ahi. Hoy
sobrevive porque ninguno de uutils/findutils/findutils-xargs/diffutils/ripgrep
trae un `sh` (medido), o sea un accidente afortunado. El dia que se adopte un
shell en el userland lo borraria y la imagen no arrancaria SIN NINGUN ERROR en
el ensamblado. Ahora `sh` esta exento explicitamente.
VERIFICADO, no supuesto:
42 tests del crate pasan (el de atestacion habia que actualizarlo: codificaba
/bin/busybox en la lista y en la parte de manipulacion; ahora exige que
busybox NO este)
stage1 reconstruido: b3:e8c78c74 (base, con busybox) → b3:f9b550e0 (con simi)
en el arbol nuevo: /bin/sh -> /usr/bin/simi · el loader como enlace ·
/usr/bin/simi presente (529 808 b) · /bin/busybox FUERA ·
/sbin/init -> arje-zero intacto · /sbin/agetty presente
enlaces del rootfs: 230 → 10 (los 220 applets de busybox, fuera)
⚠ Lo que esto NO valida todavia: el ARRANQUE. Falta product-boot-test.sh, que
pide QEMU — el hub no tiene ni /dev/kvm ni qemu; el worker tiene
qemu-system-x86_64 sin KVM, o sea por emulacion TCG (KVM=0). Hasta que eso pase
en verde, esto es «ensambla y los tests pasan», no «arranca».
⚠ Y un efecto que hay que decir: el product-rootfs pierde los ~262 enlaces de
applets, `tar` incluido (que el propio codigo citaba como no reemplazado). Las
cards usan rutas absolutas y el arranque no se toca, pero ese rootfs minimo
queda mas flaco y lo que falte lo pone el perfil.
De paso: `/store/.bootstrap-tmp` era un directorio VACIO del 22-sep con dueno
ajeno (la jaula lo ve como nobody) y bloqueaba el ensamblado con «Permission
denied» sin decir donde. Borrado.
diff --git a/crates/takana-bootstrap/src/lib.rs b/crates/takana-bootstrap/src/lib.rs
index e53d4c86..4879f2e4 100644
--- a/crates/takana-bootstrap/src/lib.rs
+++ b/crates/takana-bootstrap/src/lib.rs
@@ -186,7 +186,8 @@ pub type RootfsHash = ArtifactHash;
/// reconstruye bit a bit dentro de la VM pasa a incluir un paquete C grande; y la raíz de confianza
/// crece en ~119 binarios, de los cuales el producto usa uno. Ese último punto es el que hay que
/// revisar si algún día molesta: lo que hace falta es `agetty`, no util-linux entero.
-const STAGE1_COMPONENTS: &[&str] = &["musl", "busybox", "util-linux", "hammerd", "arje-zero"];
+const STAGE1_COMPONENTS: &[&str] =
+ &["musl", "simi", "util-linux", "hammerd", "arje-zero"];
/// La **seed card** que arje-zero lee en `/ente/seed.card.json` al boot ([SDD 12 §4](../../docs/12-init-real.md)):
/// un `Card` `Virtual` cuyos `genesis` son `hammerd` (servicio `Native` con `Restart` — el
@@ -384,7 +385,7 @@ fn assemble_rootfs(
// existir como puntos de montaje vacíos; `/ente` lleva la seed, `/var/lib/hammer` el diario.
for d in [
"proc", "sys", "sys/fs/cgroup", "dev", "dev/pts", "dev/shm", "run", "tmp", "etc", "root",
- "var", "var/lib/hammer", "ente", "usr/bin", "bin", "sbin",
+ "var", "var/lib/hammer", "ente", "usr/bin", "bin", "sbin", "lib",
] {
std::fs::create_dir_all(staging.join(d))?;
}
@@ -408,6 +409,38 @@ fn assemble_rootfs(
let init_link = staging.join("sbin/init");
let _ = std::fs::remove_file(&init_link);
std::os::unix::fs::symlink("/usr/bin/arje-zero", &init_link)?;
+
+ // ── `/bin/sh` (2026-10-22) ────────────────────────────────────────────────────────────────
+ // Lo ponía busybox: su `make install` creaba `bin/sh -> busybox` dentro de su propio
+ // artefacto. Al salir busybox del núcleo hay que crearlo acá, apuntando a `simi`.
+ //
+ // Absoluto y no relativo: es el `exec` de las cards y el shebang del driver de
+ // reconstrucción, y los dos lo resuelven desde rutas distintas.
+ let sh_link = staging.join("bin/sh");
+ let _ = std::fs::remove_file(&sh_link);
+ std::os::unix::fs::symlink("/usr/bin/simi", &sh_link)?;
+
+ // ── el shim del loader musl, como ENLACE (2026-10-22) ─────────────────────────────────────
+ // Lo hacía el driver de reconstrucción al arrancar, con dos órdenes:
+ // /bin/mkdir -p /lib
+ // /bin/cp -f /toolchain/lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1
+ // y ésas eran las ÚNICAS dos razones por las que el stage1 necesitaba un userland además del
+ // shell — o sea, las únicas que obligaban a tener busybox en la raíz de confianza. Medido
+ // leyendo el driver: todo lo demás ocurre DESPUÉS de `export PATH=/toolchain/...`.
+ //
+ // ⚠ **Es un ENLACE, no una copia, y eso es lo que hace el cambio barato.** Un enlace
+ // simbólico no exige que su destino exista al crearlo, así que el ensamblado no necesita
+ // acceso al toolchain — cero plomería. El kernel resuelve el `PT_INTERP` siguiendo enlaces
+ // como cualquier otra ruta, y `/toolchain` está montado justo cuando corren los binarios
+ // dinámicos que lo necesitan, que es el único momento en que importa.
+ //
+ // La alternativa era meter `coreutils` al núcleo por `mkdir` y `cp`: medido, son **109
+ // binarios y +3,2 MB** para usar dos. Es exactamente lo que la nota de `STAGE1_COMPONENTS` ya
+ // criticaba de `util-linux` («la raíz de confianza crece en ~119 binarios, de los cuales el
+ // producto usa uno»). Así el núcleo ENCOGE: sale busybox (964 K) y entra simi (499 K).
+ let loader = staging.join("lib/ld-musl-x86_64.so.1");
+ let _ = std::fs::remove_file(&loader);
+ std::os::unix::fs::symlink("/toolchain/lib/ld-musl-x86_64.so.1", &loader)?;
Ok(())
}
@@ -490,7 +523,7 @@ const SSHD_CONFIG: &str = "Port 22\nListenAddress 0.0.0.0\nPermitRootLogin prohi
const ATTEST_PATHS: &[&str] = &[
"usr/bin/arje-zero", // PID 1 — la raíz
"usr/bin/hammerd", // diario + bus
- "bin/busybox", // shell (`/bin/sh`) + exec del card de sshd
+ "usr/bin/simi", // shell (`/bin/sh`) + exec del card de sshd — era `bin/busybox`
"sbin/agetty", // la consola: lo ejecuta el card `console-getty`
"usr/sbin/sshd", // daemon SSH (superficie de red)
"usr/bin/netup", // configurador de red
@@ -701,11 +734,21 @@ fn assemble_product_rootfs(
}
let _ = std::fs::remove_dir_all(staging.join(".hammer"));
- // ADELGAZAR BUSYBOX (determinista, sin depender del orden de PATH del shell): por cada nombre que
- // el userland Rust provee en `/usr/bin`, retirar el symlink homónimo de busybox en los OTROS dirs
- // (`/bin`, `/sbin`, `/usr/sbin`) — busybox suele instalar `ls` en `/bin`, fuera del `/usr/bin` que
- // la hidratación ya ensombreció. Así la tool Rust es la ÚNICA en PATH. El binario busybox queda
- // (sh/ash, tar, mount, … lo no reemplazado); sólo se quitan los applets ya cubiertos por Rust.
+ // ADELGAZAR (determinista, sin depender del orden de PATH del shell): por cada nombre que el
+ // userland Rust provee en `/usr/bin`, retirar el homónimo de los OTROS dirs (`/bin`, `/sbin`,
+ // `/usr/sbin`), que es donde busybox dejaba sus applets fuera del `/usr/bin` que la hidratación
+ // ya ensombrecía. Así la tool Rust es la ÚNICA en PATH.
+ //
+ // ⚠ **Desde el 2026-10-22 busybox ya NO está en el núcleo** (`STAGE1_COMPONENTS` lleva `simi`),
+ // así que estos homónimos casi nunca existen y el bucle es un no-op barato. Se deja porque
+ // sigue siendo correcto para cualquier componente que instale en dos sitios.
+ //
+ // ⚠ Y LLEVA UNA GUARDA: `bin/sh` NO se toca nunca. Hoy ningún componente del userland provee
+ // un `usr/bin/sh` —medido: uutils, findutils, findutils-xargs, diffutils, ripgrep, ninguno—
+ // así que el enlace `/bin/sh -> /usr/bin/simi` que pone `assemble_rootfs` sobrevive por
+ // accidente. El día que alguien adopte un shell en el userland, este bucle lo BORRARÍA y la
+ // imagen no arrancaría, sin ningún error en el ensamblado. Una guarda explícita cuesta una
+ // línea y convierte un accidente afortunado en una propiedad.
let mut applet_names: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for (name, h) in userland {
let ub = store.path_of(h, name).join("usr/bin");
@@ -716,6 +759,10 @@ fn assemble_product_rootfs(
}
}
for applet in &applet_names {
+ // la guarda: el shell del producto no se retira jamás (ver la nota de arriba)
+ if applet == "sh" {
+ continue;
+ }
for d in ["bin", "sbin", "usr/sbin"] {
let p = staging.join(d).join(applet);
if std::fs::symlink_metadata(&p).is_ok() {
@@ -1379,9 +1426,13 @@ const REBUILD_DRIVER: &str = r#"#!/bin/sh
set -eu
. /etc/hammer/rebuild.env # SEED_HASH, SEED_KIND, REF_CONTENT (REF_CONTENT puede ir vacío)
# Shim del loader musl: deja correr los binarios *dinámicos* del toolchain (bwrap/git/curl/cargo/make)
-# desde el root Stage 1 estático. Usa el busybox estático (rutas absolutas) ANTES de tocar el PATH.
-/bin/mkdir -p /lib
-/bin/cp -f /toolchain/lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1
+# desde el root Stage 1 estático.
+#
+# ✅ 2026-10-22: acá había dos órdenes —`/bin/mkdir -p /lib` y `/bin/cp -f /toolchain/lib/ld-musl…`—
+# y eran las ÚNICAS que este guion ejecutaba ANTES de tocar el PATH. O sea: las únicas que obligaban
+# a tener un userland (busybox) en la raíz de confianza además del shell. Ahora el ensamblado deja
+# `/lib/ld-musl-x86_64.so.1` puesto como ENLACE a `/toolchain/lib/`, así que no hay nada que hacer
+# acá y el núcleo pudo pasar de busybox a simi. Todo lo de abajo ya corre con el PATH del toolchain.
export LD_LIBRARY_PATH=/toolchain/lib:/toolchain/usr/lib
export PATH=/toolchain/usr/bin:/toolchain/usr/sbin:/toolchain/bin:/toolchain/sbin:/usr/bin:/bin:/sbin
# overlay es módulo (CONFIG_OVERLAY_FS=m) y el initramfs arranca sin módulos cargados: el sandbox de
@@ -2339,14 +2390,21 @@ mod tests {
std::fs::create_dir_all(root.join(d)).unwrap();
}
std::fs::write(root.join("usr/bin/hammerd"), b"\x7fELF-hammerd").unwrap();
- std::fs::write(root.join("bin/busybox"), b"\x7fELF-busybox").unwrap();
+ // `usr/bin/simi` desde el 2026-10-22: era `bin/busybox`, y la lista de ATTEST_PATHS
+ // cambió con el núcleo. Este test es el control de que ese cambio no se hizo a medias.
+ std::fs::write(root.join("usr/bin/simi"), b"\x7fELF-simi").unwrap();
std::fs::write(root.join("usr/sbin/sshd"), b"\x7fELF-sshd").unwrap();
// arje-zero/netup/coreutils ausentes ⇒ no se listan (sólo binarios presentes).
let manifest = build_attestation(&root).unwrap();
assert_eq!(manifest.algo, "blake3");
let paths: Vec<&str> = manifest.entries.iter().map(|e| e.path.as_str()).collect();
- assert!(paths.contains(&"/usr/bin/hammerd") && paths.contains(&"/bin/busybox") && paths.contains(&"/usr/sbin/sshd"));
+ assert!(
+ paths.contains(&"/usr/bin/hammerd")
+ && paths.contains(&"/usr/bin/simi")
+ && paths.contains(&"/usr/sbin/sshd")
+ );
+ assert!(!paths.contains(&"/bin/busybox"), "busybox salió del núcleo el 2026-10-22");
assert!(!paths.contains(&"/usr/bin/arje-zero"), "binario ausente no se atesta");
std::fs::write(root.join("ente/attest.json"), serde_json::to_string_pretty(&manifest).unwrap()).unwrap();
@@ -2355,10 +2413,10 @@ mod tests {
assert!(rep.passed() && rep.ok.len() == manifest.entries.len());
// manipular un binario ⇒ DIVERGE (el gate de arje haría halt).
- std::fs::write(root.join("bin/busybox"), b"\x7fELF-TAMPERED-longer").unwrap();
+ std::fs::write(root.join("usr/bin/simi"), b"\x7fELF-TAMPERED-longer").unwrap();
let rep2 = verify_attestation(&root).unwrap();
assert!(!rep2.passed());
- assert_eq!(rep2.mismatched, vec!["/bin/busybox".to_string()]);
+ assert_eq!(rep2.mismatched, vec!["/usr/bin/simi".to_string()]);
// borrar un binario crítico ⇒ FALTA.
std::fs::remove_file(root.join("usr/sbin/sshd")).unwrap();