harkaq: Q2 RESUELTA — el runtime base son 5 paths, y aparece una 3ª categoría

Método (lo importante): NO se adivina, se mide. MODE=base corre SIN ninguna dep
declarada ⇒ sin clausura que pueda explicarlas, toda denegación es runtime base
por definición. Aísla la respuesta sin juicio de valor.

MODE=base, hello.c mínimo con zig cc: 18 denegaciones, 4 paths distintos
(contador del kernel 19 = 18 + canario ✓):
    8× /dev/urandom   6× /usr/lib/os-release   3× /dev/null   1× /usr/bin/env
Y el build salió rc=0: no son cosas que el build NECESITE, son cosas que INTENTA
— pero salen en todos los builds y sin tratarlas entierran la deuda real.

La partición cierra en TRES categorías, todas medidas (§4.2):
  - Contrato del sandbox: /src /out /tmp /dev/null(rw) /dev/urandom /proc
    /opt/zig — los monta BWRAP, no son Alpine ⇒ no son deuda. Ésta era la línea
    divisoria que faltaba.
  - Runtime base Alpine: /bin/sh /bin/busybox /lib/ld-musl /usr/bin/env
    /usr/lib/os-release. CINCO. ⇒ el resto de Alpine es deuda genuina, y eso
    hace viable todo el planteo.
  - Denegación ESPERADA (no estaba en el diseño): /usr/bin/gcc. zig cc lo sondea
    3× y el build sale rc=0 igual ⇒ NO es ruido a permitir, es la jaula haciendo
    su trabajo: concederla dejaría a zig invocar el gcc de Alpine por detrás,
    justo lo que la campaña "matar gcc" persigue a mano. Es D3 literal ("una
    denegación esperada no se calla, se CLASIFICA") y confirma que no tener
    `quiet` era correcto: silenciarla habría borrado el hallazgo.

El contraste con el mismo runtime base:
  MODE=base  rc=0  Impuro [ /usr/bin/gcc ]            ← esperada
  MODE=zlib  rc=1  Impuro [ /usr/lib/libz.so.1.3.2 ]  ← DEUDA PURA, sin ruido

Consecuencia de diseño para el Verdict: las denegaciones tienen que venir
CLASIFICADAS (base|esperada|deuda), no en lista plana. `Hermetico` debe
significar "cero deuda", no "cero denegaciones" — si no, ningún build real lo
alcanzaría jamás.

Gotchas medidos: /dev/null es destino de ESCRITURA (rw, no ro) y el piso duro es
/bin/sh→busybox→ld-musl (sin eso ni el canario corre ⇒ sólo SinEvidencia).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 15:28:47 -04:00
co-authored by Claude Opus 4.8
parent 24c6ab455e
commit 61171bfe7f
3 changed files with 111 additions and 23 deletions
+30
View File
@@ -168,3 +168,33 @@ estado: Impuro | canario: True | contador kernel: 3
El segundo lo cazó **D9 mismo**: el lector se negó a certificar (`SinEvidencia`) en vez de decir
`Hermetico`. El canario funcionando como se diseñó, sobre un bug de quien lo diseñó.
### Q2 resuelta: el runtime base son 5 paths (2026-07-15)
`MODE=base` corre **sin ninguna dep declarada**: sin clausura que pueda explicarlas, toda
denegación es runtime base por definición. Eso aísla la respuesta sin juicio de valor.
```sh
export BASE_EXTRA='ro /bin/sh
ro /bin/busybox
ro /lib/ld-musl-x86_64.so.1
ro /usr/bin/env
ro /usr/lib/os-release
rw /dev/null
ro /dev/urandom'
MODE=base scripts/harkaq/q2-runtime-base.sh # rc=0, sólo queda /usr/bin/gcc (esperada)
MODE=zlib scripts/harkaq/q2-runtime-base.sh # Impuro: [/usr/lib/libz.so.1.3.2] — deuda pura
```
Tres categorías, todas medidas (detalle en `docs/16-harkaq-jaula.md` §4.2):
- **Contrato**: `/src /out /tmp /dev/null(rw) /dev/urandom /proc /opt/zig` — los monta bwrap, no
son Alpine.
- **Runtime base Alpine**: `/bin/sh /bin/busybox /lib/ld-musl /usr/bin/env /usr/lib/os-release`.
Cinco. El resto de Alpine es deuda genuina.
- **Denegación esperada**: `/usr/bin/gcc` — `zig cc` lo sondea y el build sale `rc=0` igual.
Concederla dejaría a zig usar el gcc de Alpine por detrás. **Se deniega y se clasifica** (D3).
Ojo con dos: `/dev/null` es destino de **escritura** (`rw`, no `ro` — con `ro` quedan
`fs.write_file` colgando), y el piso duro es `/bin/sh`→`busybox`→`ld-musl`: sin eso ni el canario
corre y no hay diagnóstico, sólo `SinEvidencia`.
+26 -5
View File
@@ -17,23 +17,43 @@ 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)
# 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"
cat > "$WORK/src/hello.c" <<'EOF'
if [ "$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"
scripts/harkaq/harkaq-policy.sh "$DEP" > "$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"
@@ -45,6 +65,7 @@ scripts/harkaq/harkaq-policy.sh "$DEP" > "$POLICY"
[ -n "${BASE_EXTRA:-}" ] && printf '%s\n' "$BASE_EXTRA"
} >> "$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-$$"
@@ -67,10 +88,10 @@ 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"
CMD="read _ < $CANARY || true; cd /src && zig cc -mcpu=baseline hello.c $LDFLAGS -o /out/hello && echo BUILD-OK"
set +e
bwrap --overlay-src "$ROOTFS" --overlay-src "$DEP" --tmp-overlay / \
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 \