diff --git a/scripts/harkaq/harkaq-audit.c b/scripts/harkaq/harkaq-audit.c index e6ec0e2d..a45b1c17 100644 --- a/scripts/harkaq/harkaq-audit.c +++ b/scripts/harkaq/harkaq-audit.c @@ -192,6 +192,13 @@ int main(int argc, char **argv) { struct denial denials[MAX_DENIALS]; int nd = 0, canary_seen = 0, kernel_count = -1; + int drenando = 0; // el dominio ya murió: drenar lo que quede antes de emitir + // ACCESS que llegan ANTES de ver el canario: no se pueden filtrar todavía (el `domain=` del + // build se conoce al ver el canario), pero pueden ser SUYOS — el `sh` deniega cosas antes de + // llegar al probe. Descartarlos perdía registros y el contador del kernel lo cazaba (4 vs 1). + // Se bufferizan con su domain y se filtran retroactivamente. + static struct { char dom[64]; struct denial d; } previos[MAX_DENIALS]; + int n_prev = 0; char domain[64] = ""; struct timeval tv = {.tv_sec = 1}; setsockopt(nl, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); @@ -203,6 +210,9 @@ int main(int argc, char **argv) { while (time(NULL) < deadline && !parar) { ssize_t n = recv(nl, buf, sizeof(buf), 0); + // Drenando: el dominio murió y sólo faltan los ACCESS rezagados. Si el socket se agota + // (timeout de 1s) o ya cuadramos con el contador del kernel, cerramos. + if (drenando && (n <= 0 || (kernel_count >= 0 && nd + 1 >= kernel_count))) goto done; if (n <= 0) continue; for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n); h = NLMSG_NEXT(h, n)) { if (h->nlmsg_type != AUDIT_LANDLOCK_ACCESS && h->nlmsg_type != AUDIT_LANDLOCK_DOMAIN) @@ -225,9 +235,24 @@ int main(int argc, char **argv) { if (!canary_seen && !strcmp(path, canary)) { canary_seen = 1; snprintf(domain, sizeof(domain), "%s", dom); + // Rescatar del buffer los ACCESS previos que resultaron ser de MI dominio. + for (int k = 0; k < n_prev && nd < MAX_DENIALS; k++) + if (!strcmp(previos[k].dom, domain)) denials[nd++] = previos[k].d; continue; // el canario no es una denegación del build: es el sello del lector } - if (!canary_seen || strcmp(dom, domain) != 0) continue; // de otro build + if (!canary_seen) { + // Aún no sé mi domain: guardar por si resulta ser mío. + if (n_prev < MAX_DENIALS) { + snprintf(previos[n_prev].dom, sizeof(previos[n_prev].dom), "%s", dom); + field(rec, "blockers", previos[n_prev].d.blockers, sizeof(previos[n_prev].d.blockers)); + snprintf(previos[n_prev].d.path, sizeof(previos[n_prev].d.path), "%s", path); + field(rec, "dev", previos[n_prev].d.dev, sizeof(previos[n_prev].d.dev)); + field(rec, "ino", previos[n_prev].d.ino, sizeof(previos[n_prev].d.ino)); + n_prev++; + } + continue; + } + if (strcmp(dom, domain) != 0) continue; // de otro build if (nd < MAX_DENIALS) { field(rec, "blockers", denials[nd].blockers, sizeof(denials[nd].blockers)); snprintf(denials[nd].path, sizeof(denials[nd].path), "%s", path); @@ -241,7 +266,13 @@ int main(int argc, char **argv) { if (!strcmp(status, "deallocated")) { char cnt[32] = ""; if (field(rec, "denials", cnt, sizeof(cnt))) kernel_count = atoi(cnt); - goto done; // el dominio murió: el build terminó + // NO salir de una: el `deallocated` puede adelantarse a registros ACCESS que + // siguen en el buffer del socket. Con un inquilino instantáneo (`htop + // --version`) el kernel contaba 4 denegaciones y el lector recibía 1 ⇒ el + // chequeo anti-pérdida disparaba SinEvidencia. Se drena hasta cuadrar con el + // contador (o agotar el socket), y recién ahí se cierra. + drenando = 1; + continue; } } } diff --git a/scripts/harkaq/runtime-policy.sh b/scripts/harkaq/runtime-policy.sh new file mode 100755 index 00000000..7019ac7f --- /dev/null +++ b/scripts/harkaq/runtime-policy.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# runtime-policy.sh [args…] — política de RUNTIME derivada por medición (SDD 17 §3). +# +# La clausura que harkaq mide en BUILD **no** es la política de runtime: un build lee `zlib.h`, +# `make`, el compilador; el binario resultante no lee nada de eso. Son dos clausuras de dos +# sujetos distintos. Lo que sí sirve es la TÉCNICA: correr el binario bajo la jaula con política +# mínima y dejar que las denegaciones digan qué necesita. Eso ES su política de runtime, derivada +# por evidencia y no escrita a mano. +# +# scripts/harkaq/runtime-policy.sh store/-htop/usr/bin/htop --version +# +# BASELINE: el `sh` que lanza al binario también deniega cosas (locale, ld.so.cache). Se mide +# primero un `true` para conocer ese ruido y RESTARLO — si no, la política del binario se lleva +# la basura del lanzador. +# +# Cómo encaja con la concesión firmada de arje/wawa (PLAN-ATESTACION): +# - `ConcesionCapacidad` firma `(blake3(binario), u32)` en 36 bytes canónicos, zero-alloc, Ring 0 +# ⇒ es la FRONTERA gruesa. NO se toca: meterle paths rompería todas las firmas y contradice +# "capacidad = frontera física, no tabla". +# - Esta política es el DETALLE granular (fs, paths), vive en userspace y se aplica con Landlock. +# Los dos coexisten: el kernel verifica la frontera; Landlock aplica el detalle. +set -eu + +BIN="${1:?uso: runtime-policy.sh [args…]}" +shift || true +HUB="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$HUB" +BIN="$(cd "$(dirname "$BIN")" && pwd)/$(basename "$BIN")" # absoluto +POL="$(mktemp)"; BASE="$(mktemp)"; MED="$(mktemp)" +trap 'rm -f "$POL" "$BASE" "$MED"' EXIT + +# Política mínima DERIVADA (no a mano: escribirla a mano se olvida de libc.musl y el sh ni +# arranca). El sandbox de harkaq-run monta el HOST ⇒ el cierre es el del sh DEL HOST. +scripts/harkaq/harkaq-base-closure.py / /bin/sh 2>/dev/null > "$POL" +echo "ro $BIN" >> "$POL" + +denegados() { # corre $1 bajo la jaula y lista los paths denegados + HARKAQ_TIMEOUT=10 scripts/harkaq/harkaq-run.sh "$POL" "$1" 2>/dev/null \ + | grep '^{' | head -1 \ + | python3 -c 'import json,sys +try: + v=json.load(sys.stdin) + if v["estado"]=="SinEvidencia": print("SIN-EVIDENCIA:"+v.get("motivo","")[:60]); raise SystemExit + for d in v["denials"]: print(d["path"]) +except Exception: pass' +} + +echo "══ baseline: el ruido del lanzador (sh)" +denegados "true" | sort -u > "$BASE" +grep -q 'SIN-EVIDENCIA' "$BASE" && { echo " $(cat "$BASE")"; echo " ⇒ sin evidencia fiable: no se afirma ninguna política"; exit 3; } +sed 's/^/ − /' "$BASE" + +echo "══ medición: $(basename "$BIN") $*" +denegados "$BIN $*" | sort -u > "$MED" +grep -q 'SIN-EVIDENCIA' "$MED" && { echo " $(cat "$MED")"; exit 3; } + +echo +echo "══ POLÍTICA DE RUNTIME de $(basename "$BIN") (medida − baseline):" +propios=$(comm -23 "$MED" "$BASE") +if [ -z "$propios" ]; then + echo " (ninguna) — el binario no tocó NADA fuera de sí mismo." + echo " ⇒ política Landlock: sólo \`ro $BIN\`. Es el caso de un estático que no abre ficheros." +else + echo "$propios" | sed 's/^/ ro /' + echo + echo " ⇒ pegar en la política Landlock del binario instalado." +fi