Files
takana/scripts/harkaq/harkaq-exec.c
T
sergioandClaude Opus 4.8 4eecba06f4 harkaq: Fase 1 — la cadena corre de punta a punta (Hermetico / Impuro sobre kernel real)
Tres piezas nuevas, las del §4 del SDD:
  - harkaq-audit.c  lector de evidencia; HOST junto a hammerd, CAP_AUDIT_READ;
                    espera el canario (que revela el domain=), junta las
                    denegaciones de ESE dominio, cruza contra el contador del
                    kernel al liberarse, y emite el Verdict JSON de 3 estados.
  - harkaq-exec.c   aplica la política y execea el builder; DENTRO de bwrap,
                    estático (rootfs musl). Negocia el ABI por syscall (D7:
                    <min rechaza, <7 avisa SIN EVIDENCIA) y pone
                    LOG_NEW_EXEC_ON + TSYNC.
  - harkaq-run.sh   encadena lector+bwrap+exec, pone el canario con nonce y
                    espera a que el lector confirme que escucha (--ready-fd)
                    antes de arrancar el build: sin esa barrera el canario se
                    emitiría sin nadie escuchando y daría SinEvidencia por una
                    carrera, no por un problema real.

El hito, con comandos sintéticos sobre el kernel real (ABI 10):
  limpio  {"estado":"Hermetico","canario_visto":true,"contador_kernel":1,"denials":[]}
  impuro  {"estado":"Impuro","canario_visto":true,"contador_kernel":2,
           "denials":[{"blockers":"fs.read_file","path":"…/README.md",…}]}
Canario visto y contador coherente en ambos ⇒ los 3 estados de D9 funcionan.

Q1c contestada en la práctica: el privilegio vive en un helper chico con
`setcap cap_audit_read,cap_audit_control`, NO en hammerd entero.

Sutileza medida: lo que DAC ya bloquea es INVISIBLE para harkaq (el hook del
LSM no llega a correr). Descubierto porque el escenario "impuro" con
/root/.bashrc (drwx------) dio Hermetico: DAC lo bloqueó antes que Landlock.
No rompe D2 (si DAC lo bloqueó, el build tampoco lo usó ⇒ Hermetico es cierto)
pero acota el diagnóstico, y al escribir tests hay que elegir paths que DAC
permita o el control no controla nada.

Falta para cerrar Fase 1: harkaq-policy derivando de la clausura real + el
contraste receta-de-Alpinizada vs. receta-que-no. El rebuild del kernel
(SECURITY_LANDLOCK) NO bloquea el hito: sólo hace falta para que harkaq corra
DENTRO de hammer; el laptop ya está en ABI 10.

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

160 lines
6.9 KiB
C

// 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 /store/<hash>-zlib
// rw /out
//
// 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/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)
struct rule {
char path[4096];
int rw;
};
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_ro = 0, n_rw = 0;
while (fgets(line, sizeof(line), f)) {
char *nl = strchr(line, '\n');
if (nl) *nl = 0;
if (!line[0] || line[0] == '#') continue;
int rw;
char *p;
if (!strncmp(line, "ro ", 3)) { rw = 0; p = line + 3; }
else if (!strncmp(line, "rw ", 3)) { rw = 1; p = line + 3; }
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;
}
struct landlock_path_beneath_attr pb = {.allowed_access = rw ? handled : RO_RIGHTS,
.parent_fd = dfd};
// REFER sólo se concede sobre directorios; sobre un fichero suelto el kernel rechaza
// la regla entera con EINVAL. Reintentamos sin él antes de darlo por perdido.
if (ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0) < 0) {
pb.allowed_access &= ~(__u64)LANDLOCK_ACCESS_FS_REFER;
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);
rw ? n_rw++ : n_ro++;
}
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, %d ro + %d rw, logging post-exec ON\n",
abi, n_ro, n_rw);
execvp(cmd[0], cmd);
fprintf(stderr, "[harkaq] execvp %s: %m\n", cmd[0]);
return 127;
}