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 604Rust 56Shell 36Markdown 281227 Definitions

harkaq: seccomp implementado (D4) — denylist, no allowlist; el hash no se mueve

D4 declaraba seccomp "obligatorio, no opcional" y harkaq-exec tenía CERO
seccomp: el documento afirmaba algo que el código no hacía. Cerrado.

CORRECCIÓN DELIBERADA A D4: pedía ALLOWLIST por syscall, se implementó DENYLIST.
Un allowlist para builds ARBITRARIOS (compiladores, make, shells, linkers, perl)
es un blanco móvil que cada herramienta nueva rompe — el riesgo del §7 ("los
falsos positivos matan proyectos de sandboxing") aplicado a las syscalls, y con
peor final: un build que muere por una syscall legítima no da un diagnóstico
útil, da un misterio. Lo PELIGROSO sí es enumerable y estable: no hay build
honesto que cargue módulos, haga kexec o attachee un ptrace.

Denegadas: io_uring_*, ptrace, process_vm_{readv,writev}, bpf, userfaultfd,
keyctl/add_key/request_key, {init,finit,delete}_module, kexec_*,
perf_event_open, mount/umount2, open_tree/move_mount/fs*, setns.
pivot_root NO: bwrap lo usa ANTES de llegar a harkaq-exec.

EPERM y no KILL: matar deja un cadáver sin explicación; EPERM deja al build
fallar donde corresponde y al log decir por qué. Se instala DESPUÉS de
no_new_privs y de Landlock, justo antes del exec, y se hereda por fork/exec como
el dominio (D5). Si el kernel lo rechaza NO se corre el build (D7: media jaula
creyéndose entera es peor que ninguna).

Chequeo de arquitectura antes del número de syscall: los nros son POR ARCH y sin
el check un binario i386 podría colar otra syscall con el mismo número — el error
clásico de los filtros seccomp a mano.

COMPROBADO: ptrace → EPERM bajo la jaula (control positivo), y un build REAL de
zlib sale Hermetico ×3 con el hash del artefacto IDÉNTICO al de antes de seccomp
(b3:adc5c251…). Añadir media jaula no movió un byte — como debe ser: esto recorta
superficie de escape, no cambia el build.

El --seccomp <fd> de bwrap quedó SIN USAR: harkaq-exec ya corre dentro y con
no_new_privs puesto, así que instala el filtro él mismo — una pieza menos de
plumbing y el filtro queda al lado de la política que lo justifica.

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

caa23f9ab713a18d539316a80e1ec326b08a6f54 · by sergio · (3 months ago) · parent 8f7109f4e · browse the tree at this commit

Semantic delta · 13

scripts/harkaq/harkaq-exec.c

2 files · +123 −0

diff --git a/docs/16-harkaq-jaula.md b/docs/16-harkaq-jaula.md
index 9b1381a2..d47e3689 100644
--- a/docs/16-harkaq-jaula.md
+++ b/docs/16-harkaq-jaula.md
@@ -560,6 +560,33 @@ usarla.
 
 ### D4 — Landlock no basta; seccomp es obligatorio, no opcional
 
+> **Corregida al implementarla (2026-07-15).** El texto de abajo pedía **allowlist** por syscall.
+> Lo implementado es una **denylist**, y el cambio es deliberado: un allowlist para builds
+> *arbitrarios* —compiladores, make, shells, linkers, perl— es un blanco móvil que cada
+> herramienta nueva rompe. Es el riesgo del §7 ("los falsos positivos matan proyectos de
+> sandboxing") aplicado a las syscalls, y con peor final: un build que muere por una syscall
+> legítima no da un diagnóstico útil, da un misterio. Lo *peligroso*, en cambio, es enumerable y
+> estable: no hay build honesto que cargue módulos, haga kexec o attachee un ptrace.
+>
+> Denegadas (en `harkaq-exec.c`): `io_uring_*`, `ptrace`, `process_vm_{readv,writev}`, `bpf`,
+> `userfaultfd`, `keyctl`/`add_key`/`request_key`, `{init,finit,delete}_module`, `kexec_*`,
+> `perf_event_open`, `mount`/`umount2`, la familia `open_tree`/`move_mount`/`fs*`, `setns`.
+> **`pivot_root` NO se deniega**: bwrap lo usa *antes* de llegar a harkaq-exec.
+>
+> **EPERM, no KILL:** matar el proceso deja un cadáver sin explicación; EPERM deja al build
+> fallar donde corresponde y al log decir por qué. Se instala **después** de `no_new_privs` y de
+> Landlock, justo antes del `exec`, y se hereda por fork/exec igual que el dominio (D5). Si el
+> kernel lo rechaza, **no se corre el build** (D7: fallo cerrado — media jaula creyéndose entera
+> es peor que ninguna).
+>
+> **Comprobado:** `ptrace` → `EPERM` bajo la jaula; y un build real de zlib sale `Hermetico` ×3
+> con **el hash del artefacto idéntico** al de antes de seccomp (`b3:adc5c251…`). Añadir media
+> jaula no movió un byte — como debe ser: esto recorta superficie de escape, no cambia el build.
+>
+> Nota: el `--seccomp <fd>` de bwrap que se menciona abajo terminó **sin usarse**. `harkaq-exec`
+> ya corre dentro y con `no_new_privs` puesto, así que instala el filtro él mismo — una pieza
+> menos de plumbing y el filtro queda al lado de la política que lo justifica.
+
 Landlock cubre fs, puertos TCP e IPC scoping. No cubre `ptrace`, `bpf`, `userfaultfd`, `keyctl`,
 `io_uring`, montajes, ni la mitad de la superficie interesante. seccomp-bpf con allowlist por
 syscall es la otra mitad de la jaula.
diff --git a/scripts/harkaq/harkaq-exec.c b/scripts/harkaq/harkaq-exec.c
index 5e7a9e0f..686c123d 100644
--- a/scripts/harkaq/harkaq-exec.c
+++ b/scripts/harkaq/harkaq-exec.c
@@ -31,7 +31,11 @@
 
 #define _GNU_SOURCE
 #include "harkaq-uapi.h"
+#include <errno.h>
 #include <fcntl.h>
+#include <linux/filter.h>
+#include <linux/seccomp.h>
+#include <stddef.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -79,6 +83,88 @@ static int ll_restrict(int fd, __u32 f) {
      LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM |                \
      LANDLOCK_ACCESS_FS_REFER)
 
+// ------------------------------------------------------------------ seccomp (D4)
+//
+// D4: "Landlock no basta". Landlock cubre fs, puertos TCP e IPC scoping — no cubre ptrace, bpf,
+// userfaultfd, keyctl, io_uring, montajes, ni la mitad de la superficie interesante.
+//
+// DENYLIST, no allowlist (corrige a D4, que pedía allowlist). Un allowlist por syscall para
+// builds ARBITRARIOS —compiladores, make, shells, linkers, perl— es un blanco móvil: cada
+// herramienta nueva lo rompe. Es el riesgo del §7 ("los falsos positivos matan proyectos de
+// sandboxing") aplicado a las syscalls, y con peor final: un build que muere por una syscall
+// legítima no da un diagnóstico útil, da un misterio. Lo *peligroso*, en cambio, es enumerable
+// y estable: no hay build honesto que cargue módulos, haga kexec o attachee un ptrace.
+//
+// EPERM y no KILL: matar el proceso deja un cadáver sin explicación; EPERM deja al build fallar
+// donde corresponde y al log decir por qué. Y lo que importa —la evidencia— la sigue dando
+// Landlock; esto sólo recorta superficie de escape.
+static const int SYSCALLS_DENEGADAS[] = {
+    // io_uring: superficie de kernel enorme y bypassea buena parte del análisis por-syscall.
+    // Denegado explícitamente en v1 (D4). Si un builder lo necesita, que lo justifique.
+#ifdef __NR_io_uring_setup
+    __NR_io_uring_setup, __NR_io_uring_enter, __NR_io_uring_register,
+#endif
+    // Inspección/inyección entre procesos: un build no necesita mirar dentro de otro.
+    __NR_ptrace, __NR_process_vm_readv, __NR_process_vm_writev,
+#ifdef __NR_bpf
+    __NR_bpf,
+#endif
+#ifdef __NR_userfaultfd
+    __NR_userfaultfd,  // usado para ganar carreras TOCTOU en el kernel
+#endif
+    // Llavero del kernel: ni se lee ni se siembra desde un build.
+    __NR_keyctl, __NR_add_key, __NR_request_key,
+    // Módulos y kexec: un build que toca esto no es un build.
+    __NR_init_module, __NR_finit_module, __NR_delete_module, __NR_kexec_load,
+#ifdef __NR_kexec_file_load
+    __NR_kexec_file_load,
+#endif
+    // perf: superficie histórica de escalada.
+    __NR_perf_event_open,
+    // Montajes: bwrap ya armó el árbol; re-montar sólo sirve para escapar de él. `pivot_root`
+    // NO se deniega: bwrap lo usa ANTES de llegar acá (esto corre después del pivot).
+    __NR_mount, __NR_umount2,
+#ifdef __NR_open_tree
+    __NR_open_tree, __NR_move_mount, __NR_fsopen, __NR_fsconfig, __NR_fsmount,
+#endif
+    // Salirse de los namespaces que bwrap puso.
+    __NR_setns,
+};
+
+// Arma e instala el filtro. Devuelve 0 si quedó puesto, -1 si el kernel no lo aceptó.
+static int poner_seccomp(void) {
+    const int n = (int)(sizeof(SYSCALLS_DENEGADAS) / sizeof(SYSCALLS_DENEGADAS[0]));
+    // 2 (carga arch + salto) + 1 (carga nr) + n (comparaciones) + 3 (los tres RET).
+    struct sock_filter *f = calloc(n + 6, sizeof(*f));
+    if (!f) return -1;
+    int k = 0;
+    const int I_ALLOW = 3 + n, I_ERRNO = 4 + n, I_KILL = 5 + n;
+
+    // Comprobar la arquitectura ANTES de mirar el número: los números de syscall son POR ARCH, y
+    // sin este check un binario i386 podría pedir otra syscall con el mismo número y colarse. Es
+    // el error clásico de los filtros seccomp escritos a mano.
+    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 + 1);
+    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)SYSCALLS_DENEGADAS[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);
+    (void)I_ALLOW;
+
+    struct sock_fprog prog = {.len = (unsigned short)k, .filter = f};
+    // Requiere no_new_privs, que ya está puesto antes de llamar acá.
+    int r = syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
+    free(f);
+    return r;
+}
+
 int main(int argc, char **argv) {
     const char *policy = NULL;
     int abi_min = 3, i;
@@ -170,6 +256,16 @@ int main(int argc, char **argv) {
     if (ll_restrict(rs, flags) < 0) { perror("[harkaq] restrict_self"); return 4; }
     close(rs);
 
+    // seccomp DESPUÉS de landlock y de no_new_privs, y justo antes del exec: el filtro se hereda
+    // por fork/exec igual que el dominio de Landlock (D5), así que cubre todo el árbol del build.
+    // Si el kernel lo rechaza NO se sigue: D7 es fallo cerrado — correr un build creyéndolo
+    // enjaulado cuando le falta media jaula es peor que no correrlo.
+    if (poner_seccomp() < 0) {
+        fprintf(stderr, "[harkaq] RECHAZO: no pude instalar el filtro seccomp (%m).\n"
+                        "         ¿CONFIG_SECCOMP_FILTER=y? D4: seccomp no es opcional.\n");
+        return 4;
+    }
+
     if (abi < 7)
         fprintf(stderr,
                 "[harkaq] AVISO: ABI %d < 7 ⇒ el kernel no audita denegaciones. El build corre "