bzip2: cuatro comandos rotos desde siempre — /usr/bin/bzcmp -> /out/usr/bin/bzdiff
`bzcmp`, `bzegrep`, `bzfgrep` y `bzless` son symlinks con **la ruta del SANDBOX horneada dentro**. El Makefile de bzip2 los crea con `ln -s $(PREFIX)/bin/bzdiff bzcmp` —con `$(PREFIX)` DENTRO del destino— y como bzip2 **no soporta `DESTDIR`**, la receta pasa `PREFIX=/out/usr` (el mismo truco que valkey) ⇒ el destino que queda es `/out/usr/bin/bzdiff`. Al hidratar, esos cuatro apuntan a un directorio que en el sistema real no existe. ⚠ **Cinco indicadores en verde sobre cuatro comandos que no funcionan**: el artefacto tiene contenido, `bzip2`/`bzgrep`/`bzdiff` corren, `static-audit` pasa, el grafo lo cuenta y la receta REPRODUCE. Ninguna métrica existente mira a dónde apunta un enlace. Arreglo: rehacerlos RELATIVOS después del install, que es lo correcto para un artefacto direccionado por hash — un enlace relativo sigue valiendo esté el árbol montado donde esté. Verificado: los cuatro resuelven y `bzip2 -c | bzcat` sigue dando `hola`. ## El guardián, en el mismo sitio y por la misma pregunta `--auditar-raices` ya contestaba «¿esta receta dejó ficheros donde no van?». Ahora contesta también la otra mitad: «¿dejó ENLACES a una raíz que no es del FHS?». Son la misma familia —un `install` que se equivocó de destino— y comparten la definición de `FHS_RAIZ`, que es lo que evita dos listas que se desincronizan. La regla es independiente del perfil, y por eso vale sobre el store entero: un enlace a OTRO artefacto es normal (`kinfocenter -> /usr/bin/systemsettings`, los dos en `escritorio-kde`, resuelve en el rootfs fundido). Lo que nunca puede estar bien es un destino absoluto cuya primera componente no sea del FHS: `/out`, `/src`, `/tmp` son rutas del lab. Dos correcciones al propio guardián, las dos aprendidas midiendo: · **Sólo audita el artefacto VIGENTE de cada receta.** El store guarda todos los sellados históricos, así que la primera versión acusaba a bzip2 por el artefacto YA SUPERADO — el mismo sobre-reporte que `static-audit.sh` tuvo que quitarse de encima. El hash vigente sale de los grafos de estado, no de 1150 llamadas a `takana hash`. ⚠ Y hay que regenerar **los cinco** grafos: con sólo el principal regenerado, el de KDE seguía apuntando al bzip2 viejo y el filtro lo dejaba pasar. · **Un barrido que no miró NADA lo dice y sale 2.** Filtrar por hash vigente hace que un `--store` que no case con los grafos deje cero artefactos auditados, y sin la guarda eso se imprimía como «✓ 0 ofensores»: una respuesta falsa con forma de respuesta. Probado: `--store /tmp` → exit 2. El control del hallazgo es el propio arreglo, sobre datos reales y no sintéticos: antes del fix el barrido nombra los cuatro enlaces de bzip2; después, `✓ ninguno`.
This commit is contained in:
+23
-1
@@ -28,4 +28,26 @@ build = ["binutils", "make"]
|
||||
# jq/parted/shadow/procps-ng, que sí linkean con libtool); bzip2 usa Makefile crudo, así que el
|
||||
# flag llega tal cual al compilador → "error: Unknown Clang option: '-all-static'".
|
||||
compile = 'make CC="$CC" LDFLAGS="-static -no-pie"'
|
||||
install = 'make install PREFIX=/out/usr CC="$CC"'
|
||||
# ⚠ **LOS CUATRO SYMLINKS SALEN ROTOS SI NO SE REHACEN, Y EL ARTEFACTO NO LO DICE.** El `install`
|
||||
# del Makefile de bzip2 los crea así:
|
||||
#
|
||||
# cd $(PREFIX)/bin; ln -s -f $(PREFIX)/bin/bzdiff bzcmp
|
||||
#
|
||||
# — con `$(PREFIX)` DENTRO del destino. Como bzip2 **no soporta `DESTDIR`** (por eso acá va
|
||||
# `PREFIX=/out/usr`, el mismo truco que valkey), el destino que queda horneado es
|
||||
# `/out/usr/bin/bzdiff`: la ruta del SANDBOX. Al hidratar, `/usr/bin/bzcmp` apunta a un `/out` que no
|
||||
# existe en el sistema real ⇒ `bzcmp`, `bzegrep`, `bzfgrep` y `bzless` no funcionan.
|
||||
#
|
||||
# Nada lo delata: el artefacto tiene contenido, `bzip2` y `bzgrep` corren, el audit de enlace
|
||||
# estático pasa y la receta REPRODUCE. Se ve buscando en el store symlinks que no resuelven dentro de
|
||||
# su propio artefacto (2026-09-12; los únicos dos casos del corpus eran éste y uno legítimo de KDE,
|
||||
# que apunta a otro artefacto del MISMO perfil).
|
||||
#
|
||||
# Se rehacen RELATIVOS, que es lo correcto para un artefacto direccionado por hash: un enlace
|
||||
# relativo sigue valiendo esté el árbol montado donde esté.
|
||||
install = '''
|
||||
make install PREFIX=/out/usr CC="$CC"
|
||||
for par in bzcmp:bzdiff bzegrep:bzgrep bzfgrep:bzgrep bzless:bzmore; do
|
||||
ln -sf "${par#*:}" "/out/usr/bin/${par%%:*}"
|
||||
done
|
||||
'''
|
||||
|
||||
@@ -82,17 +82,84 @@ def auditar_raices(store):
|
||||
arreglarlo: los únicos que quedan son `perl` (deuda medida y decidida, ver `recipes/perl.toml`) y
|
||||
los tres rootfs/seed, que ponen ahí lo suyo por contrato."""
|
||||
import re as _re
|
||||
filas = []
|
||||
for d in sorted(glob.glob(os.path.join(store, "*"))):
|
||||
m = _re.match(r"^[0-9a-f]{64}-(.+)$", os.path.basename(d))
|
||||
if not m or not os.path.isdir(d):
|
||||
import json as _json
|
||||
|
||||
# ⚠ **SÓLO EL ARTEFACTO VIGENTE DE CADA RECETA.** El store guarda TODOS los sellados históricos
|
||||
# (bzip2 tenía dos al arreglarlo), así que auditarlos todos acusa a recetas YA SANAS por un
|
||||
# sellado viejo — el mismo sobre-reporte que `static-audit.sh` tuvo que quitarse de encima
|
||||
# («auditábamos el más reciente por mtime»). Un guardián que grita sobre lo ya arreglado se
|
||||
# empieza a ignorar.
|
||||
# El hash vigente no se recalcula con `takana hash` receta a receta (1150 procesos): sale de
|
||||
# los grafos de estado, que es donde el repo YA lo tiene escrito, y cubre a la vez el corpus y
|
||||
# las cinco colas.
|
||||
vigentes = set()
|
||||
for g in glob.glob(str(ROOT / "docs/state/build-state*.json")):
|
||||
try:
|
||||
for nodo in _json.load(open(g)).get("nodes", {}).values():
|
||||
h = nodo.get("hash")
|
||||
if h:
|
||||
vigentes.add(h[3:] if h.startswith("b3:") else h)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
|
||||
filas = []
|
||||
mirados = 0
|
||||
for d in sorted(glob.glob(os.path.join(store, "*"))):
|
||||
m = _re.match(r"^([0-9a-f]{64})-(.+)$", os.path.basename(d))
|
||||
if not m or not os.path.isdir(d) or (vigentes and m.group(1) not in vigentes):
|
||||
continue
|
||||
mirados += 1
|
||||
try:
|
||||
extra = sorted(e for e in os.listdir(d) if e not in FHS_RAIZ)
|
||||
except OSError:
|
||||
continue
|
||||
if extra:
|
||||
filas.append((len(extra), m.group(1), extra))
|
||||
filas.append((len(extra), m.group(2), extra))
|
||||
# ── SEGUNDA PREGUNTA, LA MISMA FAMILIA: symlinks que apuntan a una raíz que NO ES EL FHS ─────
|
||||
# Un `install` se puede equivocar de destino de dos maneras. Una deja ficheros donde no van (lo
|
||||
# de arriba). La otra deja **enlaces** con la ruta del SANDBOX horneada dentro:
|
||||
#
|
||||
# usr/bin/bzcmp -> /out/usr/bin/bzdiff
|
||||
#
|
||||
# El Makefile de bzip2 crea sus symlinks con `ln -s $(PREFIX)/bin/bzdiff`, y como bzip2 no
|
||||
# soporta `DESTDIR` la receta pasa `PREFIX=/out/usr` ⇒ el `/out` queda DENTRO del destino. Al
|
||||
# hidratar, esos cuatro enlaces apuntan a un directorio que en el sistema real no existe.
|
||||
#
|
||||
# ⚠ Nada lo delata: el artefacto tiene contenido, los binarios de verdad corren, el audit de
|
||||
# enlace estático pasa y la receta REPRODUCE. Cinco verdes sobre cuatro comandos rotos.
|
||||
#
|
||||
# **La regla es independiente del perfil, y por eso se puede aplicar al store entero**: un enlace
|
||||
# a otro ARTEFACTO es normal y correcto (`kinfocenter -> /usr/bin/systemsettings`: los dos están
|
||||
# en `escritorio-kde` y en el rootfs fundido resuelve). Lo que NUNCA puede estar bien es un
|
||||
# destino absoluto cuya PRIMERA componente no sea del FHS — `/out`, `/src`, `/tmp`: ésas son
|
||||
# rutas del lab, no del sistema que se instala.
|
||||
malos_enlaces = []
|
||||
for d in sorted(glob.glob(os.path.join(store, "*"))):
|
||||
m = _re.match(r"^([0-9a-f]{64})-(.+)$", os.path.basename(d))
|
||||
if not m or not os.path.isdir(d) or (vigentes and m.group(1) not in vigentes):
|
||||
continue
|
||||
for root, dirs, files in os.walk(d):
|
||||
for e in files + dirs:
|
||||
p_ = os.path.join(root, e)
|
||||
if not os.path.islink(p_):
|
||||
continue
|
||||
t = os.readlink(p_)
|
||||
if not t.startswith("/"):
|
||||
continue # relativo: siempre correcto en un CAS
|
||||
raiz = t.lstrip("/").split("/", 1)[0]
|
||||
if raiz not in FHS_RAIZ:
|
||||
malos_enlaces.append((m.group(2), os.path.relpath(p_, d), t))
|
||||
|
||||
# ⚠ **UN BARRIDO QUE NO MIRÓ NADA TIENE QUE DECIRLO.** Filtrar por hash vigente significa que un
|
||||
# `--store` que no case con los grafos (otra máquina, otro checkout, un store de prueba) deja
|
||||
# CERO artefactos auditados — y sin esta guarda eso se imprimiría como «✓ 0 ofensores», que es
|
||||
# una respuesta falsa con forma de respuesta. Es la regla de siempre: un ausente falla ruidoso.
|
||||
if vigentes and mirados == 0:
|
||||
print("!! 0 artefactos auditados: ninguno de los %d hashes vigentes está en `%s`.\n"
|
||||
" ¿`--store` apunta a otro sitio, o los grafos están sin regenerar?"
|
||||
% (len(vigentes), store))
|
||||
return 2
|
||||
|
||||
print("== raíces sucias en el STORE ENTERO ==")
|
||||
nuevos = []
|
||||
for n, nombre, extra in sorted(filas, reverse=True):
|
||||
@@ -107,6 +174,15 @@ def auditar_raices(store):
|
||||
continue
|
||||
print(" ✗ %-22s %4d (%s)" % (nombre, n, muestra))
|
||||
nuevos.append(nombre)
|
||||
print("\n== symlinks con una raíz que NO es del FHS (ruta del lab horneada) ==")
|
||||
if not malos_enlaces:
|
||||
print(" ✓ ninguno")
|
||||
else:
|
||||
for nombre, origen, destino in malos_enlaces:
|
||||
print(" ✗ %-18s %s -> %s" % (nombre, origen, destino))
|
||||
if nombre not in nuevos:
|
||||
nuevos.append(nombre)
|
||||
|
||||
if not nuevos:
|
||||
print("\n ✓ 0 ofensores NUEVOS sobre %d artefactos." % len(filas)
|
||||
if filas else "\n ✓ ningún artefacto pone nada fuera del FHS en su raíz")
|
||||
|
||||
Reference in New Issue
Block a user