gioser

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 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ 24c6ab455eb785338667e2b73bf30f76665648dd scripts/harkaq/harkaq-exec.c C · 187 líneas · 8.9 KB Historia Quién tocó qué crudo

harkaq: §4 corregido (política POR FICHERO) + Q2 con método y primeros datos reales 24c6ab455 · sergio ·

1// harkaq-exec — aplica la política y ejecuta el builder (SDD 16 §4). Corre DENTRO de bwrap,
2// como último eslabón antes del `sh -c` del build: bwrap ya puso los namespaces, el overlay y
3// la ausencia de red; esto pone el grano fino y —lo que importa— abre el canal de evidencia.
4//
5//   harkaq-exec --policy <fichero> [--abi-min 3] -- <cmd> [args...]
6//
7// Fichero de política (lo genera harkaq-policy a partir de la clausura; D1: la política se
8// DERIVA, no se escribe):
9//   ro   /usr/include/zlib.h     un fichero de la clausura: leer/ejecutar SÓLO él
10//   ro   /opt/zig                un directorio: todo el subárbol
11//   list /usr/include            listar el directorio, SIN poder leer su contenido
12//   rw   /out
13//
14// El grano POR FICHERO no es capricho: el sandbox de hammer funde las deps y el rootfs Alpine
15// en el mismo `/usr` (`--overlay-src` × N + `--tmp-overlay /`), así que /usr/include/zlib.h
16// (dep declarada) y /usr/include/stdio.h (Alpine, no declarado) son el MISMO directorio. Una
17// regla sobre /usr concede los dos y la evidencia no vale nada. En el host sí sabemos qué
18// ficheros aporta cada dep ⇒ la clausura se enumera fichero a fichero.
19//
20// `list` existe por la misma razón: pkgconf/configure NECESITAN listar /usr/lib/pkgconfig, pero
21// listar no es leer. Landlock separa READ_DIR de READ_FILE, así que se concede el listado del
22// directorio y se deniega la lectura de los .pc que la receta no declaró.
23//
24// Estático a propósito: corre dentro del rootfs Alpine (musl) del sandbox.
25//
26// D7 (fallo cerrado, honesto sobre el ABI):
27//   ABI < min      → RECHAZA el build (sin REFER/TRUNCATE no compila software real)
28//   min <= ABI < 7 → corre, pero avisa SIN EVIDENCIA (no hay audit de denegaciones)
29//   ABI >= 7       → corre con LOG_NEW_EXEC_ON: la evidencia existe
30// Nunca se afirma evidencia que el kernel no respaldó.
31
32#define _GNU_SOURCE
33#include <fcntl.h>
34#include <linux/landlock.h>
35#include <stdio.h>
36#include <stdlib.h>
37#include <string.h>
38#include <sys/prctl.h>
39#include <sys/stat.h>
40#include <sys/syscall.h>
41#include <unistd.h>
42
43static int ll_create(const struct landlock_ruleset_attr *a, size_t n, __u32 f) {
44    return syscall(SYS_landlock_create_ruleset, a, n, f);
45}
46static int ll_add(int fd, enum landlock_rule_type t, const void *a, __u32 f) {
47    return syscall(SYS_landlock_add_rule, fd, t, a, f);
48}
49static int ll_restrict(int fd, __u32 f) {
50    return syscall(SYS_landlock_restrict_self, fd, f);
51}
52
53// Conjuntos de derechos por ABI. Cada uno AÑADE a los anteriores; recortamos al ABI real del
54// kernel (D7 / §3.1: se consulta el ABI por syscall, JAMÁS por versión de kernel — hay
55// backports y el número miente).
56#define FS_ABI1                                                              \
57    (LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_WRITE_FILE |            \
58     LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR |            \
59     LANDLOCK_ACCESS_FS_REMOVE_DIR | LANDLOCK_ACCESS_FS_REMOVE_FILE |        \
60     LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_DIR |            \
61     LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK |            \
62     LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_BLOCK |          \
63     LANDLOCK_ACCESS_FS_MAKE_SYM)
64#define FS_ABI2 (FS_ABI1 | LANDLOCK_ACCESS_FS_REFER)
65#define FS_ABI3 (FS_ABI2 | LANDLOCK_ACCESS_FS_TRUNCATE)
66#define FS_ABI5 (FS_ABI3 | LANDLOCK_ACCESS_FS_IOCTL_DEV)
67
68#define RO_RIGHTS \
69    (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE)
70
71// Derechos que Landlock SÓLO admite sobre directorios. Añadir una regla con cualquiera de
72// éstos sobre un fichero regular hace que el kernel rechace la regla entera con EINVAL — y
73// como la clausura es mayormente ficheros sueltos (§4), sin esta máscara no arranca nada.
74#define DIR_ONLY_RIGHTS                                                           \
75    (LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_REMOVE_DIR |                \
76     LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_CHAR |              \
77     LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG |                  \
78     LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO |                \
79     LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM |                \
80     LANDLOCK_ACCESS_FS_REFER)
81
82int main(int argc, char **argv) {
83    const char *policy = NULL;
84    int abi_min = 3, i;
85    for (i = 1; i < argc; i++) {
86        if (!strcmp(argv[i], "--policy") && i + 1 < argc) policy = argv[++i];
87        else if (!strcmp(argv[i], "--abi-min") && i + 1 < argc) abi_min = atoi(argv[++i]);
88        else if (!strcmp(argv[i], "--")) { i++; break; }
89    }
90    if (!policy || i >= argc) {
91        fprintf(stderr, "uso: harkaq-exec --policy <f> [--abi-min N] -- <cmd> [args...]\n");
92        return 2;
93    }
94    char **cmd = &argv[i];
95
96    int abi = ll_create(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
97    if (abi < abi_min) {
98        // Fallo CERRADO: no correr un build que creemos enjaulado y no lo está.
99        fprintf(stderr,
100                "[harkaq] RECHAZO: ABI de Landlock = %d, se exige >= %d.\n"
101                "         Sin REFER/TRUNCATE no compila software real y la jaula sería ficción.\n",
102                abi, abi_min);
103        return 4;
104    }
105
106    __u64 handled = abi >= 5 ? FS_ABI5 : abi >= 3 ? FS_ABI3 : abi >= 2 ? FS_ABI2 : FS_ABI1;
107    struct landlock_ruleset_attr attr = {.handled_access_fs = handled};
108    int rs = ll_create(&attr, sizeof(attr), 0);
109    if (rs < 0) { perror("[harkaq] create_ruleset"); return 4; }
110
111    FILE *f = fopen(policy, "r");
112    if (!f) { perror("[harkaq] abrir política"); return 4; }
113    char line[4200];
114    int n_file = 0, n_dir = 0;
115    while (fgets(line, sizeof(line), f)) {
116        char *nl = strchr(line, '\n');
117        if (nl) *nl = 0;
118        if (!line[0] || line[0] == '#') continue;
119        __u64 grant;
120        char *p;
121        if (!strncmp(line, "ro ", 3)) { grant = RO_RIGHTS; p = line + 3; }
122        else if (!strncmp(line, "rw ", 3)) { grant = handled; p = line + 3; }
123        else if (!strncmp(line, "list ", 5)) { grant = LANDLOCK_ACCESS_FS_READ_DIR; p = line + 5; }
124        else {
125            fprintf(stderr, "[harkaq] política inválida: %s\n", line);
126            return 4;
127        }
128        int dfd = open(p, O_PATH | O_CLOEXEC);
129        if (dfd < 0) {
130            // Una entrada de la clausura que no existe es un bug de harkaq-policy, no del
131            // build. Fallar ruidoso: si la ignoráramos, la política sería MÁS chica que la
132            // clausura y el build denegaría cosas legítimas — falsos positivos que matan
133            // el proyecto (§7).
134            fprintf(stderr, "[harkaq] la política nombra un path inexistente: %s (%m)\n", p);
135            return 4;
136        }
137        // Recortar los derechos de-sólo-directorio cuando el destino NO lo es: el kernel
138        // rechazaría la regla entera con EINVAL. Es el caso MAYORITARIO — la clausura es una
139        // lista de ficheros.
140        struct stat st;
141        int is_dir = (fstat(dfd, &st) == 0) && S_ISDIR(st.st_mode);
142        if (!is_dir) grant &= ~(__u64)DIR_ONLY_RIGHTS;
143        grant &= handled;  // nunca conceder más de lo que el ruleset declara manejar
144        if (!grant) {
145            fprintf(stderr, "[harkaq] regla vacía tras recortar por tipo: %s\n", p);
146            close(dfd);
147            return 4;
148        }
149        struct landlock_path_beneath_attr pb = {.allowed_access = grant, .parent_fd = dfd};
150        if (ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0) < 0) {
151            fprintf(stderr, "[harkaq] add_rule(%s): %m\n", p);
152            close(dfd);
153            return 4;
154        }
155        close(dfd);
156        is_dir ? n_dir++ : n_file++;
157    }
158    fclose(f);
159
160    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("[harkaq] no_new_privs"); return 4; }
161
162    // LOG_NEW_EXEC_ON: el flag del que cuelga TODO. harkaq restringe y DESPUÉS ejecuta el
163    // builder; sin este flag el kernel no loguea ninguna denegación posterior al execve y
164    // harkaq certificaría como herméticos todos los builds, para siempre, sin un solo error
165    // visible (medido: §3.4). No es configuración, es la diferencia entre el sistema y un
166    // sello de goma.
167    __u32 flags = 0;
168    if (abi >= 7) flags |= LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
169    if (abi >= 8) flags |= LANDLOCK_RESTRICT_SELF_TSYNC;  // D5: cubre todos los hilos
170    if (ll_restrict(rs, flags) < 0) { perror("[harkaq] restrict_self"); return 4; }
171    close(rs);
172
173    if (abi < 7)
174        fprintf(stderr,
175                "[harkaq] AVISO: ABI %d < 7 ⇒ el kernel no audita denegaciones. El build corre "
176                "ENJAULADO pero SIN EVIDENCIA; el veredicto no podrá afirmar hermeticidad.\n",
177                abi);
178    else
179        fprintf(stderr,
180                "[harkaq] jaula puesta: ABI %d, clausura = %d ficheros + %d dirs, "
181                "logging post-exec ON\n",
182                abi, n_file, n_dir);
183
184    execvp(cmd[0], cmd);
185    fprintf(stderr, "[harkaq] execvp %s: %m\n", cmd[0]);
186    return 127;
187}