Files
takana/scripts/harkaq/harkaq-exec.c
T
sergioandClaude Opus 4.8 24c6ab455e harkaq: §4 corregido (política POR FICHERO) + Q2 con método y primeros datos reales
CORRECCIÓN ESTRUCTURAL del §4 (otra herencia del marco nix). El Draft 2 decía
`fs_ro: [store paths de la clausura]`. Falso: esos paths NO existen dentro del
sandbox. `Sandbox::bwrap_args` apila las deps con --overlay-src y las FUNDE en
el mismo /usr que el rootfs Alpine, a propósito, para que el compilador las
encuentre sin plumbing de flags. Medido:

  /usr/include/zlib.h   (dep DECLARADA)        dev=63 ino=13641973
  /usr/include/stdio.h  (Alpine, NO declarado) dev=63 ino=4196788
  /usr/include          (el directorio)        dev=61 ino=15  ← overlayfs, uno

Una regla sobre /usr concede las dos ⇒ la evidencia no valdría nada. Ni `dev`
distingue. ⇒ La clausura se enumera FICHERO A FICHERO (computable: en el host
sabemos qué aporta cada dep). Dos consecuencias: `list` ≠ `ro` (pkgconf NECESITA
escanear /usr/lib/pkgconfig, pero listar no es leer; Landlock separa READ_DIR de
READ_FILE) y máscara por tipo (derechos de-sólo-dir sobre un fichero ⇒ EINVAL y
la regla entera se cae).

Piezas nuevas: harkaq-policy.sh (deriva la clausura, D1) + q2-runtime-base.sh
(responde Q2 midiendo, no adivinando) + harkaq-exec con `list`/máscara por tipo.

Q2, primeros datos con un build REAL en el sandbox REAL contra la dep zlib:
  estado: Impuro | canario: True | contador kernel: 3
      fs.read_file /usr/bin/env
      fs.read_file /usr/lib/libz.so.1.3.2   ← la zlib de ALPINE!

El build se linkaba contra la zlib de Alpine en vez de la dep declarada (que
aporta libz.a). Sin harkaq eso pasa en verde y el artefacto queda dependiendo de
Alpine. ES EXACTAMENTE EL BUG DEL §0, cazado en un build real.
Piso duro del runtime base: /bin/sh → /bin/busybox → /lib/ld-musl (sin eso ni el
canario corre ⇒ el mínimo es "lo que hace falta para que el canario pueda correr").

Dos rastrillos: el canario no puede depender de un binario (`cat` moría en
/bin/cat ⇒ ahora `read _ < $CANARY`, sólo builtins) ni vivir en /tmp (la política
concede `rw /tmp` ⇒ el canario caía DENTRO de la clausura y no denegaba nada).
El segundo lo cazó D9 MISMO: el lector se negó a certificar en vez de decir
Hermetico. El canario funcionando como se diseñó, sobre un bug de quien lo diseñó.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:20:39 -04:00

188 lines
8.9 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// harkaq-exec — aplica la política y ejecuta el builder (SDD 16 §4). Corre DENTRO de bwrap,
// como último eslabón antes del `sh -c` del build: bwrap ya puso los namespaces, el overlay y
// la ausencia de red; esto pone el grano fino y —lo que importa— abre el canal de evidencia.
//
// harkaq-exec --policy <fichero> [--abi-min 3] -- <cmd> [args...]
//
// Fichero de política (lo genera harkaq-policy a partir de la clausura; D1: la política se
// DERIVA, no se escribe):
// ro /usr/include/zlib.h un fichero de la clausura: leer/ejecutar SÓLO él
// ro /opt/zig un directorio: todo el subárbol
// list /usr/include listar el directorio, SIN poder leer su contenido
// rw /out
//
// El grano POR FICHERO no es capricho: el sandbox de hammer funde las deps y el rootfs Alpine
// en el mismo `/usr` (`--overlay-src` × N + `--tmp-overlay /`), así que /usr/include/zlib.h
// (dep declarada) y /usr/include/stdio.h (Alpine, no declarado) son el MISMO directorio. Una
// regla sobre /usr concede los dos y la evidencia no vale nada. En el host sí sabemos qué
// ficheros aporta cada dep ⇒ la clausura se enumera fichero a fichero.
//
// `list` existe por la misma razón: pkgconf/configure NECESITAN listar /usr/lib/pkgconfig, pero
// listar no es leer. Landlock separa READ_DIR de READ_FILE, así que se concede el listado del
// directorio y se deniega la lectura de los .pc que la receta no declaró.
//
// Estático a propósito: corre dentro del rootfs Alpine (musl) del sandbox.
//
// D7 (fallo cerrado, honesto sobre el ABI):
// ABI < min → RECHAZA el build (sin REFER/TRUNCATE no compila software real)
// min <= ABI < 7 → corre, pero avisa SIN EVIDENCIA (no hay audit de denegaciones)
// ABI >= 7 → corre con LOG_NEW_EXEC_ON: la evidencia existe
// Nunca se afirma evidencia que el kernel no respaldó.
#define _GNU_SOURCE
#include <fcntl.h>
#include <linux/landlock.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/syscall.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);
}
// Conjuntos de derechos por ABI. Cada uno AÑADE a los anteriores; recortamos al ABI real del
// kernel (D7 / §3.1: se consulta el ABI por syscall, JAMÁS por versión de kernel — hay
// backports y el número miente).
#define FS_ABI1 \
(LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_WRITE_FILE | \
LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | \
LANDLOCK_ACCESS_FS_REMOVE_DIR | LANDLOCK_ACCESS_FS_REMOVE_FILE | \
LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_DIR | \
LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK | \
LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_BLOCK | \
LANDLOCK_ACCESS_FS_MAKE_SYM)
#define FS_ABI2 (FS_ABI1 | LANDLOCK_ACCESS_FS_REFER)
#define FS_ABI3 (FS_ABI2 | LANDLOCK_ACCESS_FS_TRUNCATE)
#define FS_ABI5 (FS_ABI3 | LANDLOCK_ACCESS_FS_IOCTL_DEV)
#define RO_RIGHTS \
(LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE)
// Derechos que Landlock SÓLO admite sobre directorios. Añadir una regla con cualquiera de
// éstos sobre un fichero regular hace que el kernel rechace la regla entera con EINVAL — y
// como la clausura es mayormente ficheros sueltos (§4), sin esta máscara no arranca nada.
#define DIR_ONLY_RIGHTS \
(LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_REMOVE_DIR | \
LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_CHAR | \
LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG | \
LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO | \
LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM | \
LANDLOCK_ACCESS_FS_REFER)
int main(int argc, char **argv) {
const char *policy = NULL;
int abi_min = 3, i;
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--policy") && i + 1 < argc) policy = argv[++i];
else if (!strcmp(argv[i], "--abi-min") && i + 1 < argc) abi_min = atoi(argv[++i]);
else if (!strcmp(argv[i], "--")) { i++; break; }
}
if (!policy || i >= argc) {
fprintf(stderr, "uso: harkaq-exec --policy <f> [--abi-min N] -- <cmd> [args...]\n");
return 2;
}
char **cmd = &argv[i];
int abi = ll_create(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
if (abi < abi_min) {
// Fallo CERRADO: no correr un build que creemos enjaulado y no lo está.
fprintf(stderr,
"[harkaq] RECHAZO: ABI de Landlock = %d, se exige >= %d.\n"
" Sin REFER/TRUNCATE no compila software real y la jaula sería ficción.\n",
abi, abi_min);
return 4;
}
__u64 handled = abi >= 5 ? FS_ABI5 : abi >= 3 ? FS_ABI3 : abi >= 2 ? FS_ABI2 : FS_ABI1;
struct landlock_ruleset_attr attr = {.handled_access_fs = handled};
int rs = ll_create(&attr, sizeof(attr), 0);
if (rs < 0) { perror("[harkaq] create_ruleset"); return 4; }
FILE *f = fopen(policy, "r");
if (!f) { perror("[harkaq] abrir política"); return 4; }
char line[4200];
int n_file = 0, n_dir = 0;
while (fgets(line, sizeof(line), f)) {
char *nl = strchr(line, '\n');
if (nl) *nl = 0;
if (!line[0] || line[0] == '#') continue;
__u64 grant;
char *p;
if (!strncmp(line, "ro ", 3)) { grant = RO_RIGHTS; p = line + 3; }
else if (!strncmp(line, "rw ", 3)) { grant = handled; p = line + 3; }
else if (!strncmp(line, "list ", 5)) { grant = LANDLOCK_ACCESS_FS_READ_DIR; p = line + 5; }
else {
fprintf(stderr, "[harkaq] política inválida: %s\n", line);
return 4;
}
int dfd = open(p, O_PATH | O_CLOEXEC);
if (dfd < 0) {
// Una entrada de la clausura que no existe es un bug de harkaq-policy, no del
// build. Fallar ruidoso: si la ignoráramos, la política sería MÁS chica que la
// clausura y el build denegaría cosas legítimas — falsos positivos que matan
// el proyecto (§7).
fprintf(stderr, "[harkaq] la política nombra un path inexistente: %s (%m)\n", p);
return 4;
}
// Recortar los derechos de-sólo-directorio cuando el destino NO lo es: el kernel
// rechazaría la regla entera con EINVAL. Es el caso MAYORITARIO — la clausura es una
// lista de ficheros.
struct stat st;
int is_dir = (fstat(dfd, &st) == 0) && S_ISDIR(st.st_mode);
if (!is_dir) grant &= ~(__u64)DIR_ONLY_RIGHTS;
grant &= handled; // nunca conceder más de lo que el ruleset declara manejar
if (!grant) {
fprintf(stderr, "[harkaq] regla vacía tras recortar por tipo: %s\n", p);
close(dfd);
return 4;
}
struct landlock_path_beneath_attr pb = {.allowed_access = grant, .parent_fd = dfd};
if (ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0) < 0) {
fprintf(stderr, "[harkaq] add_rule(%s): %m\n", p);
close(dfd);
return 4;
}
close(dfd);
is_dir ? n_dir++ : n_file++;
}
fclose(f);
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("[harkaq] no_new_privs"); return 4; }
// LOG_NEW_EXEC_ON: el flag del que cuelga TODO. harkaq restringe y DESPUÉS ejecuta el
// builder; sin este flag el kernel no loguea ninguna denegación posterior al execve y
// harkaq certificaría como herméticos todos los builds, para siempre, sin un solo error
// visible (medido: §3.4). No es configuración, es la diferencia entre el sistema y un
// sello de goma.
__u32 flags = 0;
if (abi >= 7) flags |= LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
if (abi >= 8) flags |= LANDLOCK_RESTRICT_SELF_TSYNC; // D5: cubre todos los hilos
if (ll_restrict(rs, flags) < 0) { perror("[harkaq] restrict_self"); return 4; }
close(rs);
if (abi < 7)
fprintf(stderr,
"[harkaq] AVISO: ABI %d < 7 ⇒ el kernel no audita denegaciones. El build corre "
"ENJAULADO pero SIN EVIDENCIA; el veredicto no podrá afirmar hermeticidad.\n",
abi);
else
fprintf(stderr,
"[harkaq] jaula puesta: ABI %d, clausura = %d ficheros + %d dirs, "
"logging post-exec ON\n",
abi, n_file, n_dir);
execvp(cmd[0], cmd);
fprintf(stderr, "[harkaq] execvp %s: %m\n", cmd[0]);
return 127;
}