El audit del enlace estático globeaba `recipes/*.toml` y nada más, y cerraba con
«✅ toda receta que declara link=static lo cumple»: una frase verdadera de una PARTE del corpus,
presentada como si fuera de todo él. Las cinco colas (`incoming{,-kde,-gnome,-cosmic,-wlr}`) son
~305 recetas que **nunca se auditaron**.
Lo destapé por accidente: al promover `libseccomp` de `incoming-gnome/` a `recipes/`, **el artefacto
no cambió ni un byte** y el audit pasó de MIENTEN:0 a MIENTEN:1. La mentira estaba ahí desde siempre
y lo único que la tapaba era el glob de una línea. Un guardián que mide menos de lo que su resumen
afirma es peor que no tenerlo: da por cubierto lo que no mira.
Ampliado el barrido a las colas, aparecen **3** que llevaban invisibles:
libseccomp scmp_sys_resolver 6 rebuilds (corpus 1 + incoming-gnome 5)
libgcrypt dumpsexp y 2 más 41 rebuilds (corpus 1 + incoming-kde 40)
libgpg-error gpg-error 45 rebuilds (corpus 4 + incoming-kde 41)
Radios medidos con `yupana radio`, que cruza colas — no con `grep recipes/*.toml`, que es justo el
error que este commit arregla.
En las tres, lo dinámico es un **binario auxiliar de diagnóstico**, no la librería: los consumidores
enlazan el `.a`, así que el impacto funcional hoy es NULO. Lo que está mal es la declaración, y
arreglarla cuesta ~92 rebuilds casi todos de KDE. Van con el próximo bump de cada una, cuando el
re-hash ya esté pagado — mismo criterio que la deuda de `perl`. Decisión con el número delante, no
olvido.
Por eso entran en `DEUDA_DECIDIDA` y se imprimen como `•` sin hacer fallar: si el audit fallara
siempre por tres viejas, un ofensor NUEVO se perdería entre el ruido. El resumen ahora distingue
«MIENTEN (nuevos)» de «deuda decidida».
Probado con los dos controles y no sólo con el que da verde: quitándole a propósito el
`LDFLAGS=-all-static` a `crun` y reconstruyéndola, el audit sale **1** y la nombra; restaurada, sale
**0**. O sea que la tabla de deuda no se traga a un ofensor nuevo. El artefacto de la prueba se
borró del store.
154 lines
8.9 KiB
Bash
Executable File
154 lines
8.9 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 `takana 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="${TAKANA:-${HAMMER:-$HUB/target/release/takana}}"
|
|
|
|
if [ $# -gt 0 ]; then
|
|
recetas=""
|
|
for n in "$@"; do recetas="$recetas recipes/$n.toml"; done
|
|
else
|
|
# ⚠ **LAS COLAS TAMBIÉN.** Esto globeaba SÓLO `recipes/*.toml` y remataba con «✅ toda receta que
|
|
# declara link=static lo cumple» — una frase verdadera de una PARTE del corpus presentada como si
|
|
# fuera de todo él. Las cinco colas (`incoming{,-kde,-gnome,-cosmic,-wlr}`) son ~305 recetas que
|
|
# NUNCA se auditaron. Se destapó promoviendo `libseccomp` de `incoming-gnome/` al corpus el
|
|
# 2026-09-11: el artefacto no cambió ni un byte y el audit pasó de MIENTEN:0 a MIENTEN:1. O sea
|
|
# que la mentira estaba ahí desde siempre y lo único que la tapaba era el glob de esta línea.
|
|
# Un guardián que mide menos de lo que su resumen afirma es peor que no tenerlo: da por
|
|
# cubierto lo que no mira. Ver [[guardian-que-nunca-fallo]].
|
|
recetas="$(grep -lE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' \
|
|
recipes/*.toml recipes/incoming*/*.toml 2>/dev/null)"
|
|
fi
|
|
|
|
# ── DEUDA MEDIDA Y DECIDIDA ────────────────────────────────────────────────────────────────────
|
|
# Mentiras REALES cuyo arreglo está decidido EN CONTRA por ahora. Se imprimen como `•` con el número
|
|
# delante y NO hacen fallar. El motivo de separarlas no es cosmético: si el audit falla siempre por
|
|
# tres viejas, un ofensor NUEVO se pierde entre el ruido y el guardián deja de servir — que es
|
|
# exactamente cómo se muere uno.
|
|
#
|
|
# Las tres salieron a la luz el 2026-09-11 al ampliar el barrido a las colas (antes sólo miraba
|
|
# `recipes/*.toml`). En las tres, lo dinámico es un BINARIO AUXILIAR de diagnóstico, no la librería:
|
|
# los consumidores enlazan el `.a`, así que el impacto funcional hoy es NULO. Lo que está mal es la
|
|
# DECLARACIÓN, y arreglarla se paga en rebuilds (radio medido con `yupana radio`, que cruza colas):
|
|
#
|
|
# libseccomp scmp_sys_resolver 6 rebuilds (corpus 1 + incoming-gnome 5)
|
|
# libgcrypt dumpsexp y 2 más 41 rebuilds (corpus 1 + incoming-kde 40)
|
|
# libgpg-error gpg-error 45 rebuilds (corpus 4 + incoming-kde 41)
|
|
#
|
|
# ~92 rebuilds, casi todos KDE, para que tres binarios que nadie invoca dejen de ser dinámicos. Va
|
|
# con el próximo bump de cada una, cuando el re-hash ya esté pagado y la línea cueste cero — el mismo
|
|
# criterio que la deuda de `perl`. Esto no es un olvido: es una decisión con el número delante.
|
|
DEUDA_DECIDIDA=" libseccomp libgcrypt libgpg-error "
|
|
|
|
mienten=0; honestos=0; sin=0; deuda=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. `takana 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 `takana` no está compilado: el audit sigue corriendo, sólo vuelve a
|
|
# poder sobre-reportar (como antes de que existiera `takana 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
|
|
case "$DEUDA_DECIDIDA" in
|
|
*" $n "*)
|
|
printf ' • %-18s DEUDA DECIDIDA: %d/%d ejecutable(s) dinámico(s) (%s) — auxiliar, no la librería\n' \
|
|
"$n" "$malos" "$vistos" "$primer_malo"
|
|
deuda=$((deuda + 1)); continue ;;
|
|
esac
|
|
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 (nuevos): $mienten | deuda decidida: $deuda | 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 " ✅ ninguna receta NUEVA miente (corpus + las 5 colas). Las $deuda de deuda decidida están arriba, con su radio."
|
|
exit 0
|