`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
Ningun nodo cambia de estado y el gate `--check` sigue OK: lo unico que se
mueve es `dependientes_total` de sus deps, un dependiente menos cada una. Es
la yupana cruzando TODAS las colas, onda-1 incluida, aunque ningun grafo la
cargue como cola propia.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Mismo paquete que `recipes/incoming-gnome/gnome-desktop.toml`: name, version
44.5 y sha256 identicos. La unica diferencia son las `[deps]`, y son las de
antes: a la de onda1 le faltan xkeyboard-config, iso-codes, libseccomp y todas
las variantes `-shared` que la canonica documenta como imprescindibles para el
scanner de introspeccion. Por eso moria con
Run-time dependency xkeyboard-config found: NO (tried pkg-config)
meson.build:64:17: ERROR: Dependency "xkeyboard-config" not found
que se leia como deuda de build y no lo era: arreglarla habria sido reescribirla
hasta volverla identica a la otra.
Nadie dependia de ella (los dependientes -mutter, gnome-shell, gnome-session,
gnome-settings-daemon, xdg-desktop-portal-gnome- viven en incoming-gnome y
resuelven contra su propia hermana). El gate `--check` sigue en OK y ningun
nodo cambia de estado; en los grafos solo baja `dependientes_total` de sus
deps, que es justo lo que tenia que pasar.
Sus dos artefactos quedaban sin receta que los produjera: podados y anotados en
el ledger anti-churn para que la cosecha no los devuelva.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
`state_of` decidia `sealed` con `os.path.isdir`, o sea por presencia del
nombre y no del contenido. Hoy se vio en vivo: una cosecha que murio a mitad
dejo `...-mise` sin un solo fichero y el grafo lo conto sellado — misma cifra,
misma linea, indistinguible de la corrida buena.
Es el eslabon que describe la regla 3 del CLAUDE.md: `Store::has` ya rechaza
los vacios, pero cada consumidor que mire `isdir` los vuelve a leer como
presencia. Un ausente falla ruidosamente; un vacio llega hasta el final
diciendo que todo fue bien.
Los cinco grafos salen identicos tras el cambio (no hay vacios en el store
ahora mismo), asi que corrige el criterio sin mover ningun numero.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
La bajada del store murio con "No space left on device" con 95 G libres y un
store remoto de 25 G. Dos causas que se multiplican:
- `rsync -a` NO preserva hardlinks (eso es `-H`). El store es CAS y .dmerge
hardlinkea los artefactos, asi que los 25 G que mide `du` en el origen -que
deduplica por inodo- se escriben en destino como copias enteras, una por enlace.
- `.dmerge` es cache pura: hammer la reconstruye sola, no aporta un artefacto y
es la parte mas pesada del arbol (132 G en el hub, 111,7 GiB exclusivos).
Ademas el fallo a mitad deja directorios creados sin ficheros, que es un
cache-hit envenenado: `mise` se cosecho asi, como nombre vacio. Se anade un
guardian que barre los vacios al terminar la bajada y avisa de re-correr.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
`jt`/`jf` de la BPF clásica son __u8 y en la forma de poner_seccomp() el salto más largo mide n+3:
pasadas ~252 entradas el desplazamiento da la vuelta y el filtro deja de decir lo que dice el
fichero, sin error y sin aviso. Hoy son 25 y sobra sitio — el riesgo es que agregar syscalls a una
denylist es exactamente lo que uno hace sin pensarlo.
_Static_assert, o sea cero código generado: comprobado en las dos direcciones —baja el techo a 10 y
el compilador para; y la sección .text del binario es byte a byte la misma con y sin el assert—,
que es el requisito de este fichero (nada que re-hashee).
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