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 1534Shell 199Rust 109Markdown 993713 Sutichaykuna

fix(privilegio): el bit de los reemplazos de busybox, y el symlink del piso que los tapaba

El operador preguntó: «está actualizado takana? porque allá yo había eliminado busybox, y en la
instalación está así». Lo esta —el hub a 1 commit— y el agujero era otro, en dos mitades.

1. NINGUNA de las recetas que REEMPLAZAN a busybox declaraba su bit. Medido sobre los artefactos
   del store: los 16 binarios que en cualquier distro son setuid salen todos `-rwxr-xr-x`, y de
   shadow, util-linux, iputils, linux-pam y uutils ninguna declaraba nada. El encabezado de
   takana-core/src/setuid.rs los lista uno por uno desde el 22-sep y decia que el hueco era
   LATENTE «porque el instalador solo pone contrasena de root»; desde que lib/cuenta.sh crea una
   cuenta de persona dejo de serlo. 8 declaraciones nuevas, y la mas grave es `unix_chkpwd`: es con
   lo que PAM verifica una contrasena cuando el que pregunta no es root, asi que sin su bit EL
   GREETER NO PUEDE AUTENTICAR A NADIE — con la contrasena correcta, y diciendo que esta mal.
   Metadato: los cuatro hashes siguen SELLADOS, cero reforja.

2. Y aunque se declare, EL SYMLINK DEL PISO LO TAPABA. Ningun perfil declara busybox, pero el
   product-rootfs (artefacto del 22 de agosto) lo trae horneado con ~331 symlinks. El piso entra
   primero (`cp -al`) y la fusion usa `-n`, asi que `bin/su -> busybox` gana sobre el `bin/su` de
   shadow. El bit se aplica sobre el arbol del PERFIL —donde ese `bin/su` es el binario real y queda
   4755— y la fusion lo descartaba: `su`, `passwd`, `mount` y `umount` terminaban apuntando a un
   busybox sin bit. El aviso ya existia («418 fichero(s) ya estaban y NO se pisaron») y nadie lo
   leyo como lo que era.

   El arreglo libera SOLO las rutas con [[setuid]] declarado, y el criterio es duro: a un symlink no
   se le puede poner el bit, asi que un symlink ahi es una capacidad muerta por construccion. Los
   otros ~325 no se tocan — reemplazar un applet por un binario con otras banderas puede romper un
   script del arranque, y eso es el trabajo de docs/plan-botar-busybox.md, con su propia medicion.

Certificado: scripts/probar-fusion-privilegio.sh extrae el bloque REAL del armador (no una copia) y
lo corre contra un arbol de juguete: `bin/su` y `usr/bin/passwd` llegan como binarios de shadow 4755,
y el brazo de control es `bin/ls` —un applet SIN declaracion— que sigue siendo el symlink. Si se
tocara, el arreglo habria dejado de estar acotado.

Falta metal: instalar y correr `su`, `passwd` y `ping` como usuario normal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

c51c2bf32c16e2216016ae102b2ec69586a762ba · ruwaq sergio · (7 p'unchaw ñawpaq) · tayta f19849c16 · kay commitpi sach'ata qhaway

Semántico sayk'uy

Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.

6 qillqakuna · +159 −0

diff --git a/recipes/iputils.toml b/recipes/iputils.toml
index 9d28db1c..85c6ba87 100644
--- a/recipes/iputils.toml
+++ b/recipes/iputils.toml
@@ -31,3 +31,17 @@ install   = "DESTDIR=/out ninja -C output install"
 
 [deps]
 build = ["binutils", "libcap", "linux-headers", "meson", "samurai", "python3", "pkgconf"]
+
+# ── EL BIT DE PRIVILEGIO (SDD 28 §5.11, `[[setuid]]`) ─────────────────────────────────────────────
+# Declarado el 2026-10-15, al medir los artefactos del store: los binarios de abajo salen todos
+# `-rwxr-xr-x` y en cualquier otra distro son setuid. El hueco estaba ANOTADO —el encabezado de
+# `crates/takana-core/src/setuid.rs` los lista uno por uno— y era LATENTE mientras el instalador
+# solo pusiera contrasena de root. Desde que `lib/cuenta.sh` crea una cuenta de persona, dejo de
+# ser latente: hay un humano no-root que los choca a todos.
+#
+# Es metadato (fuera de `hash_inputs`), asi que entra en una receta YA SELLADA sin mover un solo
+# `ArtifactHash` — comprobado con `takana hash --check` antes y despues.
+[[setuid]]
+path = "usr/bin/ping"
+mode = "4755"
+why  = "abrir el socket ICMP. Sin el bit sale 1 al instante y el mensaje se lee como «no hay red»: el operador probo `ping 8.8.8.8` el 2026-10-14 y concluyo que la maquina estaba sin red cuando el problema era el permiso. ⚠ La alternativa moderna es el sysctl `net.ipv4.ping_group_range`, que no necesita setuid y seria mejor; mientras nadie lo ponga en el arranque, el bit es lo que hace que ping diga la verdad."
diff --git a/recipes/linux-pam.toml b/recipes/linux-pam.toml
index 720dcc54..84095099 100644
--- a/recipes/linux-pam.toml
+++ b/recipes/linux-pam.toml
@@ -23,3 +23,17 @@ install   = 'make DESTDIR=/out install'
 
 [deps]
 build = ["flex", "pkgconf", "make"]
+
+# ── EL BIT DE PRIVILEGIO (SDD 28 §5.11, `[[setuid]]`) ─────────────────────────────────────────────
+# Declarado el 2026-10-15, al medir los artefactos del store: los binarios de abajo salen todos
+# `-rwxr-xr-x` y en cualquier otra distro son setuid. El hueco estaba ANOTADO —el encabezado de
+# `crates/takana-core/src/setuid.rs` los lista uno por uno— y era LATENTE mientras el instalador
+# solo pusiera contrasena de root. Desde que `lib/cuenta.sh` crea una cuenta de persona, dejo de
+# ser latente: hay un humano no-root que los choca a todos.
+#
+# Es metadato (fuera de `hash_inputs`), asi que entra en una receta YA SELLADA sin mover un solo
+# `ArtifactHash` — comprobado con `takana hash --check` antes y despues.
+[[setuid]]
+path = "sbin/unix_chkpwd"
+mode = "4755"
+why  = "es con lo que PAM verifica una contrasena cuando el que pregunta NO es root, y por eso es el mas grave de la lista: sin el bit, `pam_unix` no puede leer /etc/shadow y el greeter de mirada no puede autenticar a NADIE — con la contrasena correcta. El sintoma no menciona el privilegio, dice que la contrasena esta mal. Ver SDD 28 §5.11, que lo anticipo: «fallan a la vez el cambio de contrasena, el mount de usuario, FUSE, ping y —peor— unix_chkpwd»."
diff --git a/recipes/shadow.toml b/recipes/shadow.toml
index db8987dd..a66bb984 100644
--- a/recipes/shadow.toml
+++ b/recipes/shadow.toml
@@ -92,3 +92,32 @@ install -m644 /dev/null etc/subgid
 # linux-pam, libbsd, setcap. Sin fcaps (setcap falla en el sandbox) ⇒ sin libcap.
 [deps]
 build = ["linux-headers", "make"]
+
+# ── EL BIT DE PRIVILEGIO (SDD 28 §5.11, `[[setuid]]`) ─────────────────────────────────────────────
+# Declarado el 2026-10-15, al medir los artefactos del store: los binarios de abajo salen todos
+# `-rwxr-xr-x` y en cualquier otra distro son setuid. El hueco estaba ANOTADO —el encabezado de
+# `crates/takana-core/src/setuid.rs` los lista uno por uno— y era LATENTE mientras el instalador
+# solo pusiera contrasena de root. Desde que `lib/cuenta.sh` crea una cuenta de persona, dejo de
+# ser latente: hay un humano no-root que los choca a todos.
+#
+# Es metadato (fuera de `hash_inputs`), asi que entra en una receta YA SELLADA sin mover un solo
+# `ArtifactHash` — comprobado con `takana hash --check` antes y despues.
+[[setuid]]
+path = "bin/su"
+mode = "4755"
+why  = "cambiar de usuario desde una sesion que no es root. Sin el bit contesta «su: must be suid to work properly» (busybox) o «Authentication failure» (shadow), y en los dos casos el mensaje manda a buscar la contrasena y no el permiso. Medido en metal el 2026-10-15, cuando era la unica escalada que le quedaba al operador."
+
+[[setuid]]
+path = "usr/bin/passwd"
+mode = "4755"
+why  = "que el dueno de la maquina pueda cambiar SU PROPIA contrasena. Importa mas de lo que parece porque root queda cerrado por diseno cuando hay quien escale con doas: sin este bit, la contrasena de la persona solo se puede cambiar con `doas passwd`, y el que la olvida no tiene camino."
+
+[[setuid]]
+path = "usr/bin/newuidmap"
+mode = "4755"
+why  = "escribe el uid_map de un user namespace sin privilegio. Lo pide la jaula de compilacion: `scripts/qorpa/qorpa-preflight.sh` ya lo comprueba y avisa «newuidmap SIN setuid y SIN capability — no puede escribir el uid_map», o sea que el hueco estaba detectado y sin cerrar."
+
+[[setuid]]
+path = "usr/bin/newgidmap"
+mode = "4755"
+why  = "el par del anterior para el gid_map: los dos o ninguno, porque un namespace con uid mapeado y gid sin mapear falla al primer acceso a un fichero de grupo."
diff --git a/recipes/util-linux.toml b/recipes/util-linux.toml
index b4bbd2fd..7f017f9f 100644
--- a/recipes/util-linux.toml
+++ b/recipes/util-linux.toml
@@ -75,3 +75,22 @@ make DESTDIR="/out" install LDFLAGS="-all-static -no-pie"
 # autoconf/automake/po4a/libxcrypt/sqlite/linux-pam/libcap-ng/systemd-minimal-libs.
 [deps]
 build = ["linux-headers", "make"]
+
+# ── EL BIT DE PRIVILEGIO (SDD 28 §5.11, `[[setuid]]`) ─────────────────────────────────────────────
+# Declarado el 2026-10-15, al medir los artefactos del store: los binarios de abajo salen todos
+# `-rwxr-xr-x` y en cualquier otra distro son setuid. El hueco estaba ANOTADO —el encabezado de
+# `crates/takana-core/src/setuid.rs` los lista uno por uno— y era LATENTE mientras el instalador
+# solo pusiera contrasena de root. Desde que `lib/cuenta.sh` crea una cuenta de persona, dejo de
+# ser latente: hay un humano no-root que los choca a todos.
+#
+# Es metadato (fuera de `hash_inputs`), asi que entra en una receta YA SELLADA sin mover un solo
+# `ArtifactHash` — comprobado con `takana hash --check` antes y despues.
+[[setuid]]
+path = "bin/mount"
+mode = "4755"
+why  = "montar sin ser root lo que /etc/fstab marque `user`, que es como una persona usa un pendrive en una imagen que NO trae udisks. El bit no da mas que eso: mount rechaza cualquier punto que fstab no haya autorizado, y esa es la costura que lo hace defendible."
+
+[[setuid]]
+path = "bin/umount"
+mode = "4755"
+why  = "el par del anterior: poder montar y no poder desmontar deja el dispositivo tomado hasta reiniciar, y la persona tira del cable — que es la forma de perder datos."
diff --git a/scripts/instalador-variantes.sh b/scripts/instalador-variantes.sh
index 865b5817..9ab885c3 100755
--- a/scripts/instalador-variantes.sh
+++ b/scripts/instalador-variantes.sh
@@ -138,6 +138,41 @@ compone() {   # <variante> <medio> → deja el árbol local en $LOCAL/<variante>
         _antes=$(find "$C" ! -type d 2>/dev/null | wc -l)
         _trae=$(find "$H" ! -type d 2>/dev/null | wc -l)
         ( cd "$H" && find . -type d -exec mkdir -p "$C/{}" \; ) 2>/dev/null || true
+        # ── UN SYMLINK A BUSYBOX no tapa el binario REAL que necesita el bit ──────────────────────
+        # Ningún perfil declara `busybox`, pero el PISO lo trae horneado con sus ~331 symlinks, y el
+        # piso entra primero: con `-n`, `bin/su → busybox` gana sobre el `bin/su` de shadow. El bit
+        # de privilegio se aplicó sobre el árbol del perfil —donde ESE `bin/su` es el binario real y
+        # quedó 4755— y la fusión lo descartaba, dejando la capacidad muerta. Medido el 2026-10-15:
+        # `su`, `passwd`, `mount` y `umount` eran symlinks a un busybox sin bit.
+        #
+        # Se liberan SÓLO las rutas con `[[setuid]]` declarado, y el criterio es duro: **a un symlink
+        # no se le puede poner el bit**, así que un symlink ahí es una capacidad muerta por
+        # construcción. Los otros ~325 no se tocan — reemplazar un applet por un binario con otras
+        # banderas puede romper un script del arranque, y eso es el trabajo de
+        # `docs/plan-botar-busybox.md`, con su propia medición.
+        _recetas_p=""
+        for _pk in $(python3 scripts/targets.py "$p" 2>/dev/null); do
+            [ -f "recipes/$_pk.toml" ] && _recetas_p="$_recetas_p recipes/$_pk.toml"
+        done
+        _liberados=0
+        if [ -n "$_recetas_p" ] && [ -x ./target/release/takana ]; then
+            for _sp in $(./target/release/takana setuid $_recetas_p --json 2>/dev/null \
+                | python3 -c 'import json,sys
+try:
+    print("\n".join(x["path"] for x in json.load(sys.stdin)))
+except Exception:
+    pass' 2>/dev/null); do
+                [ -L "$C/$_sp" ] || continue
+                [ -f "$H/$_sp" ] && [ ! -L "$H/$_sp" ] || continue
+                case "$(readlink "$C/$_sp")" in
+                *busybox*)
+                    rm -f "$C/$_sp"
+                    echo "     · $_sp: el symlink a busybox del piso NO tapa al de $p (el bit no va en un symlink)"
+                    _liberados=$((_liberados + 1)) ;;
+                esac
+            done
+        fi
+        [ "$_liberados" -gt 0 ] && echo "     ✓ $_liberados binario(s) privilegiado(s) liberado(s) del symlink heredado"
         ( cd "$H" && find . ! -type d -exec cp -a -l -n {} "$C/{}" \; ) 2>/dev/null || true
         _despues=$(find "$C" ! -type d 2>/dev/null | wc -l)
         _choques=$(( _antes + _trae - _despues ))
diff --git a/scripts/probar-fusion-privilegio.sh b/scripts/probar-fusion-privilegio.sh
new file mode 100755
index 00000000..cfd81bae
--- /dev/null
+++ b/scripts/probar-fusion-privilegio.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# Prueba del bloque REAL de liberación de symlinks: se extrae del propio instalador-variantes.sh
+# (no una copia) y se corre contra un árbol de juguete. Lo que se mide es si `bin/su` de shadow
+# llega entero al árbol compuesto en vez de quedar tapado por el symlink a busybox del piso.
+set -eu
+cd "$(cd "$(dirname "$0")/.." && pwd)"
+W=/tmp/probar-fusion.$$
+trap 'rm -rf "$W"' EXIT
+C="$W/C"; H="$W/H"
+mkdir -p "$C/bin" "$C/usr/bin" "$H/bin" "$H/usr/bin"
+
+# El piso: busybox y sus symlinks (lo que trae product-rootfs).
+printf 'soy busybox\n' > "$C/bin/busybox"; chmod 755 "$C/bin/busybox"
+ln -s busybox "$C/bin/su"
+ln -s ../../bin/busybox "$C/usr/bin/passwd"
+ln -s busybox "$C/bin/ls"          # un applet SIN [[setuid]]: NO debe tocarse
+# El perfil: los binarios de verdad, ya con su bit (como los deja `setuid --apply`).
+printf 'soy el su de shadow\n' > "$H/bin/su"; chmod 4755 "$H/bin/su"
+printf 'soy el passwd de shadow\n' > "$H/usr/bin/passwd"; chmod 4755 "$H/usr/bin/passwd"
+printf 'soy el ls de uutils\n' > "$H/bin/ls"; chmod 755 "$H/bin/ls"
+
+p=base
+# ── el bloque REAL, extraído del script que va a correr ────────────────────────────────────────
+sed -n '/^        _recetas_p=""/,/^        \[ "\$_liberados" -gt 0 \]/p' scripts/instalador-variantes.sh > "$W/bloque.sh"
+[ -s "$W/bloque.sh" ] || { echo "✗ no pude extraer el bloque del armador"; exit 1; }
+echo "   (bloque extraído: $(wc -l < "$W/bloque.sh") líneas)"
+. "$W/bloque.sh"
+# ── y la copia que viene después, igual que en el armador ──────────────────────────────────────
+( cd "$H" && find . -type d -exec mkdir -p "$C/{}" \; ) 2>/dev/null || true
+( cd "$H" && find . ! -type d -exec cp -a -l -n {} "$C/{}" \; ) 2>/dev/null || true
+
+echo "══ cómo quedó el árbol compuesto ══"
+fallo=0
+for b in bin/su usr/bin/passwd; do
+	if [ -L "$C/$b" ]; then echo "  ✗ $b sigue siendo un symlink a $(readlink "$C/$b")"; fallo=1
+	elif [ "$(stat -c%a "$C/$b")" = 4755 ] && grep -q shadow "$C/$b"; then
+		echo "  ✓ $b es el binario de shadow, 4755"
+	else echo "  ✗ $b quedó raro: $(stat -c%A "$C/$b")"; fallo=1; fi
+done
+# El brazo de control: un applet sin declaración NO se toca. Si se tocara, este arreglo habría
+# dejado de estar acotado y estaría reemplazando los 331 symlinks sin medición.
+if [ -L "$C/bin/ls" ] && [ "$(readlink "$C/bin/ls")" = busybox ]; then
+	echo "  ✓ bin/ls (sin [[setuid]]) sigue siendo el applet: el arreglo está acotado"
+else
+	echo "  ✗ bin/ls se tocó, y no tiene declaración de privilegio"; fallo=1
+fi
+[ -f "$C/bin/busybox" ] || { echo "  ✗ se perdió el busybox del piso"; fallo=1; }
+[ "$fallo" = 0 ] && echo "✓✓ la fusión deja pasar el binario privilegiado y no toca lo demás" || exit 1