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>
74 lines
3.5 KiB
Bash
Executable File
74 lines
3.5 KiB
Bash
Executable File
#!/bin/sh
|
|
# harkaq-run — corre un comando enjaulado y emite su Verdict (SDD 16, Fase 1).
|
|
#
|
|
# harkaq-run.sh <fichero-política> <cmd...>
|
|
#
|
|
# Encadena las tres piezas: harkaq-audit escuchando en el HOST, bwrap poniendo los namespaces,
|
|
# harkaq-exec poniendo la política dentro. El canario (D9) lleva un nonce único por corrida: es
|
|
# lo que distingue "build limpio" de "lector ciego" —que producen los mismos bytes— y de paso
|
|
# es la clave primaria que atribuye los registros a ESTE build y no a otro de la granja (§3.5).
|
|
#
|
|
# Variables: HARKAQ_BIN (default ~/.cache/harkaq), NONCE (default: derivado del pid).
|
|
set -eu
|
|
|
|
BIN="${HARKAQ_BIN:-$HOME/.cache/harkaq}"
|
|
POLICY="$1"
|
|
shift
|
|
|
|
NONCE="${NONCE:-$$-$(date +%s 2>/dev/null || echo 0)}"
|
|
CANARY="/tmp/harkaq-canary-$NONCE"
|
|
|
|
# El canario es un fichero que EXISTE en el sandbox y que la política deja deliberadamente
|
|
# fuera de la clausura. Su fuente da igual (sólo importa que el open rebote): usamos algo
|
|
# mínimo y siempre presente.
|
|
CANARY_SRC="$(mktemp)"
|
|
echo "harkaq-canary $NONCE" > "$CANARY_SRC"
|
|
trap 'rm -f "$CANARY_SRC" "$FIFO" 2>/dev/null || true' EXIT
|
|
|
|
# Arrancar el lector ANTES del build y esperar a que confirme que escucha. Sin esta barrera,
|
|
# las primeras denegaciones —el canario entre ellas— se emitirían sin nadie escuchando y el
|
|
# veredicto sería SinEvidencia por una carrera, no por un problema real.
|
|
FIFO="$(mktemp -u)"
|
|
mkfifo "$FIFO"
|
|
VERDICT="$(mktemp)"
|
|
trap 'rm -f "$CANARY_SRC" "$FIFO" "$VERDICT" 2>/dev/null || true' EXIT
|
|
|
|
"$BIN/harkaq-audit" --canary "$CANARY" --timeout "${HARKAQ_TIMEOUT:-120}" --ready-fd 3 \
|
|
3>"$FIFO" >"$VERDICT" &
|
|
AUDIT_PID=$!
|
|
read -r _ < "$FIFO" || true # bloquea hasta que el lector esté escuchando
|
|
|
|
# El probe del canario va DENTRO del `sh -c`, o sea DESPUÉS del execve: es el único sitio donde
|
|
# valida lo que tiene que validar (que el logging post-exec funciona). Si lo hiciera harkaq-exec
|
|
# antes de ejecutar, sería una denegación same-exec —que el kernel loguea por defecto— y no
|
|
# probaría nada sobre el caso que importa (§3.4).
|
|
# Sin redirects a /dev/null: exige /dev en la clausura y, si falta, sh falla en la redirección y
|
|
# el probe no llega a correr — el error medía otra cosa (§3.5, nota de método).
|
|
# Y sin `cat`: dependía de /bin/cat (applet de busybox); fuera de la clausura, el probe moría
|
|
# antes de probar nada. Sólo builtins: `read` + redirección abre el fichero igual y dispara el
|
|
# hook del LSM, sin depender de un solo binario del sistema.
|
|
CMD="read _ < $CANARY || true; $*"
|
|
|
|
# harkaq-exec y la política van bajo /tmp (tmpfs) y NO bajo /: con `--ro-bind / /` la raíz es de
|
|
# sólo lectura y bwrap no puede crear ahí el punto de montaje. Que vivan en /tmp no los mete en
|
|
# la clausura: se leen y se ejecutan ANTES del restrict_self — la jaula empieza en el `sh -c`.
|
|
set +e
|
|
bwrap --unshare-all --ro-bind / / --tmpfs /tmp \
|
|
--ro-bind "$CANARY_SRC" "$CANARY" \
|
|
--ro-bind "$BIN/harkaq-exec" /tmp/harkaq-exec \
|
|
--ro-bind "$POLICY" /tmp/harkaq.policy \
|
|
--die-with-parent \
|
|
/tmp/harkaq-exec --policy /tmp/harkaq.policy -- /bin/sh -c "$CMD"
|
|
BUILD_RC=$?
|
|
set -e
|
|
|
|
# Gracia para que llegue el `deallocated` (el kernel lo emite con retraso: RCU/workqueue), y
|
|
# después despertar al lector si sigue esperando — pasa cuando el build ni arrancó y el canario
|
|
# nunca se emitió. Sin esto se comería el timeout entero por un fallo trivial.
|
|
sleep 2
|
|
kill -TERM "$AUDIT_PID" 2>/dev/null || true
|
|
wait "$AUDIT_PID" 2>/dev/null || true
|
|
cat "$VERDICT"
|
|
echo "[harkaq-run] build rc=$BUILD_RC" >&2
|
|
exit $BUILD_RC
|