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 1411Shell 141Markdown 84Rust 832898 Definiciones

harkaq: Q1 CERRADA — la evidencia llega; + D9 (canario deliberado) y el banco de pruebas

Q1 era el gate del proyecto (¿llegan los registros AUDIT_LANDLOCK_* a un
lector?). Medido en el laptop (ABI 10) con scripts/harkaq/q1-audit.c:

  ✅ VIABLE. El multicast AUDIT_NLGRP_READLOG entrega
     blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369
     + exe=/comm= del que lo intentó. Es el diagnóstico de de-Alpinización
     que el SDD promete, saliendo del kernel sin instrumentar nada.

Dos precondiciones que el Draft 2 no tenía, ambas medidas:
  - audit_enabled=1 NO viene de fábrica (sin audit=1 en cmdline, sin auditd)
    ⇒ cero registros de cualquier escenario. AUDIT_SET o audit=1 horneado.
  - LOG_NEW_EXEC_ON es OBLIGATORIO: same-exec=2 registros, new-exec=0,
    new-exec-logon=4. harkaq restringe y DESPUÉS execea el builder ⇒ sin el
    flag certificaría herméticos TODOS los builds, denials=[], sin un error.

D9 (nueva, no negociable): el canario lo fabrica harkaq. Se investigó si el
registro `deallocated denials=N` servía de verificación cruzada gratis: NO.
Medido con ventana de 6s, un dominio con flags default y denegación post-exec
no emite NADA (ni allocated, ni access, ni el contador); y el `allocated` es
perezoso (llega pegado al primer denial logueado) ⇒ su ausencia tampoco prueba
nada. Un build hermético y un lector ciego son bit-idénticos: denials=[]. El
Verdict pasa a 3 estados (Hermetico / Impuro / SinEvidencia).
El contador SÍ sirve para lo otro: nº ACCESS == denials caza registros
perdidos por backlog (riesgo real con la granja en paralelo).

Nota de método (§3.4): la 1ª corrida dio 0 en los 3 escenarios y "confirmaba"
la hipótesis. Era el instrumento roto (AUDIT_GET con NLM_F_ACK: el ACK llegaba
antes que la respuesta → enabled=-1 → nunca se encendía el audit). Se cazó
sólo porque el CONTROL (same-exec) también dio 0, y eso era imposible. Es el
modo de falla de harkaq en vivo sobre su propio banco. El test ahora aborta
(exit 4) si no confirma audit_enabled=1.

Nuevas Q1b (¿el lector ve a través del userns de bwrap? ¿cómo se atribuye un
registro a SU build con la granja en paralelo?) y Q1c (privilegio de
harkaq-audit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

ad0948cfb0c688359c19967ef6406415213ea445 · por sergio · (hace 3 meses) · padre 9478f4c06 · ver el árbol en este commit

Delta semántico · 29

scripts/harkaq/q1-audit.c

3 archivos · +407 −10

diff --git a/docs/16-harkaq-jaula.md b/docs/16-harkaq-jaula.md
index b057ae26..12ee7755 100644
--- a/docs/16-harkaq-jaula.md
+++ b/docs/16-harkaq-jaula.md
@@ -169,6 +169,68 @@ resultó ser la correcta por razones de seguridad que nadie había escrito todav
 
 **Fase 3 se elimina del plan.** El riesgo de cronograma desaparece.
 
+### 3.4 Q1 resuelta: la evidencia llega, con dos precondiciones y un canario obligatorio
+
+Experimento `scripts/harkaq/q1-audit.c` (2026-07-15), corrido en el laptop (ABI 10). Tres
+escenarios: denegación sin `execve`, con `execve` y flags por defecto, y con `execve` +
+`LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON`.
+
+**La evidencia llega, y es exactamente la que el diseño necesita.** Un lector no privilegiado
+del grupo multicast `AUDIT_NLGRP_READLOG` recibe:
+
+```
+domain=146973ef2 blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369
+domain=146973ef2 status=allocated mode=enforcing pid=3359 uid=0 exe="…/q1-audit" comm="q1-audit"
+```
+
+`blockers` + `path` + `dev` + `ino` + el `exe` que lo intentó. Ese registro **es** el diagnóstico
+de de-Alpinización de §0, y sale del kernel sin instrumentar nada.
+
+**Precondición 1 — `audit_enabled=1`.** Medido: el laptop arranca con `audit_enabled=0` (sin
+`audit=1` en el cmdline, sin `auditd`). Con audit apagado **no se emite ningún registro, de
+ningún escenario**. Se enciende con `AUDIT_SET` por netlink (`CAP_AUDIT_CONTROL`) o con `audit=1`
+en el cmdline — que para el kernel de hammer es horneable, igual que el resto del cmdline metal.
+
+**Precondición 2 — `LOG_NEW_EXEC_ON` es obligatorio, y su ausencia es indetectable.** Medido:
+
+| Escenario | Registros `ACCESS` |
+|---|---|
+| `same-exec` (sin `execve`) | **2** — logueado por defecto |
+| `new-exec` (flags por defecto) | **0** |
+| `new-exec-logon` (`LOG_NEW_EXEC_ON`) | **4** |
+
+Misma jaula, misma denegación, `cat` rebotó con `EACCES` en los tres. harkaq **restringe y después
+hace `execve`** del builder (bwrap → `/bin/sh -c` → make → `zig cc`): es el caso `new-exec`. Sin el
+flag, harkaq certificaría como herméticos **todos** los builds, `denials=[]`, para siempre, sin un
+solo error visible. El flag no es configuración: es la diferencia entre el sistema y un sello de goma.
+
+**No hay canario gratis en el kernel.** Se investigó si el registro `status=deallocated denials=N`
+podía servir de verificación cruzada. Medido, con una ventana de escucha de 6s: **un dominio con
+flags por defecto cuya única denegación es post-`exec` no emite absolutamente nada** — ni
+`allocated`, ni `access`, ni el `deallocated` con su contador. El contador existe **sólo para
+dominios que ya loguean**. Y el `allocated` se emite **perezosamente**, pegado al primer denial
+logueado (mismo evento de audit), así que un build genuinamente limpio tampoco lo produce: su
+presencia no puede usarse como prueba de que los flags tomaron.
+
+⇒ **D9 (nueva decisión cerrada, abajo): el canario lo fabrica harkaq.**
+
+Lo que el contador sí resuelve, cuando el logging está bien puesto: exigir
+`nº de registros ACCESS recibidos == denials` del `deallocated` detecta **registros perdidos** por
+desborde del backlog de audit — un riesgo real con la granja construyendo en paralelo. Son dos
+chequeos para dos fallas distintas (mala configuración vs. pérdida), y hacen falta los dos.
+
+> **Nota de método, que vale más que el resultado.** La primera corrida del experimento dio 0
+> registros en los tres escenarios y parecía confirmar la hipótesis. Era un bug del instrumento:
+> el `AUDIT_GET` pedía `NLM_F_ACK`, el ACK llegaba antes que la respuesta (el kernel la manda
+> asíncrona), el lector lo tomaba por fallo, `enabled` quedaba en `-1`, la rama que encendía el
+> audit nunca corría y el kernel no emitía nada. Sólo se detectó porque el **control**
+> (`same-exec`, que debía loguear) también dio 0, y eso era imposible.
+>
+> Es el modo de falla de harkaq, en vivo, sobre su propio banco de pruebas: un sistema cuyo
+> producto es la *ausencia* de algo no se rompe ruidosamente — dice que todo está bien. El
+> experimento ahora aborta con código 4 si no puede **confirmar** `audit_enabled=1`, en vez de
+> reportar cero. La misma disciplina es la que D9 le impone a harkaq.
+
 ---
 
 ## 4. Arquitectura
@@ -316,6 +378,38 @@ Ojo con la semántica documentada — un socket datagram ya conectado antes del
 enviando; y el scoping no admite reglas: si un dominio está scoped, no hay forma de permitir un
 recurso puntual fuera. Es todo-o-nada, lo cual para nosotros está bien.
 
+### D9 — El canario deliberado: `denials=[]` no se cree, se gana
+
+(Nace de la medición de §3.4. Es la decisión que Q1 obligó a tomar.)
+
+**El problema.** Un build hermético y un lector ciego producen exactamente los mismos bytes:
+`denials=[]`. Y §3.4 midió que el kernel **no ofrece ninguna forma de distinguirlos** — un dominio
+sin logging es invisible, y el registro `allocated` es perezoso, así que su ausencia tampoco
+prueba nada. Sin resolver esto, D2 (la evidencia negativa) no vale nada: la afirmación más fuerte
+del sistema sería indistinguible de su falla más común.
+
+**La decisión.** Cada build provoca una denegación conocida, **después del `execve`**, y harkaq
+**exige verla**. El canario es un path que existe en el sandbox y que la política deja
+deliberadamente fuera de la clausura (p.ej. un `--ro-bind` a una ruta fija que el ruleset no
+concede). El probe se antepone al comando del builder — misma vía que `Sandbox::run` ya usa
+(`/bin/sh -c`), así que corre en el dominio y del lado correcto del `exec`.
+
+**El veredicto pasa a ser de tres estados, no de dos:**
+
+| Canario | Otras denegaciones | Veredicto |
+|---|---|---|
+| visto | ∅ | `Hermetico` — y *ahora sí* significa algo |
+| visto | ≥1 | `Impuro { denials }` — el diagnóstico útil |
+| **no visto** | cualquiera | **`SinEvidencia`** — el lector no es confiable; no se afirma nada |
+
+El canario cubre la mala configuración (flag ausente, audit apagado, lector caído). El contador
+`deallocated denials=N` de §3.4 cubre la otra falla, la pérdida de registros por backlog: se exige
+`nº de ACCESS == N`. Dos chequeos, dos fallas, ninguno redundante.
+
+**Por qué es un no-negociable.** Es la misma disciplina de D7 (fallo cerrado) y de D3 (cero
+quieting), aplicada al único punto donde el sistema podría mentirse a sí mismo en silencio. Y no
+es hipotético: el banco de pruebas de Q1 ya cayó en esa trampa una vez (§3.4, nota de método).
+
 ---
 
 ## 6. Fases
@@ -324,9 +418,15 @@ recurso puntual fuera. Es todo-o-nada, lo cual para nosotros está bien.
 Landlock ausente-pero-a-un-flag en el kernel de hammer, cero FODs y fetcher ya construido.
 
 **Fase 1 — El hito demostrable (una tarde).**
-Encender `SECURITY_LANDLOCK` en `recipes/linux.toml`. Tomar **una** receta real de hammer.
-Derivar su política de la clausura. Correrla bajo Landlock+seccomp dentro del bwrap actual. Emitir
-el `Verdict` con `denials = []`.
+Precondiciones, todas ya medidas en §3 y ninguna especulativa:
+1. `scripts/config -e SECURITY_LANDLOCK` en `recipes/linux.toml` (+ rebuild, ~35min, cambia el hash).
+2. `audit=1` horneado en el cmdline del kernel metal — o `AUDIT_SET` desde hammerd (§3.4, Q1c).
+3. `LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON` en el `restrict_self` (§3.4). **Sin esto no hay nada.**
+4. El canario de D9 antepuesto al comando del builder.
+
+Después: tomar **una** receta real de hammer. Derivar su política de la clausura. Correrla bajo
+Landlock+seccomp dentro del bwrap actual. Emitir el `Verdict` con `denials = []` **y el canario
+visto**.
 Luego correr **una receta que sepamos no de-Alpinizada** y mostrar que la jaula nombra exactamente
 qué usó sin declarar, con path/dev/ino.
 **Ese par de corridas es el argumento entero del proyecto**, y se enseña en dos minutos.
@@ -379,13 +479,20 @@ vibra.
 
 ## 8. Preguntas abiertas
 
-- **Q1 (Fase 1, la que importa):** ¿cómo llegan los registros `AUDIT_LANDLOCK_*` a `harkaq-audit`?
-  El subsistema de audit del kernel pide `CAP_AUDIT_READ` para leer por netlink, y suele tener un
-  único consumidor (`auditd`) con el que habría que competir o coordinar. Además el build corre
-  bajo `--unshare-all` (userns) y `exec`ea varias veces, lo cual interactúa con los flags de
-  logging de `restrict_self` (`LOG_NEW_EXEC_ON` y compañía). **Esto se prueba antes que nada**: si
-  la evidencia no sale del kernel de forma confiable, no hay proyecto. Es el primer experimento,
-  no el último.
+- **Q1 — ✅ CERRADA (2026-07-15).** La evidencia llega, con `blockers`/`path`/`dev`/`ino`/`exe`, por
+  el multicast `AUDIT_NLGRP_READLOG`. Precondiciones medidas: `audit_enabled=1` (no viene de
+  fábrica) y `LOG_NEW_EXEC_ON` (sin él, cero registros tras el `exec`). No hay canario gratis ⇒
+  D9. Detalle y mediciones en §3.4. **El gate del proyecto está pasado: harkaq es viable.**
+- **Q1b (Fase 1, lo que Q1 destapó):** ¿el lector funciona igual con el build **dentro del userns
+  de bwrap**? El audit no está namespaceado, así que `harkaq-audit` lee desde el host (junto a
+  hammerd) y debería ver los denials de adentro — pero hay que medirlo, no suponerlo. Y de paso:
+  ¿cómo se atribuye un registro a *su* build cuando la granja corre varios en paralelo? El campo
+  `domain=` es la clave candidata; el `pid=` del `allocated` es el puente, pero §3.4 midió que ese
+  registro es perezoso. **Riesgo concreto:** sin atribución fiable, los builds paralelos se
+  contaminan la evidencia entre sí.
+- **Q1c:** ¿quién corre `harkaq-audit` y con qué privilegio? Necesita `CAP_AUDIT_READ` (leer) y
+  `CAP_AUDIT_CONTROL` (encender el audit, si no se hornea `audit=1` en el cmdline). En el laptop
+  hizo falta root. ¿hammerd ya lo tiene, o hace falta un helper con capabilities acotadas?
 - **Q2 (Fase 2):** ¿qué es exactamente el "runtime base" que toda receta necesita y ninguna
   declara (`/bin/sh`, la libc del rootfs, `/opt/zig`)? ¿Va en la clausura implícitamente, o se
   declara y se vuelve parte del hash de la receta? *Esta es la decisión de diseño de verdad*, y
diff --git a/scripts/harkaq/README.md b/scripts/harkaq/README.md
new file mode 100644
index 00000000..23ad6c67
--- /dev/null
+++ b/scripts/harkaq/README.md
@@ -0,0 +1,42 @@
+# harkaq — banco de pruebas
+
+Ver `docs/16-harkaq-jaula.md`. Acá viven los experimentos que sostienen las afirmaciones del SDD:
+si el doc dice "medido", el que lo midió está en este directorio.
+
+## `q1-audit.c` — el gate del proyecto (Q1, ✅ cerrado 2026-07-15)
+
+Responde: *¿llegan los registros `AUDIT_LANDLOCK_*` a un lector?* Todo harkaq cuelga de eso — su
+producto es la evidencia, no la jaula.
+
+```sh
+gcc -O1 -Wall -o q1-audit q1-audit.c
+for s in same-exec new-exec new-exec-logon; do sudo ./q1-audit $s; done
+```
+
+Necesita root (`CAP_AUDIT_READ` para leer el multicast, `CAP_AUDIT_CONTROL` para encender el
+audit). **Enciende `audit_enabled=1` globalmente y no lo restaura** — reversible, no persiste al
+reboot (no toca el cmdline), pero deja `dmesg` más ruidoso mientras tanto.
+
+### Resultados en el laptop (ABI 10, kernel 7.2.0-rc3)
+
+| Escenario | Registros `ACCESS` | Qué prueba |
+|---|---|---|
+| `same-exec` | 2 | el kernel loguea por defecto sin `execve` |
+| `new-exec` | **0** | **flags por defecto ⇒ ciego tras `execve`** — el caso real de harkaq |
+| `new-exec-logon` | 4 | `LOG_NEW_EXEC_ON` lo arregla |
+
+Los registros traen `blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369` y el
+`exe=` que lo intentó. Análisis completo en `docs/16-harkaq-jaula.md` §3.4.
+
+### Por qué aborta con código 4
+
+Si no puede **confirmar** `audit_enabled=1`, aborta en vez de reportar 0 registros. No es
+paranoia: la primera corrida dio 0 en los tres escenarios y parecía confirmar la hipótesis. Era un
+bug del instrumento (`AUDIT_GET` con `NLM_F_ACK` → el ACK llegaba antes que la respuesta → nunca
+se encendía el audit). Se detectó sólo porque el **control** (`same-exec`) también dio 0, y eso era
+imposible.
+
+Es el modo de falla de harkaq sobre su propio banco de pruebas: un sistema cuyo producto es la
+*ausencia* de algo no falla ruidosamente — dice que todo está bien. De ahí sale D9 (el canario
+deliberado), y de ahí sale la regla de este directorio: **todo experimento lleva un control que
+debe dar positivo.** Un experimento sin control no mide, tranquiliza.
diff --git a/scripts/harkaq/q1-audit.c b/scripts/harkaq/q1-audit.c
new file mode 100644
index 00000000..cb6f3059
--- /dev/null
+++ b/scripts/harkaq/q1-audit.c
@@ -0,0 +1,248 @@
+// Q1 de harkaq (SDD 16 §8): ¿llegan los registros AUDIT_LANDLOCK_ACCESS a un lector?
+//
+// Prueba las tres cosas de las que cuelga el proyecto:
+//   1. ¿El audit del kernel emite el registro? (audit_enabled, kauditd)
+//   2. ¿Un lector multicast (AUDIT_NLGRP_READLOG) los recibe? (CAP_AUDIT_READ)
+//   3. ¿SOBREVIVEN AL execve? — harkaq restringe y DESPUÉS ejecuta el builder.
+//      El default del kernel NO loguea tras exec (linux/landlock.h:80-83).
+//      Sin LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON, harkaq vería denials=[] SIEMPRE.
+//
+// Uso (como root):  ./q1-audit same-exec | new-exec | new-exec-logon
+//
+// El "víctima" se autoencierra con una política que sólo permite leer /tmp, y
+// después intenta leer /etc/passwd (fuera de la clausura) → EACCES → debe generar
+// un AUDIT_LANDLOCK_ACCESS. El padre escucha el netlink y lo imprime.
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/audit.h>
+#include <linux/landlock.h>
+#include <linux/netlink.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+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_restrict(int fd, __u32 f) {
+    return syscall(SYS_landlock_restrict_self, fd, f);
+}
+
+#define ACCESS_FS_ROUGHLY_READ                                    \
+    (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | \
+     LANDLOCK_ACCESS_FS_EXECUTE)
+
+// ---------------------------------------------------------------- netlink audit
+
+static int nl_open(void) {
+    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
+    if (fd < 0) { perror("socket(NETLINK_AUDIT)"); return -1; }
+    struct sockaddr_nl sa = {0};
+    sa.nl_family = AF_NETLINK;
+    sa.nl_pid = 0;
+    // Grupo multicast de sólo-lectura: exactamente para lectores como harkaq-audit.
+    sa.nl_groups = 1 << (AUDIT_NLGRP_READLOG - 1);
+    if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
+        perror("bind(AUDIT_NLGRP_READLOG)  <-- ¿falta CAP_AUDIT_READ?");
+        close(fd);
+        return -1;
+    }
+    return fd;
+}
+
+// Manda un AUDIT_GET/AUDIT_SET. `payload` NULL ⇒ GET.
+static int nl_send(int fd, int type, const void *payload, size_t len) {
+    struct {
+        struct nlmsghdr h;
+        char data[256];
+    } req = {0};
+    req.h.nlmsg_len = NLMSG_LENGTH(len);
+    req.h.nlmsg_type = type;
+    // SIN NLM_F_ACK: la respuesta de AUDIT_GET la manda el kernel de forma ASÍNCRONA
+    // (audit_send_reply usa un kthread), así que el ACK llegaba ANTES que el AUDIT_GET
+    // y el lector lo tomaba por "fallo". Pedir sólo REQUEST y filtrar por tipo al leer.
+    req.h.nlmsg_flags = NLM_F_REQUEST;
+    req.h.nlmsg_seq = 1;
+    req.h.nlmsg_pid = getpid();
+    if (payload) memcpy(req.data, payload, len);
+    struct sockaddr_nl sa = {0};
+    sa.nl_family = AF_NETLINK;
+    return sendto(fd, &req, req.h.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa));
+}
+
+// Lee el estado del audit por un socket dedicado (unicast). Devuelve enabled o -1.
+static int audit_status(int *enabled, int *pid) {
+    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
+    if (fd < 0) return -1;
+    if (nl_send(fd, AUDIT_GET, NULL, 0) < 0) { close(fd); return -1; }
+    struct timeval tv = {.tv_sec = 1};
+    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+    // Drenamos hasta encontrar el AUDIT_GET: puede venir detrás de otros mensajes.
+    char buf[8192];
+    for (int i = 0; i < 8; i++) {
+        ssize_t n = recv(fd, buf, sizeof(buf), 0);
+        if (n <= 0) break;
+        for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n);
+             h = NLMSG_NEXT(h, n)) {
+            if (h->nlmsg_type == AUDIT_GET) {
+                struct audit_status *st = (struct audit_status *)NLMSG_DATA(h);
+                *enabled = st->enabled;
+                *pid = st->pid;
+                close(fd);
+                return 0;
+            }
+            if (h->nlmsg_type == NLMSG_ERROR) {
+                struct nlmsgerr *e = (struct nlmsgerr *)NLMSG_DATA(h);
+                if (e->error)
+                    fprintf(stderr, "[q1] AUDIT_GET rechazado: %s\n", strerror(-e->error));
+            }
+        }
+    }
+    close(fd);
+    return -1;
+}
+
+static int audit_enable(void) {
+    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
+    if (fd < 0) return -1;
+    struct audit_status st = {0};
+    st.mask = AUDIT_STATUS_ENABLED;
+    st.enabled = 1;
+    int r = nl_send(fd, AUDIT_SET, &st, sizeof(st));
+    close(fd);
+    return r < 0 ? -1 : 0;
+}
+
+// ---------------------------------------------------------------- la víctima
+
+// Se autoencierra: sólo /tmp legible. Todo lo demás → EACCES + registro de audit.
+static void jail(__u32 restrict_flags) {
+    struct landlock_ruleset_attr attr = {.handled_access_fs = ACCESS_FS_ROUGHLY_READ};
+    int rs = ll_create(&attr, sizeof(attr), 0);
+    if (rs < 0) { perror("landlock_create_ruleset"); exit(2); }
+
+    // La "clausura declarada": /tmp y el intérprete. Nada más.
+    const char *allowed[] = {"/tmp", "/usr/lib", "/lib", "/bin", "/usr/bin", NULL};
+    for (int i = 0; allowed[i]; i++) {
+        int dfd = open(allowed[i], O_PATH | O_CLOEXEC);
+        if (dfd < 0) continue;
+        struct landlock_path_beneath_attr pb = {.allowed_access = ACCESS_FS_ROUGHLY_READ,
+                                                .parent_fd = dfd};
+        if (ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0) < 0) perror("add_rule");
+        close(dfd);
+    }
+    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("no_new_privs"); exit(2); }
+    if (ll_restrict(rs, restrict_flags) < 0) { perror("landlock_restrict_self"); exit(2); }
+    close(rs);
+}
+
+int main(int argc, char **argv) {
+    const char *scenario = argc > 1 ? argv[1] : "new-exec";
+
+    int enabled = -1, apid = -1;
+    if (audit_status(&enabled, &apid) == 0)
+        fprintf(stderr, "[q1] audit: enabled=%d auditd_pid=%d\n", enabled, apid);
+    else
+        fprintf(stderr, "[q1] audit: no pude leer el estado (¿sin CAP_AUDIT_CONTROL?)\n");
+
+    // `!= 1` y no `== 0`: si el GET falló (enabled=-1) igual hay que intentar encenderlo.
+    // El bug de la primera corrida fue justo este: GET falló → enabled=-1 → no entró acá →
+    // audit_enabled siguió en 0 → CERO registros en los 3 escenarios → un "hallazgo" que
+    // era el instrumento roto.
+    if (enabled != 1) {
+        fprintf(stderr, "[q1] audit no habilitado (enabled=%d) → AUDIT_SET\n", enabled);
+        if (audit_enable() < 0) perror("[q1] AUDIT_SET");
+        usleep(100000);
+        if (audit_status(&enabled, &apid) == 0)
+            fprintf(stderr, "[q1] audit: enabled=%d (tras AUDIT_SET)\n", enabled);
+    }
+    // GATE: sin audit_enabled=1 el kernel no emite NADA y el test no mide lo que cree medir.
+    // Abortar es obligatorio: un 0 acá significaría "instrumento roto", no "sin denegaciones".
+    if (enabled != 1) {
+        fprintf(stderr,
+                "[q1] ABORTO: no pude confirmar audit_enabled=1. Cualquier resultado sería\n"
+                "     un falso negativo del test, no una medición. (¿root? ¿audit=1?)\n");
+        return 4;
+    }
+
+    // Sin CAP_AUDIT_READ no hay lector, pero la víctima corre igual: así una corrida
+    // sin privilegio valida al menos que la JAULA deniega (la mitad barata del test).
+    int nl = nl_open();
+    if (nl < 0)
+        fprintf(stderr, "[q1] sin lector (correr como root para la prueba completa)\n");
+    else
+        fprintf(stderr, "[q1] lector multicast OK. escenario=%s\n", scenario);
+
+    __u32 flags = 0;
+    if (!strcmp(scenario, "new-exec-logon")) flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
+
+    pid_t pid = fork();
+    if (pid == 0) {
+        usleep(200000);  // dejamos que el padre esté escuchando
+        jail(flags);
+        if (!strcmp(scenario, "same-exec")) {
+            // Denegación SIN execve: el default del kernel SÍ la loguea.
+            int fd = open("/etc/passwd", O_RDONLY);
+            fprintf(stderr, "[victima] open(/etc/passwd) = %d (%s)\n", fd, strerror(errno));
+        } else {
+            // Denegación TRAS execve: el caso REAL de harkaq (bwrap → sh -c → make).
+            fprintf(stderr, "[victima] execve(/bin/cat /etc/passwd)\n");
+            execl("/bin/cat", "cat", "/etc/passwd", (char *)NULL);
+            perror("execl");
+        }
+        _exit(0);
+    }
+
+    // El pid del hijo permite atribuir los registros DOMAIN (traen pid=) a NUESTRO dominio y
+    // no al de una corrida anterior que se libera tarde — el error que casi me como.
+    fprintf(stderr, "[q1] hijo pid=%d (atribuir los DOMAIN por este pid)\n", pid);
+    char buf[16384];
+    int landlock_records = 0, total = 0;
+    if (nl < 0) {
+        waitpid(pid, NULL, 0);
+        fprintf(stderr, "\n[q1] jaula validada; evidencia NO probada (hace falta root).\n");
+        return 3;
+    }
+    // Deadline de pared, NO "cortar al primer timeout": el registro `status=deallocated
+    // denials=N` se emite cuando el dominio se libera, DESPUÉS de que el hijo muere, y con
+    // retraso (RCU/workqueue). Con la ventana de 2s se perdía y aparecía en la corrida
+    // siguiente, haciéndolo pasar por el dominio equivocado. Escuchamos 6s pasado el exit.
+    struct timeval tv = {.tv_sec = 1};
+    setsockopt(nl, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+    time_t deadline = time(NULL) + 6;
+    while (time(NULL) < deadline) {
+        ssize_t n = recv(nl, buf, sizeof(buf), 0);
+        if (n <= 0) continue;  // timeout de 1s: seguimos hasta el deadline
+        for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n);
+             h = NLMSG_NEXT(h, n)) {
+            total++;
+            if (h->nlmsg_type == AUDIT_LANDLOCK_ACCESS ||
+                h->nlmsg_type == AUDIT_LANDLOCK_DOMAIN) {
+                landlock_records++;
+                printf("[REGISTRO %s] %.*s\n",
+                       h->nlmsg_type == AUDIT_LANDLOCK_ACCESS ? "ACCESS" : "DOMAIN",
+                       (int)(NLMSG_PAYLOAD(h, 0)), (char *)NLMSG_DATA(h));
+            }
+        }
+    }
+    waitpid(pid, NULL, 0);
+
+    fprintf(stderr, "\n[q1] VEREDICTO escenario=%s: %d registros landlock (%d msgs audit total)\n",
+            scenario, landlock_records, total);
+    if (landlock_records == 0)
+        fprintf(stderr, "[q1] ⇒ SIN EVIDENCIA. harkaq reportaría denials=[] (falso 'hermético').\n");
+    else
+        fprintf(stderr, "[q1] ⇒ EVIDENCIA ENTREGADA. Q1 viable por esta vía.\n");
+    return 0;
+}