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…
static-audit: el '28 mienten' estaba INFLADO por mi propio bug (ls|head -1)
1#!/bin/sh
2# static-audit.sh [receta…] — ¿`link = "static"` es VERDAD, o sólo una declaración?
3#
4# scripts/static-audit.sh # audita todo el store
5# scripts/static-audit.sh curl expat # sólo ésas
6#
7# EL HALLAZGO (2026-07-17, destapado migrando procps-ng): `link = "static"` se cumple *casi*
8# siempre — pero en todo paquete que enlaza con **libtool** se ignora EN SILENCIO. libtool lee el
9# `-static` del lab como "preferí los `.a` de libtool", no como flag al linker. El binario sale
10# dinámico y la receta jura que es estático. Con gcc pasaba igual: no es una regresión de zig, es
11# un agujero que nadie había medido.
12#
13# Por qué importa, más allá de la pureza:
14#
15# 1. NO CORREN. El curl sellado, ejecutado en el host: `Error relocating /lib/libz.so.1:
16# __snprintf_chk: symbol not found`. Su `NEEDED: libc.so` es el soname de la musl que zig
17# bundlea; en el host `/lib/libc.so` son 255 bytes de *linker script*, no una biblioteca.
18# El artefacto sólo funciona dentro del sandbox, donde ese nombre resuelve a otra cosa.
19# 2. ARRASTRAN GCC. helix, yazi, git-absorb, cargo-audit y tuc traen `libgcc_s.so.1` de Alpine
20# DENTRO del binario final. Es deuda de matar-gcc que ningún `compiler = "gcc"` declara:
21# invisible para el frente entero hasta esta medición.
22# 3. Un NEEDED es una entrada NO declarada — lo mismo que harkaq (SDD 16) mide en build, pero en
23# RUNTIME. Un binario que dice "no dependo de nada" y depende, rompe el cono de `affected.py`
24# (SDD 17 §4): un CVE en zlib no alcanzaría a un curl que declara enlace estático.
25#
26# EL FIX, por receta: `LDFLAGS="-all-static -no-pie"` en `compile` **y** en `install` (libtool
27# RELINKEA al instalar, así que sólo en compile no basta), nunca en configure. Patrón ya usado en
28# jq/parted/shadow/procps-ng.
29#
30# Por qué esto es un script y no un gate duro dentro de `hammer build`: hacerlo fallar hoy rompe
31# 28 recetas selladas de golpe, varias del sistema base (curl, util-linux, sudo). Primero se
32# arreglan con evidencia, después se cierra la puerta. Mientras tanto esto es la evidencia.
33#
34# Exit: 0 si nadie miente, 1 si alguno miente (sirve de gate en CI cuando la lista llegue a cero).
35set -eu
36
37HUB="$(cd "$(dirname "$0")/.." && pwd)"
38cd "$HUB"
39STORE="${STORE:-$PWD/store}"
40
41if [ $# -gt 0 ]; then
42 recetas=""
43 for n in "$@"; do recetas="$recetas recipes/$n.toml"; done
44else
45 recetas="$(grep -lE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' recipes/*.toml)"
46fi
47
48mienten=0; honestos=0; sin=0
49for f in $recetas; do
50 n=$(basename "$f" .toml)
51 grep -qE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' "$f" || continue
52 # `ls -dt`: el artefacto MÁS RECIENTE, no el alfabéticamente primero. El store guarda TODOS los
53 # sellados de una receta (expat tenía 5, de junio a hoy) y el glob ordena por hash, que es
54 # ruido: auditaba un artefacto de hace tres semanas y reportaba dinámica una receta ya
55 # arreglada. Mismo bug de clase que comparar `libpng.a` con `libpng16.a` por `find | head -1`.
56 #
57 # HONESTIDAD: "más reciente" ≠ "vigente". Lo vigente sería el artefacto cuyo hash corresponde a
58 # la receta de HOY, y hammer no expone ese hash sin construir (no hay `hammer hash`/dry-run).
59 # Con el store al día ambos coinciden; si acabás de construir una receta vieja, no. Por eso el
60 # audit se corre DESPUÉS del rebuild, nunca antes.
61 art=$(ls -dt "$STORE"/*-"$n" 2>/dev/null | head -1) || true
62 [ -z "${art:-}" ] && { sin=$((sin + 1)); continue; }
63
64 # El primer ELF *ejecutable* del artefacto. Una `.so` tiene NEEDED por definición y no prueba
65 # nada; sólo el ejecutable dice si el enlace final fue estático.
66 bin=""
67 for b in $(find "$art" -type f -perm -u+x 2>/dev/null | head -40); do
68 if file -b "$b" 2>/dev/null | grep -q 'ELF.*executable'; then bin="$b"; break; fi
69 done
70 [ -z "$bin" ] && { sin=$((sin + 1)); continue; }
71
72 needed=$(readelf -d "$bin" 2>/dev/null | grep -oE 'Shared library: \[[^]]+\]' \
73 | sed 's/.*\[//;s/\]//' | tr '\n' ' ') || true
74 if [ -n "$needed" ]; then
75 printf ' ✗ %-18s dice static, es DINÁMICO → %s\n' "$n" "$needed"
76 case "$needed" in *libgcc_s*) printf ' %-18s ↑ arrastra libgcc_s de Alpine: deuda de matar-gcc no declarada\n' "" ;; esac
77 case "$needed" in *libc.so*) printf ' %-18s ↑ `libc.so` es la musl de zig: en el host son 255B de linker script ⇒ NO CORRE fuera del sandbox\n' "" ;; esac
78 mienten=$((mienten + 1))
79 else
80 honestos=$((honestos + 1))
81 fi
82done
83
84echo
85echo "══ estáticos de verdad: $honestos | MIENTEN: $mienten | sin artefacto o sin ELF: $sin"
86[ "$mienten" -gt 0 ] && {
87 echo " fix: LDFLAGS=\"-all-static -no-pie\" en compile Y en install (libtool relinkea al instalar)"
88 exit 1
89}
90echo " ✅ toda receta que declara link=static lo cumple"
91exit 0