Los 8 ejecutables decian static y enlazaban musl dinamicamente. La huella que lo
delata sin mirar el build: `readelf --dyn-syms` daba 21475 entradas contra ~2700
de un binario dinamico normal — eso es --export-dynamic, no un enlace corriente.
NO venia del meson de gtk (ahi no aparece la palabra) sino de un .pc DE
DEPENDENCIA: `gmodule-2.0.pc` trae literalmente `Libs: -Wl,--export-dynamic`, y
`meson.build:426` pedia `dependency('gmodule-2.0')`. El wrapper de cc del lab
(`sandbox.rs:597`) tira el `-static` ante --export-dynamic ⇒ lo ponia el lab y
lo sacaba el lab, por un flag heredado de una dep.
No hace falta parchear el .pc de glib: upstream ya publica la misma libreria sin
el flag. `gmodule-no-export-2.0.pc` trae el `-lgmodule-2.0 -pthread` de verdad y
los otros dos solo hacen Requires sobre el añadiendo --export-dynamic. Es la
palanca prevista, no un apaño — de hecho `gio-2.0.pc` de la propia glib ya usa
la variante no-export.
REGLA GENERAL: cualquier receta que enlace gmodule hereda --export-dynamic y
pierde el -static SIN AVISO. Si link=static no pega y hay glib de por medio,
mirar los .pc de las deps antes que el build del proyecto.
Y entonces aparecio un bug que SOLO existe en estatico: los 8 morian con SIGSEGV
hasta en `--help`. Backtrace con gdb:
g_module_symbol (module=0x0, symbol_name="gtk_progress_get_type")
_gtk_module_has_mixed_deps (module_to_check=0x0) at gtk/gtkmain.c:474
do_pre_parse_initialization () at gtk/gtkmain.c:498
gtk_init_check () at gtk/gtkmain.c:635
`_gtk_module_has_mixed_deps(NULL)` hace `g_module_open(NULL,0)` y pasa el
resultado a `g_module_symbol` sin comprobarlo. En musl ESTATICO `dlopen(NULL)`
devuelve NULL siempre ⇒ deref de nulo en el offset 8, en el arranque. Con libc
dinamica el bug no se ve nunca. Cuarto parche de la receta: la comprobacion que
falta. Devolver FALSE es correcto ademas de seguro — en un estatico no hay
modulos cargables, asi que no puede haber simbolos de GTK 2/3 mezclados.
Control: 423 ficheros intactos, NEEDED=0 en los ocho, .dynsym de 21475 a 0, los
ocho arrancan, gtk4-path-tool hace trabajo real, y `builder-tool validate`
devuelve EXACTAMENTE el mismo «Could not initialize windowing system» que daba
el binario dinamico anterior.
Cae en cascada el re-hash de 7 dependientes; se reconstruyen a continuacion.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
No era purismo. El binario sellado, ejecutado fuera del sandbox:
Error relocating /lib/libncursesw.so.6: __vfprintf_chk: symbol not found
Salia dinamico contra `libncursesw.so.6` y `libc.so`, y NINGUN artefacto del
corpus provee esas dos —el de ncurses solo empaqueta .a—, asi que solo corria
dentro del rootfs de Alpine del sandbox. Esta en los perfiles base, cli,
escritorio-cosmic y escritorio-sway. Mismo sintoma que curl en su dia.
Causa: `configure.ac:1223` pone `LOCAL_LDFLAGS=-rdynamic` en linux* (para que
`enable -f` pueda resolver simbolos de bash), y el wrapper del lab quita el
`-static` ante `-rdynamic`. Es la misma raiz que htop y e2fsprogs.
`--enable-static-link` SOLO NO ALCANZA: pone `STATIC_LD=-static`
(`Makefile.in:170`) pero queda en la misma linea que el `-rdynamic` de
`BASE_LDFLAGS`, y el wrapper mira la linea entera. Van los dos.
PRECIO MEDIDO, y es un cambio de contenido: el artefacto pasa de 122 a 16
ficheros. No es accidente sino acoplamiento de upstream — `configure.ac:981`
salta `AC_CHECK_LIB(dl, dlopen)` cuando `opt_static_link=yes`, y sin
`HAVE_DLOPEN` no se construyen ni instalan los ~40 cargables de `usr/lib/bash/`,
sus 62 headers ni `usr/lib/pkgconfig/bash.pc`. Es correcto: un binario estatico
no puede dlopen, e instalarlos seria publicar una capacidad inexistente. Y no se
pierde nada que funcionara: dinamico no eran «cargables que funcionan», era «un
bash que no arranca». `yupana radio bash` da 0 dependientes.
Control: NEEDED=0, arranca, dice 5.3.0(1)-release (x86_64-pc-linux-musl) y pasa
arrays, asociativos, aritmetica, sustitucion de comandos y `=~`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
`e2fsck/Makefile.in:119` enlaza con `$(RDYNAMIC)` y es el UNICO de las 31
herramientas que lo hace (`configure.ac:109` lo pone en `-rdynamic`; ningun otro
Makefile.in del arbol lo menciona). El wrapper de cc del lab
(`sandbox.rs:597`) tira el `-static` inyectado ante `-rdynamic`, asi que e2fsck
—y sus tres alias fsck.ext{2,3,4}— salian dinamicos mientras las otras 27
herramientas del mismo build salian estaticas. Eso explica exactamente el
«4 de 31» que reporto el audit estricto.
Upstream pide `-rdynamic` para simbolizar el backtrace del manejador de senales
fatales, no para enlazar. Con estatico el backtrace pierde nombres, que es lo
que `link = "static"` ya pedia.
`RDYNAMIC=` va en compile Y en install (install relinkea).
Control: NEEDED=0 en los cuatro, los 149 ficheros del artefacto intactos, y
prueba funcional real — mke2fs crea un ext4 de 16M y e2fsck lo recorre las cinco
pasadas limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Arreglando las tres hojas salio que el guardian tenia dos agujeros, y los dos
devuelven un pase en falso, que es la peor direccion posible para un gate.
1. `break` en el PRIMER ELF ejecutable. e2fsprogs: el primero que encontraba era
`bin/lsattr` (estatico) ⇒ receta HONESTA, mientras `e2fsck` y los tres
`fsck.ext*` eran dinamicos. 4 de 31, invisibles.
2. `find | head -40`. bash trae 42 ejecutables y CUARENTA son los modulos
cargables `usr/lib/bash/*`, que son ELF *shared object* y no matchean
`ELF.*executable`. El cap cortaba la lista ANTES de llegar a `bin/bash` ⇒ el
audit concluia «sin ELF» y eso en el resumen se lee como que no hay nada que
objetar. El shell del perfil base, dinamico, sin que nadie lo viera.
Con dwarves el `break` habia acertado de casualidad —el primero tambien era
dinamico— y por eso reportaba 1 donde habia 10.
Ahora recorre TODOS los ejecutables, sin cap, cuenta cuantos mienten sobre
cuantos hay, y nombra un ejemplo: «gtk4 dice static, 8 de 8 ejecutables
DINAMICOS (p.ej. usr/bin/gtk4-rendernode-tool)».
Barrido completo con el metodo estricto: 684 estaticos de verdad, MIENTEN 3
(gtk4, bash, e2fsprogs), 59 sin artefacto. Con el metodo viejo, sobre el mismo
store, salia 685/1/60. La diferencia son los dos que se escapaban.
Es «no comprobar no es aprobar» aplicado al propio guardian — el mismo fallo que
`hammer kernel contract` ya habia tenido que corregir en SDD 25 §4.ter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
La receta decia `link = "static"` y el audit marcaba `pahole`. Mirando el
artefacto entero no era uno: eran los DIEZ ejecutables, contra cinco librerias.
El `-static` NUNCA faltaba. `build/CMakeFiles/pahole.dir/link.txt` —donde cmake
deja la linea de enlace literal— lo tenia puesto en la posicion 9, y mas
adelante en la misma linea habia un `.so` de ruta absoluta; el wrapper de cc del
lab (`sandbox.rs:597`) quita el `-static` ante cualquier `.so` en la linea, asi
que lo ponia cmake y lo sacaba el wrapper. Ese fichero es el sitio donde mirar
la proxima vez que `link = "static"` no pegue en un proyecto cmake.
Eran TRES `find_*` metiendo `.so`, no uno, y cayeron de a uno:
· FindDWARF.cmake → libelf.so ⇒ -DDWARF_LIBRARY/-DELF_LIBRARY a los .a
· find_package(ZLIB) (CMakeLists:52) → /usr/lib/libz.so ⇒ -DZLIB_LIBRARY
Sin `-static`, ademas, `-llzma`/`-lbz2` resolvian a las .so DEL ROOTFS de
Alpine y no a los artefactos de xz/bzip2 (que solo traen .a): entradas no
declaradas, de las que rompen el cono de affected.py.
`-DCMAKE_FIND_LIBRARY_SUFFIXES=.a` NO sirve y se probo: Platform/Linux.cmake lo
asigna como variable NORMAL en `project()` y esa tapa a la del cache.
Control, ademas de NEEDED=0 en los diez y la lista de ficheros intacta: el
pahole nuevo ARRANCA en el host (el viejo moria con `Error relocating
/lib/libelf.so.1: rawmemchr: symbol not found`), dice v1.30 y emite una seccion
.BTF real con `-J`, que es lo que hace CONFIG_DEBUG_INFO_BTF. Con eso el punto 3
de la deuda de SDD 25 H7 queda pagado y la herramienta probada de punta a punta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
El `-lwayland-client` que salia de pkg-config resolvia a libwayland-client.so.0
y `link = "static"` era mentira (audit 2026-08-31). `--prefer-static` hace que
meson pida `pkg-config --static` y prefiera los .a; el artefacto de wayland
trae libwayland-client.a, asi que hay con que.
De paso saca el .so de la linea de enlace, que es lo que hacia que el wrapper
del lab tirara el `-static` inyectado (`sandbox.rs:597`) — misma raiz que htop.
Control: NEEDED=0 en todos los ejecutables ELF, lista de ficheros intacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
QUINTA causa del frente link=static, y la unica que no es culpa de la receta:
`Makefile.am:243` hace `AM_LDFLAGS += -rdynamic` bajo `if HTOP_LINUX`, y el
wrapper de cc del lab (`sandbox.rs:597`) quita el `-static` inyectado ante
`-rdynamic`/`--export-dynamic`/un `.so` en la linea. La regla del wrapper es
correcta —`-static` no se sostiene contra un link intrinsecamente dinamico—,
pero el `-rdynamic` de htop es solo para simbolizar el backtrace del manejador
de fallos, no una necesidad de enlace.
Efecto que tenia: `-lncursesw` resolvia a la libncursesw.so.6 DEL ROOTFS de
Alpine en vez del artefacto de ncurses (que solo trae .a) — una entrada no
declarada, de las que rompen el cono de affected.py.
`AM_LDFLAGS=-static` va en compile Y en install: si `make install` relinkea sin
el override devuelve el binario dinamico (la leccion de parted).
Seguro: el UNICO dlopen del arbol esta en linux/LibSensors.c y la receta ya
pasa --disable-sensors; delayacct queda en `no` por no estar libnl en [deps].
Control: NEEDED=0 en todos los ejecutables ELF, y la lista de ficheros del
artefacto no perdio nada contra el sellado anterior.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
El iterador pide CAP_BPF+CAP_PERFMON, y en tawasuyu el barrido de /proc de hoy
vive en `sandokan-local`, que corre SIN privilegio (se mete en un user
namespace a proposito). El unico proceso privilegiado es `sandokan-daemon`
—lo dice su propio cortafuegos.rs—. Verificado leyendo el repo espejo, no
supuesto: W2 por esta via no es un reemplazo en el lugar, cruza la frontera.
Es un coste de diseño que el cociente de 7x no muestra.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Cae el quinto de los seis experimentos nombrados. Un `iter/task` que emite
registros binarios saca la foto de procesos cerca de 7x mas barato que el
barrido de /proc (6,9-8,0x en cuatro corridas de 33 rondas y las dos
direcciones), pero el numero que decide es otro: cuesta 1,75x el piso de
`readdir` contra 12,1x de /proc. Leer los campos deja de tener precio; lo que
se paga es enumerar.
Dos barreras comprobadas, no supuestas: el programa se ata por id de tipo BTF
del vmlinux (attach_btf_id=80137, prog_type=26) — sin DEBUG_INFO_BTF no hay a
que atarse, no hay version de esto que esquive H7 —, y hace falta
CAP_BPF+CAP_PERFMON, o sea que el consumidor es el supervisor, no la Card.
Y los dos caminos NO dicen lo mismo: 24 de 245 comm difieren, todos kworkers,
porque /proc SINTETIZA el nombre pegandole la workqueue. Un reemplazo vendido
como «la misma informacion mas rapido» tenia una diferencia de contenido, y
solo aparecio cotejando registro contra registro.
H7 deja de decir «pahole en el lab y un re-hasheo». Son cuatro cosas:
DEBUG_INFO_NONE=y en los .config sellados (BTF exige DWARF antes), el kernel
no declara la receta dwarves aunque este sellada, ese pahole es DINAMICO pese
a link="static" (INTERP + 5 NEEDED) y corre dentro del sandbox del build, y
BTF mete la version de pahole en la bit-repro con el lab fuera de hash_inputs.
Ya hay dos consumidores nombrados, no cero: este iterador y sched-ext.
Lo que sigue SIN medir es el precio en bytes del bzImage, que es como se pago
H1 — se mide construyendo, y esa es la primera tarea el dia que se decida.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
`Restart=always` + `RestartSec=30` sin límite: si una receta no entra en la RAM de la
caja, el OOM killer la mata, systemd relanza a los 30 s, el loop recorre la cola por glob
alfabético y vuelve a la MISMA receta. En el LXC dev.gioser.net eso duró 30 horas con
`clang18`, y dejó la máquina con presión de I/O `full avg300=43` —todo bloqueado en disco
casi la mitad del tiempo— y sshd incapaz de completar el banner. Diagnosticarla desde
fuera era imposible: parecía red o disco. El único rastro eran dos `oom-kill` en el
journal, que sólo se ven desde dentro.
StartLimitBurst=3 / StartLimitIntervalSec=3600: a los 3 arranques en una hora systemd se
rinde y deja la unidad en `failed`, VISIBLE en `systemctl status`. OOMPolicy=stop: un OOM
no es un fallo transitorio, si no entra ahora no entra en 30 segundos.
Rendirse no pierde nada: el hub ya tolera workers ausentes —cosecha-cron dice "siembra
falló" y sigue con el siguiente— y un worker parado y diagnosticable vale más que uno que
se reinicia para siempre.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Las 24 en deuda selladas, más clang18. base 52/52, cli 75/75, mirada 33/33, KDE 163/163,
sway 129/129, GNOME 119/119, COSMIC 90/90. Cero artefactos vacíos en 2103.
Las dos últimas (`cosmic-settings`, `cosmic-applets`) no cedieron al paralelismo: habían
caído con 3, 2 y 1 core. Lo que las destrabó fue MEMORIA, y la vía compatible con T9 es
zram, no swap en disco: se midió que el zram comprime a 4x real (1,9 G de páginas en
478 MB de RAM) y que los 4 GiB de `swap-zram` son sólo su default, sin justificación.
Se añadió un SEGUNDO dispositivo zram en vez de redimensionar el primero: redimensionar
exige `swapoff`, y con 1,94 G dentro y 994 MB de RAM libre eso es el escenario exacto que
volteó la máquina el 2026-08-17 —el propio script lo advierte y tiene freno—. Verificado
`backing_dev=none` en el dispositivo nuevo, que es el único agujero de zram contra T9.
`cosmic-settings` selló después con 3 cores, la misma config con la que había fallado ⇒
el paralelismo nunca fue la causa, sólo esquivaba el síntoma.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
`targets.toml` declaraba gnome-session, gnome-settings-daemon y gdm como raíces del
perfil, mientras el encabezado de las tres recetas dice "APARCADA por diseño" desde el
2026-08-07, verificado contra el meson.build de cada tag: las tres mueren en GTK3, que es
una de las tres deudas que el frente GNOME aparcó a propósito (GTK3 / X11 / PAM).
Dos documentos del repo decían cosas opuestas, y el que se mira primero es el grafo. El
resultado era deuda FANTASMA: escritorio-gnome reportaba 124/127 con 3 en `never` para
siempre, se leía como trabajo pendiente, y el worker las reintentaba en cada ciclo. En
esta misma sesión me hizo afirmar dos veces que GNOME estaba "a 3 recetas de cerrar".
No bloquean el escritorio: gnome-shell no depende de gnome-session ni de g-s-d, ni en
build ni para arrancar. El camino vivo es mutter → gnome-shell, lanzado por arje; el
único que pedía gnome-session era gdm.
Las recetas SIGUEN en el repo con su análisis intacto. Si algún día se autora GTK3, se
vuelven a añadir a `paquetes` y el objetivo reaparece solo.
escritorio-gnome: 119/119, CIERRA.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
project_plan es target_root.join(rel) + escribir por ruta, igual que hydrate: un symlink de
directorio dejado por una generación anterior se seguía. Reproducido — la generación 2 escribía
usr/share/pkg/archivo FUERA del root y ApplyReport salía en verde.
Con --root / no cambia nada (todo empieza por /); el caso real es , que es como se arma una imagen.
Se mide el DIRECTORIO PADRE, no el destino: un Replaced sobre un symlink que apunta afuera es
legítimo porque rename pisa el symlink en vez de escribir a través de él, y medir el destino lo
rechazaría por error. Y raiz_real() resuelve el tramo existente del root dejando pegado el que
todavía no existe, para que la comprobación valga también sobre una imagen nueva.
Dos tests, uno por dirección: el que sale falla ruidoso, el que se queda dentro (usr-merge,
temas de iconos) sigue proyectando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Buscando el sitio de H3 —«harkaq valida rutas a mano»— resultó que harkaq NO valida rutas a
mano: delega en Landlock. Donde no se validaba NADA era en hydrate.
Reproducido: hidratar es target_fhs.join(rel) y escribir por ruta, así que un symlink de
directorio ya puesto en el FHS se sigue. Artefacto A trae usr/share/pkg → /algún/lado (los
symlinks se replican literales, y así debe ser); al hidratar B, usr/share/pkg/archivo aterrizaba
FUERA del root y hydrate devolvía Ok(1). Con --into sobre una imagen eso es escribir en el
anfitrión diciendo que todo fue bien.
Cuánto se estaba disparando: cero. En el store hay 160 symlinks absolutos y NINGUNO apunta a un
directorio; de ~42 000 relativos ninguno sale de su artefacto. Mina desactivada, no incendio.
La comprobación va por DIRECTORIO (un canonicalize por fichero sería un realpath por entrada,
T8) y la semántica es la de RESOLVE_IN_ROOT, no la de NO_SYMLINKS: un symlink que se queda
dentro tiene que seguir andando —usr-merge /lib → usr/lib, los 22@2x → 22 de los iconos— y hay
test para cada dirección. Lo que queda de la propuesta (la versión sin carrera, con openat2 +
linkat/renameat) queda escrito con su precio: arrastra el camino de patchelf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
El gate de SDD 25 §4.ter existía y NADIE lo corría. Un kernel reconstruido sin MEMCG volvería
a pasar inadvertido — y ese fallo no se ve del lado del desarrollo, porque el kernel de la
máquina de trabajo sí trae MEMCG: sólo se ve comprobando el artefacto.
Mismo cable que el vigía de fuentes y por la misma razón que está escrita ahí arriba: lo que
nadie refresca envejece hacia el optimismo. Ahora cada ciclo deja el veredicto en
docs/state/kernel-contract.txt y el git log lo muestra como el resto del khipu.
El fichero se escribe por temporal y sólo se mueve si tiene contenido: contract sale != 0
cuando el contrato no se cumple —y ese rojo es justo lo que hay que guardar—, pero un fichero
vacío se leería como «nada que objetar».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Se construyeron en gioser, no en el worker: el LXC dev.gioser.net lleva ~14 h sin ssh.
Quedan `cosmic-applets` y `cosmic-settings`, que superan el techo de memoria de la caja
INCLUSO con un solo core (7,7 G con un suelo de 2,3 G que ponen gitea y los agentes) — no
es paralelismo, es el pico de un `rustc` solo.
El método que las trajo: capar por entorno con `taskset` (nunca `-j` en la fase, que
cambiaría el ArtifactHash) bajando 3→2→1 cores, con un vigía que corta el lote si el swap
libre baja de 250 MB. Cada bajada compró recetas. El vigía cortó cuatro veces y la máquina
sobrevivió las cuatro; el LXC pasó por el mismo pico sin guardián y no volvió.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
hammer kernel contract --sealed: 5 de 5 vigentes cumplen su perfil, exit 0. Antes eran 2 de 11.
Una Card que pide memory.max ahora encuentra el fichero en vez de dejar un warn! y correr sin
tope.
Lo medido, no lo supuesto:
- el x86_64_defconfig de 7.1 ya NO trae MEMCG=y ⇒ había que pedirlo explícito;
- MEMCG no tiene ningún depends on, así que el olddefconfig no podía tragárselo — comprobado
igual contra el .config producido, que es la regla del guardián;
- precio: +80 KiB en linux-generic (+0,49%), +84 en linux-metal, +88 en linux-metal-dual,
+80 en linux-gioser (+0,71%). Medio punto de kernel por contabilidad por unidad y presión.
Evidencia cruda en docs/evidencia/tasas-kernel-2026-08-29/contrato-h1-pagado.txt, con el
diff-back del plan de gioser sobre su config nuevo (35 cumplidos, 0 incumplidos).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Faltaban dos agujeros del mismo tamaño que el que este guardián vino a tapar:
1. La receta derivada de gioser vive fuera de recipes/ a propósito, así que sus deps no
resolvían y su hash no se podía calcular ⇒ su sellado viejo se comprobaba como si fuera el
vigente. Ahora se le presta el catálogo (base_dir), salvo que traiga patches — que base_dir
también los resuelve y moverlo los rompería en silencio.
2. Un objetivo del contrato cuya receta de hoy no tiene NINGÚN sellado simplemente no aparecía
en el barrido, y no aparecer se leía como que no había nada que objetar. Ahora se nombra con
el hash que le tocaría y sale != 0: no comprobar no es aprobar.
Hoy eso dice, con nombre y hash, exactamente los 4 kernels que hay que construir para dar H1
por pagado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
pre_exec apaga el camino posix_spawn de Rust y devuelve el spawn a fork+exec: 231 µs contra
3 949 µs con 256 MB tocados en el padre (SDD 25 T1). hammer no usa ninguno hoy, pero nadie lo
comprobaba.
El test se comprobó en los dos sentidos: inyectando un pre_exec en sandbox.rs se pone rojo, y
si el barrido no encuentra fuentes también falla — un guardián que aprueba por vacío es el
fallo de CLAUDE.md §3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
La receta derivada inlinea la fase configure de su base, así que el MEMCG de linux-metal sólo
llega acá regenerando. Aprovecha el re-hasheo obligado para soltar la concesión del -j1: vuelve
al -j"$(nproc)" que emite el armador, que es la regla del repo — capar el paralelismo en la
receta cambia el ArtifactHash aunque el binario sea idéntico, y el throttling va por entorno.
b3:b026fe14… reemplaza a b3:8ffd5710… (que era la variante -j1 sin MEMCG).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
El store guarda todos los sellados, no el último. Como `--sealed` los miraba a todos por
igual, al cambiar una receta de kernel el gate quedaba rojo para siempre por artefactos que
nadie va a volver a construir — y un portón que no puede ponerse verde deja de leerse.
Ahora clasifica cada sellado contra el ArtifactHash de la receta de hoy (misma vigencia que
`hammer hash --check`, lab incluido): el vigente bloquea, el superado sale en su propia
sección con lo que le falta, porque sigue siendo cierto que una máquina que arranque ese
kernel corre sus Cards sin tope.
Dos negativas explícitas: si no se puede resolver la vigencia se comprueba TODO y se dice por
qué; y cero vigentes con superados a la vista sale != 0 en vez de verde — el vacío leído como
presencia es justo el fallo que este guardián vino a arreglar.
Recetas derivadas incluidas: --recipes mira `recipes/` y `docs/state/kernel-plans/`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Sin CONFIG_MEMCG el fichero memory.max no existe y arje-incarnate descarta el error: una
Card pedía un tope de memoria y corría sin ninguno, dejando sólo un warn!. El x86_64_defconfig
de 7.1 ya no trae MEMCG=y, así que había que pedirlo explícito.
Va en tanda con PSI (que el contrato declara como wants) para pagar de una sola vez el
re-hasheo: la fase configure ES la identidad del artefacto. linux.toml queda intocado a
propósito — es el baseline of_tree del selfhost-verify y no hospeda Cards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
`diff-back` contesta «¿el .config honra el PLAN?»; el contrato contesta la otra
mitad, «¿el kernel trae lo que el userland usa por nombre?». Son distintas: un
kernel puede honrar su plan al pie de la letra y no tener MEMCG, que nunca
estuvo en ningún plan porque nadie lo pidió.
Queda dicho también que hoy sale ROJO a propósito (2 de 11) y por qué.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
- §0 y §4.bis: el «sin MEMCG» deja de ser deducción del defconfig. Los ONCE
.config sellados traen `# CONFIG_MEMCG is not set` y `# CONFIG_PSI is not
set`, y ninguno tiene CFS_BANDWIDTH (⇒ tampoco existe `cpu.max`; hoy nadie
lo pide). Y se nombra por qué sobrevivió: el kernel de la máquina de
desarrollo SÍ los trae — el fallo sólo existe del lado del artefacto.
- §4.ter: el guardián `hammer kernel contract`, con las tres decisiones de
diseño (por perfil, contra el .config y no la receta, apagado ≠ ausente).
- §3: de tres sorpresas a cinco (taskstats no es más rápido que /proc; el
sondeo es ciego, no lento).
- §9: reescrita. Cuatro de los seis experimentos nombrados quedaron medidos
con root; siguen abiertos el iterador BPF (detrás de H7) y el Intel con PTI.
- §5, §7 y §10: filas medidas, H1 marcado como pendiente-pero-vigilado,
H8 (el contrato es parte del release) y H9 (reflink alimenta SDD 18).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
SDD 25 §9 listó seis experimentos que no se pudieron correr y por qué. Cuatro
de ellos sólo pedían privilegios, y esta sesión los tuvo. Programas y salidas
crudas en docs/evidencia/tasas-kernel-2026-08-29/.
1. proc connector (bench_connector.c) — el sondeo de /proc no es lento, es
CIEGO: 200 procesos de vida mínima lanzados uno cada 10 ms, sondeando cada
100 ms se ven **0 de 200**; por connector, **200 de 200**. Y esperar el
evento no cuesta nada frente a waitpid (116,0 vs 114,8 µs, dentro del ruido),
con 0 eventos perdidos en 1400.
Sorpresa: taskstats por genetlink **no es más rápido** que parsear
/proc/self/stat (2 688 vs 2 697 ns). Su valor es el CONTENIDO (delay
accounting), no la velocidad.
2. reflink (bench_reflink.c) — el «copiar en O(1)» existe y NO está en ext4:
FICLONE no soportado, y copy_file_range ≈ read+write (13,7 vs 14,5 ms).
En xfs y btrfs es PLANO: 64 MiB en 0,010 ms y 512 MiB —ocho veces más— en
0,020 ms, contra 512,7 ms de read+write. **25 600× a 512 MiB.**
3. io_uring SQPOLL (bench_sqpoll.c) — no rescata el veredicto de T12: sobre
datos cacheados pierde igual. ext4: pread 554 ns < io_uring 618 < SQPOLL 868.
tmpfs: pread 749 < SQPOLL 1 409 < io_uring 2 030 (ahí sí mejora al io_uring
normal, 1,4×, pero sigue perdiendo contra la syscall). Y se come un core.
4. syscalls por invocación de compilador — con el strace DEL PROPIO CORPUS
(store/3c1fd8c0…-strace), que es la respuesta a «no hay strace en esta
máquina». gcc: 1 803 syscalls por un `-c` trivial, **1 095 con error (61%),
919 de ellas readlink** — la canonicalización de rutas, que T8 no medía.
Pero al piso de 78,7 ns son 0,14 ms sobre 30,5 ms de compilado: **el cruce
al kernel es el 0,5%**. La tormenta de rutas vale milisegundos, no minutos.
Condiciones distintas a la corrida original (load ~1,3 sobre 4 vCPU contra 7,3)
⇒ los absolutos NO se comparan entre corridas, sólo los cocientes dentro de una.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
SDD 25 §4 dejó el hallazgo escrito y sin guardián: los kernels de hammer se
construyen sin CONFIG_MEMCG, `memory.max` no existe, y arje descarta el error
al escribirlo ⇒ una Card pide un tope de memoria, corre SIN tope, y la única
huella es un `warn!`. Nadie lo veía porque la máquina de desarrollo SÍ trae
MEMCG: el fallo sólo existe del lado del artefacto sellado.
`hammer kernel contract` declara qué pedazos de interfaz de kernel usa el
userland POR NOMBRE (con consumidor, fichero y CÓMO FALLA HOY si no está) y los
comprueba contra un `.config` YA PRODUCIDO — no contra la receta: entre el
`scripts/config -e X` y el `.config` hay un `olddefconfig` que puede tragarse el
símbolo en silencio.
Por perfil, no global — misma lección que el gate de hardware: `linux.toml` es
el kernel de QEMU del selfhost-verify, no hospeda Cards, y su hash es
load-bearing del baseline `of_tree`. Exigirle contabilidad de memoria sería
rechazar una receta sana.
Medido sobre el store: **2 de 11 configs sellados cumplen su perfil**; los 9
`anfitrion-cards` fallan por MEMCG (apagado A MANO: `# CONFIG_MEMCG is not
set`) y les falta PSI. El kernel vivo de esta máquina pasa las 11 exigidas —
que es exactamente por qué el bug sobrevivió.
Distingue apagado explícito de ausente (un símbolo que el .config ni nombra
puede ser un renombrado entre versiones), y un kernel sin perfil declarado
queda SIN COMPROBAR en vez de contar como aprobado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
En Debian `patch` entra de regalo dentro de `build-essential`; al desglosar la rama dnf a
gcc/g++/make sueltos se cayó sin que nadie lo notara. El campo `patches = [...]` de una
receta lo aplica hammer DEL LADO DEL HOST, fuera del sandbox, así que no alcanza con que
el rootfs lo traiga —lo trae, `/usr/bin/patch` está en el alpine de los dos labs— y por
eso el diagnóstico "rootfs laptop ≠ worker" no aplicaba acá.
Medido en dev.gioser.net: `cairo-shared` moría con `spawn patch: No such file or
directory` y detrás caían `pango` y `gtk4`. El stack GUI entero de GNOME por un binario
de 129 KB ausente en el host.
Regla que queda escrita: toda herramienta que hammer invoque HOST-SIDE va en esta lista,
no en `[deps]` de la receta — declararla en la receta re-hashearía cairo y todo GNOME
debajo, para arreglar algo que no es de la receta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Tercer sitio con la misma pregunta mal hecha, encontrado con el grep que la lección del
commit anterior pedía hacer: `has` (tapado 2026-08-10), `seal` (tapado hoy) y `hydrate`,
que seguía con `is_dir()` pelado. Un directorio vacío pasaba el chequeo, proyectaba 0
ficheros y devolvía un HydrateReport de éxito.
Duele especial acá porque el escritorio se hidrata así (`hammer hydrate <hash> --into`,
137/137 en KDE): un FHS proyectado desde nada sale «OK» y falla mucho más tarde, sin
rastro de qué artefacto faltaba.
Queda a propósito sin tocar `hammer-mirror::index`: ahí el vacío se autocorrige, porque
indexa por `of_tree` y el contenido difiere del origen ⇒ el sync lo trae.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
`has()` define presencia como «tiene al menos una entrada» —hay test desde hace tiempo—
pero `seal()` usaba `is_dir()`, que también es cierto para un directorio vacío. Con eso
`seal` devolvía Ok, tiraba el árbol recién construido, y el build logueaba `sealed` y
salía 0. Dos nociones distintas de «está» en el mismo struct, y la que decide si el
trabajo se guarda era la mala.
Medido en el worker dev.gioser.net: 1457 directorios vacíos en el store se comían cada
sellado. `openssl-threads` compilaba entero —headers, libcrypto.a, libssl.a, los .pc— y
sellaba NADA; python3 se construía después sin openssl y quedaba sin `_ssl`, y con eso
morían glib y 4 recetas más de GNOME. El error salía a tres recetas de distancia de la
causa y no nombraba openssl ni una vez. Probado quitando el vacío: el MISMO build sella
150 ficheros con OPENSSL_THREADS, mismo hash.
Se usa `remove_dir` y no `remove_dir_all`: sólo tiene éxito sobre un vacío, así que si
algo dejó contenido ahí entremedio falla ruidoso en vez de borrar un artefacto bueno.
No mueve hashes (testigo zlib idéntico).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Es el mismo fósil que el propio script documenta para GNOME, repetido con las colas
que nacieron después. Las 25 recetas en deuda de COSMIC no fallaban: nadie las
intentaba, porque su cola no estaba en la lista. Una cola ausente no da error, da
silencio — el mismo modo de fallo que "el corpus no está en las QUEUES del worker".
Se añade la regla en el comentario: cola nueva y línea QUEUES, en el mismo commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4