`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