// 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 [--abi-min 3] -- [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/-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 #include #include #include #include #include #include #include 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 [--abi-min N] -- [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; }