6b469a81d2df36d069b67f6528234dc373f16816
450
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
639948f2cd |
atuq: la página de inicio y la pestaña nueva, verificadas contra el propio motor
La v0.3 las puso por extensión de sistema y quedó como afirmación. Que el XPI
esté en el artefacto no dice nada: el override lo puede rechazar el gestor de
extensiones, lo puede pisar una política, o el navegador puede arrancar con el
chrome viejo cacheado. Ninguna de las tres falla ruidosamente — se ve una pestaña
nueva perfectamente normal, que es de otro.
Se mide preguntándole AL MOTOR, no mirando el disco: una sonda con permiso
`browserSettings` lee `homepageOverride` y `newTabPageOverride`.
positivo moz-extension://…/inicio.html (las dos)
control about:home · about:newtab
El control negativo borra el XPI de `inicio` dentro del overlay temporal —el
rootfs real no se toca— y exige el resultado contrario.
Y una escotilla `ATUQ_DIR` en las dos sondas nuevas, con su aviso a gritos: el
corpus es compartido y hoy mismo el perfil PGO v2 de otro frente re-hasheó
`firefox` y con él `atuq`, así que el artefacto VIGENTE no existe en ningún store
hasta que alguien pague un build de horas. Sin escotilla no se puede correr una
sola prueba de atuq en esa ventana; con ella se corre contra un artefacto viejo A
SABIENDAS, y por eso el resultado no se puede citar como «atuq de hoy pasa».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
e74d449655 |
perfilar.sh: la corrida de entrenamiento PGO, con las tres defensas puestas
Estaba en un scratchpad y se perdía; ahora vive en el repo con el porqué de cada guarda, porque las tres se pagaron durante la primera corrida: 1. --unshare-pid en el bwrap. Sin él los hijos SOBREVIVEN al sandbox: un http.server quedó vivo 19 HORAS, retuvo el puerto y envenenó las corridas siguientes con «Address in use». 2. Puerto aleatorio por corrida. Elimina la colisión de raíz en vez de detectarla: si otro proceso tiene un puerto, este arranque usa otro. 3. Marcador único por corrida, servido desde una copia ESCRIBIBLE del corpus. La versión anterior horneaba el marcador como constante del script, y eso rompía justo lo que el chequeo existe para probar: el servidor huérfano servía el MISMO fichero de marca y el chequeo lo daba por bueno. Verificaba «alguien sirve este contenido», no «este servidor es el mío». Con el marcador por corrida, un servidor ajeno devuelve otra cosa y se aborta. Y queda escrito por qué los sandboxes de Firefox van apagados en el entrenamiento: con ellos los hijos mueren con signal 11, el proceso que renderiza no nace, el servidor registra 0 GET y el único .profraw es el del padre arrancando — un perfil de nada, con todo en verde. Es lo que hace el profileserver.py de Mozilla. Sólo aplica al entrenamiento. La espera al servidor va con reintento y no con un sleep fijo: 2 s concluían «no responde» sobre uno que sí iba a responder. |
||
|
|
c13be792ab |
atuq: el camino de native messaging EXISTE — sonda, y tres cosas que no eran obvias
Todo el §6 del SDD 26 —sct, descargas al CAS, archivo con RAG, torrent— pasa por
un solo mecanismo: un proceso Rust hablando native messaging con la extensión.
Antes de escribir ese crate en tawasuyu conviene saber si el camino existe en
NUESTRO build, que es propio, rebrandeado y con MOZ_REQUIRE_SIGNING vacío.
Existe. Una extensión de diez líneas y un host de tres:
positivo MENSAJE {"ok":true}
control DESCONECTADO No such native application puente_atuq
El control no sólo falla: NOMBRA la causa, que es lo que confirma que la ruta del
manifiesto es la que se probó.
TRES COSAS MEDIDAS QUE NO ERAN OBVIAS:
1. El manifiesto va en `/usr/lib/mozilla/native-messaging-hosts/`, NO en el
appdir. Gecko lo busca por `XRESysNativeManifests`, que en Linux sale de un
`/usr/lib/mozilla` compilado, y el rebranding a atuq no lo mueve.
2. Un host que escribe y SALE pierde el mensaje. La primera sonda hacía printf y
terminaba: el puerto llegaba a onDisconnect «sin error y sin mensaje», o sea
el peor informe posible — parece que el camino no existe. Con un sleep detrás
del printf, el mensaje aparece. El host de verdad es un proceso largo, así que
en producción no se nota; en una prueba, sí.
3. `ExtensionSettings.install_url` con `file://` NO instala nada, y sin una línea
de log. Probado con normal_installed y con force_installed, y con el XPI dentro
y fuera del appdir: ninguna instala. Lo que instala las extensiones de atuq es
el ESCANEO de `distribution/extensions/`. La política sirve para fijarlas y
configurarlas; leerla como «esto es lo que las instala» es un error fácil,
porque los dos mecanismos apuntan a los mismos ficheros y se tapan uno al otro.
`sendNativeMessage` no sirve para diagnosticar: devuelve «An unexpected error
occurred» para todo. El error del puerto de `connectNative` es el único que
nombra la causa.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
8a0496dbb3 |
corpus de maquetación para el PGO: 10 páginas nuestras
Medida la ganancia del PGO ayer: -9,4% en una página de DOM/maquetación y -1,0% en 3d-raytrace de SunSpider. La razón de la diferencia es que un benchmark JIT-bound es casi CIEGO al PGO: el bucle caliente no lo ejecuta el C++ de SpiderMonkey sino el código máquina que el JIT emite en runtime, y el PGO optimiza el intérprete, el GC y el propio JIT — no lo que el JIT produce. El corpus de Mozilla está dominado en número de páginas por SunSpider, o sea que entrenaba mucho justo donde el PGO menos rinde. Estas diez ejercitan el camino que sí es C++ de punta a punta: flex flexbox anidado, wrap, alineaciones rejilla CSS Grid: pistas, áreas nombradas, auto-fit tablas tablas grandes con table-layout FIJO y AUTO (dos algoritmos) texto columnas, shaping, reflujo por cambio de ancho selectores DOM profundo contra 600 reglas, muchas que NO casan pintado degradados, sombras, opacidad, mix-blend-mode transformar transforms y contextos de apilamiento svg paths, degradados, clip desbordes overflow anidado, position:sticky, scroll programático reflujo layout thrashing: leer y escribir geometría alternadamente Reglas que cumplen todas, y no son de estilo: · DETERMINISTAS (LCG propio, ni Math.random ni Date ni red) — dos corridas hacen lo mismo, así que dos perfiles difieren por el timing de los contadores y no por haber visitado caminos distintos. · AUTOCONTENIDAS — el perfilado corre sin red. · NUESTRAS — nada de páginas ajenas capturadas, que traerían licencia y fragilidad. · ACOTADAS — 16 a 32 s cada una; el perfilado arranca un navegador POR PÁGINA. Validadas las diez contra el firefox sellado: todas renderizan y producen captura. `flex` hubo que acortarla de 40 a 12 cajas raíz: con 40 la página medía 68.241 px de alto y la captura moría con «Failed to allocate a surface due to invalid size». El layout ocurría igual, pero sin captura no se ejercita el pintado, que es justo la mitad que se venía a entrenar. |
||
|
|
7d0dc72de0 |
test-atuq-ruteo: los puertos fijos daban un fallo del producto que no existía
Esta prueba dio «NO concluyente — la pestaña sin contenedor no salió directa»
dos veces seguidas, y la causa no tenía nada que ver con atuq: **otro frente de
esta misma máquina tenía levantado un `python3 -m http.server 8099`** desde hacía
dos horas. La oreja del destino no podía atarse, no llegaba nada, y el informe
publicaba un fallo inexistente. En un repo que comparten varios agentes, un
puerto fijo es estado compartido sin dueño.
Y había un segundo bug que es el que lo hizo dañino: el `bind` vivía DENTRO del
hilo de la oreja, donde `fatal()` no puede matar el proceso — `SystemExit` en un
hilo secundario sólo termina ese hilo. Así que la prueba imprimía «el puerto está
ocupado, la medición no valdría nada» y **seguía adelante hasta publicar un
veredicto**. Un guardián que avisa de que no puede medir y mide igual es peor que
uno que no mide.
Ahora las dos orejas se atan en el hilo principal, con el puerto 0: lo elige el
kernel. Si no hay puertos, no hay prueba.
Con eso, y contra el atuq de hoy (d36ae188, el del arreglo del LD_LIBRARY_PATH):
positivo destino 40843 · proxy 38503 → GET /directo al destino, SOCKS5 al proxy
control destino 40917 · proxy 37113 → las dos directas, cero al proxy
O sea que el ruteo por contenedor del §6.8 sigue en pie y no lo rompió nada de
hoy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
ec45c3fa97 |
dunst-headless: --via-atuq — la cadena entera, empezando en una página web
El modo por defecto prueba la cadena del SISTEMA (`notify-send` → bus → dunst).
Éste prueba la que motivó todo el hilo del §6.10: una página llama a
`new Notification(...)`, `libxul` hace `dlopen("libnotify.so.4")` —que no es
NEEDED de ningún ELF, o sea invisible para cualquier auditor—, eso habla D-Bus,
el bus ACTIVA dunst y dunst dibuja. Cinco piezas, y la única forma de saber que
están las cinco es verlo.
El veredicto acá no puede ser el diff de píxeles solo: el navegador ocupa la
pantalla y repinta por su cuenta. Lo decisivo es el `onshow` del objeto
Notification, que es el motor diciendo que el sistema ACEPTÓ la notificación; el
diff queda como corroboración y la captura «antes» se toma con atuq ya pintado.
--via-atuq MOSTRADA #33 · 151.174 píxeles
--via-atuq --negative-control «ERROR al mostrar» · 0 píxeles
El control negativo también se lee distinto según el modo, y no por comodidad:
sin el .service, lo que tiene que faltar en el modo navegador es el onshow.
Captura en docs/evidencia/atuq-notificacion-web-sway-2026-09-07.png: dos globos
de dunst sobre la ventana de atuq, en sway headless, sin que nadie haya lanzado
el daemon a mano.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
4f9ee5430f |
dunst: sway ya atiende las notificaciones, y hay un píxel que lo prueba
Era la única mitad que quedaba abierta del mapa del §6.10: KDE atiende
`org.freedesktop.Notifications` con plasma-workspace, GNOME con gnome-shell y
COSMIC con cosmic-notifications; sway no tenía a NADIE, así que una página que
pedía notificar mandaba el mensaje al bus y se lo comía el silencio.
`dunst` 1.12.2 y no `mako` —que es el de la casa wlroots— por dos razones
medidas: el nombre `mako` YA ESTÁ OCUPADO en el corpus por el motor de plantillas
de mesa, y el cierre del perfil incluye las herramientas de build, así que la
imagen terminaría con dos artefactos homónimos peleando por las mismas rutas; y
mako habla D-Bus por sd-bus, que en musl es una receta nueva (basu), mientras
dunst habla por GDBus, que ya está sellado. Cero dependencias nuevas.
DOS GUARDIANES EN LA PROPIA RECETA, porque las dos cosas que importan no se ven
en que compile:
- `X11=0` es una intención; el hecho se lee en el ELF. dunst compila los dos
backends por defecto y su config.mk avisa: sin wayland «forzará xwayland». En
esta distro eso sella un binario que arranca, toma el bus y NO PINTA NUNCA. La
fase exige `libwayland-client` entre los NEEDED y prohíbe `libX11`.
- El `.service` de D-Bus es la mitad que importa: sin systemd, al daemon lo
levanta EL BUS. Se comprueba que reclame `org.freedesktop.Notifications` y se
le saca la línea `SystemdService=`, que acá no puede significar nada.
Y `DUNSTIFY=0`: entró primero en 1 y el binario **segfaultea hasta en
`--help`** (rc=139 en las tres pruebas). La causa es el cuadro que este repo ya
tiene escrito: nuestro `libnotify.so.4` es compartido y arrastra la cadena glib
`.so`, mientras esta cola enlaza la GLib estática ⇒ dos copias de GObject en un
proceso. No se pierde nada: el emisor de la casa es `notify-send`, que viene
DENTRO del artefacto de libnotify y ya está en las cuatro imágenes. Hay un
guardián que frena el día que alguien vuelva a poner DUNSTIFY=1 sin leer.
LA PRUEBA, en `scripts/wlr/dunst-headless.sh`: sway headless + bus de sesión +
`notify-send`, y NADIE lanza dunst a mano — lo activa el bus por el .service,
que es el mismo camino de una página web en atuq. La evidencia es un diff de
píxeles antes/después acotado al cuadrante donde dunst dibuja, porque un log en
verde es compatible con una pantalla vacía y el color no distingue (swaynag pinta
su barra de error en el mismo rojo).
positivo 14832 píxeles cambiados · servidor: dunst knopwob 1.12.2 1.2
control 0 píxeles · ServiceUnknown · notify-send rc=1
El control negativo borra el .service dentro del overlay temporal y EXIGE que no
se dibuje nada: probado en los dos sentidos.
Captura en docs/evidencia/dunst-sway-notificacion-2026-09-07.png.
Vigía de sonames: escritorio-sway 203 → 204 nodos, 512 sonames, 0 sin proveedor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
f3aaab75c6 |
test-atuq-rootfs: el triaje decía «sin receta de ffmpeg» y era falso
La familia `libavcodec.so.*` estaba clasificada como hueco con el motivo «sin receta de ffmpeg en el corpus». `recipes/ffmpeg.toml` existe, está sellada, publica `libavcodec.so.61` —uno de los once sonames que sondea libxul— y ya viajaba en la clausura de los cuatro escritorios arrastrada por `mpv`. Pasa a ruido: lo que falta son las OTRAS versiones del soname, y Firefox recorre la lista hasta que una carga. `libva` igual: la receta está y ahora también en el rootfs del runner. Sigue siendo hueco, pero por la otra mitad —las tres mesa van con `-Dgallium-va=disabled` y `-Dvideo-codecs=` vacío, así que no hay un solo `*_drv_video.so` que cargar—, y el motivo ahora lo dice. Con eso el mapa pasa de 47 cadenas sin proveedor a 7 huecos reales. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
a44fc4f06a |
atuq: AV1 no reproducía, y la causa era UN elemento de LD_LIBRARY_PATH
Gecko SE COME EL PRIMER ELEMENTO de `LD_LIBRARY_PATH` al lanzar el proceso RDD
—el que decodifica vídeo—. El lanzador ponía `/usr/lib/atuq` una sola vez, o sea
primero, así que el RDD arrancaba sin él, no encontraba `libmozavcodec.so` /
`libmozavutil.so` (el ffvpx bundleado, donde vive dav1d) y anotaba:
PlatformDecoderModule FFVPX: Link result: NoProvidedLib
El navegador NO falla ahí: se cae al ffmpeg del sistema, que cubre
H.264/AAC/VP8/VP9/MP3/FLAC/Opus. Pero AV1 por software NO lo cubre —el
decodificador `av1` de ffmpeg es sólo hwaccel y nuestra receta va sin dav1d—, así
que un vídeo AV1 se quedaba en `readyState=1` para siempre, sin un error, sin un
NEEDED faltante y sin una cadena ausente. Media función apagada en silencio, que
es la forma de fallo de esta casa.
Tres corridas que sólo cambian esa variable, con el mismo artefacto:
LD=/usr/lib/atuq:/usr/lib:/lib → RDD NoProvidedLib AV1 ✗
LD=/usr/lib/atuq:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓
LD=/relleno:/usr/lib/atuq:/usr/lib → RDD Success AV1 ✓
El arreglo es repetir el appdir. Feo y correcto mientras no haya `patchelf` en el
corpus para grabar `RUNPATH=$ORIGIN`, que borraría la variable entera.
Y el guardián que sale de este punto ciego, `scripts/test-atuq-codecs.sh`: abre
cinco muestras versionadas en `scripts/fixtures/codecs/` y mira si `currentTime`
AVANZA — «se creó el decodificador» no es «decodifica». El veredicto sale por
`dump()` al stdout, así que no necesita ni red ni servidor, y corre headless para
que sirva en el worker. Con `--negative-control` se saltea el lanzador y EXIGE
que AV1 falle: probado en los dos sentidos, 5/5 y control ✓.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
|
||
|
|
b6d000eca3 |
atuq-nested: el runner medía un rootfs MÁS POBRE que la imagen, y su caché no cacheaba
Tres cosas en el mismo fichero, las tres medidas hoy: 1. `ffmpeg` y `libva` entran a las raíces. NO son recetas nuevas: las dos están selladas en el corpus y ya viven en la clausura de los cuatro escritorios, arrastradas por `mpv` (verificado con `yupana.membresia`, no leyendo el TOML). Faltaban acá, o sea que el runner abría un rootfs sin libavcodec y desde ahí se concluía que la imagen no tiene códecs. El instrumento otra vez, no el artefacto. 2. El bucle de hidratación salía 1 SIEMPRE: `$VIGENTES` termina en `\n`, `echo` agrega otro, la última vuelta lee la línea vacía, `[ -n "$d" ]` da 1 y con `set -e` el script moría sin imprimir una sola línea, justo después de hidratar bien las 41 raíces. Ahora `continue` en la vacía y un `hydrate` que falla grita. 3. La comparación del sello NUNCA daba igual: el lado izquierdo lleva su `\n` final y `$(cat …)` lo recorta ⇒ «DESACTUALIZADO» en cada corrida y 3013 ficheros rehidratados de más. Los dos lados pasan ahora por la misma sustitución de comandos. Y `ROOTFS_ONLY=1`, que corta después de hidratar: lo necesita el guardián de códecs, que corre headless y tiene que usar ESTA lista de raíces y no una copia. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs |
||
|
|
6886b1541c |
libnotify: el primer hueco que el mapa del §6.10 encontró Y cerró
`libxul.so` lleva la cadena `libnotify.so.4` adentro y la abre por dlopen cuando
una página pide permiso para notificar. Nadie en el corpus la proveía, así que la
función quedaba apagada SIN UN SOLO MENSAJE: el navegador arranca, la web pide
notificaciones, y no pasa nada. No lo veía ninguna herramienta porque no hay
NEEDED en ningún ELF — es el tercer escalón, y sobrevivió a vigia-sonames con los
cinco perfiles en CERO.
Receta nueva, 0.8.8, SÓLO en variante compartida y eso no es un olvido: a un
`dlopen("libnotify.so.4")` una `.a` no le sirve de nada, así que una receta
estática sería un artefacto que nadie puede consumir.
Dos cosas que la receta se comió y quedan escritas:
1. `libpng-shared` hace falta porque las deps de hammer NO son transitivas: el
`.pc` de gdk-pixbuf-2.0 declara `Requires: libpng` y el meson muere con un
mensaje que nombra a gdk-pixbuf —que sí está— en vez de a lo que falta.
2. El guardián informaba «18 bytes» y parecía una librería vacía: `stat -c%s` no
sigue el symlink, y `libnotify.so.4` apunta a `libnotify.so.4.0.0`. El
artefacto estaba bien y el MENSAJE mentía. Con `-L` son 162.928 bytes. Se
arregla el mensaje porque es lo que alguien va a leer a las tres de la mañana,
y de paso el guardián exige el fichero real, no sólo el nombre.
⚠ Y lo que NO arregla, escrito en la receta para que nadie lea de más: libnotify
no trae daemon, manda org.freedesktop.Notifications por D-Bus. Tenerla resuelve
la mitad —que firefox la encuentre—; la otra mitad es que en la imagen haya
alguien escuchando ese nombre.
Verificado con el propio mapa: `--dlopen` pasa de 25 huecos a 24 y libnotify.so.4
desaparece; el soname viejo `.so.1` se reclasifica de «hueco» a «ruido», que es lo
que ahora es. El guardián normal sigue en CERO con un soname más pedido (66).
Queda pendiente la membresía de perfil en `docs/state/targets.toml`, que es
catálogo compartido y no lo toco sin decidirlo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
7a81b6480c |
atuq §6.10: el tercer escalón — qué NO puede hacer el navegador, y por qué
hammer-9f nombró el punto ciego que ni su vigía ni mi guardián podían cubrir:
una librería que sólo aparece como CADENA LITERAL dentro de un dlopen(). No hay
NEEDED en ningún ELF, así que ningún auditor de readelf la encuentra. La
jerarquía queda:
NEEDED del ejecutable -> lo vemos los dos
NEEDED de un .so dlopeado -> lo vemos los dos
dlopen("libfoo.so.1") literal -> NO LO VE NINGUNO
Y un navegador vive de eso. Firefox sondea ffmpeg, VA-API, vulkan y libnotify
por nombre, y cuando no están NO FALLA: apaga la función y sigue. No hay línea
roja; hay una función que nadie ofrece y nadie reclama. Es la forma que ya costó
caro con OBS y su dlopen("libGL.so.1").
`--dlopen` busca esas cadenas y las cruza contra el rootfs. Es un HEURÍSTICO y se
declara como tal —una cadena no prueba un dlopen y su ausencia no prueba que no
lo haya—, así que no falla nunca: imprime un mapa triado. Lo afirmable es lo
contrario, que es lo útil: si la cadena está y el fichero no, esa función no
existe en esta imagen.
De 85 cadenas, 47 sin proveedor. Siete son huecos de verdad:
códecs del sistema (H.264/AAC) sin receta de ffmpeg — el más caro
notificaciones web sin receta
llavero (libsecret) RECETA YA EXISTE en incoming-gnome
sonidos (libcanberra) receta en incoming-kde
WebGPU (vulkan-loader) receta en incoming-kde
vídeo por hardware (VA-API) sin receta
lectura en voz alta sin receta
Tres de los siete son promoción, no autoría. Ocho son decisiones ya tomadas
(libGL por Wayland-only sin GLX; libcurl porque sólo lo usa el pingsender de
telemetría) y siete son ruido de musl o sonames viejos. El triaje va en una tabla
del propio script, no escondido en un `if`, porque es criterio y no medición: ahí
se puede discutir.
Sin triar: 0. Si aparece una cadena nueva, el informe la marca «SIN TRIAR» en vez
de tragársela.
Lo que esto cambia de fondo: hasta hoy la pregunta era «¿arranca?» y la respuesta
era sí. La que faltaba era «¿y qué NO puede hacer?», que ninguna métrica del repo
respondía porque todas miran presencia y ésta mira ausencia declarada por el
propio binario.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
a20d1b7453 |
atuq: un guardián para el rootfs del runner — y encontró SIETE sonames más cayendo al lab
La lista de raíces de `atuq-nested.sh` se mantiene A MANO y la jaula monta
`.dev-fs/alpine` como capa de abajo. Esas dos cosas juntas hacen que una raíz que
falte NO se note: el navegador arranca igual, resolviendo contra el lab. Ayer eso
costó `gcc-libs`. La regla del repo es que cada punto ciego se convierte en un
guardián, así que acá está el guardián en vez del parche.
`scripts/test-atuq-rootfs.py` mira el objeto que la jaula monta de verdad —el
directorio HIDRATADO— y no el grafo de recetas, que es lo que ya cubre
`vigia-sonames.py`. Son preguntas distintas: la lista del runner no sale del
grafo, así que el grafo no puede auditarla.
Lo primero que hizo fue encontrar SIETE sonames más que se estaban resolviendo
contra el lab, y no son cosmética:
libexpat.so.1, libzstd.so.1 <- los pide mesa (iris_dri, libEGL, libgbm)
libdbus-1.so.3 <- lo pide pipewire; lo trae `dbus-shared`, no `dbus`
libbz2.so.1 <- freetype
libudev.so.1 <- libspa-alsa
libsndfile.so.1, libncursesw.so.6
Los siete tienen proveedor en el corpus. Agregados a las raíces: el rootfs pasa
de 7 huecos a CERO, y la única excepción que queda es `libc.so`, que va en una
lista explícita porque ningún artefacto lo provee — las imágenes lo copian del
devfs. Si algún día hay receta que lo provea, esa lista se achica y el guardián
se vuelve más estricto solo.
CONTROL NEGATIVO incluido, que sin él esto no probaría nada:
`--negative-control` esconde libstdc++.so.6 y exige que el guardián lo cace.
Corrido: lo caza, y nombra a quién lo pide (atuq, atuq-bin).
Y `scripts/test-atuq-ruteo.py` vuelve a pasar entero contra el rootfs completo,
o sea que el ruteo por contenedor está probado ahora sobre un rootfs que no le
pide nada al lab salvo el intérprete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
7389d44569 |
atuq-nested: faltaba gcc-libs, y su ausencia hacía MENTIR a la prueba entera
`atuq` declara `NEEDED libstdc++.so.6` y `libgcc_s.so.1` —lo hereda de firefox, que va con clang++ y la libstdc++ COMPARTIDA— y su clausura ya estaba bien: `recipes/atuq.toml` declara `runtime = ["gcc-libs"]` desde |
||
|
|
2e093f9cab |
atuq §6.8: probado que el paquete sale por el proxy del contenedor — y con control negativo
Era lo único que la v0.5 dejó sin probar, y las tres puertas que había
encontrado siguen cerradas: no hay flag de línea de comandos para el
userContextId, Marionette no toma `-remote-allow-system-access` en la jaula, y
`tabs.create({cookieStoreId})` exige el permiso `cookies` — que no se le da a la
extensión del proxy sólo para que pueda probarse a sí misma.
La cuarta puerta estaba abierta: el fichero de SESIÓN guarda el userContextId de
cada pestaña y Gecko lo restaura. Se fabrica a mano — `mozLz40\0` + tamaño + un
bloque LZ4, y un bloque de sólo literales es LZ4 válido: veinte líneas, sin
librería.
Y la medición no le pregunta nada al navegador: le pone dos oídos en la red y
mira a cuál llama. Dos pestañas piden la MISMA url, una sin contenedor y otra en
«Banco»:
al destino (8099) GET /directo <- la de sin contenedor, directa
al proxy (9099) \x05\x01\x00 <- saludo SOCKS5 de la de «Banco»
Ese saludo sólo aparece si Gecko decidió hablar con un proxy para esa petición, y
el único que se lo pudo indicar es proxy.onRequest mirando el cookieStoreId. La
pestaña sin contenedor no es decorativa: sin ella, «todo fue por el proxy» y «el
ruteo anda» se verían iguales.
CONTROL NEGATIVO, que es lo que este repo se exige desde hoy: con
`--negative-control` no se configura el proxy y se exige lo contrario — las dos
pestañas directas y nadie llamando al proxy. Corrido: pasa. La única diferencia
entre las dos corridas es una línea de configuración y el observable se da vuelta
entero. Sin ese modo, una prueba que se hubiera vuelto ciega se vería idéntica a
una que funciona.
Tres detalles sin los cuales la prueba mide un silencio y se lee como fallo:
restore_on_demand=false, allow_hijacking_localhost=true, y el
triggeringPrincipal_base64 en cada entrada de sesión.
El artefacto se resuelve por `hammer hash` y no por glob, que es la lección de
hammer-03 de esta madrugada: con dos artefactos de la misma receta en el store,
`ls | head -1` es una ruleta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
|
||
|
|
44b6032522 |
vigia-sonames: dos falsos positivos menos — los cinco perfiles en CERO
NO hacía falta una receta de perl. `libperl.so` está en el artefacto, son 9,6 MB
en usr/lib/perl5/5.40.2/x86_64-linux/CORE/, y el binario la encuentra por su
RUNPATH, que apunta justo ahí. El vigía no la veía porque perl la construye con
-Duseshrplib y NO le pone SONAME: el código sólo registraba el nombre del fichero
`if son:`. Un ELF compartido sin SONAME se resuelve POR NOMBRE DE FICHERO, y eso
es exactamente lo que había que indexar.
El otro falso positivo era el último «hueco» del informe:
usr/lib/go/src/debug/elf/testdata/libtiffxx.so_ — un ELF de MUESTRA que Go
shipea para los tests de su propio paquete debug/elf, pidiendo libc.so.6 (glibc)
en una distro musl. Un binario que nadie ejecuta. `testdata/` no es cierre.
Importa arreglar un falso positivo aunque «sólo» sea ruido: un vigía que grita
en falso enseña a ignorarlo, y así es como libstdc++.so.6 —que SÍ rompía el
navegador en los cuatro escritorios— estuvo en esta misma salida sin que nadie
la mirara.
escritorio-mirada 41 nodos · 231 sonames · 0 sin proveedor
escritorio-kde 306 nodos · 2083 sonames · 0 sin proveedor
escritorio-gnome 190 nodos · 657 sonames · 0 sin proveedor
escritorio-cosmic 161 nodos · 498 sonames · 0 sin proveedor
escritorio-sway 201 nodos · 509 sonames · 0 sin proveedor
PROBADO CON UNA ROTURA A PROPÓSITO, porque un vigía todo-verde se ve igual que
uno roto. Quitando `runtime = ["gcc-libs"]` de firefox SOLO no pasa nada —atuq
lo declara también y es raíz de los mismos perfiles, así que la librería entra
igual; el primer control estaba mal montado—. Quitándolo de las DOS, kde y
cosmic vuelven a reportar libstdc++.so.6 y libgcc_s.so.1 exactamente. Restaurado
después.
|
||
|
|
02facebe28 |
deps.runtime: que ALGUIEN las lea — de 29 huecos de soname a 6
Tres arreglos que son el mismo: un campo que nadie lee es un campo que miente. 1. yupana._deps() leía SÓLO `deps.build`. Como yupana es la base de todas las herramientas de grafo, una dep de EJECUCIÓN no existía para ninguna: ni el vigía de sonames, ni la membresía de perfiles, ni el rootfs hidratado. Medido: firefox declaraba `runtime = ["gcc-libs"]` y el cierre de las cuatro imágenes seguía sin libstdc++.so.6, así que el navegador no arrancaba y el vigía lo seguía reportando como hueco DESPUÉS de haberlo arreglado. `deps.runtime` está en el esquema de hammer desde siempre. La unión es la definición de cierre: para CORRER hacen falta las dos. 2. build-state.py, lo mismo y por lo mismo. 3. El vigía entra en el LATIDO y deja docs/state/sonames.txt. Existía desde antes y contesta la pregunta que el grafo no contesta —no «¿está sellado?» sino «¿arranca?»— pero NADIE LO CORRÍA: no estaba en cosecha-cron y no dejaba fichero de estado. Por eso libstdc++.so.6, que rompía el navegador en los CUATRO perfiles, estuvo en su salida sin que nadie lo leyera, y se redescubrió arrancando atuq a mano. Un vigía que hay que acordarse de invocar no se distingue de no tenerlo. Y se BORRA scripts/audit-needed.sh, que escribí ayer sin ver que vigia-sonames.py ya hacía exactamente esto, con la misma frase en la cabecera. Dos herramientas que miden lo mismo divergen y la que nadie mira es la que miente; la que se queda es la que ya existía, que además reporta POR PERFIL y encontró más cosas. python3 declara sus cuatro deps de ejecución (readline/sqlite/lzma/bz2): son módulos de la stdlib que se cargan por dlopen, así que no rompen el arranque sino un `import` — un fallo que aparece lejos y no menciona a python. Resultado, con todo aplicado: 29 huecos -> 6, y los que quedan son otra clase. `libperl.so` es empaquetado de la receta perl; `libc.so.6` lo pide el `go` prebuilt y es un soname de GLIBC en una distro musl, que es un síntoma distinto. Los dos quedan anotados en docs/state/sonames.txt, que ahora se regenera solo. |
||
|
|
5b9777bdbd |
declarar gcc-libs como dep de EJECUCIÓN en las seis — y el navegador arranca
Las seis recetas que el audit daba por colgantes declaran ahora
`runtime = ["gcc-libs"]`. `deps.runtime` NO entra en el ArtifactHash —medido:
firefox sigue en b3:8116bdec, atuq en b3:f2960991, waterfox en b3:88b5a762—
así que esto NO reconstruye nada. Lo que cambia es la CLAUSURA, que es lo que se
hidrata en la imagen.
EVIDENCIA DE QUE ARREGLA EL FALLO REAL, no de que compila: `atuq` bajo sway
headless, sin GPU, renderizando about:buildconfig con sus tablas y la barra de
contenedores de la v0.5. Antes moría con decenas de «Error relocating:
_ZNKSt5ctypeIcE13_M_widen_initEv: symbol not found».
docs/evidencia/atuq-arranca-con-gcc-libs-2026-09-06.png
Y el audit se corrige, porque SUB-REPORTABA en dos formas:
· Miraba sólo ejecutables y `head -12` por artefacto. El siguiente NEEDED que
faltaba estaba en una LIBRERÍA (libmozsandbox.so pedía libnspr4.so), así que
daba «6 recetas» cuando el artefacto de firefox declara 30 sonames. Ahora
recorre TODOS los ELF.
· No contaba lo que el propio artefacto TRAE. Firefox bundlea su nspr/nss y
las encuentra por el LD_LIBRARY_PATH que fija su lanzador; contarlas como
colgantes era un falso positivo.
Con las dos correcciones y gcc-libs en el store, el corpus baja de 25 artefactos
colgantes a TRES, y son otra cosa:
sqlite-shared libreadline.so.8
python3 libreadline.so.8
naabu libdl.so.2 libpthread.so.0 libc.so.6
Los dos primeros piden una receta que falta (readline). El tercero pide sonames
de GLIBC, que en una distro musl es un síntoma distinto y merece su propia
mirada. Quedan anotados, no arreglados.
|
||
|
|
2be672415c |
audit-needed: cazar deps de ejecución colgantes — firefox no arranca en la distro
Encontrado corriendo el navegador, no leyendo. Lancé `atuq` bajo el sway
headless y murió con decenas de «Error relocating: _ZNKSt5ctypeIcE...: symbol
not found». El binario SELLADO de firefox declara:
NEEDED libstdc++.so.6 NEEDED libgcc_s.so.1 NEEDED libc.so
y las dos primeras NO EXISTEN en el store ni en ningún cierre hidratado: sólo en
.dev-fs/alpine, o sea en el LAB. Es exactamente la lección que la memoria
`quitar-dep-no-apaga-funcion` describe: el proyecto la encuentra en el sysroot
del lab y el artefacto sella con un NEEDED que nadie provee.
LO QUE LO HACE CARO ES QUE NINGUNA MÉTRICA LO VE. build-state.json dice
`sealed`; verificar-repro.sh dice que REPRODUCE bit a bit; los tres guardianes de
la receta pasan (RLBox en el binario, MOZ_REQUIRE_SIGNING, BuildID determinista)
— y el binario no arranca fuera del lab. El verde responde «¿construye?», nunca
«¿corre?».
El audit recorre el store, indexa qué sonames PROVEE el corpus y resta los NEEDED
de cada ELF. No mira el lab a propósito: mirarlo diría que todo está bien, que es
justo la ilusión que causó el problema.
Resultado de hoy: 25 artefactos colgantes, 6 recetas distintas —atuq, firefox,
waterfox, librsvg, spidermonkey, mesa-llvmpipe— y sólo DOS sonames faltan en todo
el corpus. O sea que falta UNA receta que provea las libs de runtime de gcc.
Gravedad: firefox y atuq son raíces de los CUATRO perfiles de escritorio
(cosmic, gnome, kde, sway), así que hoy las cuatro imágenes llevan un navegador
que no puede arrancar. La receta de firefox razona sobre esto en su comentario
—«clang++ de Alpine usa la libstdc++ COMPARTIDA ⇒ el NEEDED existe»— pero cierra
el requisito de BUILD y deja abierto el de EJECUCIÓN. hammer tiene
`[deps] runtime` y ninguna de las seis lo declara.
|
||
|
|
dc1391e611 |
siembra: anclar la exclusión de PNG — sin anclar rompe las recetas que traen PNG
La siembra excluía `*.png` sin anclar, o sea PNG en CUALQUIER sitio, incluido
recipes/. Dos consecuencias y las dos malas:
a) rsync PROTEGE DE BORRADO lo que excluye. Un directorio cuyo único resto es
un .png no se puede vaciar y por lo tanto no se puede borrar nunca, aunque
el hub lo haya eliminado. Medido: recipes/atuq/extension/atuq128.png
sobrevivió a un rename hecho en el hub, y el log del latido venía repitiendo
`cannot delete non-empty directory: recipes/atuq/extension` sin que nadie lo
leyera como un error.
b) Un PNG NUEVO de una receta tampoco viaja. Y hay recetas cuyo `[source] dir`
lleva PNG dentro: los 9 iconos de recipes/atuq/branding/icons/.
El efecto medido: `atuq` daba b3:f2960991 en el hub y b3:b33e81a5 en el worker,
por ESE único fichero de más. El worker no podía coincidir con el hub ni
construyendo bien.
Lo bueno es que no era silencioso, y por diseño: hammer SÍ hashea el árbol de un
`[source] dir` (recipe.rs, `dir:<hash>`), así que la divergencia sale como otro
ArtifactHash en vez de como dos bytes distintos en la misma dirección. Es
exactamente la propiedad que le falta al lab y a la versión de hammer.
Lo que se quería ahorrar eran las capturas: docs/evidencia/ (4,4 M) y el PNG
suelto de la raíz (1 M). Eso ahora se excluye POR RUTA. Los PNG de recipes/
viajan, que es lo correcto: ahí son fuente, no adorno.
Borrado el fichero rancio del worker; hub y worker vuelven a coincidir en
b3:f2960991.
|
||
|
|
b6a35bc2bc |
worker-loop: que el watchdog no se lleve el post-mortem
El watchdog barre work/sources/* cada 180 s con un suelo de 2 minutos, así que
el árbol de una receta que ACABA DE FALLAR se destruye antes de que nadie lo
mire. Medido hoy: waterfox murió a las 03:20:15 tras 36 minutos y 23.980 pasos,
enlazando libxul.so con un `collect2: error: ld returned 1 exit status` SIN
mensaje del linker delante — o sea justo el caso en que hace falta el objdir. A
las 03:22 work/sources/ estaba vacío. Reproducirlo cuesta otros 36 minutos.
Lo que más molesta es que la doctrina ya estaba escrita en el repo, en dos
sitios, y este watchdog la contradecía sin que nadie lo notara:
campana-deuda.sh:138 «NO se poda tras un ✗: el árbol de una receta que falló
es el post-mortem»
poda-fuentes.sh suelo de 24 h, «deja el post-mortem del día»
No se arregla subiendo el suelo a secas: este watchdog existe para que una tanda
Go no llene el disco vendoreando, y ahí los minutos importan de verdad (80 G en
una tanda, con el I/O-wait disparando el load). Se arregla siendo agresivo SÓLO
cuando el recurso escasea: con el disco por debajo de DISK_HIGH el suelo pasa a
SUELO_FRIO_MIN (120 min por defecto, configurable); en cuanto el disco aprieta
vuelve a los 2 minutos de siempre. Un árbol frío con disco al 37% no le hace
daño a nadie.
Probado en los dos sentidos: árbol de 5 min -> se borra con suelo 2, protegido
con suelo 120; árbol de 3 h -> se borra con los dos (ya no es post-mortem).
Ojo al leerlo: `df --output=pcent` da el porcentaje USADO, no el libre. La
primera versión de este parche llamaba `libre` a esa variable.
|
||
|
|
7638e4e2be |
worker-loop: recompilar hammer POR CICLO, no sólo al arrancar
El bloque de rebuild que ya existía corre una sola vez, al arrancar el loop, y
este loop vive días — hoy el worker llevaba 8 de uptime. El source SÍ sigue
llegando fresco cada 30 min (la siembra rsync-ea el repo), así que el worker
acaba con FUENTE NUEVO y BINARIO VIEJO.
Eso es peor que el fósil de la golden que motivó el bloque original, porque nada
lo delata: la versión de hammer NO entra en el ArtifactHash, así que el build se
comporta distinto y build-state.json sigue en verde. Misma familia que el lab
fuera de hash_inputs, sin siquiera el aviso del lock.
Caso real que lo motiva, de hoy: el worker corría un hammer de las 04:54 y el
arreglo que materializa los submódulos git había entrado a las 15:11 (
|
||
|
|
4fae3ee1ff |
prueba que los guardianes de la cadena wasm sirven — con CONTROL
Un guardián que nunca falló no se sabe si sirve, y uno que falla SIEMPRE se ve idéntico a uno que funciona. Por eso el control con el sysroot intacto —que tiene que PASAR— no es un extra: es la mitad de la prueba. El método lo aportó la sesión hammer-f8 (scripts/test-atuq-politica.py hace lo mismo con el cruce política<->XPI). Le pone delante a la cadena las cuatro formas conocidas de romperla —sin la sonda include/c++/v1, sin cstring, sin libc++.a, sin libc.a— y exige que las cuatro maten, usando la invocación EXACTA del configure de firefox dentro del lab. No construye nada, no toma work/.farm-build.lock y no escribe en el store: trabaja sobre copias temporales de artefactos ya sellados, así que corre con la granja a pleno. Y el control se ganó el sueldo en su primera corrida: salió en ROJO. La causa no era un guardián flojo sino este script, que resolvía el artefacto con y agarraba el VIEJO —el de antes de que la receta creara el directorio-sonda—. Misma trampa que el cache-hit que congela regresiones: un artefacto anterior se hace pasar por el actual y la prueba mide otra cosa. Sin el control se habría leído como «4 en verde, guardianes ok». Ahora el artefacto se resuelve por de la receta, nunca por glob. 5 en verde, 0 en rojo. |
||
|
|
c5ebbda933 |
atuq v0.5: proxy por contenedor — el primer diferenciador del §6 que se paga entero
Es el único de la lista del SDD 26 §6 que no pasa por el host de native
messaging del §7: es API de Firefox y nada más. Cada contenedor —Personal,
Trabajo, Banco, Compras— puede salir por su propio proxy.
Tres piezas y ninguna alcanza sola: `atuq.cfg` prende los contenedores (vienen
apagados), `policies.json` los CREA con `Containers.Default` —el único
mecanismo que los pone en un perfil NUEVO— y deja la config de fábrica en
`3rdparty.Extensions`, y `extensions/proxy/` los enruta con `proxy.onRequest`,
que es lo único que ve el `cookieStoreId` de la petición.
Las cuatro se comprobaron DENTRO del artefacto de firefox antes de escribir una
línea, no en la documentación de Mozilla: `Containers` y `3rdparty` en el
`policies-schema.json` de browser/omni.ja, `cookieStoreId` en el
`schemas/proxy.json` de omni.ja, y `storage.managed` leyendo
`getExtensionPolicy(id)` en ext-storage.js. Es la regla del §2.sexies: la
pregunta no es si Mozilla lo tiene, es si NUESTRO build lo tiene.
Tres decisiones que valen más que el código:
1. La config se indexa por NOMBRE de contenedor y no por `cookieStoreId`: el id
depende del orden en que se crearon, así que la misma configuración aplicada
a otro perfil apuntaría a otro contenedor.
2. FAIL CLOSED. Un contenedor con proxy configurado que no se pudo honrar no
sale directo: va a un destino cerrado y el navegador muestra el error. Salir
directo sería una fuga silenciosa — la misma familia que el artefacto vacío
de la regla 3, el fallo que llega hasta el final diciendo que todo fue bien.
3. `proxyDNS` PRENDIDO por defecto: sin él la consulta DNS sale por la línea que
se quería evitar. Es la fuga clásica de esta configuración.
Y lo que no promete está arriba de todo en la página de opciones, no en un pie:
separación de tráfico, NO anonimato; para anonimato, Tor Browser. El §4 cumplido
donde el usuario lo lee.
De paso, una verdad que estaba escrita en dos sitios pasa a tener un dueño: el
id de cada extensión sale ahora del `manifest.json` y no de una constante de
rebrand.py, el nombre del XPI se deriva de él, y el `install_url` de la política
se cruza contra la ruta donde el fichero quedó escrito de verdad. El icono se
inyecta desde branding/icons/ en vez de estar copiado byte a byte dentro de cada
extensión. Agregar una tercera extensión es ahora un directorio.
PROBADO, corriendo el árbol en la misma jaula que atuq-nested.sh:
· captura de about:preferences#containers con los cuatro contenedores y sus
iconos, más el containers.json del perfil;
· extensions.json del perfil nombra las dos extensiones;
· `console.info: "atuq/proxy: 4 contenedor(es) enrutado(s)"` — leyó la config
de fábrica por storage.managed Y la casó con los contenedores de la política;
· scripts/test-atuq-politica.py: cinco formas de desincronizar política y XPI,
las cinco matan el build, y el control con la política intacta pasa.
NO probado y dicho por su nombre: que una petición hecha en «Banco» salga por el
proxy de «Banco». Pide automatizar la UI y queda pendiente.
Dos obstáculos del método, que valen para la próxima. La consola de una
extensión es CONTENIDO: `devtools.console.stdout.chrome` (que viene en true) no
la incluye, hace falta `...stdout.content`. Y un `moz-extension://` NO se abre
desde la línea de comandos —muere con `NS_NOINTERFACE [nsIFileURL.file]` y abre
la home en su lugar—, además de que `--screenshot` dispara al `load`, que puede
ocurrir antes de que arranquen las extensiones. Eso último destapó un fallo real
y arreglado: la página de opciones confundía «no hay contenedores» con «la API
no está» y mostraba un mensaje FALSO.
Verificado contra firefox b3:352d7880; se reconstruye contra el firefox con
RLBox cuando selle. La otra mitad de esta unidad —el rename de
recipes/atuq/extension/ a extensions/inicio/— entró sin querer en
|
||
|
|
8a2c6fad39 |
repro: «no pude comparar» tampoco es no-determinismo — segundo sitio, misma lección
Una tanda volvió a llenar el libro de veredictos falsos: 47 recetas sanas anotadas como
NO-DETERMINISMO. Esta vez la cadena empezó antes de donde yo había mirado.
El `mv` que APARTA el artefacto falló (el directorio de apartado no estaba) y yo nunca comprobaba que
hubiera funcionado. A partir de ahí todo lo que sigue es basura: el artefacto se queda en su sitio,
`hammer why-differs` compara contra una ruta que no existe y devuelve `Error: store: no existe…`, y
mi código leía cualquier salida no-cero como «difieren». Un error de comparación disfrazado de
veredicto, 47 veces.
Es la MISMA lección que arreglé hace un rato para el build, en un segundo sitio que no miré:
**no poder medir no es un resultado negativo**. Dos guardas nuevas:
· el apartado se comprueba (`mv || SIN VEREDICTO`) y la receta se salta ruidosamente;
· `why-differs` se lee, no sólo su exit code: si dice `Error:`/`no existe`, es SIN VEREDICTO, se
restaura el artefacto y NO se anota nada.
Probadas las dos: con el apartadero sin permiso de escritura sale «no pude apartar el artefacto ⇒
SIN VEREDICTO», el artefacto queda intacto y el libro no crece.
Comprobado además que la tanda mala no perdió NADA: como el `mv` fallaba, los 47 artefactos nunca
salieron del store (`faltan: 0`). El daño fue sólo el registro, y está purgado — el libro queda con
51 verificaciones buenas.
La regla, ya por triplicado hoy: cuando un guardián no puede establecer algo, el estado es *sin
veredicto*, y eso no se escribe. Un libro que anota lo que no midió se cree igual que uno que sí.
|
||
|
|
a48885e143 |
repro: podar el árbol de fuentes tras cada veredicto sano — el barrido ya no llena el disco
Un barrido dejaba un árbol extraído por receta en `work/sources` y no limpiaba hasta el final. En el
hub eso llevó el disco del 95% al 98% DOS VECES hoy, y un disco lleno no se lee como disco lleno: se
lee como recetas rotas. Con la poda por veredicto, `work/sources` se queda en 3 MB durante toda la
tanda en vez de crecer hasta 7,7 G.
No cuesta nada porque `fetch.rs` borra y re-extrae el árbol en CADA build por diseño: guardarlo entre
recetas de un barrido no ahorra un segundo.
Dos límites deliberados:
· Sólo se poda cuando el veredicto es SANO. Si algo falló o divergió, el árbol es justamente lo que
hace falta para mirarlo — un verificador que limpia la escena del problema que acaba de encontrar
sirve para poco. Es la misma razón por la que los ejemplares divergentes se conservan.
· Sólo el árbol de ESA receta. Las deps extraídas se quedan: son compartidas, y borrarlas mientras
otra cosa las usa es exactamente el ADR 0012.
Con esto la cobertura se puede levantar en el HUB, que es donde están los 1157 artefactos. El worker
sólo tiene los que construyó —el último barrido allá reportó 47 de 60 «sin artefacto»— así que como
máquina de cobertura no sirve por más disco que tenga.
|
||
|
|
058162c6d3 |
licencias: la cola de pendientes también se escribe — «lo que falta y por qué» es dato
Las 33 que quedan sólo existían en la salida del script, o sea que se perdían al cerrar la terminal. Para quien tiene que decidirlas —y son decisiones legales, no mecánicas— «qué falta y POR QUÉ no se puede afirmar» es tan dato como los veredictos. `docs/licencias-pendientes.tsv` las deja listadas con su motivo: cuáles traen varios ficheros de licencia que dicen cosas distintas (gmp, ffmpeg, clang18, llvm18, los `gi-*`), cuáles tienen un LICENSE que no reconozco y con qué frase empieza (fuse3, lsof), y cuáles no traen nada en el árbol (boost, pigz, sqlite-shared). Al revés que `licencias-evidencia.tsv`, ésta se REGENERA entera en cada corrida. No es un registro de lo que se probó sino una foto de lo que queda, y una pendiente resuelta tiene que DESAPARECER de acá —si se acumulara, la lista de trabajo mentiría hacia arriba para siempre. |
||
|
|
d7d9cf8fd2 |
repro: «la 2ª reconstrucción no construyó» NO es no-determinismo — mi propio bug, y lo pagó el libro
Corriendo una tanda con `MAX_SECONDS=45` se llenó el disco a mitad, los rebuilds empezaron a morir por ENOSPC, y la rama que yo mismo había escrito para distinguir DERIVA de NO-DETERMINISMO metió «falló» y «difiere» en la misma condición ⇒ **21 recetas sanas quedaron anotadas como NO-DETERMINISMO** en un libro que pretende ser autoritativo. Un registro falso y persistente es peor que no tener registro: el propósito del libro es que nadie tenga que volver a mirar, así que una entrada equivocada se cree para siempre. Es la trampa de siempre —un disco lleno se lee como receta rota, y la pista es que mueren varias seguidas— y el script ORIGINAL ya la evitaba: «no es lo mismo «no reproduce» que «no construye acá». Distinguirlos importa: mezclarlos inventaría un problema de determinismo». Lo rompí al añadir la segunda reconstrucción. Restituido: si la 2ª no construye, se restaura el artefacto, se cuenta como fallo y **no se anota nada** — sin veredicto es un estado legítimo. Reparado además el daño: las 21 entradas falsas purgadas del libro (quedan 27 verificaciones buenas), y `angle-grinder` repuesto desde `store/.divergen/`, donde lo había dejado la clasificación errónea. Eso sí funcionó como debía: el ejemplar estaba guardado y entero, no perdido. Y queda anotado en el propio script el límite del filtro por tiempo que provocó todo: el sidecar de `store/.times/` lo escribe el WORKER, así que acota el build de esa receta EN LA MÁQUINA QUE LO MIDIÓ. Una receta Rust de 40 s allá con la caché de cargo caliente son muchos minutos acá en frío —`angle-grinder` y `amp` entraron por el filtro y se comieron el disco— y encima el número no cuenta la CASCADA de deps que falten en el store local, que es la misma sorpresa que dio `gjs`. Con disco justo conviene pasar las recetas a mano. |
||
|
|
efe6e001d2 |
repro: un libro de lo verificado, y por fin un número de cobertura — era 2 de 1157
`verificar-repro.sh` sorteaba una muestra y se olvidaba. Sin registro no había forma de contestar la
pregunta que importa —¿qué fracción del corpus se comprobó alguna vez que reproduce?— y encima las
mismas recetas salían sorteadas una y otra vez mientras otras no se miraban jamás. La primera medida
con el libro puesto: **2 de 1157**. El invariante central del proyecto no se había verificado de
forma acumulativa prácticamente en nada, y eso no se veía porque cada corrida daba «✅ PUERTA
SUPERADA» sobre su propia muestra.
`docs/state/repro-verificado.tsv` va con la misma clave auto-invalidante que el libro de fuzz:
(receta, **ArtifactHash**). El hash resume la receta MÁS su cierre de deps, así que una entrada deja
de casar en cuanto algo aguas arriba cambia. Un «ya lo verifiqué» que sobreviviera a un re-hash sería
una mentira; éste no puede serlo.
Con eso el muestreo pasa a sortear entre lo NO verificado, así que corridas sucesivas ACUMULAN
cobertura en vez de repetir (con `TODO=1` se sortea entre todas, para re-verificar a propósito), y
`--coverage` da el número sin construir nada.
Verificadas en esta tanda: itstool, markupsafe, musl-obstack, hicolor-icon-theme, musl-fts, npth,
poppler-render-check, doas, libffi y packaging REPRODUCEN; `when` derivaba y quedó al día. Cero
no-determinismos. Van 11 de 1157.
⚠ Y una trampa que casi me come, anotada acá porque el próximo la va a pisar: elegí candidatos «por
artefacto chico» y la lista empezó con **nodejs**, cuyo artefacto pesa 0,2 MB y cuyo build son horas
de V8. El tamaño del artefacto NO dice nada del costo de construirlo. Para elegir muestra barata hay
que mirar tiempos de build, no bytes de salida.
|
||
|
|
d5863ae554 |
verificar-repro: el apartadero iba a tmpfs, no tomaba el lock, y dejaba el veredicto a medias
Tres defectos, encontrados corriéndolo y pagando uno de ellos: perdí el artefacto de `aichat` al interrumpir una corrida. Está reconstruyéndose en el worker. **1. El apartadero estaba en `/tmp`.** Para verificar hay que sacar el artefacto de su sitio y dejar que hammer lo reconstruya; eso iba a un `mktemp -d`. La cabecera prometía «un verificador que destruye lo que verifica es peor que no tenerlo», y esa ubicación rompía la promesa en tres sitios a la vez: `/tmp` es **tmpfs**, así que apartar copiaba el artefacto a RAM en una caja de 7,6 GiB sin swap; al ser otro sistema de ficheros el `mv` no era un rename atómico sino copiar-y-borrar, o sea que una interrupción a mitad lo dejaba en ninguna parte; y lo que sobreviviera moría al reiniciar. Ahora va a `store/.verificar-repro`: el `mv` es instantáneo, no cuesta RAM ni disco, y lo apartado SOBREVIVE a un kill — la corrida siguiente lo repone sola antes de empezar. **2. Llamaba a `hammer build` sin tomar el `flock`.** Es la regla 1 del repo, y saltársela arriesga el árbol compartido de `work/sources` de forma irreversible (ADR 0012). Lo toma el script y no quien lo llama, por lo mismo que `poda-fuentes.sh`: así no hay forma de correrlo mal — y correrlo mal fue exactamente lo que hice. Con `-E 3`, porque sin eso «no conseguí el lock» sale como exit 1, que es lo que este script usa para «hay divergencias»: informar «el corpus no reproduce» cuando lo que pasaba era que había otro build corriendo es peor que no correr. **3. Confundía DERIVA con NO-DETERMINISMO y lo delegaba en la memoria de quien leía.** Que el guardado difiera de una reconstrucción de hoy puede ser que el lab se movió (deriva, sano) o que las mismas entradas dan salidas distintas (no-determinismo, bug). La cabecera decía «corré el script dos veces y mirá la segunda» — con lo que la primera corrida informaba «DIVERGE» sobre cosas sanas y entrenaba a no creerle. Ahora, ante una diferencia, reconstruye una segunda vez y compara las dos reconstrucciones ENTRE SÍ: si coinciden es DERIVA, si no, NO-DETERMINISMO. El rebuild extra sólo se paga cuando ya hubo una diferencia. Y cuando es no-determinismo de verdad, los dos ejemplares se conservan en `store/.divergen/` en vez de borrarse: antes el guardián tiraba la única evidencia justo en el caso que existe para encontrar. Medido de paso, y es la parte buena: `libassuan`, `libksba`, `libffi` y `packaging` —artefactos del 21 de agosto— reproducen BIT A BIT con el lab de hoy. `scdoc` derivaba y ya está al día; `gron`, `age` y `anew` reproducen. La rama de DERIVA se probó ensuciando a propósito una copia guardada: clasifica bien y el store queda con la reconstrucción limpia. |
||
|
|
59a0e56a16 |
licencias 97% (1132/1165): «no hay fichero» y «no reconozco el texto» no son lo mismo
El script decía «sin COPYING ni declaración en el árbol» para `lsof`, `when`, `wpa_supplicant`,
`perl-xml-parser` y `fuse3` — y las cinco SÍ tienen un COPYING o un LICENSE en la raíz. Lo que
pasaba es que ninguna de mis huellas reconocía su texto, que es un problema distinto y se acciona
distinto: uno pide buscar la licencia en otro sitio, el otro pide una huella nueva o leerla. Decir lo
mismo de los dos casos mandaba a mirar donde no era. Ahora el mensaje nombra el fichero y cita su
primera línea, y con eso tres se resolvieron leyéndolas:
· `wpa_supplicant` — su COPYING no es una licencia sino un aviso: «the project has chosen to use
only the BSD license option for future distribution», y remite al README. El README trae las tres
cláusulas clásicas ⇒ BSD-3-Clause. Sin seguir esa remisión no había veredicto posible.
· `when` — «under the same terms as Perl, or, at your option, under version 2 of the GPL». «Los
mismos términos que Perl» ES la disyunción Artistic-1.0-Perl OR GPL-1.0-or-later, y la opción de
GPL-2 ya queda cubierta por el `-or-later`.
· `perl-xml-parser` — su LICENSE es el texto íntegro de la Artistic License 2.0.
Ese último obligó a cerrar un lazo que yo mismo había dejado abierto: el script se niega a declarar
un SPDX cuyo TEXTO no tengamos en `licenses/`, porque la obligación legal es entregar el texto y no
el nombre. Pero `licencias-textos.sh` deriva qué textos bajar DE las licencias ya declaradas ⇒ una
licencia genuinamente nueva no podía entrar por ningún lado. El orden que lo resuelve es: declarar
con evidencia, bajar el texto, comprobar que llegó. `licenses/` pasa de 44 a 48 textos y el único que
queda sin texto canónico es `LicenseRef-qorpa-ajena-no-enumerable`, que es un identificador nuestro y
SPDX no publica.
Quedan 33. `lsof` (licencia propia de Purdue) y `fuse3` (aviso compuesto: LGPL para la librería, GPL
para las utilidades) siguen pendientes a propósito, y el resto son expresiones que decide una
persona: gmp, ffmpeg, clang18, llvm18 y los `gi-*` traen VARIOS ficheros de licencia que dicen cosas
distintas, y en ffmpeg la respuesta ni siquiera está en el árbol —la deciden los flags de la receta.
Los 3 ArtifactHash, idénticos antes y después.
|
||
|
|
b87835f719 |
licencias 96% (1129/1165): la fuente git también es evidencia, y el script deja de llamarse «tarball»
Faltaban tres recetas cuya fuente es un repo, no un tarball: `fcft`, `foot` y `glab`. El commit pineado cumple exactamente el mismo papel que el sha256 —es el árbol EXACTO que construimos— así que la evidencia estaba igual de disponible, sólo que por otra puerta. `miembros_de_git()` la abre sin clonar el árbol entero: `--depth 1 --filter=blob:limit=64k` sobre el commit. Un COPYING nunca pasa de 64k y los fuentes donde vive la concesión tampoco; los que sí pasan —binarios, assets— son justo los que no interesan. Un repo de cientos de megas se resuelve con unos pocos. Las tres salieron del `license:` que el propio autor escribe en su `meson.build` (fcft, foot) y del texto del LICENSE (glab). Los repos por SSH se saltan diciéndolo: este script hace fetch anónimo y no tiene claves. Son `hammerd` y `portal-probe`, y ya están resueltas por otra vía (su repo es este mismo). `analizar()` pasa a consumir un iterable de `(ruta, bytes)` en vez de abrir el tar ella misma, así que las dos fuentes comparten TODA la lógica de veredicto: la jerarquía de evidencia, la regla de sólo-la-raíz, la unión de identificaciones y la exigencia de que la concesión hable de la misma versión que el COPYING. Un criterio, dos puertas. Y el script se renombra `licencias-tarball.py` → `licencias-fuente.py`, porque el nombre viejo pasó a ser mentira en el momento en que aprendió a leer git. Un nombre que describe la implementación de ayer manda a la gente a buscar en el sitio equivocado. Los 3 ArtifactHash, idénticos antes y después. |
||
|
|
db7fd81236 |
parches: los 8 FUZZ del catálogo, mirados uno por uno — y un libro que se auto-invalida
`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.
|
||
|
|
f6e690d994 |
vigia-imagen: un módulo QML registrado en C++ no está «sin instalar» — se comprueba, no se recuerda
Los DOS únicos ✗ que reportaba este vigía eran falsos, y de la misma clase: un `import` no siempre se
satisface con un directorio y su `qmldir`. El greeter de la pantalla de bloqueo hace
`qmlRegisterUncreatableType<PamAuthenticator>("org.kde.kscreenlocker", …)` — el módulo existe SÓLO
dentro del proceso que carga ese `.qml` y en disco no hay nada que instalar. Buscarlo en el sistema
de ficheros da ✗ con la imagen perfecta.
Los dos, verificados uno por uno antes de tocar el script:
· `org.kde.kscreenlocker` ← `usr/lib64/libexec/kscreenlocker_greet`. Y valía la pena mirarlo: los
que lo importan son `lockscreen/LockScreenUi.qml` y `MainBlock.qml` de plasma-desktop, o sea la
pantalla de bloqueo de verdad. Si hubiera faltado no era un diálogo de ajustes.
· `org.kde.newstuff.core` ← `usr/lib/qt6/qml/org/kde/newstuff/libnewstuffqmlplugin.so`.
Antes esto se tapaba anotando el módulo a mano en `QML_EN_RUNTIME`. `registrado_en_binario()` lo
contesta ahora con evidencia —el URI queda como literal en `.rodata`, así que se busca en los ELF del
cierre— y además NOMBRA al registrador, que es lo que permite auditar el veredicto sin repetirlo.
Mira primero los artefactos cuyo nombre comparte una palabra con el URI, que es donde está casi
siempre, y sólo si no aparece recorre el resto.
⇒ la lista escrita a mano SE BORRA ENTERA. Al vaciarla, los cuatro módulos que tenía se resolvieron
solos, cada uno nombrando su binario (plasmashell, kwin_wayland). Y salió gratis un hallazgo que
ninguna lista podía dar: `org.kde.kwin.effect` no se importa en NINGÚN cierre — era una entrada
muerta, y nadie tenía cómo saberlo.
Los cinco perfiles quedan en ✓ de qml, sin un solo ✗ en todo el informe.
La regla detrás: un guardián que grita en falso no se endurece, se ignora. Cuando el 100% de sus
hallazgos son falsos positivos, el trabajo no es anotarlos — es enseñarle a preguntar bien.
|
||
|
|
52d3bbfeb3 |
atuq-nested: CAPTURA=<png> — la prueba en el píxel, en una variable
Durante horas dije «mirá tu pantalla» y reporté «el proceso vive» como si fuera lo mismo. No lo es: la ventana estaba ahí desde el principio y lo que fallaba era el branding DENTRO del zip, que ningún log iba a contar. `grim` ya estaba en el corpus (`incoming-wlr`), se hidrata en el mismo rootfs y captura la pantalla real contra el mismo socket wayland desde dentro de la jaula. Ahora es parte del runner: con `CAPTURA=/ruta/foto.png` se lanza atuq, se espera a que pinte y se deja el PNG afuera para mirarlo. De paso, la resolución de raíces prueba el corpus Y `incoming-wlr`, en ese orden — el mismo que usa la resolución de deps (sibling-first, luego el catálogo padre). Sin eso, `grim` no se encontraba y el script habría dicho «falta un artefacto» sobre una receta que existe. |
||
|
|
748b2f604c |
yupana: el khipu se descubre, no se escribe a mano — cosmic y wlr eran invisibles
Dos sitios llevaban la lista de grafos codificada como `(kde, gnome, corpus)` y **omitían `build-state-cosmic.json` y `build-state-wlr.json`**, que son dos de los cuatro escritorios. Es exactamente la forma del fallo que motivó este fichero y que su cabecera describe: medir contra un subconjunto de las colas y que el número salga chico sin que nada avise. Medido: `_estado()` devolvía `?` para los 36 nudos propios de cosmic y para 12 de los 14 de wlr. Pero el daño peor no era el `?`. `_estado()` elegía el fichero por la cola —gnome si `incoming-gnome`, corpus si `corpus`, **KDE para todo lo demás**— así que un nudo de `incoming-wlr` se buscaba en el grafo de KDE. `gdk-pixbuf` existe en `corpus` Y en `incoming-wlr` con recetas y hashes distintos, y la consulta por el de wlr devolvía el estado del de corpus, con toda confianza. Coincidía en `sealed`, así que nunca se notó. Un `?` se ve; una respuesta de la receta equivocada, no. Ahora `_khipus()` los descubre por glob (un grafo nuevo entra solo; ninguno se puede olvidar) y `_estados_por_cola()` indexa por **(cola, nombre)**, que es la clave real: el mismo nombre vive en varias colas siendo recetas distintas. Si la cola no tiene ese nudo, la respuesta es `?` — que es la verdad, y no el estado de su homónimo. `keystones` compartía el mismo índice por nombre suelto: pasa a `(cola, nombre)` en las tres consultas. Los tres invariantes de `test-yupana-radio.py` siguen verdes, y las 50 discrepancias de cosmic+wlr bajan a 0. |
||
|
|
96855001c6 |
licencias: 96% (1124/1164) — --fetch para los tarballs que no estaban en caché
Ocho paquetes más (giflib, pcre2, nghttp2, libogg, libvorbis, speexdsp, libuv, tea) con la misma
regla de evidencia. Su tarball no estaba en `work/tarballs/`, así que el modo `--fetch` lo baja a un
temporal, lo verifica contra el sha256 QUE LA RECETA PINEA —o es el árbol exacto o no se mira— y lo
borra. No se escribe en la caché de hammer a propósito: es suya, y un fichero puesto ahí por otro
camino es una vía de envenenamiento que nadie audita.
Dos correcciones al criterio, las dos porque produjo una afirmación falsa:
· `gmp` salía GPL-3.0-or-later. Su `COPYING` es la GPLv3, pero al lado trae `COPYING.LESSERv3` y
`COPYINGv2` porque la biblioteca es LGPL. La regla anterior —«el `COPYING` a secas gana al
sufijado»— arregla socat, cuyo `COPYING.OpenSSL` es una excepción, y rompe gmp, donde el extra
nombra otra VERSIÓN y no una dep. Esa diferencia es demasiado fina para codificarla sin
equivocarse, así que ahora se identifican TODOS los ficheros de la raíz y sólo hay veredicto si
dicen lo mismo. gmp, ffmpeg y los tres `gi-*` quedan pendientes, que es lo correcto: en ffmpeg
la respuesta ni siquiera está en el árbol, la deciden los flags de la receta.
· Pero «varios ficheros» no es «ambigüedad»: el `COPYING` de pcre2 son dos líneas apuntando a
`LICENCE.md`, y los dos dicen BSD-3. Por eso se unen las identificaciones en vez de contar
ficheros — ambiguo es que digan cosas DISTINTAS.
Y el TSV pasa a ser ACUMULATIVO. Una receta ya sembrada sale del conjunto de entrada porque ya tiene
`license`, así que regenerar el fichero entero borraba su cita — y la cita es el rastro de
auditoría, lo único que deja revisar un veredicto sin repetir el trabajo. Un fichero de evidencia
que se olvida de lo que ya probó no es evidencia.
Los 10 ArtifactHash afectados, idénticos antes y después.
|
||
|
|
ba1fb2d3a8 |
licencias: 95% (1114/1164) — la evidencia sale del tarball pineado, offline
`licencias.sh --sembrar` escribe desde una tabla curada a mano y `licencias-desambiguar.sh` resuelve
`-only` vs `-or-later` preguntándole a la búsqueda de código de GitHub. Los dos dejan fuera lo mismo:
lo que nadie curó y lo que no vive en GitHub.
Pero `work/tarballs/` indexa cada tarball por su sha256, así que el árbol EXACTO que la receta pinea
ya está en disco: la declaración del propio autor, en el commit que construimos, sin red. De las 76
sin licencia, 55 tenían su tarball cacheado y 25 salieron con evidencia citable.
`scripts/licencias-tarball.py` la busca en tres niveles: la declaración del autor (`Cargo.toml`,
`meson.build`), un único fichero en `LICENSES/` (REUSE, que usa KDE), y el texto del COPYING más la
CONCESIÓN buscada en las cabeceras de los fuentes — excluyendo COPYING/LICENSE, porque el apéndice
de la GPL trae literalmente «or (at your option) any later version» y buscarla ahí da siempre
positivo siendo plantilla. Es la regla que ya fijó `licencias-desambiguar.sh`, aplicada al árbol
pineado en vez de a GitHub.
La primera versión resolvía 40, y CUATRO estaban mal. Las dejo escritas porque son la forma del
error, no accidentes:
· `socat` salía BSD-2 por su `COPYING.OpenSSL`, que es la excepción, no la licencia. Un `COPYING`
a secas gana ahora a cualquier sufijado.
· `pigz` salía Apache-2.0 por `zopfli/COPYING`: la licencia de una pieza VENDORIZADA leída como la
del contenedor. Sólo se mira la raíz.
· `openssh` salía MIT porque su `LICENCE` es un compendio de cuatro y me quedaba con la primera
que pegara. Si el texto trae varias, no hay veredicto: lo compone un humano.
· `nano` salía GPL-3.0-**or-later** citando el «either version 2» de su `aclocal.m4` — plantilla de
autotools, no del proyecto, y encima de otra versión. Ahora la cita tiene que venir de un fichero
del autor Y hablar de la misma versión mayor que el COPYING.
Y dos bugs míos que producían el mismo daño en silencio: la marca de BSD-3 era «Neither the name of»,
que libpcap y libzip no usan («The names of the authors may not be used to endorse») ⇒ se declaraban
BSD-2; y el mayor de versión lo sacaba de `"GPL-3.0".rsplit("-",1)[0][-1]`, que da «L», así que la
comprobación de coherencia rechazaba TODA cita válida.
La validación final no es una regex: un identificador vale si tenemos su texto en `licenses/`. La
obligación legal es acompañar el binario del TEXTO, así que un SPDX que no podemos entregar no
adelanta nada y sí crea una afirmación que no se sostiene. Eso es lo que atrapa el «GPL2+» que meson
deja escribir en `gsd-schemas` y `libgdm`, que no es un SPDX sino taquigrafía.
Nada se sembró a ojo: cada línea de `docs/licencias-evidencia.tsv` lleva la cita que la decidió, y
las 50 que quedan salen listadas con lo que SÍ se encontró en vez de rellenarse.
Comprobado que no se invalida nada: los 26 ArtifactHash afectados son idénticos antes y después
(`license` no está en `hash_inputs`) — medido, no supuesto.
|
||
|
|
5d06a3b83d |
atuq ABRE: el app_id estaba compilado en el binario, y el árbol copiado llegaba de sólo lectura
Con la cadena GTK3 arreglada, atuq arrancó de verdad: parent vivo, CERO procesos de contenido muertos, y pintando una página local. Quedaban dos cosas. 1. SE ANUNCIABA COMO `firefox-default`. El application.ini decía RemotingName=atuq y aun así el proceso se presentaba así — porque esa cadena es un MOZ_APP_REMOTINGNAME horneado en el ELF, y es la que Gecko pasa a g_set_prgname(), o sea el **app_id de Wayland**. Consecuencia visible: el escritorio no casaba la ventana con nuestro .desktop (StartupWMClass=atuq) ⇒ icono genérico y sin agrupar. Se parchea EN SITIO, no recompilando: cambiarlo en recipes/firefox.toml brandearía la BASE como atuq, y firefox tiene que seguir siendo firefox para los otros forks. Es seguro porque es una cadena C terminada en NUL en el pool de .rodata —`firefox-default\0` son 16 bytes exactos y se escriben 16—, y se EXIGE una única ocurrencia: si upstream la duplica o la renombra, falla en vez de parchear el sitio equivocado. Verificado después: 0 ocurrencias de la vieja, y el log dice `(atuq:26423)`. 2. UN BUG QUE SÓLO SE VE FUERA DE ROOT. `cp -a` preserva los modos y los ficheros del store están sellados sin permiso de escritura; todo lo que viene después los modifica. En el worker pasaba inadvertido porque corre como root, que ignora los bits. El primer build local murió con `PermissionError: /out/usr/lib/atuq/application.ini`. Se arregla con `chmod -R u+w` en la receta y no en el entorno: un artefacto no debe depender de con qué uid lo construiste. Y el runner pasa a hidratar las variantes `-shared`: en runtime una `.a` no sirve de nada, y son las que gtk3 declara NEEDED desde el arreglo del cuadro de las dos Pango. Este atuq se construyó EN LOCAL en segundos, que era la promesa entera del diseño derivado del SDD 26: iterar el envoltorio sin volver a pagar un build de Gecko. |
||
|
|
a828e74c40 |
la cadena GTK3 se enlazaba estática dentro de dos .so: cuatro variantes -shared y xkeyboard-config al corpus
Con `atk` arreglado, atuq llegó más lejos y murió igual, ahora con Pango:
GLib-GObject-CRITICAL: cannot register existing type 'PangoFontMap'
… decenas de líneas … tipo '<invalid>'
MISMO CUADRO, CULPABLE DISTINTO Y MÁS GRANDE. `gtk3` produce DOS objetos compartidos —libgtk-3.so y
libgdk-3.so— y libxul declara NEEDED las dos, así que se cargan siempre juntas. Con las variantes
ESTÁTICAS de sus deps, cada una se llevaba adentro su propia copia. Medido con el mismo instrumento
que cazó a atk, `nm -D --defined-only`, preguntando quién DEFINE cada símbolo:
pango_font_map_get_type → libgdk-3.so, libgtk-3.so, libgailutil-3.so
cairo_create → libgdk-3.so, libgtk-3.so
gdk_pixbuf_get_type → libgdk-3.so, libgtk-3.so
hb_buffer_create → libgdk-3.so, libgtk-3.so, libgailutil-3.so
Y se comprobó el otro lado: libxul NO embebe ninguna —enlaza libgtk-3/libgdk-3 como debe—, así que
la duplicación es toda interna del par GTK3.
Cuatro variantes nuevas: harfbuzz-shared, cairo-shared, gdk-pixbuf-shared, pango-shared. Sólo cambia
el modo de librería; se conservan todos los switches del canónico para no arrastrar deps nuevas.
DOS DEUDAS ANOTADAS, NO OLVIDADAS: `fribidi` y `pixman` siguen estáticos —no existe variante— pero
quedan embebidos en UNA sola .so cada uno, así que no hay copia que colisione; y `libepoxy` sí queda
en las dos, y se deja porque no registra tipos de GObject ni mantiene estado global. Si algún día
otra .so del mismo proceso los embebe, vuelve el cuadro.
`firefox` swapea las mismas cuatro: declarar las ESTÁTICAS junto a un gtk3 que trae las compartidas
son dos artefactos peleando por el mismo `pango.pc`, que es la otra forma conocida de este fallo.
Y `xkeyboard-config` SUBE AL CORPUS. Existía idéntica en las cuatro colas de escritorio (un solo md5
entre las cuatro, verificado antes de mover) y atuq, que vive en el corpus, no podía alcanzar
ninguna: sibling-first y después el catálogo padre, nunca una cola hermana. Sin sus datos el
navegador ni pinta («xkbcommon: failed to add default include path /usr/share/X11/xkb»). Mismo hash
en el corpus que en las colas ⇒ cero rebuilds, y las copias de las colas se quedan donde están.
|
||
|
|
d2527284b9 |
atuq-nested: el rootfs se revalida por HASH, no por existencia
El script daba por bueno el rootfs si el directorio estaba: «¿ya está hidratado?». Es la pregunta equivocada. Tras el re-hash de la cadena atk→gtk3→firefox→atuq el directorio seguía ahí con los artefactos VIEJOS, así que habría abierto contento la versión que acabábamos de arreglar y el diagnóstico habría sido buscar en el sitio equivocado. Misma forma del cache-hit que congela regresiones. Ahora resuelve los hashes vigentes, los anota en `.raices` y rehidrata cuando difieren. De paso: nada de `diff <(...)`, que es de bash. El shebang dice /bin/sh y un script que sólo anda cuando /bin/sh resulta ser bash es una trampa que salta en otra máquina. |
||
|
|
0f7d19232e |
atuq: el lanzador deja de ser un symlink, y un runner para abrirlo en la pantalla que ya tenés
Dos cosas que salieron de intentar ABRIRLO, que es lo único que distingue «sellado» de «usable».
1. EL LANZADOR. Con `/usr/bin/atuq` como symlink, el navegador moría antes de pintar:
XPCOMGlueLoad error for file /usr/lib/atuq/libmozsandbox.so:
Error loading shared library libnspr4.so: No such file or directory
El motor carga sus propias librerías desde `/usr/lib/atuq` y ni el binario ni esas `.so` traen
RPATH/RUNPATH — comprobado con `readelf -d`, no supuesto. Pasa a ser un script que exporta
`LD_LIBRARY_PATH` (lo mismo que hacen Debian y Fedora) y hace `exec`, para que el proceso que
queda sea el motor y `/proc/self/exe` siga resolviendo el appdir. El `LD_LIBRARY_PATH` además
tiene que HEREDARSE: Firefox lanza un proceso por pestaña y todos cargan las mismas librerías.
La alternativa limpia es grabar RPATH=$ORIGIN con `patchelf` —es lo que hace Alpine— pero
`patchelf` todavía no existe como receta del corpus; cuando exista, esto vuelve a ser un symlink.
2. `scripts/atuq-nested.sh` — hidrata el cierre de runtime y abre atuq como ventana anidada en el
compositor que ya está delante (waypipe, mirada, sway). Trae dos cosas aprendidas a golpes:
· EL ROOTFS VA EN EL MISMO MOUNT QUE EL STORE. `hydrate` proyecta con hardlinks y `linkat()`
rechaza cruzar un punto de montaje aunque sea el mismo filesystem. Acá el store es /dev/sdb
bind-monteado y `work/` vive en /dev/sdc: hidratar a `work/…` muere con «Invalid cross-device
link (os error 18)». El volumen entero está en /mnt/cosecha, así que el rootfs va ahí. Se
comprueba con `findmnt -T`, nunca con `stat -c %d`.
· `dejavu-fonts` va en las raíces por necesidad, no por completismo: un navegador sin una sola
fuente arranca, pinta y muestra cuadraditos. Ya nos costó una tarde en GNOME.
Y no saltea en silencio: si falta un artefacto de la lista, sale con error en vez de armar un
rootfs al que le faltan tres paquetes y falla tres capas más abajo.
|
||
|
|
442a319499 |
lab re-pineado con lld (d1e341d5) — y el sha «determinista» lo ensuciaba un log
Al ir a re-pinear la imagen para propagar el `lld` a la granja, hub y worker empaquetaron a shas DISTINTOS. La imagen se creó para que el sha VERIFIQUE, así que un desacuerdo ahí no se pinea: se mide. Los dos rootfs son idénticos: 9550 entradas con la misma lista de ficheros, los mismos tamaños, y CERO diferencias de metadato (modo, nlink, destino de symlink) en las 8778 entradas de fichero y symlink. La única diferencia de contenido en todo el rootfs era `var/log/apk.log`, donde apk anota la FECHA de cada operación — o sea que dos labs equivalentes empaquetaban distinto sólo porque las instalaciones ocurrieron en momentos distintos. Un sha que no se puede reproducir desde un rootfs equivalente no verifica nada, que es justo lo que este fichero existe para dar. Se excluye ese log del tar (no todo `var/log`: cambio mínimo; apk lo recrea solo). Con la exclusión, hub y worker dan el MISMO tar: 179d04f054f28d8dafe7c626d6b8c686a33a00a96715ee544a9bcf6f62cf586d. ⇒ LA GRANJA YA ESTABA SINCRONIZADA Y AHORA SE PUEDE DEMOSTRAR, sin correr `--traer` en un worker que está construyendo firefox — reemplazarle el rootfs a mitad de build habría sido la forma cara de descubrir lo mismo. Nueva imagen publicada: hammer/lab/lab-d1e341d5….tar.zst (310 M), pin actualizado en bootstrap-devfs.sh. Queda en el comentario cómo comparar dos máquinas: se compara el TAR y no el `.tar.zst`, porque el sha comprimido depende de la versión de zstd de cada máquina y dos labs idénticos con zstd distintos darían un falso desacuerdo. |
||
|
|
cb3ecd5a00 |
firefox pasa a clang+lld con LTO: la puerta de las optimizaciones costaba apk add lld, no una receta de LLVM
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). |
||
|
|
aa200a4e7e |
vigía de parches: probar que agarran ANTES de las cuatro horas de build
`hammer build` aplica los parches DESPUÉS de materializar las dependencias, así que en una receta hoja de la plataforma Gecko el `patch` que no agarra se descubre detrás de horas de compilar OTRA COSA: waterfox estrena su primer build reconstruyendo nodejs entero (4414 objetivos de V8, a -j2 porque la propia receta capa por RAM). La receta de waterfox dice, con todas las letras, que si patch falla «falla TEMPRANO, antes de compilar nada». Con el orden real de hammer eso no era cierto. Este vigía es lo que lo vuelve cierto: saca de los propios parches los ficheros que tocan (21 en waterfox), los trae por sparse-checkout blob:none, y aplica los once acumulativos y en orden como hace fetch.rs. Un minuto en vez de cuatro horas. LO QUE MIDE NO ES SÍ/NO, SON TRES ESTADOS. `patch` también responde «sí, adivinando»: cuando el contexto no casa aplica igual con FUZZ, y con el `--silent` de fetch.rs eso sale por exit 0 sin que nadie se entere. Un parche con fuzz puede haber editado el sitio correcto o cualquier otro, y el exit code no distingue. Por eso `ok` / `FUZZ` / `FALLA`, y el del medio existe para no perderse. El desplazamiento no se marca: sólo dice que el fichero creció por arriba. VEREDICTO SOBRE LA APUESTA DE WATERFOX (parches de 154 sobre base 153.1.0): SE SOSTIENE. Los once entran; los dos que entran con fuzz —time64 y fix-rust-target— se verificaron a mano y los dos editan el sitio correcto. El contexto que no casa es cosmético: comillas simples vs dobles de un reformateo con black, y un brazo `cfg` de más. Y UN HALLAZGO DE PASO: `firefox-patches/time64.patch` tiene la cabecera MENTIROSA. Su diffstat anuncia tres ficheros y el cuerpo entrega dos — falta el hunk de `wgpu-hal/src/vulkan/adapter.rs`. Como firefox 154 sella igual, nadie lo había notado. Por eso los ficheros se leen del cuerpo y nunca del diffstat. El vigía nació mintiendo en la dirección cara y por eso se probó contra recetas selladas antes de commitear: suponía el prefijo `a/`+`b/` de git y marcaba FALLA sobre gawk, que sella perfecto — su parche es un `diff -upr` a secas con caminos `gawk-5.1.0.orig/`. Se descarta el primer componente se llame como se llame, que es lo que significa el -p1 con el que se aplican. Flags en inglés (--all, --git-only, --keep, --strict) por la regla 4 del repo. |
||
|
|
9834f57052 |
yupana: --help imprimía literalmente None
Todo el encabezado del fichero era comentario `#`, así que __doc__ era None y `main()` hace `print(__doc__)` en dos caminos: `--help` y verbo desconocido. La puerta única de la metodología no sabía explicarse, y el error de verbo tampoco decía cuáles son los verbos válidos. La ayuda operativa pasa a docstring; el diseño y el porqué se quedan arriba como comentario. Cotejada contra el despacho real: los 11 verbos de la ayuda existen y los 11 que existen están en la ayuda (faltaba `sembrar`). |
||
|
|
28d98dc5f9 |
drenaje: medía 5 de 7 imágenes y las otras 2 desaparecían en silencio
targets.toml declara 7 perfiles; drenaje.json listaba 5. escritorio-cosmic (cola incoming-cosmic) y escritorio-sway (cola incoming-wlr) declaraban una cola que ningún grafo de GRAFOS contenía, así que cargar() devolvía None y --todos hacía `continue` pelado. Dos imágenes enteras fuera del artefacto sin dejar rastro, y los dos números —5 medidos, 7 declarados— no se cruzaban en ningún lado. Mismo olvido que ya costó 17 días de build-state-wlr.json congelado: la lista de grafos crece a mano y se queda atrás cuando se abre una cola. Tres cambios: - GRAFOS suma los grafos de cosmic y wlr. - Un perfil sin medir se anota en el artefacto (`sin_medir`), se grita por stdout y drenar.py sale 1. Regla 3 del repo: un ausente falla ruidosamente. - cosecha-cron deja de tragarse la salida; filtra las líneas ⚠ al log, que si no llegaban como un "falló" mudo que no dice cuál imagen falta. Medido: las 2 imágenes que nadie miraba estaban limpias, 0 en deuda. Ahora son 7/7 verificadas en vez de 5 verificadas y 2 supuestas. |
||
|
|
97023c3306 |
granja: PODA_FUENTES nace encendida y respeta el árbol de la receta que falló
Causa raíz de la deuda KDE de hoy: la cascada del 2026-09-03 corrió campana-deuda SIN PODA_FUENTES=1 (cero menciones en su log). Los 71 árboles de fuentes se acumularon en /dev/sdc a ~300 MB cada uno y lo llenaron en la receta 52; las 8 últimas murieron por disco, no por receta. Un default que hay que acordarse de encender no es una defensa. No poda tras un ✗: el árbol de la receta fallida es el post-mortem, y es justo el caso en que alguien va a mirarlo. Mismo criterio que el suelo de 24 h de poda-fuentes.sh, aplicado por resultado en vez de por edad. Probado: sobrevive tras ✗, poda tras ✓, y PODA_FUENTES=0 sigue apagándola. |
||
|
|
3f26d37d17 |
granja: guardián de disco en campana-deuda — corta en vez de anotar ✗ falsos
La cascada KDE del 2026-09-03 llenó el disco en la receta 52 de 71 y siguió moliendo: las 8 últimas murieron en 1-20 s con «No space left on device» y el bucle las anotó ✗, el mismo símbolo que una receta que no compila. El grafo del día siguiente decía «8 en deuda, clase c» y mandó a buscar un bug inexistente. Mide el MÍNIMO de dos filesystems, no uno: en gioser store/ (/dev/sdb, 109 G) y work/ (/dev/sdc, 53 G) son discos distintos y el que se llenó fue el de work/, donde vive el árbol de fuentes. Mirando sólo el store el vigía habría leído 109 G y dejado moler igual. Al cruzar el suelo corta con exit 3 y lista las recetas SIN INTENTAR, que no es lo mismo que fallidas. |
||
|
|
1645e251bc |
cosmic anidado: el panel dibuja en una ventana — y el cierre del perfil NO puede hacer GL
Hermano de `sway-nested.sh` para el cuarto escritorio. Levanta cosmic-comp nesteado y sus clientes;
sale con el panel entero: app-library, app-list, siete applets, pop-launcher, cosmic-toplevel y los
dos procesos del portal.
**El hallazgo, que es de la IMAGEN y no del arnés**: `/usr/lib/dri` del rootfs hidratado trae
`iris_dri.so` y NADA MÁS — el driver de Intel real. Sin `swrast_dri.so`, EGL no inicializa y
cosmic-comp muere con `Egl(InitFailed(NotInitialized))` detrás de dos `MESA-LOADER: failed to open
{zink,swrast}`. O sea que el cierre 133/133 de `escritorio-cosmic` **no puede componer en ninguna
máquina que no sea Intel** — ni en una VM, ni nesteado.
No es nuevo, es un hueco de DECLARACIÓN: `scripts/gnome/qemu-desktop-image.sh` ya lo dice en su
cabecera y copia `mesa-llvmpipe` encima con `--remove-destination`. Cada script de imagen inyecta a
mano algo que `targets.toml` no declara — justo la divergencia que `hydrate-profile.py` hace
visible. Y no se arregla declarándolo: `mesa` y `mesa-llvmpipe` instalan los MISMOS `.so`
(libEGL/libgbm/libGLESv2), así que las dos en un perfil colisionan fichero a fichero — la figura de
las dos poppler. Los scripts lo resuelven PISANDO, que es decisión de imagen y no arista de grafo.
Acá se hace lo mismo y se dice: `mesa-llvmpipe` entra como TERCER overlay, el último, o sea el de
mayor prioridad. El artefacto se elige por `hammer hash`, no por el más reciente del store.
Y `cosmic-session` no sirve para nestear: no le pasa `WAYLAND_DISPLAY` a cosmic-comp —para ella el
compositor ES el display server— y su stderr lo captura `launch_pad`, que sólo repite «cosmic-comp
exited with error code 1» y lo reinicia en bucle, así que la causa no aparece en ningún log. Por eso
el modo por defecto es `bare`: compositor a mano y clientes apuntados a SU socket.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGyBj4QyeCZiwLMkP9GJp6
|