`vigia-parches.py --all` sobre las 99 aplicaciones de parche del catálogo: **0 FALLA**, y 14 líneas
FUZZ (8 pares parche/fuente distintos). FUZZ significa que `patch` metió el cambio ADIVINANDO dónde
porque el contexto no casaba, y si cayó en el sitio correcto sólo lo dice el diff. Nadie los había
mirado. Los miré todos:
· **libxml2 / CVE-2026-6732** — el que más importaba, un parche de seguridad con fuzz 2 en sus dos
hunks. Cayó bien: los dos dentro de `xmlParseReference()`, las 4 llamadas pasan `ctxt->userData`
y no sobrevive ningún `sax->characters(ctxt,` en el fichero. El fuzz era por un offset de 226
líneas, no por el sitio.
· **doas / rowhammer** — el otro sensible, toca la decisión de privilegio. Cae dentro de
`checkconfig()` y queda `rv=permit(...); if(rv==0)→permit`, coherente con el `if(rv!=0)→EPERM`
de `main()`. Y el fuzz lo causa un parche ANTERIOR de la propia cadena (el `#ifdef DOAS_CONFDIR`
que inserta `configuration-directory.patch`), no un cambio de upstream — que es una causa que no
se me habría ocurrido sin abrirlo.
· **wayland**, **mesa** (×3), **cairo** (×2), **firefox** (time64 y fix-rust-target): todos en su
sitio, cada uno comprobado contra lo que el propio parche declara querer.
· **gnupg / 0001-include-unistd** — hallazgo: el parche está OBSOLETO. Añade `#include <unistd.h>`
y upstream YA lo trae dos líneas más abajo, así que sólo lo duplica. Inocuo (el header tiene
guardas) y por eso el fuzz 2: cambió el contexto porque upstream lo incorporó. Quitarlo re-hashea
gnupg, así que se paga cuando se re-selle por otro motivo.
Y para que esto no se repregunte en cada corrida —lo que vuelve ruido al vigía, y así es como se
pierde el FALLA del día que aparezca— los veredictos van a `docs/state/fuzz-verificado.tsv` con un
cuarto estado, FUZZ✓.
La clave del libro NO es (receta, parche) sino (receta, parche, HUELLA), donde la huella resume el
texto del parche MÁS el pin de la fuente. Tocá el parche o subí la versión y la huella cambia, la
entrada deja de casar y el vigía vuelve a preguntar. Es lo contrario de una lista de excepciones: no
hay forma de silenciar algo y que siga silenciado cuando cambió. Y el vigía imprime la línea lista
para pegar debajo de cada FUZZ sin verificar, porque calcular la huella a mano es justo la fricción
que hace que nadie lo anote.
Los dos FUZZ de waterfox quedan FUERA del libro a propósito: no los verifiqué en esta ronda y
waterfox está fuera de alcance. Van a seguir saliendo como FUZZ, que es lo honesto — el libro dice
lo que se miró, no lo que se supone.
`scripts/vigia-sonames.py` sobre los cinco perfiles marcaba `liblzma.so.5` en GNOME pedido por
`libadwaita` y por `python3`. La columna de QUIÉN lo pide es la que decide, y acá decidió fuerte:
mirando el artefacto sellado, el `NEEDED` no lo lleva un binario suelto sino
**`usr/lib/libadwaita-1.so.0`**, que es la librería contra la que enlaza toda app GTK4/GNOME de la
imagen. Sin proveedor del SONAME el loader falla en TODAS, no en una.
Misma fuga de siempre —el `xz` canónico del corpus es `.a`, así que el rootfs lo resolvía contra el
sysroot Alpine DEL LAB— y por eso invisible al store: el lab no entra en `hash_inputs`.
La receta `-shared` existía sólo en `incoming-kde`. Se promueve al corpus con el mismo procedimiento
que `bzip2-shared` el 2026-09-03, y comprobando lo mismo antes de creerlo: el hash es idéntico desde
las dos rutas (b3:174289d5) ⇒ un solo artefacto y cero rebuilds. Si hubiera diferido, sería otra
receta y no valdría la promoción.
Va sólo en GNOME: en cosmic y sway ese soname lo pide únicamente `python3`, que es herramienta de
build y no viaja en la imagen.
GNOME pasa de 3 sonames sin proveedor a 2. De los dos que quedan, `libperl.so` es de `perl` (build),
y `libreadline.so.8` lo pide `usr/bin/sqlite3` —el shell de la CLI, NO `libsqlite3.so.0`—, así que
las apps están bien y lo roto es el comando. Queda anotado como verruga, no como rotura.
`scripts/vigia-sonames.py` sobre los cinco perfiles: `escritorio-mirada` era el único que aún tenía
la fuga al sysroot del lab en componentes de RUNTIME. `mesa-swrast` y `wayland` salen con
`NEEDED libexpat.so.1`, `libz.so.1` y `libffi.so.8`; las recetas canónicas de expat, zlib y libffi
son `--disable-shared`; ningún artefacto del cierre publicaba esos SONAME. O sea que el rootfs los
resolvía contra el Alpine DEL LAB y el USB sólo arrancaba donde hubiera Alpine debajo — que es peor
que una dep faltante, porque el lab no entra en `hash_inputs` y el store no puede ni notarlo.
Es la misma fuga que los otros cuatro perfiles cerraron el 2026-09-03. mirada quedó fuera porque su
lista nació como copia literal del PKGS de `mirada-usb.sh` y nadie la revisó desde entonces. La
dirección hoy está invertida —el script deriva su PKGS de `targets.py escritorio-mirada`— así que
arreglarlo acá arregla el USB, y no hay dos listas que puedan divergir. Actualicé el comentario, que
seguía diciendo «lift verbatim».
Las tres viven en `corpus`, la misma cola del perfil, y ya están selladas ⇒ CERO rebuilds: sólo
entran a la clausura.
NO sumo `bzip2-shared` ni `ncurses-shared`, que sí llevan los otros perfiles: acá esos sonames los
pide únicamente `python3`, que es herramienta de build y no viaja en la imagen. El vigía imprime
siempre QUIÉN pide cada soname justamente para poder separar eso; sin esa columna su informe no se
puede triar y uno acaba tapando ruido.
Verificado: mirada pasa de 10 sonames sin proveedor a 7, y los 7 que quedan son todos de `python3` y
`perl`.
Hasta ahora atuq estaba sellado y NO estaba en ninguna imagen. Es exactamente la lección que este
fichero ya aprendió con `foot` en el perfil de sway: una receta sellada que ningún perfil declara no
la lleva nadie, y la métrica de clausura no lo puede ver porque mide lo DECLARADO.
Va en los cuatro por el mismo motivo que `mpv` y desde el mismo sitio: vive en el CORPUS, y una
receta resuelve sibling-first y después el catálogo padre, así que las cuatro colas lo alcanzan.
Arrastra la cadena GTK3 en sus variantes `-shared`, y el comentario lo dice: con las estáticas,
libgtk-3 y libgdk-3 se llevaban cada una su copia de pango/cairo y el navegador no llegaba a pintar.
NOTA sobre la métrica: el grafo del hub sólo reporta base/cli/escritorio-mirada — los cuatro
escritorios no aparecen en `by_profile` porque sus raíces viven en las colas. Es previo a este
cambio y no lo introduce; queda anotado porque significa que esta declaración NO se ve todavía en
`build-state.json`.
El SDD 26 estimó que habilitar PGO/LTO exigía una receta `llvm-toolchain` (clang+lld+libc++ desde
fuente). ERA CARO DE MÁS. Al mirar el lab en vez de suponerlo:
- `.dev-fs/alpine` YA TRAE clang22 + llvm22 22.1.8 — la MISMA major que usa el APKBUILD de Alpine
para este mismo Firefox (`_llvmver=22`).
- `Compiler::Clang` YA EXISTE en hammer, cableado de punta a punta: `parse_compiler` lo acepta y
`hammer-build/src/lib.rs` pone CC=clang, CXX=clang++ y AR=llvm-ar. NINGUNA receta lo usaba.
- Lo único que faltaba era `ld.lld`. `apk add lld` ⇒ lld22 22.1.8, dos paquetes, cero upgrades.
CON ESO CAEN LOS TRES MUROS QUE OBLIGABAN A gcc, sin perder lo que gcc daba: el sondeo de linker se
satisface con `--enable-linker=lld`, el `ar` lo pone hammer solo, y el `NEEDED` de la stdlib de C++
existe porque clang++ de Alpine usa la libstdc++ COMPARTIDA — la prueba no es teórica, Alpine
construye este Firefox con clang22 y sin libcxx en sus makedepends.
LA HUELLA DEL LAB NO SE MOVIÓ, Y SE MIDIÓ ANTES DE TOCAR NADA. `lld` no casa ningún prefijo de
TOOLCHAIN_PREFIXES (hammer-core/src/lab.rs), así que los 43 paquetes que entran en `hash_inputs`
salieron idénticos ⇒ los 837 artefactos sellados quedan intactos. Eso es lo que hace barato el
cambio HOY, y a la vez es un agujero escrito en los dos sitios: la versión de lld no es parte de la
identidad del artefacto, y sólo expone a las recetas `compiler="clang"`, que hoy es una. Meter
"lld" en la lista de prefijos es lo correcto y cuesta re-hashear el corpus entero: próxima campaña.
En el mozconfig entran, además de lld: `--enable-lto=cross`, `--enable-packed-relative-relocs` y
`--with-unsigned-addon-scopes=app,system`. El último no es cosmético: sin él un Firefox de release
rechaza las extensiones que la distro deja en distribution/extensions/, así que la capacidad de atuq
de shipear su propio `sct` se decide ACÁ, en la base, y no en el overlay del derivado.
PGO no entra en esta pasada y el porqué queda escrito en la receta: el perfil se junta corriendo el
navegador (Alpine y Arch usan xvfb-run; nosotros no tenemos X11 ⇒ sway headless) y el profdata NO es
determinista, así que tiene que sellarse como artefacto propio y consumirse por hash.
ThinLTO se capa con la misma cuenta que -j y por la misma razón que ella no es un literal: un número
fijo ataría el ArtifactHash a la RAM de quien escribió la receta.
Nuevo ArtifactHash: b3:6f2a3b2f6db4452ed0d2d3f4e2ff7cd6562a86878d4360653859720be1c3d94d
(el firefox 154.0 sellado con gcc queda SUPERADO, no perdido).