tawasuyu

sergio/takana

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

TOML 1488Shell 182Rust 107Markdown 963619 Sutichaykuna

⎇ 84e3737e10e17927380ed3e232e50cebfdd39123 docs/evidencia/tasas-kernel-2026-08-29/bench_jaula.c C · 486 siq'ikuna · 22.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

tasas: T17/T18 — la jaula cobra por syscall, y el filtro largo no es lo que cobra 46dba5861 · Sergio ·

1/* bench_jaula.c — lo que cobra la JAULA por syscall, no por proceso (SDD 25 T17/T18).
2 *
3 * SDD 25 midió el sandbox por NAMESPACES (T14: ~1 ms por proceso). Lo que no midió es el grano
4 * fino que harkaq (SDD 16) pone dentro: un filtro seccomp y un ruleset Landlock. Esos dos no
5 * cobran por proceso — cobran por SYSCALL, que es la unidad en la que un build hace millones.
6 *
7 * Dos preguntas, cada una con su control negativo:
8 *
9 *  A) seccomp. El filtro de harkaq (`scripts/harkaq/harkaq-exec.c`) es una DENYLIST: una cadena
10 *     lineal de `BPF_JEQ` sobre `seccomp_data.nr` y, al final, `RET ALLOW`. O sea que toda syscall
11 *     LEGÍTIMA recorre la cadena ENTERA antes de que la dejen pasar: la peor posición posible,
12 *     para el caso más frecuente. La pregunta es si eso se paga.
13 *
14 *     La hipótesis es que NO, y no por casualidad: desde 5.11 el kernel construye un BITMAP de
15 *     acción constante (`seccomp_cache_prepare`). Si para un número de syscall el filtro devuelve
16 *     ALLOW sin mirar los argumentos —cosa que `seccomp_is_const_allow()` decide leyendo el
17 *     programa BPF entero, instrucción por instrucción— ese número queda marcado y el programa
18 *     NO SE EJECUTA nunca más. El filtro de harkaq usa sólo `BPF_LD|W|ABS` sobre `nr`/`arch`,
19 *     `BPF_JMP|BPF_JEQ|BPF_K` y `BPF_RET|BPF_K`: es exactamente lo que el analizador sabe seguir.
20 *
21 *     El control que lo convierte en medición y no en fe: se instala un GEMELO de la misma
22 *     longitud con UNA instrucción de más al principio —una carga de `args[0]`— que hace que
23 *     `seccomp_is_const_allow()` se rinda. Mismo largo, misma cadena, sin bitmap. Si el primero
24 *     sale plano en N y el segundo crece con N, el bitmap existe y es lo que paga la cuenta.
25 *     Si los DOS salen planos, la medición no discrimina y no dice nada.
26 *
27 *  B) Landlock. La política de harkaq se deriva FICHERO A FICHERO (`harkaq-policy.sh`, D1),
28 *     porque adentro la dep declarada y el rootfs Alpine comparten `/usr`. Una dep como boost son
29 *     15 829 ficheros ⇒ ~17 000 reglas. La pregunta es si un ruleset de ese tamaño encarece cada
30 *     `open`, y cuánto cuesta armarlo.
31 *
32 * Control negativo obligatorio en las dos: si la jaula NO quedó puesta (una syscall denegada que
33 * NO da EPERM, un fichero fuera del ruleset que SÍ se abre), el programa sale con 2. Medir una
34 * jaula apagada da números preciosos y falsos — CLAUDE.md §3.
35 *
36 *   cc -O2 -o bench_jaula bench_jaula.c && ./bench_jaula [rondas] [dir-temporal]
37 *
38 * No pide root. Cada configuración corre en un hijo propio: ni seccomp ni Landlock se pueden
39 * quitar una vez puestos, así que el proceso que mide es siempre de usar y tirar.
40 */
41#define _GNU_SOURCE
42#include <dirent.h>
43#include <errno.h>
44#include <fcntl.h>
45#include <linux/audit.h>
46#include <linux/filter.h>
47#include <linux/landlock.h>
48#include <linux/seccomp.h>
49#include <stddef.h>
50#include <stdio.h>
51#include <stdlib.h>
52#include <string.h>
53#include <sys/mman.h>
54#include <sys/prctl.h>
55#include <sys/stat.h>
56#include <sys/syscall.h>
57#include <sys/utsname.h>
58#include <sys/wait.h>
59#include <time.h>
60#include <unistd.h>
61
62#define ROUNDS_MAX 15
63static int ROUNDS = 9;
64
65static inline double now_ns(void) {
66    struct timespec t;
67    clock_gettime(CLOCK_MONOTONIC, &t);  /* vDSO: no cruza al kernel, no se mide a sí mismo */
68    return t.tv_sec * 1e9 + t.tv_nsec;
69}
70static int cmpd(const void *a, const void *b) {
71    double x = *(const double *)a, y = *(const double *)b;
72    return (x > y) - (x < y);
73}
74/* Resultados de los hijos: memoria compartida, porque cada medición vive en su propio proceso. */
75static double *shared;
76static void rep(const char *n, double *r, int c) {
77    qsort(r, c, sizeof(double), cmpd);
78    printf("%-52s %9.1f %9.1f\n", n, r[0], r[c / 2]);
79    fflush(stdout);
80}
81
82/* ------------------------------------------------------------------ A) seccomp */
83
84/* Números de syscall que NO existen: la cadena se recorre entera sin que ninguno case, que es
85 * exactamente lo que le pasa a toda syscall legítima bajo la denylist de harkaq. */
86#define NR_FALSO_BASE 900
87
88/* Arma el filtro con la MISMA forma que `harkaq-exec.c::poner_seccomp`.
89 * `mirar_args` antepone una carga de `seccomp_data.args[0]`, que es lo único que cambia — y lo
90 * único que hace falta para que el analizador de acción constante se rinda. */
91static struct sock_filter *armar(int n, int mirar_args, unsigned short *out_len) {
92    int extra = mirar_args ? 2 : 0;
93    struct sock_filter *f = calloc(n + 6 + extra, sizeof(*f));
94    if (!f) return NULL;
95    int k = 0;
96    const int I_ERRNO = 4 + n + extra, I_KILL = 5 + n + extra;
97
98    if (mirar_args) {
99        /* offsetof(seccomp_data, args) no es `nr` ni `arch` ⇒ seccomp_is_const_allow() devuelve
100         * false y el bitmap queda vacío. El salto nunca se toma: 0xdeadbeef no es un argumento. */
101        f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
102                                              offsetof(struct seccomp_data, args));
103        f[k] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0xdeadbeefu,
104                                            I_KILL - k - 1, 0);
105        k++;
106    }
107    f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
108                                          offsetof(struct seccomp_data, arch));
109    f[k] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0,
110                                        I_KILL - k - 1);
111    k++;
112    f[k++] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
113                                          offsetof(struct seccomp_data, nr));
114    for (int i = 0; i < n; i++, k++)
115        f[k] = (struct sock_filter)BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K,
116                                            (unsigned)(NR_FALSO_BASE + i), I_ERRNO - k - 1, 0);
117    f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW);
118    f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K,
119                                          SECCOMP_RET_ERRNO | (EPERM & SECCOMP_RET_DATA));
120    f[k++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS);
121    *out_len = (unsigned short)k;
122    return f;
123}
124
125/* El límite que nadie ve: `jt`/`jf` de la BPF clásica son __u8. Con la cadena de harkaq, el salto
126 * más largo mide N+3, así que a partir de ~252 entradas el desplazamiento DA LA VUELTA y el
127 * filtro deja de decir lo que dice su código fuente — sin error, sin aviso. */
128#define N_MAX_SEGURO 250
129
130static long instalar(int n, int mirar_args) {
131    unsigned short len = 0;
132    struct sock_filter *f = armar(n, mirar_args, &len);
133    if (!f) return -1;
134    struct sock_fprog prog = {.len = len, .filter = f};
135    long r = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
136    free(f);
137    return r;
138}
139
140/* La syscall que se cronometra: `getppid` por número, para no depender de si la libc la cachea. */
141static double t_syscall(long iters) {
142    double t0 = now_ns();
143    for (long i = 0; i < iters; i++) syscall(SYS_getppid);
144    return (now_ns() - t0) / iters;
145}
146
147/* Corre una configuración de seccomp en un hijo. Devuelve 0 si el hijo terminó bien. */
148static int hijo_seccomp(int n, int mirar_args, long iters, int verificar) {
149    pid_t p = fork();
150    if (p < 0) return -1;
151    if (p == 0) {
152        if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) _exit(3);
153        if (n >= 0 && instalar(n, mirar_args) < 0) _exit(4);
154        /* Control negativo: con filtro puesto, un número de la denylist TIENE que dar EPERM.
155         * Sin esto, un filtro que el kernel aceptó pero no aplica se mediría como "gratis". */
156        if (verificar && n > 0) {
157            errno = 0;
158            long r = syscall(NR_FALSO_BASE, 0, 0, 0, 0, 0, 0);
159            if (!(r == -1 && errno == EPERM)) _exit(5);
160        }
161        for (int q = 0; q < ROUNDS; q++) shared[q] = t_syscall(iters);
162        _exit(0);
163    }
164    int st = 0;
165    waitpid(p, &st, 0);
166    return (WIFEXITED(st) && WEXITSTATUS(st) == 0) ? 0 : (WIFEXITED(st) ? WEXITSTATUS(st) : -1);
167}
168
169/* Coste de INSTALAR el filtro: el kernel recorre el programa entero una vez POR NÚMERO DE SYSCALL
170 * para armar el bitmap, así que esto crece con N aunque la ejecución no. Es precio por proceso
171 * (por fase de build), no por syscall.
172 *
173 * Una instalación por hijo, y no un lazo de instalaciones apiladas, por dos razones que salieron
174 * midiendo: (1) el kernel limita el total de instrucciones de la cadena (MAX_INSNS_PER_PATH), y
175 * apilar 200 filtros de 64 entradas ya no entra —da EINVAL, que se leería como "el filtro es
176 * ilegal" cuando lo ilegal es la pila—; (2) cada instalación posterior atraviesa los filtros ya
177 * puestos, así que el lazo mide una cosa distinta en cada vuelta. Un solo `seccomp()` por proceso
178 * es además lo que hace harkaq. */
179static int hijo_instalar(int n, int mirar_args) {
180    for (int q = 0; q < ROUNDS; q++) {
181        pid_t p = fork();
182        if (p < 0) return -1;
183        if (p == 0) {
184            if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) _exit(3);
185            unsigned short len = 0;
186            struct sock_filter *f = armar(n, mirar_args, &len);
187            if (!f) _exit(4);
188            struct sock_fprog prog = {.len = len, .filter = f};
189            double t0 = now_ns();
190            long r = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
191            double t1 = now_ns();
192            if (r < 0) _exit(4);
193            shared[q] = (t1 - t0) / 1000.0; /* µs */
194            _exit(0);
195        }
196        int st = 0;
197        waitpid(p, &st, 0);
198        if (!(WIFEXITED(st) && WEXITSTATUS(st) == 0))
199            return WIFEXITED(st) ? WEXITSTATUS(st) : -1;
200    }
201    return 0;
202}
203
204/* ------------------------------------------------------------------ B) Landlock */
205
206static int ll_create(const struct landlock_ruleset_attr *a, size_t n, __u32 f) {
207    return syscall(SYS_landlock_create_ruleset, a, n, f);
208}
209static int ll_add(int fd, enum landlock_rule_type t, const void *a, __u32 f) {
210    return syscall(SYS_landlock_add_rule, fd, t, a, f);
211}
212static int ll_self(int fd, __u32 f) { return syscall(SYS_landlock_restrict_self, fd, f); }
213
214/* Mismo juego de accesos que harkaq pide en ABI >= 5, recortado a lo que el ABI vivo soporte. */
215static __u64 accesos_manejados(int abi) {
216    __u64 h = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_WRITE_FILE |
217              LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR |
218              LANDLOCK_ACCESS_FS_REMOVE_DIR | LANDLOCK_ACCESS_FS_REMOVE_FILE |
219              LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_DIR |
220              LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK |
221              LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_BLOCK |
222              LANDLOCK_ACCESS_FS_MAKE_SYM;
223    if (abi >= 2) h |= LANDLOCK_ACCESS_FS_REFER;
224    if (abi >= 3) h |= LANDLOCK_ACCESS_FS_TRUNCATE;
225    if (abi >= 5) h |= LANDLOCK_ACCESS_FS_IOCTL_DEV;
226    return h;
227}
228
229static char base[512];
230static int n_ficheros_creados = 0;
231#define POR_DIR 256
232
233static void ruta_de(int i, char *out, size_t n) {
234    snprintf(out, n, "%s/d%03d/f%03d", base, i / POR_DIR, i % POR_DIR);
235}
236
237/* El árbol de prueba: M ficheros repartidos en directorios de 256, más un fichero TESTIGO que
238 * NUNCA entra al ruleset — es con lo que se comprueba que la jaula está puesta de verdad. */
239static int armar_arbol(int m) {
240    char p[600];
241    for (int d = 0; d <= m / POR_DIR; d++) {
242        snprintf(p, sizeof p, "%s/d%03d", base, d);
243        if (mkdir(p, 0700) < 0 && errno != EEXIST) return -1;
244    }
245    for (int i = n_ficheros_creados; i < m; i++) {
246        ruta_de(i, p, sizeof p);
247        int fd = open(p, O_CREAT | O_WRONLY | O_TRUNC, 0600);
248        if (fd < 0) return -1;
249        if (write(fd, "x", 1) != 1) { close(fd); return -1; }
250        close(fd);
251    }
252    if (m > n_ficheros_creados) n_ficheros_creados = m;
253    snprintf(p, sizeof p, "%s/TESTIGO", base);
254    int fd = open(p, O_CREAT | O_WRONLY | O_TRUNC, 0600);
255    if (fd < 0) return -1;
256    close(fd);
257    return 0;
258}
259
260/* Construye el ruleset de M reglas fichero-a-fichero (más el `list` de cada directorio), tal como
261 * lo hace harkaq. Devuelve el fd del ruleset y, por `us_setup`, lo que costó armarlo. */
262static int construir_ruleset(int m, int abi, double *us_setup) {
263    struct landlock_ruleset_attr attr = {.handled_access_fs = accesos_manejados(abi)};
264    int rs = ll_create(&attr, sizeof attr, 0);
265    if (rs < 0) return -1;
266    char p[600];
267    double t0 = now_ns();
268    for (int d = 0; d <= (m ? (m - 1) / POR_DIR : 0); d++) {
269        snprintf(p, sizeof p, "%s/d%03d", base, d);
270        int dfd = open(p, O_PATH | O_CLOEXEC);
271        if (dfd < 0) { close(rs); return -1; }
272        struct landlock_path_beneath_attr pb = {.allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
273                                                .parent_fd = dfd};
274        int r = ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0);
275        close(dfd);
276        if (r < 0) { close(rs); return -1; }
277    }
278    for (int i = 0; i < m; i++) {
279        ruta_de(i, p, sizeof p);
280        int fd = open(p, O_PATH | O_CLOEXEC);
281        if (fd < 0) { close(rs); return -1; }
282        struct landlock_path_beneath_attr pb = {.allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
283                                                .parent_fd = fd};
284        int r = ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0);
285        close(fd); /* la regla se queda con la referencia al inodo: el fd no hay que retenerlo */
286        if (r < 0) { close(rs); return -1; }
287    }
288    *us_setup = (now_ns() - t0) / 1000.0;
289    return rs;
290}
291
292static double t_open(const char *ruta, long iters) {
293    double t0 = now_ns();
294    for (long i = 0; i < iters; i++) {
295        int fd = open(ruta, O_RDONLY | O_CLOEXEC);
296        if (fd >= 0) close(fd);
297    }
298    return (now_ns() - t0) / iters;
299}
300static double t_stat(const char *ruta, long iters) {
301    struct stat st;
302    double t0 = now_ns();
303    for (long i = 0; i < iters; i++) stat(ruta, &st);
304    return (now_ns() - t0) / iters;
305}
306
307/* Qué se cronometra dentro del hijo. `open` sobre un fichero CON regla es el caso frecuente de un
308 * build; `open` DENEGADO se midió esperando que fuera el peor caso —recorrer el árbol hasta la
309 * raíz sin encontrar regla que case— y resultó ser MÁS BARATO que uno concedido, porque falla
310 * antes de montar el descriptor: la denegación no es un riesgo de coste; `stat` está para
311 * responder si Landlock cobra en la syscall que un compilador hace más — Landlock no engancha
312 * `getattr`, y esa línea es lo que lo demuestra en vez de afirmarlo. */
313enum medida { MED_OPEN, MED_OPEN_DENEGADO, MED_STAT };
314
315/* m < 0 ⇒ sin Landlock (el control). Si no, ruleset de m reglas y se mide lo que pida `qué`. */
316static int hijo_landlock_med(int m, int abi, long iters, double *us_setup_out, enum medida que);
317static int hijo_landlock(int m, int abi, long iters, double *us_setup_out) {
318    return hijo_landlock_med(m, abi, iters, us_setup_out, MED_OPEN);
319}
320static int hijo_landlock_med(int m, int abi, long iters, double *us_setup_out, enum medida que) {
321    int tuberia[2];
322    if (pipe(tuberia) < 0) return -1;
323    pid_t p = fork();
324    if (p < 0) return -1;
325    if (p == 0) {
326        close(tuberia[0]);
327        double us = 0;
328        if (m >= 0) {
329            if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) _exit(3);
330            int rs = construir_ruleset(m, abi, &us);
331            if (rs < 0) _exit(4);
332            if (ll_self(rs, 0) < 0) _exit(5);
333            close(rs);
334            /* Control negativo: el TESTIGO no está en ninguna regla. Si se puede abrir, la jaula
335             * no está puesta y todo lo de abajo mediría un `open` sin Landlock. */
336            char t[600];
337            snprintf(t, sizeof t, "%s/TESTIGO", base);
338            int fd = open(t, O_RDONLY);
339            if (fd >= 0) { close(fd); _exit(6); }
340            if (errno != EACCES && errno != EPERM) _exit(7);
341        }
342        if (write(tuberia[1], &us, sizeof us) != (ssize_t)sizeof us) _exit(8);
343        close(tuberia[1]);
344        char r[600];
345        if (que == MED_OPEN_DENEGADO)
346            snprintf(r, sizeof r, "%s/TESTIGO", base);
347        else
348            ruta_de(m > 0 ? m / 2 : 0, r, sizeof r);
349        for (int q = 0; q < ROUNDS; q++)
350            shared[q] = (que == MED_STAT) ? t_stat(r, iters) : t_open(r, iters);
351        _exit(0);
352    }
353    close(tuberia[1]);
354    double us = 0;
355    if (read(tuberia[0], &us, sizeof us) != (ssize_t)sizeof us) us = -1;
356    close(tuberia[0]);
357    int st = 0;
358    waitpid(p, &st, 0);
359    if (us_setup_out) *us_setup_out = us;
360    return (WIFEXITED(st) && WEXITSTATUS(st) == 0) ? 0 : (WIFEXITED(st) ? WEXITSTATUS(st) : -1);
361}
362
363/* ------------------------------------------------------------------ main */
364
365static void fatal(const char *que, int code) {
366    fprintf(stderr, "\nABORTA: %s (codigo %d).\n", que, code);
367    fprintf(stderr, "Un banco que mide una jaula apagada da numeros preciosos y falsos.\n");
368    exit(2);
369}
370
371int main(int argc, char **argv) {
372    if (argc > 1) {
373        ROUNDS = atoi(argv[1]);
374        if (ROUNDS < 3 || ROUNDS > ROUNDS_MAX) ROUNDS = 9;
375    }
376    const char *tmpdir = argc > 2 ? argv[2] : "/tmp";
377    snprintf(base, sizeof base, "%s/bench_jaula.%d", tmpdir, (int)getpid());
378    if (mkdir(base, 0700) < 0) { perror("mkdir"); return 1; }
379
380    shared = mmap(NULL, sizeof(double) * ROUNDS_MAX, PROT_READ | PROT_WRITE,
381                  MAP_SHARED | MAP_ANONYMOUS, -1, 0);
382    if (shared == MAP_FAILED) { perror("mmap"); return 1; }
383
384    struct utsname u;
385    uname(&u);
386    int abi = ll_create(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
387    printf("bench_jaula — lo que cobra la jaula POR SYSCALL (SDD 25 T17/T18)\n");
388    printf("kernel %s · landlock ABI %d · %d rondas · arbol en %s\n", u.release, abi, ROUNDS, base);
389    if (abi < 1) fatal("este kernel no tiene Landlock: no hay nada que medir", abi);
390
391    double r[ROUNDS_MAX];
392    long ITERS = 300000;
393
394    /* --- A.1 · el filtro en ejecucion: ¿la cadena lineal se paga? ------------------------- */
395    printf("\n--- A.1) seccomp: ns por getppid segun el LARGO del filtro ---\n");
396    printf("%-52s %9s %9s\n", "", "min", "mediana");
397    int rc = hijo_seccomp(-1, 0, ITERS, 0);
398    if (rc) fatal("el hijo sin filtro no termino bien", rc);
399    memcpy(r, shared, sizeof(double) * ROUNDS);
400    rep("  sin filtro (piso)", r, ROUNDS);
401
402    int enes[] = {4, 25, 64, 128, N_MAX_SEGURO};
403    for (unsigned k = 0; k < sizeof enes / sizeof *enes; k++) {
404        char nom[80];
405        rc = hijo_seccomp(enes[k], 0, ITERS, 1);
406        if (rc) fatal("filtro denylist: el hijo no termino bien (5=no dio EPERM)", rc);
407        memcpy(r, shared, sizeof(double) * ROUNDS);
408        snprintf(nom, sizeof nom, "  denylist de %3d (forma harkaq, analizable)%s", enes[k],
409                 enes[k] == 25 ? "  <-- harkaq" : "");
410        rep(nom, r, ROUNDS);
411    }
412    for (unsigned k = 0; k < sizeof enes / sizeof *enes; k++) {
413        char nom[80];
414        rc = hijo_seccomp(enes[k], 1, ITERS, 1);
415        if (rc) fatal("filtro que mira args: el hijo no termino bien", rc);
416        memcpy(r, shared, sizeof(double) * ROUNDS);
417        snprintf(nom, sizeof nom, "  gemelo de %3d que MIRA args[0] (sin bitmap)", enes[k]);
418        rep(nom, r, ROUNDS);
419    }
420
421    /* --- A.2 · el precio por proceso: armar el bitmap ------------------------------------- */
422    printf("\n--- A.2) seccomp: us por INSTALAR el filtro (precio por proceso) ---\n");
423    printf("%-52s %9s %9s\n", "", "min", "mediana");
424    for (unsigned k = 0; k < sizeof enes / sizeof *enes; k++) {
425        char nom[80];
426        rc = hijo_instalar(enes[k], 0);
427        if (rc) fatal("instalacion: el hijo no termino bien", rc);
428        memcpy(r, shared, sizeof(double) * ROUNDS);
429        snprintf(nom, sizeof nom, "  instalar denylist de %3d (arma el bitmap)", enes[k]);
430        rep(nom, r, ROUNDS);
431    }
432    for (unsigned k = 0; k < sizeof enes / sizeof *enes; k++) {
433        char nom[80];
434        rc = hijo_instalar(enes[k], 1);
435        if (rc) fatal("instalacion del gemelo: el hijo no termino bien", rc);
436        memcpy(r, shared, sizeof(double) * ROUNDS);
437        snprintf(nom, sizeof nom, "  instalar gemelo de %3d (sin bitmap que armar)", enes[k]);
438        rep(nom, r, ROUNDS);
439    }
440
441    /* --- B · Landlock ---------------------------------------------------------------------- */
442    printf("\n--- B) landlock: ns por open+close segun el TAMANO del ruleset ---\n");
443    printf("%-52s %9s %9s\n", "", "min", "mediana");
444    long IT_OPEN = 40000;
445    int emes[] = {1, 16, 256, 4096, 16384};
446    if (armar_arbol(emes[sizeof emes / sizeof *emes - 1]) < 0) { perror("armar_arbol"); return 1; }
447
448    rc = hijo_landlock(-1, abi, IT_OPEN, NULL);
449    if (rc) fatal("el hijo sin landlock no termino bien", rc);
450    memcpy(r, shared, sizeof(double) * ROUNDS);
451    rep("  sin landlock (piso)", r, ROUNDS);
452
453    double setup[8];
454    for (unsigned k = 0; k < sizeof emes / sizeof *emes; k++) {
455        char nom[80];
456        rc = hijo_landlock(emes[k], abi, IT_OPEN, &setup[k]);
457        if (rc) fatal("landlock: el hijo no termino bien (6=el TESTIGO se abrio igual)", rc);
458        memcpy(r, shared, sizeof(double) * ROUNDS);
459        snprintf(nom, sizeof nom, "  ruleset de %5d reglas fichero-a-fichero%s", emes[k],
460                 emes[k] == 16384 ? "  <-- ~boost" : "");
461        rep(nom, r, ROUNDS);
462    }
463    /* --- B.1.bis · qué syscalls toca Landlock y cuál es su peor caso ----------------------- */
464    printf("\n--- B.1.bis) landlock con %d reglas: otras dos syscalls ---\n", emes[4]);
465    printf("%-52s %9s %9s\n", "", "min", "mediana");
466    rc = hijo_landlock_med(-1, abi, IT_OPEN, NULL, MED_STAT);
467    if (rc) fatal("stat sin landlock: el hijo no termino bien", rc);
468    memcpy(r, shared, sizeof(double) * ROUNDS);
469    rep("  stat sin landlock (piso)", r, ROUNDS);
470    rc = hijo_landlock_med(emes[4], abi, IT_OPEN, NULL, MED_STAT);
471    if (rc) fatal("stat con landlock: el hijo no termino bien", rc);
472    memcpy(r, shared, sizeof(double) * ROUNDS);
473    rep("  stat con landlock (Landlock NO engancha getattr)", r, ROUNDS);
474    rc = hijo_landlock_med(emes[4], abi, IT_OPEN, NULL, MED_OPEN_DENEGADO);
475    if (rc) fatal("open denegado: el hijo no termino bien", rc);
476    memcpy(r, shared, sizeof(double) * ROUNDS);
477    rep("  open DENEGADO (no monta el fd: sale mas barato)", r, ROUNDS);
478
479    printf("\n--- B.2) landlock: ms por ARMAR el ruleset (precio por proceso) ---\n");
480    for (unsigned k = 0; k < sizeof emes / sizeof *emes; k++)
481        printf("  %5d reglas: %8.2f ms  (%6.2f us/regla)\n", emes[k], setup[k] / 1000.0,
482               setup[k] / emes[k]);
483
484    printf("\n(el arbol de prueba queda en %s; borrarlo a mano)\n", base);
485    return 0;
486}