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>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user