> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
tasas: T17/T18 — la jaula cobra por syscall, y el filtro largo no es lo que cobra
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
diff --git a/docs/25-tasas-del-kernel.md b/docs/25-tasas-del-kernel.md
index 54b1dd23..1570b685 100644
--- a/docs/25-tasas-del-kernel.md
+++ b/docs/25-tasas-del-kernel.md
@@ -36,7 +36,14 @@ El espejo de este documento para el lado tawasuyu es
cada 100 ms vio **cero**; el proc connector, los 200. Y **el «copiar en O(1)» existe pero no en
ext4**: en xfs/btrfs un reflink de 512 MiB tarda 0,020 ms contra 512,7 ms de `read+write`
(§9.2).
-5. **Cuando `/proc` sí se sondea entero, hay algo cerca de 7× más barato** (§9.5, medido
+5. **La jaula de harkaq cobra por syscall, y cobra poco** (T17/T18, medido 2026-09-01): su filtro
+ seccomp pone +12,4 ns por syscall **sin importar cuántas entradas tenga la denylist** —el bitmap
+ de acción constante del kernel hace que el largo sea gratis; mirar un argumento, no—, y Landlock
+ pone +473 ns por `open`, de los cuales sólo +225 se deben a pasar de 1 regla a las ~17 000 de
+ una clausura fichero a fichero. Sobre un compilado entero son **0,23–0,31%**: una décima parte
+ del presupuesto de <5% de SDD 16. Lo caro de enjaular sigue siendo el ~1,09 ms por proceso de
+ los namespaces (T14).
+6. **Cuando `/proc` sí se sondea entero, hay algo cerca de 7× más barato** (§9.5, medido
2026-08-31): un iterador BPF `task` saca la misma foto **al precio de listar el directorio**
—1,75× el piso de `readdir` contra 12,1× de `/proc`—. Su portón es `DEBUG_INFO_BTF`, que sigue
apagado, y H7 ya no dice sólo que está apagado: **cuesta +14,97% de bzImage** (+2,42 MiB,
@@ -48,7 +55,9 @@ El espejo de este documento para el lado tawasuyu es
- **Máquina**: `momento`, KVM sobre AMD EPYC-Rome, 4 vCPU, 7,7 GiB RAM, Linux 7.1.4,
glibc/Artix. Store en `/dev/sdb` (ext4), árbol en `/dev/sdc` (ext4), `/tmp` tmpfs.
- **Mitigaciones activas**: retpolines (`spectre_v2`), retbleed, SSBD por prctl. **Sin PTI**
- (AMD, `meltdown: Not affected`). En un Intel con PTI el piso de syscall sería peor.
+ (AMD, `meltdown: Not affected`). En un Intel con PTI el piso de syscall sería peor — cuánto, no
+ se puede averiguar acá: **este invitado tampoco expone PCID**, y sin PCID el PTI forzado mide el
+ peor caso y no el de una máquina real (§9.6).
- **La máquina estaba CARGADA**: load 7,3 sobre 4 vCPU, un build de LLVM en vuelo, 773 MiB
libres. ⇒ **los absolutos son cotas superiores; la señal está en los cocientes dentro de
una misma corrida**, que es como está escrito todo lo de abajo.
@@ -240,7 +249,135 @@ ruido. Gratis, y ya está en mainline.
`pthread_create`+`join` = **11,9 µs**, contra 109 µs de `fork+_exit` con el padre vacío:
**9×**, y la distancia crece con el tamaño del padre (T1).
-## 3. Las cinco sorpresas
+### T17 · La jaula cobra POR SYSCALL — y el filtro largo no es lo que cobra
+
+T14 midió el sandbox por **namespaces**, que cobra por proceso. Adentro, harkaq (SDD 16) pone dos
+cosas más que cobran por **syscall**, que es la unidad en la que un build hace millones: un filtro
+seccomp y un ruleset Landlock. Medido con `bench_jaula.c` → `jaula.txt` (2026-09-01, mismo
+`momento`, 9 rondas, mínimos; **los cocientes son todos de esa corrida**).
+
+El filtro de `harkaq-exec.c` es una **denylist**: una cadena lineal de `BPF_JEQ` sobre
+`seccomp_data.nr` con 25 entradas y, al final, `RET ALLOW`. O sea que **toda syscall legítima
+recorre la cadena entera** antes de que la dejen pasar: la peor posición posible para el caso más
+frecuente. La intuición dice que eso se paga y que hay que ordenar la lista por frecuencia.
+
+| filtro | ns/`getppid` | vs piso |
+|---|---|---|
+| sin filtro | **75,0** | — |
+| denylist de 4 | 86,9 | +11,9 |
+| **denylist de 25 (la de harkaq)** | **87,4** | **+12,4 (+17%)** |
+| denylist de 64 | 88,5 | +13,5 |
+| denylist de 128 | 86,6 | +11,6 |
+| denylist de 250 | 88,3 | +13,3 |
+
+**Plano.** Un filtro 62 veces más largo cuesta lo mismo. La razón es que desde 5.11 el kernel arma
+un **bitmap de acción constante**: `seccomp_is_const_allow()` lee el programa BPF instrucción por
+instrucción y, para cada número de syscall cuyo veredicto no dependa de los argumentos, marca un
+bit — y a partir de ahí **el programa no se ejecuta nunca más**. El filtro de harkaq usa sólo
+`BPF_LD|W|ABS` sobre `nr`/`arch`, `BPF_JMP|BPF_JEQ|BPF_K` y `BPF_RET|BPF_K`, que es exactamente lo
+que ese analizador sabe seguir. Los +12 ns que quedan son el bit, no la cadena.
+
+**Y no es fe, es un control.** El mismo banco instala un **gemelo** de la misma longitud con una
+instrucción de más al principio —una carga de `args[0]`— que hace que el analizador se rinda.
+Mismo largo, misma cadena, sin bitmap:
+
+| gemelo que mira `args[0]` | 4 | 25 | 64 | 128 | 250 |
+|---|---|---|---|---|---|
+| ns/`getppid` | 111,6 | 104,4 | 111,1 | 121,4 | **139,9** |
+
+Ahí sí crece: **0,158 ns por instrucción de la cadena**. Si los dos hubieran salido planos, la
+medición no discriminaría y no diría nada; salen distintos, así que el bitmap existe y es lo que
+paga la cuenta.
+
+**La regla, que es lo que hay que llevarse**: un filtro seccomp es gratis **mientras no mire un
+argumento**. El día que alguien quiera filtrar `ioctl` por el número de petición o `clone` por sus
+flags, ese número de syscall —y sólo ése— se cae del bitmap y pasa a pagar la cadena entera en
+cada llamada. La longitud de la denylist no es la variable; **mirar argumentos, sí**.
+
+**El bitmap se paga al instalar, y se cobra por proceso, no por syscall:**
+
+| N | instalar la denylist (arma bitmap) | instalar el gemelo (no lo arma) |
+|---|---|---|
+| 4 | 26,5 µs | 14,4 µs |
+| **25 (harkaq)** | **69,8 µs** | 21,7 µs |
+| 64 | 140,6 µs | 26,5 µs |
+| 128 | 252,8 µs | 31,6 µs |
+| 250 | 395,4 µs | 52,2 µs |
+
+≈ **1,5 µs por entrada de la denylist**, porque el kernel recorre el programa entero una vez por
+cada número de syscall. A N=25 el bitmap cuesta 48,1 µs de más al instalar y ahorra 17,0 ns por
+syscall ⇒ **se amortiza en 2 830 syscalls**, que es una invocación y media de gcc (§9.4: 1 803).
+Y harkaq lo instala **una vez por fase de build**, no por proceso: el filtro y su bitmap se heredan
+por `fork`/`exec`, así que lo paga el primer proceso de la fase y lo cobran los miles que siguen.
+
+**Dos cosas del filtro de harkaq que salieron de mirarlo para medirlo, y que no son de rendimiento:**
+
+1. **`jt`/`jf` de la BPF clásica son `__u8`.** En la forma de `poner_seccomp()` el salto más largo
+ mide N+3, así que la denylist tiene un **techo de ~252 entradas**: pasado eso el desplazamiento
+ da la vuelta y el filtro deja de decir lo que dice su código fuente. Hoy son 25 y no hay
+ problema; el problema es que crecer la lista es justo lo que uno hace sin pensarlo.
+2. **`f[k++] = BPF_JUMP(..., I_KILL - k - 1 + 1)` modifica y lee `k` en la misma expresión** — sin
+ punto de secuencia, o sea UB en C11. Funciona porque gcc y clang leen `k` *después* del
+ incremento, y el `+1` final es exactamente la compensación de eso (comprobado con los dos, `-O0`
+ y `-O2`, en esta máquina). Si algún compilador leyera antes, el salto caería una instrucción
+ más allá del final: el verificador clásico lo rechazaría y harkaq saldría por su rama de
+ `RECHAZO`. Falla ruidoso, no silencioso — pero está apoyado en UB.
+
+### T18 · Landlock: el precio es ESTAR enjaulado, no cuántas reglas hay
+
+La política de harkaq se deriva **fichero a fichero** (D1: adentro, la dep declarada y el rootfs
+Alpine comparten `/usr`, así que una regla sobre `/usr` concedería las dos y la evidencia dejaría
+de significar algo). Una dep como boost son 15 829 ficheros ⇒ **~17 000 reglas**. La pregunta
+obvia es si un ruleset así encarece cada `open`.
+
+| ruleset | ns/`open`+`close` | vs piso |
+|---|---|---|
+| sin Landlock | **791,7** | — |
+| 1 regla | 1 264,7 | **+473 (+60%)** |
+| 16 reglas | 1 278,4 | +487 |
+| 256 reglas | 1 309,3 | +518 |
+| 4 096 reglas | 1 365,9 | +574 |
+| **16 384 reglas (~boost)** | **1 489,5** | **+698 (+88%)** |
+
+**Todo el precio está en el primer escalón.** Estar bajo Landlock cuesta +473 ns; multiplicar el
+ruleset por 16 384 agrega +225 ns más (+18% sobre el `open` ya enjaulado). La forma es la de una
+búsqueda en árbol, no la de un barrido. **La decisión de diseño discutible de harkaq —enumerar la
+clausura fichero a fichero en vez de conceder directorios— resulta ser casi gratis**, y eso ahora
+está medido en vez de supuesto.
+
+Dos medidas más, con 16 384 reglas puestas:
+
+- **`stat` no paga**: 522,8 ns sin Landlock, 508,4 con — o sea nada, dentro del ruido. Landlock no
+ engancha `getattr`, sólo la apertura y la modificación de rutas. Importa porque `stat` es la
+ syscall que un build hace más (T8), y porque el instinto es suponer que la jaula toca todo.
+- **Un `open` DENEGADO sale más barato que uno concedido**: 1 081,1 contra 1 489,5. Se midió
+ esperando lo contrario —el peor caso, subir el árbol hasta la raíz sin encontrar regla— y el
+ resultado es que fallar antes de montar el descriptor ahorra más de lo que cuesta buscar. **La
+ denegación no es un riesgo de coste**, que era la duda que justificaba medirla.
+
+**Armar el ruleset sí es lineal**: 30,46 ms para 16 384 reglas (**1,86 µs por regla**, un `open`
+`O_PATH` + un `landlock_add_rule` + un `close` cada una). Es precio **por fase de build**, igual
+que el filtro: 30,5 ms contra una fase que dura de segundos a minutos. Pero es la misma advertencia
+de T14 con otro número: **si algún día se enjaulara por-exec**, 20 000 execs × 30,6 ms = **10
+minutos** de puro armado de jaula.
+
+### T17+T18 · Lo que la jaula le cuesta a un compilado, en total
+
+Con los números de §9.4 (un `gcc` = 1 803 syscalls y 228 `openat` sobre 30,5 ms de compilado):
+
+| | cuenta | µs |
+|---|---|---|
+| seccomp | 1 803 syscalls × 12,4 ns | 22,4 |
+| Landlock | 101 `open` que llegan al hook × 473–698 ns | 48–70 |
+| **total** | | **70–93 µs sobre 30 500 µs = 0,23–0,31%** |
+
+Las 127 `openat` que fallan con `ENOENT` no llegan al hook de Landlock —la resolución de ruta
+falla antes—, así que la cota alta contando las 228 sería 0,43–0,60%. **En cualquiera de las dos
+lecturas, el grano fino de harkaq gasta menos de la décima parte del presupuesto de <5% de SDD 16**,
+y lo caro de enjaular sigue siendo lo que ya decía T14: el ~1,09 ms por proceso de los namespaces,
+que es diez veces todo esto junto.
+
+## 3. Las seis sorpresas
Las anoto aparte porque contradicen el consejo de manual:
@@ -257,6 +394,10 @@ Las anoto aparte porque contradicen el consejo de manual:
5. **Sondear `/proc` no es lento: es CIEGO** (§9.1) — de 200 procesos de vida mínima lanzados uno
cada 10 ms, un sondeo cada 100 ms vio **cero**. Toda la discusión de «cuánto cuesta el barrido»
(T3) presupone que el barrido VE lo que hay, y para procesos cortos no lo ve.
+6. **Un filtro seccomp más largo NO cuesta más** (T17) — el consejo de manual es acortar la lista y
+ ordenarla por frecuencia; con el bitmap de acción constante, 4 entradas y 250 valen lo mismo.
+ Lo que sí cuesta es **mirar un argumento**: eso saca a esa syscall del bitmap y le devuelve la
+ cadena entera. La variable no es el largo, es si el veredicto depende de los args.
## 4. El hallazgo cruzado: los kernels de hammer no tienen `MEMCG`
@@ -554,6 +695,25 @@ Su precio, concreto:
exige H8— o sea un ⚠ permanente en `docs/state/kernel-contract.txt`, que el cron commitea cada
30 min. Un aviso fijo que no corresponde a ningún problema es exactamente cómo se deja de leer un
vigía. La receta derivada queda junto a la evidencia, con las instrucciones para reconstruirla.
+- **H10 · El filtro de harkaq se queda como está — y ahora se sabe por qué.** T17/T18 midieron el
+ grano fino de la jaula y la conclusión es que **no hay nada que optimizar**: 0,23–0,31% de un
+ compilado, contra el ~1,09 ms por proceso de los namespaces (T14) que es diez veces más. Tres
+ cosas que sí quedan escritas para que no se rompan por accidente:
+
+ 1. **No reordenar la denylist «por frecuencia» ni recortarla «para que sea rápida».** Es plana en
+ su longitud, por el bitmap de acción constante. Es trabajo cero con riesgo de romper la
+ política.
+ 2. **Un filtro que mire argumentos deja de ser gratis, y sólo para las syscalls que mire.** Si
+ algún día hace falta (filtrar `ioctl` por petición, `clone` por flags), el precio es la cadena
+ entera en cada llamada de esa syscall — 0,158 ns por instrucción, medido. Se decide con ese
+ número delante, no con la intuición de que «un filtro es un filtro».
+ 3. **La denylist tiene un techo de ~252 entradas** por el `__u8` de los saltos de la BPF clásica,
+ y `poner_seccomp()` no lo comprueba. Hoy son 25. Un `_Static_assert` cuesta nada y evita que
+ crecer la lista rompa el filtro en silencio — la misma forma de fallo que CLAUDE.md §3.
+
+ Y la política **fichero a fichero** de D1 queda confirmada por el lado del coste: cambiarla por
+ reglas de directorio ahorraría 225 ns por `open` de los 698, o sea el 32% de lo barato, a cambio
+ de que la evidencia deje de significar algo. No se toca.
- **H8 · El contrato de capacidades es parte del release del kernel** (§4.ter). Toda receta de
kernel nueva declara su `[[target]]` con perfil; un kernel sin perfil sale **SIN COMPROBAR**, no
aprobado. Y toda capacidad nueva entra con consumidor —ruta y símbolo— o no entra: «estaría bueno
@@ -732,8 +892,20 @@ barrido vacío es infinitamente rápido y no mide nada: es CLAUDE.md §3 aplicad
### 9.6 · Lo que sigue abierto
-1. **Un Intel con PTI**: todos los pisos de syscall de acá son de un AMD sin PTI. En el laptop
- (TigerLake) las mismas medidas darían peor, y ahí T3/T6 duelen más.
+1. **Un Intel con PTI — y el experimento no es el que decía esta línea.** Decía: «en el laptop
+ (TigerLake) las mismas medidas darían peor». Eso compara **dos máquinas**, así que mezcla PTI
+ con microarquitectura, frecuencia y hipervisor: cualquier diferencia se le podría atribuir a
+ cualquiera de las cuatro. El experimento que aísla la tasa es **una sola máquina con `pti=on` y
+ con `pti=off`** —el kernel acepta `pti=on` aunque el CPU no sea vulnerable, así que forzarlo es
+ posible—, midiendo el piso de syscall **en función del working set tocado**: sin PTI es plano,
+ y con PTI crece, porque lo que PTI cobra es el cambio de CR3 y la TLB que se lleva puesta.
+
+ **Y `momento` no sirve ni forzándolo, por una razón que salió mirando**: este invitado **no
+ tiene PCID** (`grep pcid /proc/cpuinfo` no devuelve nada; 88 flags en total). Sin PCID cada
+ entrada y salida del kernel con PTI es un vaciado completo de TLB, o sea el régimen **peor
+ caso**, que no es el que pagaría un TigerLake —que sí trae `pcid`+`invpcid`—. Medirlo acá daría
+ una cota superior verdadera y una respuesta engañosa a la pregunta que importa. ⇒ **el
+ experimento necesita el laptop**, y ahí necesita las dos corridas, no una.
## 10. Evidencia
@@ -772,6 +944,15 @@ derivada, con las instrucciones para reconstruirla) y `config-7.1.2-generic-btf`
producido, que es donde se comprueba que BTF sobrevivió al `olddefconfig`). **No es un
microbenchmark: es un kernel construido**, así que sus números no se cruzan con los de §2 ni §9.
+**Quinta corrida (T17/T18, la jaula por syscall, 2026-09-01)**: `bench_jaula.c` → `jaula.txt`. Se
+compila con `cc -O2 -o bench_jaula bench_jaula.c` y se corre `./bench_jaula [rondas] [dir-temporal]`
+**sin root** —ni seccomp con `no_new_privs` ni Landlock lo piden—. Deja un árbol de 16 384 ficheros
+en el directorio temporal que hay que borrar a mano. Cada configuración vive en un hijo propio,
+porque ni el filtro ni el ruleset se pueden quitar una vez puestos. **Y aborta con exit 2 si la
+jaula no quedó puesta**: comprueba que un número de la denylist da `EPERM` y que un fichero testigo
+fuera del ruleset da `EACCES`, porque medir una jaula apagada da números preciosos y falsos
+(CLAUDE.md §3, la misma regla que el exit 2 de §9.5).
+
**El pago de H1** (2026-08-30): `contrato-h1-pagado.txt` — el barrido entero con los cuatro kernels
reconstruidos (5 de 5 vigentes en verde, 10 superados listados) y el `diff-back` del plan de gioser
contra su `.config` nuevo. Es la única salida de esta carpeta que sí cambia si se vuelve a correr:
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/bench_jaula.c b/docs/evidencia/tasas-kernel-2026-08-29/bench_jaula.c
new file mode 100644
index 00000000..a0698b9f
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/bench_jaula.c
@@ -0,0 +1,486 @@
+/* bench_jaula.c — lo que cobra la JAULA por syscall, no por proceso (SDD 25 T17/T18).
+ *
+ * SDD 25 midió el sandbox por NAMESPACES (T14: ~1 ms por proceso). Lo que no midió es el grano
+ * fino que harkaq (SDD 16) pone dentro: un filtro seccomp y un ruleset Landlock. Esos dos no
+ * cobran por proceso — cobran por SYSCALL, que es la unidad en la que un build hace millones.
+ *
+ * Dos preguntas, cada una con su control negativo:
+ *
+ * A) seccomp. El filtro de harkaq (`scripts/harkaq/harkaq-exec.c`) es una DENYLIST: una cadena
+ * lineal de `BPF_JEQ` sobre `seccomp_data.nr` y, al final, `RET ALLOW`. O sea que toda syscall
+ * LEGÍTIMA recorre la cadena ENTERA antes de que la dejen pasar: la peor posición posible,
+ * para el caso más frecuente. La pregunta es si eso se paga.
+ *
+ * La hipótesis es que NO, y no por casualidad: desde 5.11 el kernel construye un BITMAP de
+ * acción constante (`seccomp_cache_prepare`). Si para un número de syscall el filtro devuelve
+ * ALLOW sin mirar los argumentos —cosa que `seccomp_is_const_allow()` decide leyendo el
+ * programa BPF entero, instrucción por instrucción— ese número queda marcado y el programa
+ * NO SE EJECUTA nunca más. El filtro de harkaq usa sólo `BPF_LD|W|ABS` sobre `nr`/`arch`,
+ * `BPF_JMP|BPF_JEQ|BPF_K` y `BPF_RET|BPF_K`: es exactamente lo que el analizador sabe seguir.
+ *
+ * El control que lo convierte en medición y no en fe: se instala un GEMELO de la misma
+ * longitud con UNA instrucción de más al principio —una carga de `args[0]`— que hace que
+ * `seccomp_is_const_allow()` se rinda. Mismo largo, misma cadena, sin bitmap. Si el primero
+ * sale plano en N y el segundo crece con N, el bitmap existe y es lo que paga la cuenta.
+ * Si los DOS salen planos, la medición no discrimina y no dice nada.
+ *
+ * B) Landlock. La política de harkaq se deriva FICHERO A FICHERO (`harkaq-policy.sh`, D1),
+ * porque adentro la dep declarada y el rootfs Alpine comparten `/usr`. Una dep como boost son
+ * 15 829 ficheros ⇒ ~17 000 reglas. La pregunta es si un ruleset de ese tamaño encarece cada
+ * `open`, y cuánto cuesta armarlo.
+ *
+ * Control negativo obligatorio en las dos: si la jaula NO quedó puesta (una syscall denegada que
+ * NO da EPERM, un fichero fuera del ruleset que SÍ se abre), el programa sale con 2. Medir una
+ * jaula apagada da números preciosos y falsos — CLAUDE.md §3.
+ *
+ * cc -O2 -o bench_jaula bench_jaula.c && ./bench_jaula [rondas] [dir-temporal]
+ *
+ * No pide root. Cada configuración corre en un hijo propio: ni seccomp ni Landlock se pueden
+ * quitar una vez puestos, así que el proceso que mide es siempre de usar y tirar.
+ */
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/audit.h>
+#include <linux/filter.h>
+#include <linux/landlock.h>
+#include <linux/seccomp.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#define ROUNDS_MAX 15
+static int ROUNDS = 9;
+
+static inline double now_ns(void) {
+ struct timespec t;
+ clock_gettime(CLOCK_MONOTONIC, &t); /* vDSO: no cruza al kernel, no se mide a sí mismo */
+ return t.tv_sec * 1e9 + t.tv_nsec;
+}
+static int cmpd(const void *a, const void *b) {
+ double x = *(const double *)a, y = *(const double *)b;
+ return (x > y) - (x < y);
+}
+/* Resultados de los hijos: memoria compartida, porque cada medición vive en su propio proceso. */
+static double *shared;
+static void rep(const char *n, double *r, int c) {
+ qsort(r, c, sizeof(double), cmpd);
+ printf("%-52s %9.1f %9.1f\n", n, r[0], r[c / 2]);
+ fflush(stdout);
+}
+
+/* ------------------------------------------------------------------ A) seccomp */
+
+/* Números de syscall que NO existen: la cadena se recorre entera sin que ninguno case, que es
+ * exactamente lo que le pasa a toda syscall legítima bajo la denylist de harkaq. */
+#define NR_FALSO_BASE 900
+
+/* Arma el filtro con la MISMA forma que `harkaq-exec.c::poner_seccomp`.
+ * `mirar_args` antepone una carga de `seccomp_data.args[0]`, que es lo único que cambia — y lo
+ * único que hace falta para que el analizador de acción constante se rinda. */
+static struct sock_filter *armar(int n, int mirar_args, unsigned short *out_len) {
+ int extra = mirar_args ? 2 : 0;
+ struct sock_filter *f = calloc(n + 6 + extra, sizeof(*f));
+ if (!f) return NULL;
+ int k = 0;
+ const int I_ERRNO = 4 + n + extra, I_KILL = 5 + n + extra;
+
+ if (mirar_args) {
+ /* offsetof(seccomp_data, args) no es `nr` ni `arch` ⇒ seccomp_is_const_allow() devuelve
+ * false y el bitmap queda vacío. El salto nunca se toma: 0xdeadbeef no es un argumento. */
+ f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+ offsetof(struct seccomp_data, args));
+ f[k] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0xdeadbeefu,
+ I_KILL - k - 1, 0);
+ k++;
+ }
+ f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+ offsetof(struct seccomp_data, arch));
+ f[k] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0,
+ I_KILL - k - 1);
+ k++;
+ f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
+ offsetof(struct seccomp_data, nr));
+ for (int i = 0; i < n; i++, k++)
+ f[k] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
+ (unsigned)(NR_FALSO_BASE + i), I_ERRNO - k - 1, 0);
+ f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
+ f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K,
+ SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA));
+ f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS);
+ *out_len = (unsigned short)k;
+ return f;
+}
+
+/* El límite que nadie ve: `jt`/`jf` de la BPF clásica son __u8. Con la cadena de harkaq, el salto
+ * más largo mide N+3, así que a partir de ~252 entradas el desplazamiento DA LA VUELTA y el
+ * filtro deja de decir lo que dice su código fuente — sin error, sin aviso. */
+#define N_MAX_SEGURO 250
+
+static long instalar(int n, int mirar_args) {
+ unsigned short len = 0;
+ struct sock_filter *f = armar(n, mirar_args, &len);
+ if (!f) return -1;
+ struct sock_fprog prog = {.len = len, .filter = f};
+ long r = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
+ free(f);
+ return r;
+}
+
+/* La syscall que se cronometra: `getppid` por número, para no depender de si la libc la cachea. */
+static double t_syscall(long iters) {
+ double t0 = now_ns();
+ for (long i = 0; i < iters; i++) syscall(SYS_getppid);
+ return (now_ns() - t0) / iters;
+}
+
+/* Corre una configuración de seccomp en un hijo. Devuelve 0 si el hijo terminó bien. */
+static int hijo_seccomp(int n, int mirar_args, long iters, int verificar) {
+ pid_t p = fork();
+ if (p < 0) return -1;
+ if (p == 0) {
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) _exit(3);
+ if (n >= 0 && instalar(n, mirar_args) < 0) _exit(4);
+ /* Control negativo: con filtro puesto, un número de la denylist TIENE que dar EPERM.
+ * Sin esto, un filtro que el kernel aceptó pero no aplica se mediría como "gratis". */
+ if (verificar && n > 0) {
+ errno = 0;
+ long r = syscall(NR_FALSO_BASE, 0, 0, 0, 0, 0, 0);
+ if (!(r == -1 && errno == EPERM)) _exit(5);
+ }
+ for (int q = 0; q < ROUNDS; q++) shared[q] = t_syscall(iters);
+ _exit(0);
+ }
+ int st = 0;
+ waitpid(p, &st, 0);
+ return (WIFEXITED(st) && WEXITSTATUS(st) == 0) ? 0 : (WIFEXITED(st) ? WEXITSTATUS(st) : -1);
+}
+
+/* Coste de INSTALAR el filtro: el kernel recorre el programa entero una vez POR NÚMERO DE SYSCALL
+ * para armar el bitmap, así que esto crece con N aunque la ejecución no. Es precio por proceso
+ * (por fase de build), no por syscall.
+ *
+ * Una instalación por hijo, y no un lazo de instalaciones apiladas, por dos razones que salieron
+ * midiendo: (1) el kernel limita el total de instrucciones de la cadena (MAX_INSNS_PER_PATH), y
+ * apilar 200 filtros de 64 entradas ya no entra —da EINVAL, que se leería como "el filtro es
+ * ilegal" cuando lo ilegal es la pila—; (2) cada instalación posterior atraviesa los filtros ya
+ * puestos, así que el lazo mide una cosa distinta en cada vuelta. Un solo `seccomp()` por proceso
+ * es además lo que hace harkaq. */
+static int hijo_instalar(int n, int mirar_args) {
+ for (int q = 0; q < ROUNDS; q++) {
+ pid_t p = fork();
+ if (p < 0) return -1;
+ if (p == 0) {
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) _exit(3);
+ unsigned short len = 0;
+ struct sock_filter *f = armar(n, mirar_args, &len);
+ if (!f) _exit(4);
+ struct sock_fprog prog = {.len = len, .filter = f};
+ double t0 = now_ns();
+ long r = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
+ double t1 = now_ns();
+ if (r < 0) _exit(4);
+ shared[q] = (t1 - t0) / 1000.0; /* µs */
+ _exit(0);
+ }
+ int st = 0;
+ waitpid(p, &st, 0);
+ if (!(WIFEXITED(st) && WEXITSTATUS(st) == 0))
+ return WIFEXITED(st) ? WEXITSTATUS(st) : -1;
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ B) Landlock */
+
+static int ll_create(const struct landlock_ruleset_attr *a, size_t n, __u32 f) {
+ return syscall(SYS_landlock_create_ruleset, a, n, f);
+}
+static int ll_add(int fd, enum landlock_rule_type t, const void *a, __u32 f) {
+ return syscall(SYS_landlock_add_rule, fd, t, a, f);
+}
+static int ll_self(int fd, __u32 f) { return syscall(SYS_landlock_restrict_self, fd, f); }
+
+/* Mismo juego de accesos que harkaq pide en ABI >= 5, recortado a lo que el ABI vivo soporte. */
+static __u64 accesos_manejados(int abi) {
+ __u64 h = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_WRITE_FILE |
+ LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR | LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_DIR |
+ LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK |
+ LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_BLOCK |
+ LANDLOCK_ACCESS_FS_MAKE_SYM;
+ if (abi >= 2) h |= LANDLOCK_ACCESS_FS_REFER;
+ if (abi >= 3) h |= LANDLOCK_ACCESS_FS_TRUNCATE;
+ if (abi >= 5) h |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
+ return h;
+}
+
+static char base[512];
+static int n_ficheros_creados = 0;
+#define POR_DIR 256
+
+static void ruta_de(int i, char *out, size_t n) {
+ snprintf(out, n, "%s/d%03d/f%03d", base, i / POR_DIR, i % POR_DIR);
+}
+
+/* El árbol de prueba: M ficheros repartidos en directorios de 256, más un fichero TESTIGO que
+ * NUNCA entra al ruleset — es con lo que se comprueba que la jaula está puesta de verdad. */
+static int armar_arbol(int m) {
+ char p[600];
+ for (int d = 0; d <= m / POR_DIR; d++) {
+ snprintf(p, sizeof p, "%s/d%03d", base, d);
+ if (mkdir(p, 0700) < 0 && errno != EEXIST) return -1;
+ }
+ for (int i = n_ficheros_creados; i < m; i++) {
+ ruta_de(i, p, sizeof p);
+ int fd = open(p, O_CREAT | O_WRONLY | O_TRUNC, 0600);
+ if (fd < 0) return -1;
+ if (write(fd, "x", 1) != 1) { close(fd); return -1; }
+ close(fd);
+ }
+ if (m > n_ficheros_creados) n_ficheros_creados = m;
+ snprintf(p, sizeof p, "%s/TESTIGO", base);
+ int fd = open(p, O_CREAT | O_WRONLY | O_TRUNC, 0600);
+ if (fd < 0) return -1;
+ close(fd);
+ return 0;
+}
+
+/* Construye el ruleset de M reglas fichero-a-fichero (más el `list` de cada directorio), tal como
+ * lo hace harkaq. Devuelve el fd del ruleset y, por `us_setup`, lo que costó armarlo. */
+static int construir_ruleset(int m, int abi, double *us_setup) {
+ struct landlock_ruleset_attr attr = {.handled_access_fs = accesos_manejados(abi)};
+ int rs = ll_create(&attr, sizeof attr, 0);
+ if (rs < 0) return -1;
+ char p[600];
+ double t0 = now_ns();
+ for (int d = 0; d <= (m ? (m - 1) / POR_DIR : 0); d++) {
+ snprintf(p, sizeof p, "%s/d%03d", base, d);
+ int dfd = open(p, O_PATH | O_CLOEXEC);
+ if (dfd < 0) { close(rs); return -1; }
+ struct landlock_path_beneath_attr pb = {.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ .parent_fd = dfd};
+ int r = ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0);
+ close(dfd);
+ if (r < 0) { close(rs); return -1; }
+ }
+ for (int i = 0; i < m; i++) {
+ ruta_de(i, p, sizeof p);
+ int fd = open(p, O_PATH | O_CLOEXEC);
+ if (fd < 0) { close(rs); return -1; }
+ struct landlock_path_beneath_attr pb = {.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
+ .parent_fd = fd};
+ int r = ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0);
+ close(fd); /* la regla se queda con la referencia al inodo: el fd no hay que retenerlo */
+ if (r < 0) { close(rs); return -1; }
+ }
+ *us_setup = (now_ns() - t0) / 1000.0;
+ return rs;
+}
+
+static double t_open(const char *ruta, long iters) {
+ double t0 = now_ns();
+ for (long i = 0; i < iters; i++) {
+ int fd = open(ruta, O_RDONLY | O_CLOEXEC);
+ if (fd >= 0) close(fd);
+ }
+ return (now_ns() - t0) / iters;
+}
+static double t_stat(const char *ruta, long iters) {
+ struct stat st;
+ double t0 = now_ns();
+ for (long i = 0; i < iters; i++) stat(ruta, &st);
+ return (now_ns() - t0) / iters;
+}
+
+/* Qué se cronometra dentro del hijo. `open` sobre un fichero CON regla es el caso frecuente de un
+ * build; `open` DENEGADO se midió esperando que fuera el peor caso —recorrer el árbol hasta la
+ * raíz sin encontrar regla que case— y resultó ser MÁS BARATO que uno concedido, porque falla
+ * antes de montar el descriptor: la denegación no es un riesgo de coste; `stat` está para
+ * responder si Landlock cobra en la syscall que un compilador hace más — Landlock no engancha
+ * `getattr`, y esa línea es lo que lo demuestra en vez de afirmarlo. */
+enum medida { MED_OPEN, MED_OPEN_DENEGADO, MED_STAT };
+
+/* m < 0 ⇒ sin Landlock (el control). Si no, ruleset de m reglas y se mide lo que pida `qué`. */
+static int hijo_landlock_med(int m, int abi, long iters, double *us_setup_out, enum medida que);
+static int hijo_landlock(int m, int abi, long iters, double *us_setup_out) {
+ return hijo_landlock_med(m, abi, iters, us_setup_out, MED_OPEN);
+}
+static int hijo_landlock_med(int m, int abi, long iters, double *us_setup_out, enum medida que) {
+ int tuberia[2];
+ if (pipe(tuberia) < 0) return -1;
+ pid_t p = fork();
+ if (p < 0) return -1;
+ if (p == 0) {
+ close(tuberia[0]);
+ double us = 0;
+ if (m >= 0) {
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) _exit(3);
+ int rs = construir_ruleset(m, abi, &us);
+ if (rs < 0) _exit(4);
+ if (ll_self(rs, 0) < 0) _exit(5);
+ close(rs);
+ /* Control negativo: el TESTIGO no está en ninguna regla. Si se puede abrir, la jaula
+ * no está puesta y todo lo de abajo mediría un `open` sin Landlock. */
+ char t[600];
+ snprintf(t, sizeof t, "%s/TESTIGO", base);
+ int fd = open(t, O_RDONLY);
+ if (fd >= 0) { close(fd); _exit(6); }
+ if (errno != EACCES && errno != EPERM) _exit(7);
+ }
+ if (write(tuberia[1], &us, sizeof us) != (ssize_t)sizeof us) _exit(8);
+ close(tuberia[1]);
+ char r[600];
+ if (que == MED_OPEN_DENEGADO)
+ snprintf(r, sizeof r, "%s/TESTIGO", base);
+ else
+ ruta_de(m > 0 ? m / 2 : 0, r, sizeof r);
+ for (int q = 0; q < ROUNDS; q++)
+ shared[q] = (que == MED_STAT) ? t_stat(r, iters) : t_open(r, iters);
+ _exit(0);
+ }
+ close(tuberia[1]);
+ double us = 0;
+ if (read(tuberia[0], &us, sizeof us) != (ssize_t)sizeof us) us = -1;
+ close(tuberia[0]);
+ int st = 0;
+ waitpid(p, &st, 0);
+ if (us_setup_out) *us_setup_out = us;
+ return (WIFEXITED(st) && WEXITSTATUS(st) == 0) ? 0 : (WIFEXITED(st) ? WEXITSTATUS(st) : -1);
+}
+
+/* ------------------------------------------------------------------ main */
+
+static void fatal(const char *que, int code) {
+ fprintf(stderr, "\nABORTA: %s (codigo %d).\n", que, code);
+ fprintf(stderr, "Un banco que mide una jaula apagada da numeros preciosos y falsos.\n");
+ exit(2);
+}
+
+int main(int argc, char **argv) {
+ if (argc > 1) {
+ ROUNDS = atoi(argv[1]);
+ if (ROUNDS < 3 || ROUNDS > ROUNDS_MAX) ROUNDS = 9;
+ }
+ const char *tmpdir = argc > 2 ? argv[2] : "/tmp";
+ snprintf(base, sizeof base, "%s/bench_jaula.%d", tmpdir, (int)getpid());
+ if (mkdir(base, 0700) < 0) { perror("mkdir"); return 1; }
+
+ shared = mmap(NULL, sizeof(double) * ROUNDS_MAX, PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+ if (shared == MAP_FAILED) { perror("mmap"); return 1; }
+
+ struct utsname u;
+ uname(&u);
+ int abi = ll_create(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
+ printf("bench_jaula — lo que cobra la jaula POR SYSCALL (SDD 25 T17/T18)\n");
+ printf("kernel %s · landlock ABI %d · %d rondas · arbol en %s\n", u.release, abi, ROUNDS, base);
+ if (abi < 1) fatal("este kernel no tiene Landlock: no hay nada que medir", abi);
+
+ double r[ROUNDS_MAX];
+ long ITERS = 300000;
+
+ /* --- A.1 · el filtro en ejecucion: ¿la cadena lineal se paga? ------------------------- */
+ printf("\n--- A.1) seccomp: ns por getppid segun el LARGO del filtro ---\n");
+ printf("%-52s %9s %9s\n", "", "min", "mediana");
+ int rc = hijo_seccomp(-1, 0, ITERS, 0);
+ if (rc) fatal("el hijo sin filtro no termino bien", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ rep(" sin filtro (piso)", r, ROUNDS);
+
+ int enes[] = {4, 25, 64, 128, N_MAX_SEGURO};
+ for (unsigned k = 0; k < sizeof enes / sizeof *enes; k++) {
+ char nom[80];
+ rc = hijo_seccomp(enes[k], 0, ITERS, 1);
+ if (rc) fatal("filtro denylist: el hijo no termino bien (5=no dio EPERM)", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ snprintf(nom, sizeof nom, " denylist de %3d (forma harkaq, analizable)%s", enes[k],
+ enes[k] == 25 ? " <-- harkaq" : "");
+ rep(nom, r, ROUNDS);
+ }
+ for (unsigned k = 0; k < sizeof enes / sizeof *enes; k++) {
+ char nom[80];
+ rc = hijo_seccomp(enes[k], 1, ITERS, 1);
+ if (rc) fatal("filtro que mira args: el hijo no termino bien", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ snprintf(nom, sizeof nom, " gemelo de %3d que MIRA args[0] (sin bitmap)", enes[k]);
+ rep(nom, r, ROUNDS);
+ }
+
+ /* --- A.2 · el precio por proceso: armar el bitmap ------------------------------------- */
+ printf("\n--- A.2) seccomp: us por INSTALAR el filtro (precio por proceso) ---\n");
+ printf("%-52s %9s %9s\n", "", "min", "mediana");
+ for (unsigned k = 0; k < sizeof enes / sizeof *enes; k++) {
+ char nom[80];
+ rc = hijo_instalar(enes[k], 0);
+ if (rc) fatal("instalacion: el hijo no termino bien", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ snprintf(nom, sizeof nom, " instalar denylist de %3d (arma el bitmap)", enes[k]);
+ rep(nom, r, ROUNDS);
+ }
+ for (unsigned k = 0; k < sizeof enes / sizeof *enes; k++) {
+ char nom[80];
+ rc = hijo_instalar(enes[k], 1);
+ if (rc) fatal("instalacion del gemelo: el hijo no termino bien", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ snprintf(nom, sizeof nom, " instalar gemelo de %3d (sin bitmap que armar)", enes[k]);
+ rep(nom, r, ROUNDS);
+ }
+
+ /* --- B · Landlock ---------------------------------------------------------------------- */
+ printf("\n--- B) landlock: ns por open+close segun el TAMANO del ruleset ---\n");
+ printf("%-52s %9s %9s\n", "", "min", "mediana");
+ long IT_OPEN = 40000;
+ int emes[] = {1, 16, 256, 4096, 16384};
+ if (armar_arbol(emes[sizeof emes / sizeof *emes - 1]) < 0) { perror("armar_arbol"); return 1; }
+
+ rc = hijo_landlock(-1, abi, IT_OPEN, NULL);
+ if (rc) fatal("el hijo sin landlock no termino bien", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ rep(" sin landlock (piso)", r, ROUNDS);
+
+ double setup[8];
+ for (unsigned k = 0; k < sizeof emes / sizeof *emes; k++) {
+ char nom[80];
+ rc = hijo_landlock(emes[k], abi, IT_OPEN, &setup[k]);
+ if (rc) fatal("landlock: el hijo no termino bien (6=el TESTIGO se abrio igual)", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ snprintf(nom, sizeof nom, " ruleset de %5d reglas fichero-a-fichero%s", emes[k],
+ emes[k] == 16384 ? " <-- ~boost" : "");
+ rep(nom, r, ROUNDS);
+ }
+ /* --- B.1.bis · qué syscalls toca Landlock y cuál es su peor caso ----------------------- */
+ printf("\n--- B.1.bis) landlock con %d reglas: otras dos syscalls ---\n", emes[4]);
+ printf("%-52s %9s %9s\n", "", "min", "mediana");
+ rc = hijo_landlock_med(-1, abi, IT_OPEN, NULL, MED_STAT);
+ if (rc) fatal("stat sin landlock: el hijo no termino bien", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ rep(" stat sin landlock (piso)", r, ROUNDS);
+ rc = hijo_landlock_med(emes[4], abi, IT_OPEN, NULL, MED_STAT);
+ if (rc) fatal("stat con landlock: el hijo no termino bien", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ rep(" stat con landlock (Landlock NO engancha getattr)", r, ROUNDS);
+ rc = hijo_landlock_med(emes[4], abi, IT_OPEN, NULL, MED_OPEN_DENEGADO);
+ if (rc) fatal("open denegado: el hijo no termino bien", rc);
+ memcpy(r, shared, sizeof(double) * ROUNDS);
+ rep(" open DENEGADO (no monta el fd: sale mas barato)", r, ROUNDS);
+
+ printf("\n--- B.2) landlock: ms por ARMAR el ruleset (precio por proceso) ---\n");
+ for (unsigned k = 0; k < sizeof emes / sizeof *emes; k++)
+ printf(" %5d reglas: %8.2f ms (%6.2f us/regla)\n", emes[k], setup[k] / 1000.0,
+ setup[k] / emes[k]);
+
+ printf("\n(el arbol de prueba queda en %s; borrarlo a mano)\n", base);
+ return 0;
+}
diff --git a/docs/evidencia/tasas-kernel-2026-08-29/jaula.txt b/docs/evidencia/tasas-kernel-2026-08-29/jaula.txt
new file mode 100644
index 00000000..b531cabb
--- /dev/null
+++ b/docs/evidencia/tasas-kernel-2026-08-29/jaula.txt
@@ -0,0 +1,60 @@
+# bench_jaula — SDD 25 T17/T18 · lo que cobra la jaula POR SYSCALL
+# corrida 2026-09-01T15:04:12Z en momento
+# AMD EPYC-Rome Processor · 4 vCPU
+# load 1.39 0.92 0.64 · libres 3250 MiB
+# meltdown: Not affected · PCID: NO
+# bpf_jit_enable: 1 · arbol de prueba sobre ext4
+
+bench_jaula — lo que cobra la jaula POR SYSCALL (SDD 25 T17/T18)
+kernel 7.1.4-artix1-1 · landlock ABI 9 · 9 rondas · arbol en /home/sergio/.claude/jobs/f6b6451d/tmp/bench_jaula.16441
+
+--- A.1) seccomp: ns por getppid segun el LARGO del filtro ---
+ min mediana
+ sin filtro (piso) 75.0 82.3
+ denylist de 4 (forma harkaq, analizable) 86.9 90.1
+ denylist de 25 (forma harkaq, analizable) <-- harkaq 87.4 91.3
+ denylist de 64 (forma harkaq, analizable) 88.5 91.5
+ denylist de 128 (forma harkaq, analizable) 86.6 91.0
+ denylist de 250 (forma harkaq, analizable) 88.3 95.5
+ gemelo de 4 que MIRA args[0] (sin bitmap) 111.6 116.5
+ gemelo de 25 que MIRA args[0] (sin bitmap) 104.4 111.1
+ gemelo de 64 que MIRA args[0] (sin bitmap) 111.1 117.6
+ gemelo de 128 que MIRA args[0] (sin bitmap) 121.4 129.8
+ gemelo de 250 que MIRA args[0] (sin bitmap) 139.9 145.1
+
+--- A.2) seccomp: us por INSTALAR el filtro (precio por proceso) ---
+ min mediana
+ instalar denylist de 4 (arma el bitmap) 26.5 27.5
+ instalar denylist de 25 (arma el bitmap) 69.8 79.2
+ instalar denylist de 64 (arma el bitmap) 140.6 182.8
+ instalar denylist de 128 (arma el bitmap) 252.8 380.0
+ instalar denylist de 250 (arma el bitmap) 395.4 492.2
+ instalar gemelo de 4 (sin bitmap que armar) 14.4 19.9
+ instalar gemelo de 25 (sin bitmap que armar) 21.7 28.9
+ instalar gemelo de 64 (sin bitmap que armar) 26.5 38.4
+ instalar gemelo de 128 (sin bitmap que armar) 31.6 52.1
+ instalar gemelo de 250 (sin bitmap que armar) 52.2 77.9
+
+--- B) landlock: ns por open+close segun el TAMANO del ruleset ---
+ min mediana
+ sin landlock (piso) 791.7 848.4
+ ruleset de 1 reglas fichero-a-fichero 1264.7 1302.1
+ ruleset de 16 reglas fichero-a-fichero 1278.4 1310.7
+ ruleset de 256 reglas fichero-a-fichero 1309.3 1354.6
+ ruleset de 4096 reglas fichero-a-fichero 1365.9 1465.1
+ ruleset de 16384 reglas fichero-a-fichero <-- ~boost 1489.5 1581.6
+
+--- B.1.bis) landlock con 16384 reglas: otras dos syscalls ---
+ min mediana
+ stat sin landlock (piso) 522.8 552.5
+ stat con landlock (Landlock NO engancha getattr) 508.4 559.8
+ open DENEGADO (no monta el fd: sale mas barato) 1081.1 1212.3
+
+--- B.2) landlock: ms por ARMAR el ruleset (precio por proceso) ---
+ 1 reglas: 0.04 ms ( 36.47 us/regla)
+ 16 reglas: 0.05 ms ( 3.08 us/regla)
+ 256 reglas: 0.74 ms ( 2.88 us/regla)
+ 4096 reglas: 7.18 ms ( 1.75 us/regla)
+ 16384 reglas: 30.46 ms ( 1.86 us/regla)
+
+(el arbol de prueba queda en /home/sergio/.claude/jobs/f6b6451d/tmp/bench_jaula.16441; borrarlo a mano)