#!/bin/sh # static-audit.sh [receta…] — ¿`link = "static"` es VERDAD, o sólo una declaración? # # scripts/static-audit.sh # audita todo el store # scripts/static-audit.sh curl expat # sólo ésas # # EL HALLAZGO (2026-07-17, destapado migrando procps-ng): `link = "static"` se cumple *casi* # siempre — pero en todo paquete que enlaza con **libtool** se ignora EN SILENCIO. libtool lee el # `-static` del lab como "preferí los `.a` de libtool", no como flag al linker. El binario sale # dinámico y la receta jura que es estático. Con gcc pasaba igual: no es una regresión de zig, es # un agujero que nadie había medido. # # Por qué importa, más allá de la pureza: # # 1. NO CORREN. El curl sellado, ejecutado en el host: `Error relocating /lib/libz.so.1: # __snprintf_chk: symbol not found`. Su `NEEDED: libc.so` es el soname de la musl que zig # bundlea; en el host `/lib/libc.so` son 255 bytes de *linker script*, no una biblioteca. # El artefacto sólo funciona dentro del sandbox, donde ese nombre resuelve a otra cosa. # 2. ARRASTRAN GCC. helix, yazi, git-absorb, cargo-audit y tuc traen `libgcc_s.so.1` de Alpine # DENTRO del binario final. Es deuda de matar-gcc que ningún `compiler = "gcc"` declara: # invisible para el frente entero hasta esta medición. # 3. Un NEEDED es una entrada NO declarada — lo mismo que harkaq (SDD 16) mide en build, pero en # RUNTIME. Un binario que dice "no dependo de nada" y depende, rompe el cono de `affected.py` # (SDD 17 §4): un CVE en zlib no alcanzaría a un curl que declara enlace estático. # # EL FIX, por receta: `LDFLAGS="-all-static -no-pie"` en `compile` **y** en `install` (libtool # RELINKEA al instalar, así que sólo en compile no basta), nunca en configure. Patrón ya usado en # jq/parted/shadow/procps-ng. # # Por qué esto es un script y no un gate duro dentro de `hammer build`: hacerlo fallar hoy rompe # 28 recetas selladas de golpe, varias del sistema base (curl, util-linux, sudo). Primero se # arreglan con evidencia, después se cierra la puerta. Mientras tanto esto es la evidencia. # # Exit: 0 si nadie miente, 1 si alguno miente (sirve de gate en CI cuando la lista llegue a cero). set -eu HUB="$(cd "$(dirname "$0")/.." && pwd)" cd "$HUB" STORE="${STORE:-$PWD/store}" HAMMER="${HAMMER:-$HUB/target/release/hammer}" if [ $# -gt 0 ]; then recetas="" for n in "$@"; do recetas="$recetas recipes/$n.toml"; done else recetas="$(grep -lE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' recipes/*.toml)" fi mienten=0; honestos=0; sin=0 for f in $recetas; do n=$(basename "$f" .toml) grep -qE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' "$f" || continue # EL ARTEFACTO VIGENTE, por hash — NO el más reciente por mtime. `hammer hash` calcula el # ArtifactHash de la receta de HOY sin construir (~2ms, puro sobre las recetas). El store guarda # TODOS los sellados históricos de una receta (expat tenía 5); auditar el más reciente por mtime # sobre-reportaba (acusaba a dbus/libnl, ya sanas, por un sellado anterior a sus flags). Ahora # auditamos EXACTAMENTE el sellado que corresponde a la receta actual, o ninguno. # # Fallback a `ls -dt` si `hammer` no está compilado: el audit sigue corriendo, sólo vuelve a # poder sobre-reportar (como antes de que existiera `hammer hash`). if [ -x "$HAMMER" ]; then h=$("$HAMMER" --store "$STORE" hash "$f" 2>/dev/null) || h="" if [ -n "$h" ] && [ -d "$STORE/${h#b3:}-$n" ]; then art="$STORE/${h#b3:}-$n" else # No hay sellado para la receta VIGENTE ⇒ no hay nada honesto que auditar (rancio no # cuenta). Es el "sin artefacto" legítimo: la receta cambió y falta rebuild. sin=$((sin + 1)); continue fi else art=$(ls -dt "$STORE"/*-"$n" 2>/dev/null | head -1) || true [ -z "${art:-}" ] && { sin=$((sin + 1)); continue; } fi # TODOS los ELF *ejecutables* del artefacto, no el primero. Una `.so` tiene NEEDED por # definición y no prueba nada; sólo los ejecutables dicen si el enlace final fue estático. # # ⚠ Antes esto miraba UNO y cortaba (`break`). Con dwarves se vio lo que eso deja pasar: sus # DIEZ ejecutables eran dinámicos y el audit reportaba uno — acertó de casualidad, porque el # primero también lo era. El caso que se escapaba en silencio es el inverso: primer binario # estático y el resto no, que se leía como receta honesta. Es «no comprobar no es aprobar» # aplicado al propio guardián (2026-08-31). vistos=0; needed=""; primer_malo=""; malos=0 for b in $(find "$art" -type f -perm -u+x 2>/dev/null); do file -b "$b" 2>/dev/null | grep -q 'ELF.*executable' || continue vistos=$((vistos + 1)) nb=$(readelf -d "$b" 2>/dev/null | grep -oE 'Shared library: \[[^]]+\]' \ | sed 's/.*\[//;s/\]//' | tr '\n' ' ') || true if [ -n "$nb" ]; then malos=$((malos + 1)) [ -z "$primer_malo" ] && { primer_malo="${b#$art/}"; needed="$nb"; } fi done [ "$vistos" -eq 0 ] && { sin=$((sin + 1)); continue; } if [ -n "$needed" ]; then if [ "$malos" -gt 1 ]; then printf ' ✗ %-18s dice static, %d de %d ejecutables DINÁMICOS (p.ej. %s) → %s\n' \ "$n" "$malos" "$vistos" "$primer_malo" "$needed" else printf ' ✗ %-18s dice static, es DINÁMICO (%s) → %s\n' "$n" "$primer_malo" "$needed" fi case "$needed" in *libgcc_s*) printf ' %-18s ↑ arrastra libgcc_s de Alpine: deuda de matar-gcc no declarada\n' "" ;; esac 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 mienten=$((mienten + 1)) else honestos=$((honestos + 1)) fi done echo echo "══ estáticos de verdad: $honestos | MIENTEN: $mienten | sin artefacto o sin ELF: $sin" [ "$mienten" -gt 0 ] && { echo " fix: LDFLAGS=\"-all-static -no-pie\" en compile Y en install (libtool relinkea al instalar)" exit 1 } echo " ✅ toda receta que declara link=static lo cumple" exit 0