tawasuyu

sergio/takana

> 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…

TOML 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

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.

63ca0ce9b64ff220a2dbbc3ee8f56c6bb9771769 · ruwaq Sergio · (8 h ñawpaq) · tayta 40a71e2e8 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 6

crates/takana-bootstrap/src/lib.rs

1 qillqakuna · +73 −15

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();