Files
sergioandClaude Opus 4.8 ed614e6898 harkaq: el runtime base DERIVADO (§4.3) + el diagnóstico sobre un configure real (§4.4)
harkaq-base-closure.py: deriva el cierre dinámico del runtime base. Es D1
aplicado a la base — mantenerla a mano sería el "alguien mantiene un perfil de
permisos" que D1 dice que mata a todos los sandboxes. No usa ldd (resolvería
contra el HOST, no contra el rootfs Alpine): lee los DT_NEEDED del ELF y
resuelve dentro del rootfs por las rutas de musl. Emite symlink Y destino: el
kernel denuncia el fichero real (libz.so.1.3.2) pero el build abre por el nombre
corto (libz.so.1).

VALIDACIÓN: el cierre derivado de {sh,busybox,bash,coreutils,env} reproduce
EXACTAMENTE las 7 librerías que las rondas 2-3 habían descubierto a mano, en una
sola pasada, y además caza los symlinks y libc.musl-x86_64.so.1 que se habían
escapado. El método es computable, no adivinado.

§4.4 — con el entorno del Sandbox real replicado (CC="zig cc -mcpu=baseline",
AR, SOURCE_DATE_EPOCH, LC_ALL=C), configure llega mucho más lejos: 205
denegaciones del kernel, y clasificadas:

  esperadas (170): /usr/bin/gcc, /usr/bin/ldd        ← sondas de compilador
  DEUDA (34) en 8 paths:
      /usr/bin/{ld,nm,objdump,strip}                 ← binutils de ALPINE
      /usr/bin/{make,getconf}
      /usr/lib/gcc/x86_64-alpine-linux-musl          ← libdir del gcc de ALPINE
      /opt                                           ← bug de política

Es la tesis del §0 hecha dato: un configure que se creía hermético va a buscar
los binutils y el libdir de gcc de Alpine. Y la clasificación es lo que lo hace
legible — sin ella son 205 denegaciones planas y el hallazgo queda enterrado;
con ella son 8 paths accionables. Coincide con los swaps del selfhost-verify y
con la campaña "matar gcc" (recipes/binutils.toml existe justo porque zig provee
as/ld/ar pero el resto se toma de Alpine).

Bug de política encontrado por el propio experimento: /opt sale como deuda
porque la política concede `ro /opt/zig` pero no deja LISTAR el padre. Regla
general: todo directorio concedido necesita `list` en sus ancestros, o el escaneo
del padre es un falso positivo. harkaq-policy.sh ya lo hace para la clausura de
las deps; falta para las superficies de contrato.

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

135 lines
5.9 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
# MODE=base → build SIN ninguna dep declarada. Por definición, TODA denegación es runtime base:
# no hay clausura que pueda explicarla. Aísla la respuesta a Q2 sin juicio de valor.
# MODE=zlib → el mismo build contra la dep zlib del store. Lo que sobre por encima del runtime
# base ES la deuda de de-Alpinización, sin ambigüedad.
MODE="${MODE:-zlib}"
[ -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"
if [ "$MODE" = configure ]; then
# Una fuente autotools REAL. `configure` sondea el sistema entero (cientos de tests con
# sed/grep/awk/rm...), así que es la prueba de si el runtime base de 5 paths medido contra
# un `zig cc` mínimo aguanta, o si es POR FORMA DE BUILD. Es la pregunta que decide si la
# métrica de <5% de la Fase 2 es alcanzable.
DEPS=""
SRCDIR="${Q2_SRC:-work/sources/libgpg-error-7a85413f2bc354f4}"
[ -d "$SRCDIR" ] || { echo "falta $SRCDIR"; exit 1; }
cp -a "$SRCDIR"/. "$WORK/src/"
elif [ "$MODE" = base ]; then
DEPS=""
LDFLAGS=""
cat > "$WORK/src/hello.c" <<'EOF'
#include <stdio.h>
int main(void) { printf("hola\n"); return 0; }
EOF
else
DEPS=$(ls -d store/*-zlib | head -1)
LDFLAGS="-lz"
cat > "$WORK/src/hello.c" <<'EOF'
#include <zlib.h>
#include <stdio.h>
int main(void) { printf("zlib %s\n", zlibVersion()); return 0; }
EOF
fi
# 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"
# Sin deps (MODE=base) la clausura declarada es VACÍA: la política son sólo las
# superficies de contrato del sandbox. Todo lo demás que el build toque, se ve.
: > "$POLICY"
[ -n "$DEPS" ] && scripts/harkaq/harkaq-policy.sh $DEPS > "$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"
# Denegaciones ESPERADAS (§4.2): se deniegan a propósito y se declaran acá para que el
# clasificador no las cuente como deuda. harkaq-exec las ignora (empiezan con #).
[ -n "${EXPECT:-}" ] && printf '# expect %s\n' $EXPECT
} >> "$POLICY"
echo "── MODE=$MODE deps=[${DEPS:-ninguna}]" >&2
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.
if [ "$MODE" = configure ]; then
BUILD='cd /src && ./configure --prefix=/usr >/tmp/conf.log 2>&1; echo "configure rc=$?"'
else
BUILD='cd /src && zig cc -mcpu=baseline hello.c '"$LDFLAGS"' -o /out/hello && echo BUILD-OK'
fi
CMD="read _ < $CANARY || true; $BUILD"
set +e
bwrap --overlay-src "$ROOTFS" $(for d in $DEPS; do printf -- "--overlay-src %s " "$d"; done) --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 \
--setenv CC "zig cc -mcpu=baseline" \
--setenv CXX "zig c++ -mcpu=baseline" \
--setenv AR "zig ar" \
--setenv SOURCE_DATE_EPOCH 1 --setenv LC_ALL C --setenv LANG C --setenv TZ UTC \
/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
scripts/harkaq/harkaq-verdict.py "$VERDICT" "$POLICY" --human