El directorio se movio de verdad, asi que las referencias absolutas dentro del
repo ya eran incorrectas. Tambien los ejemplos que usaban ~/hammer.
NO se tocan docs/evidencia/: son REGISTRO de lo que se corrio ese dia, y
reescribir una ruta ahi adentro falsifica la evidencia. Que nombren una ruta que
ya no existe es correcto: existia cuando se midio.
645 líneas. Los ADR entran porque en este repo SON documentos vivos, no
registros inmutables: el 0013 tiene 5 commits, el 0009 dos. Eso se comprobó
antes de decidir, no se asumió por convención general.
EXCLUIDOS por ser REGISTRO o generado: docs/evidencia/ (6), el HANDOFF de la
noche de KDE (1) y docs/state/ (24, se regenera solo). Reescribir un comando
dentro de una evidencia la falsifica.
Y el ADR 0016 se excluye de todo barrido, con un aviso adentro para el próximo
que barra: habla SOBRE el renombre, así que necesita seguir diciendo 'hammer'.
El barrido se lo llevó puesto y lo dejó titulado 'Renombre del sistema: takana
→ takana'; revertido.
Congelados, verificados uno por uno con controles: /opt/hammer, /var/lib/hammer,
/usr/bin/hammer, /mnt/vvv/hammer, la URL de gitea, hammer-farm.service,
hammer-live-install.sh, BRIEFING-hammer.md, hammerd, hammer-recover y
HAMMER_LIVE.
Handoff de vuelta desde tawasuyu (SDD 25 §8): las seis tareas del otro lado
estan cerradas, y eso dejo este contrato mintiendo de dos formas.
- Alta de `proceso-por-descriptor` (pidfd_open / pidfd_send_signal). Se usa
desde W1 y no estaba declarada. `symbols` vacio a proposito: no depende de
ningun CONFIG_*, es interfaz del core desde Linux 5.3. Al no estar en ningun
perfil no se comprueba contra un .config; entra porque el contrato es la
lista de lo que se USA, y si manana el minimo de kernel baja de 5.3, esta
linea es la que lo dice.
- Los cinco consumidores de cgroup llevaban numero de linea y quedaron viejos
con W1-W4. Van por nombre de funcion, y la regla de escritura queda arriba.
- Los `silent` de esos cinco describian el bug que W4 arreglo ("solo emite un
warn!", "silencio total"). Un guardian que dice que hay un punto ciego donde
ya no lo hay es peor que no tenerlo.
- `contabilidad-por-tarea` decia que sandokan sondea /proc para medir una
unidad. Desde W2 eso sale del cgroup en O(1); el que sigue sondeando /proc es
el monitor de procesos del SISTEMA, que es otro consumidor.
Y en SDD 25 §8, el estado real: las seis cerradas, mas W4.bis, donde el hallazgo
no fue el que este documento suponia. En arje el limite no se descartaba: NO SE
PEDIA. El camino `plain` -el de casi todas las Cards- no creaba cgroup, y
`apply_rlimits_to_cgroup` la llamaba solo shuma.
Verificado con el binario, no de memoria: `hammer kernel contract --list` carga
las 14 capacidades, y `hammer kernel contract --profile anfitrion-cards` sigue
en verde (11 exigidas presentes, 13 miradas).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiYsdSwF1nxjBTTsa1empe
`f[k++] = BPF_JUMP(..., I_KILL - k - 1 + 1)` leía y modificaba `k` en la misma
expresión, sin punto de secuencia: UB en C11. Andaba porque gcc y clang leen `k`
después del incremento y ese `+1` era la compensación exacta, pero si algún
compilador leyera antes el salto caería una instrucción más allá del final.
El índice se fija ahora en `I_ARCH` antes de usarlo. El código generado es
IDÉNTICO —mismo objdump del objeto a -O1 y a -O2, la única línea que difiere es
el nombre del fichero—, que es lo que prueba que es la misma cuenta escrita sin
UB y no un arreglo que además mueve el salto. Comprobado además que la jaula
sigue puesta: ptrace desde dentro da EPERM y un ejecutable fuera de la clausura
no arranca.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Nadie había medido qué cuesta el apilado de capas de `bwrap_args`. El 95% de las
recetas (1 108 de 1 171) construye con las deps APILADAS, hasta ~27.
Un lookup que FALLA cuesta +1 925 ns por capa (a 64 capas, 124 µs = 1 580 syscalls
nulas), pero se paga UNA VEZ POR RUTA: la dentry fusionada queda cacheada y el
montaje dura toda la fase. Un `cc -O2 -c` toca 381 rutas distintas que fallan ⇒
2,9 ms por fase a 24 capas, la misma décima de por ciento que la jaula de T17/T18.
⇒ `merge_deps_layer` no es una optimización de velocidad: sólo se pagaría sola a
partir de ~35 200 primeros fallos en una misma fase. Es el rodeo de un muro, y el
muro quedó bisecado al byte: `mount(2)` monta 41 capas (4 059 B de opciones) y
falla en 42 con un `ENOENT` que miente. El `mount(8)` de util-linux se rinde entre
8 y 12, así que bisecarlo desde la shell da un muro FALSO.
Consecuencias anotadas en sandbox.rs: no bajar el umbral «para que vaya más
rápido», y podar `.dmerge` es gratis en rendimiento. Y hay puerta medida: la API
de montaje nueva (`fsconfig lowerdir+`) monta las 64 capas que `mount(2)` rechaza.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
SDD 25 tenía medido el sandbox por namespaces (T14, ~1 ms por proceso) pero no el grano fino que
harkaq pone dentro, que cobra por SYSCALL. `bench_jaula.c` lo mide con control negativo en las dos
mitades (si la jaula no quedó puesta, sale con 2).
seccomp: el filtro de harkaq es una denylist lineal, así que toda syscall legítima recorre la
cadena entera — y aun así es PLANO en su longitud (86,9 ns con 4 entradas, 88,3 con 250, sobre un
piso de 75,0). Lo que lo hace plano es el bitmap de acción constante del kernel, y eso no se supone:
un gemelo de la misma longitud con una carga de args[0] —que hace que el analizador se rinda— sí
crece, 111,6 → 139,9 ns. La regla que sale de ahí es que un filtro es gratis mientras no mire un
argumento. El bitmap se paga al instalar (1,5 µs por entrada) y se amortiza en 2 830 syscalls, o
sea una invocación y media de gcc, una sola vez por fase de build.
landlock: el precio es ESTAR enjaulado (+473 ns por open), no cuántas reglas hay — pasar de 1 regla
a las 16 384 de una clausura fichero a fichero agrega +225 ns más. La política por-fichero de D1,
que era la decisión discutible, resulta casi gratis. Y dos negativas medidas: `stat` no paga
(Landlock no engancha getattr) y un open DENEGADO sale más barato que uno concedido, al revés de lo
que se esperaba al medirlo.
En total, la jaula le cuesta a un compilado 0,23–0,31%: una décima parte del presupuesto de <5% de
SDD 16.
De paso, dos cosas del filtro que salieron de mirarlo para medirlo: la denylist tiene un techo de
~252 entradas por el __u8 de los saltos de la BPF clásica, y el jf se calcula leyendo y modificando
`k` en la misma expresión (UB en C11, benigno con gcc y clang, comprobado).
Y §9.6 queda con el experimento de PTI bien planteado: no es «el laptop daría peor» (compara dos
máquinas y mezcla cuatro variables) sino la misma máquina con pti=on y pti=off. momento no sirve ni
forzándolo: este invitado no expone PCID, así que mediría el peor caso y no el de un TigerLake.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Era el ultimo hueco medible de SDD 25 en esta maquina: H7 tenia el precio en cuatro partes
pero no el numero en bytes, que es como se pago H1.
Se construyo una copia derivada de linux-generic con UNA sola diferencia en el .config
(DEBUG_INFO_DWARF5 + DEBUG_INFO_BTF) y todo lo demas byte a byte igual:
bzImage sin BTF : 16 937 984 B
bzImage con BTF : 19 473 408 B (+2 535 424 B = +2,42 MiB = +14,97%)
seccion .BTF : 7 888 166 B sin comprimir, entran comprimidos 3,11x
Contra la unica vara comparable: H1 (MEMCG+PSI) costo +80 KiB / +0,49% del mismo bzImage
=> BTF cuesta 31 veces eso. No lo decide, pero lo saca de "un re-hasheo y ya".
Sobre linux-generic y no sobre linux, que es mas barato: BTF tiene depends on BPF_SYSCALL y
el .config sellado de linux lo trae APAGADO, asi que medir ahi habria mezclado dos precios.
TRES cosas que salieron construyendo y no leyendo:
1. Una QUINTA parte del precio que nadie habia nombrado: hace falta python3. BTF hace que
kbuild descienda a tools/bpf/resolve_btfids, que compila un libbpf vendorizado cuyo
Makefile genera bpf_helper_defs.h con un script de Python (Error 127).
2. El numero de Artix fallaba en la direccion CONTRARIA a la esperable. El doc decia que sus
6,4 MB "no dicen nada de uno monolitico y pelado"; el nuestro sale MAS GRANDE, 7,5 MiB.
Artix es modular y su .BTF cubre solo el core built-in; el nuestro es monolitico y los
tipos de i915+nouveau+radeon+iwlwifi entran todos.
3. Una trampa de kconfig que es CLAUDE.md §3 dentro de make: scripts/pahole-version.sh
imprime 0 si pahole no esta en el PATH, el depends on PAHOLE_VERSION >= 122 deja de
cumplirse, y olddefconfig BORRA la linea en silencio. El build sale OK y sella un kernel
SIN BTF diciendo que todo fue bien. Por eso la receta comprueba el .config PRODUCIDO y
sale 1 si no esta. Salio OK, lo que ademas paga la parte (3) del precio: el pahole
estatico de ayer se encuentra y se ejecuta DENTRO del sandbox.
El artefacto de medicion se borro a proposito: `hammer kernel contract --sealed` lo listaba
como SIN COMPROBAR (no declara [[target]], que es lo que exige H8), o sea un aviso permanente
en un fichero que el cron commitea cada 30 min. Un aviso fijo que no corresponde a ningun
problema es como se deja de leer un vigia. La receta derivada queda en la evidencia.
Nada del corpus se re-hasheo: la derivada vive fuera de recipes/ y el catalogo se le presta
por symlinks.
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
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
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
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
- §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
16 tasas de Linux medidas en momento (KVM, 4 vCPU, bajo carga): piso de
syscall, /proc como API de texto, fork vs espacio de direcciones del padre,
enlazado dinámico, VFS, io_uring, THP, namespaces. Programas en C y salidas
crudas en docs/evidencia/tasas-kernel-2026-08-29/.
Lo que sale de medir y después verificar contra las recetas: los kernels de
hammer se construyen sin CONFIG_MEMCG (no está en x86_64_defconfig y no tiene
default y), y arje/sandokan escriben memory.max descartando el error. Un tope
de memoria pedido por una Card no se aplica y sólo deja un warn.
Tres resultados que contradicen el consejo de manual y quedan documentados:
THP 3x más lento que 4K con defrag=madvise, io_uring 2,8x más lento que pread
sobre tmpfs, y stat cuesta lo mismo en tmpfs que en ext4 (es tasa del VFS).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih