Files
hammer/scripts/static-audit.sh
T
sergioandClaude Opus 4.8 1e823d377b hammer hash: dry-run que faltaba — cierra el sobre-reporte del static-audit de raíz
El agujero que dejé señalado 3 veces esta sesión: nada podía saber el sellado VIGENTE de una receta
sin construirla, así que static-audit.sh auditaba el más reciente por mtime (ls -dt) y acusaba a
recetas ya sanas (dbus/libnl) por un sellado anterior a sus flags.

FIX = subcomando `hammer hash <receta> [--check]`. Calcula el ArtifactHash puro sobre las recetas
(source_id + compiler/target/link + patches + flags + fases + hashes de deps recursivos) SIN bajar
fuentes ni compilar. artifact_hash() ya era pub; el CLI sólo lo expone. Cero cambios en la lógica
de hashing ⇒ NINGÚN sellado se mueve.

Verificado: `hash` da EXACTAMENTE el mismo hash que `build` (samurai, byte a byte); `--check` sobre
receta editada → NO-SELLADO en 2ms, exit 1, sin construir nada.

static-audit.sh ahora selecciona el artefacto VIGENTE por hash, no el más reciente por mtime. Con
fallback a ls -dt si hammer no está compilado. Efecto en el store completo: las ~65 recetas cuyo
sellado no es el vigente pasan de 'auditadas' (falsa cobertura) a 'sin artefacto' (deuda de rebuild
REAL, ahora visible): estáticas de verdad 615 | MIENTEN 0 | sin artefacto 123. Corre en 15s, sin
build. El '58 sin medir' de antes estaba enmascarando ~65 recetas más.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:08:29 -04:00

103 lines
5.3 KiB
Bash
Executable File

#!/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
# El primer ELF *ejecutable* del artefacto. Una `.so` tiene NEEDED por definición y no prueba
# nada; sólo el ejecutable dice si el enlace final fue estático.
bin=""
for b in $(find "$art" -type f -perm -u+x 2>/dev/null | head -40); do
if file -b "$b" 2>/dev/null | grep -q 'ELF.*executable'; then bin="$b"; break; fi
done
[ -z "$bin" ] && { sin=$((sin + 1)); continue; }
needed=$(readelf -d "$bin" 2>/dev/null | grep -oE 'Shared library: \[[^]]+\]' \
| sed 's/.*\[//;s/\]//' | tr '\n' ' ') || true
if [ -n "$needed" ]; then
printf ' ✗ %-18s dice static, es DINÁMICO → %s\n' "$n" "$needed"
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