`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
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