Files
takana/scripts/harkaq/q2-runtime-base.sh
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

101 lines
4.2 KiB
Bash
Executable File

#!/bin/sh
# Q2 — ¿qué es el "runtime base" implícito? (SDD 16 §8)
#
# La pregunta de diseño que decide si la métrica de la Fase 2 mide deuda real de de-Alpinización
# o mide ruido. Ninguna receta declara /bin/sh, la libc o /opt/zig, y ninguna debería. Pero si el
# runtime base se define de más, la política concede Alpine entero y la evidencia no vale nada.
#
# Método: NO adivinarlo. Correr un build real con `política = clausura declarada` y dejar que
# las denegaciones lo nombren. Lo que aparezca es, por definición, lo que todo build necesita y
# nadie declara.
#
# Replica el sandbox real (`Sandbox::bwrap_args`): overlay del rootfs Alpine + las deps apiladas,
# `--tmp-overlay /`, /proc, /dev, /tmp, zig en /opt/zig, fuentes en /src, salida en /out.
set -eu
cd "$(dirname "$0")/../.."
BIN="${HARKAQ_BIN:-$HOME/.cache/harkaq}"
ROOTFS=.dev-fs/alpine
ZIG=.dev-fs/tools/zig
DEP=$(ls -d store/*-zlib | head -1)
[ -d "$ROOTFS" ] || { echo "falta $ROOTFS"; exit 1; }
[ -d "$ZIG" ] || { echo "falta $ZIG"; exit 1; }
WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/src" "$WORK/out"
cat > "$WORK/src/hello.c" <<'EOF'
#include <zlib.h>
#include <stdio.h>
int main(void) { printf("zlib %s\n", zlibVersion()); return 0; }
EOF
# La clausura DECLARADA (D1) + las superficies que el sandbox define por contrato, no la receta:
# /src (fuentes), /out (DESTDIR), /tmp (HOME). Nada de Alpine.
POLICY="$WORK/policy"
scripts/harkaq/harkaq-policy.sh "$DEP" > "$POLICY"
{
echo "rw /src"
echo "rw /out"
echo "rw /tmp"
echo "ro /opt/zig"
# BASE_EXTRA: lo que las corridas previas demostraron que hace falta. Empieza vacío y se
# llena SÓLO con lo que el kernel denunció — nunca por corazonada. Cada línea de acá es una
# respuesta medida a Q2, no una conjetura.
[ -n "${BASE_EXTRA:-}" ] && printf '%s\n' "$BASE_EXTRA"
} >> "$POLICY"
echo "── política: $(grep -c '^ro ' "$POLICY") ro, $(grep -c '^list ' "$POLICY") list, $(grep -c '^rw ' "$POLICY") rw" >&2
NONCE="q2-$$"
# El canario va en la RAÍZ, no en /tmp: la política concede `rw /tmp` (es el HOME del build), así
# que un canario ahí caería DENTRO de la clausura, se leería sin problema y no generaría ninguna
# denegación ⇒ SinEvidencia eterno. El canario tiene que vivir donde la política NO alcanza, por
# construcción. En el sandbox real la raíz es escribible (`--tmp-overlay /`) y bwrap puede crear
# ahí el punto de montaje. (Lo cazó el propio D9: el lector se negó a certificar.)
CANARY="/harkaq-canary-$NONCE"
CSRC=$(mktemp); echo canario > "$CSRC"
FIFO=$(mktemp -u); mkfifo "$FIFO"
VERDICT=$(mktemp)
trap 'rm -rf "$WORK" "$CSRC" "$FIFO" "$VERDICT"' EXIT
"$BIN/harkaq-audit" --canary "$CANARY" --timeout "${HARKAQ_TIMEOUT:-60}" --ready-fd 3 \
3>"$FIFO" >"$VERDICT" &
AUDIT_PID=$!
read -r _ < "$FIFO" || true
# El probe del canario usa SÓLO builtins del shell: `read` + redirección. Con `cat` dependía de
# /bin/cat (otro applet de busybox) y, si no estaba en la clausura, el probe moría sin probar
# nada — el canario tiene que ser lo más barato e independiente del sistema.
CMD="read _ < $CANARY || true; cd /src && zig cc -mcpu=baseline hello.c -lz -o /out/hello && echo BUILD-OK"
set +e
bwrap --overlay-src "$ROOTFS" --overlay-src "$DEP" --tmp-overlay / \
--proc /proc --dev /dev --tmpfs /tmp \
--ro-bind "$ZIG" /opt/zig \
--bind "$WORK/src" /src --bind "$WORK/out" /out \
--ro-bind "$CSRC" "$CANARY" \
--ro-bind "$BIN/harkaq-exec" /tmp/harkaq-exec \
--ro-bind "$POLICY" /tmp/harkaq.policy \
--chdir /src --unshare-all --die-with-parent \
--setenv PATH /opt/zig:/usr/bin:/bin \
--setenv HOME /tmp \
/tmp/harkaq-exec --policy /tmp/harkaq.policy -- /bin/sh -c "$CMD"
RC=$?
set -e
sleep 2
kill -TERM "$AUDIT_PID" 2>/dev/null || true
wait "$AUDIT_PID" 2>/dev/null || true
echo "── build rc=$RC" >&2
echo "── denegaciones (= candidatos a runtime base):" >&2
python3 -c '
import json,sys
v=json.load(open(sys.argv[1]))
print(" estado:", v["estado"], "| canario:", v["canario_visto"], "| contador kernel:", v["contador_kernel"])
for d in v["denials"]:
print(" ", d["blockers"], d["path"])
if not v["denials"]: print(" (ninguna)")
' "$VERDICT"