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>
This commit is contained in:
+57
-7
@@ -304,8 +304,10 @@ primaria (acá) y — vía el contador — detección de pérdida.
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ harkaq-policy (puro, testeable sin kernel) │
|
||||
│ clausura(deps) → PolicySpec { │
|
||||
│ fs_ro: [store paths de la clausura + base runtime]│
|
||||
│ fs_ro: [UN FICHERO POR ENTRADA — ver §4.1] │
|
||||
│ fs_list: [dirs de la clausura: listar, no leer] │
|
||||
│ fs_rw: [/src, /out, /tmp, /cache] │
|
||||
│ base: [el runtime implícito — §8 Q2] │
|
||||
│ scoped: UNIX_ABSTRACT | SIGNAL │
|
||||
│ } │
|
||||
│ PolicyId = blake3(postcard(PolicySpec)) │
|
||||
@@ -339,8 +341,42 @@ testeable con `cargo test` en cualquier máquina, incluso sin Landlock). `harkaq
|
||||
la receta. El acoplamiento vive sólo en `hammer-build`.
|
||||
|
||||
Nota de encaje: `Sandbox::bwrap_args` ya construye la lista de deps (`self.deps`) — es
|
||||
literalmente el input de `clausura()`. Y `merge_deps_layer()` ya las funde en una capa. El
|
||||
`PolicySpec` se deriva de datos que la struct `Sandbox` ya tiene en la mano.
|
||||
literalmente el input de `clausura()`. El `PolicySpec` se deriva de datos que la struct `Sandbox`
|
||||
ya tiene en la mano.
|
||||
|
||||
### 4.1 La política es POR FICHERO, no por path del store (corrección medida)
|
||||
|
||||
El Draft 2 decía `fs_ro: [store paths de la clausura]`. **Es falso, y era otra herencia del marco
|
||||
nix**, donde cada dep se ve dentro del sandbox como un `/nix/store/<hash>-x` distinto. En hammer
|
||||
no: `Sandbox::bwrap_args` apila cada dep con `--overlay-src` y las **funde en el mismo `/usr`**
|
||||
que el rootfs Alpine, a propósito, para que el compilador y pkgconf las encuentren en las rutas
|
||||
estándar sin plumbing de flags.
|
||||
|
||||
Medido dentro del sandbox real:
|
||||
|
||||
```
|
||||
/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 solo
|
||||
```
|
||||
|
||||
Los paths del store **no existen dentro del sandbox**, y una regla sobre `/usr/include` concede
|
||||
las dos cosas por igual — la evidencia dejaría de significar nada. Ni siquiera `dev` distingue:
|
||||
overlayfs deja pasar el inode de la capa inferior y ambas capas están en la misma partición.
|
||||
|
||||
**La clausura se enumera fichero a fichero.** Es computable sin esfuerzo: en el *host* sabemos
|
||||
exactamente qué aporta cada dep (`store/<hash>-zlib/usr/include/zlib.h` → `/usr/include/zlib.h`;
|
||||
la traducción es quitar el prefijo del store). `harkaq-policy.sh` lo hace hoy.
|
||||
|
||||
Dos consecuencias de diseño:
|
||||
|
||||
1. **`list` ≠ `ro`.** pkgconf y los tests de `configure` **necesitan** escanear `/usr/lib/pkgconfig`,
|
||||
pero listar no es leer. Landlock separa `READ_DIR` de `READ_FILE`, así que se concede el
|
||||
listado del directorio y se deniega la lectura de los `.pc` que la receta no declaró. Sin esta
|
||||
separación, todo `ls` sería un falso positivo.
|
||||
2. **Máscara por tipo.** Los derechos de-sólo-directorio (`READ_DIR`, `MAKE_*`, `REFER`, …) sobre
|
||||
un fichero regular hacen que el kernel rechace la regla entera con `EINVAL`. Como la clausura
|
||||
es mayormente ficheros sueltos, sin recortar por tipo no arranca ni la primera regla.
|
||||
|
||||
---
|
||||
|
||||
@@ -590,10 +626,24 @@ vibra.
|
||||
- **Q1d (Fase 1, menor):** el `exe=` del registro trae la ruta **del host**. Con el sandbox real
|
||||
(`--bind <src> /src`) hay que confirmar qué ruta reporta para un builder que vive en `/src`, y si
|
||||
sirve para algo o basta con `domain=`.
|
||||
- **Q2 (Fase 2):** ¿qué es exactamente el "runtime base" que toda receta necesita y ninguna
|
||||
declara (`/bin/sh`, la libc del rootfs, `/opt/zig`)? ¿Va en la clausura implícitamente, o se
|
||||
declara y se vuelve parte del hash de la receta? *Esta es la decisión de diseño de verdad*, y
|
||||
determina si la métrica de la Fase 2 mide deuda real o ruido.
|
||||
- **Q2 (Fase 2) — EN CURSO, con método y primeros datos.** ¿Qué es el "runtime base" que toda
|
||||
receta necesita y ninguna declara? *Sigue siendo la decisión de diseño de verdad* — determina si
|
||||
la métrica de la Fase 2 mide deuda real o ruido. **El método está resuelto: no se adivina, se
|
||||
mide.** `scripts/harkaq/q2-runtime-base.sh` corre un build real en el sandbox real con
|
||||
`política = clausura declarada` y deja que las denegaciones nombren el resto.
|
||||
|
||||
Medido (2026-07-15), compilando contra la dep `zlib` del store:
|
||||
- **Piso duro:** `/bin/sh` → `/bin/busybox` → `/lib/ld-musl-x86_64.so.1`. Sin esto ni el canario
|
||||
corre (el `execvp` de `/bin/sh` rebota) ⇒ `SinEvidencia` y cero diagnóstico. El runtime base
|
||||
mínimo es, literalmente, *lo que hace falta para que el canario pueda correr*.
|
||||
- **Candidato:** `/usr/bin/env` — legítimo, ninguna receta lo declara ni debería.
|
||||
- **Y el hallazgo que justifica el proyecto:** `fs.read_file /usr/lib/libz.so.1.3.2`. 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. Con harkaq el kernel lo
|
||||
nombra, con path e inode. **Es exactamente el bug de §0, cazado en un build real.**
|
||||
|
||||
Falta: correrlo sobre el catálogo y separar el runtime base (pocos, estables, en todas las
|
||||
recetas) de la deuda real (variable, por receta). Esa separación *es* la respuesta a Q2.
|
||||
- **Q3 (Fase 2):** ¿`/proc` y `/dev` mínimos rompen builds reales? (`/proc/self` suele hacer falta;
|
||||
`/proc/sys` casi nunca.) bwrap ya monta `--proc /proc --dev /dev`.
|
||||
- **Q4:** ¿el `Verdict` entra en `Recipe::hash_inputs`? Por simetría con `Evidence` (H1), **no**:
|
||||
|
||||
@@ -128,3 +128,43 @@ arquitectónica falsa, a partir de cero estímulo.
|
||||
Van tres veces en una sola sesión (`NLM_F_ACK`, el redirect a `/dev/null`, el exec) que un cero
|
||||
resultó ser el instrumento y no el kernel. Un veredicto que no verifica su propio estímulo es el
|
||||
falso `Hermetico` de D9, pero del lado del banco.
|
||||
|
||||
## `harkaq-policy.sh` + `q2-runtime-base.sh` — la clausura y el runtime base
|
||||
|
||||
`harkaq-policy.sh <dir-dep>...` deriva la política de la clausura (D1: no se escribe, se deriva).
|
||||
Enumera **fichero a fichero** —no por directorio— porque el sandbox funde las deps y Alpine en el
|
||||
mismo `/usr` (§4.1). Emite `list` de los directorios para que pkgconf/configure puedan escanear
|
||||
sin poder leer lo no declarado.
|
||||
|
||||
`q2-runtime-base.sh` responde Q2 midiendo, no adivinando: corre un build real en el sandbox real
|
||||
con `política = clausura` y deja que las denegaciones nombren el runtime base. Se itera con
|
||||
`BASE_EXTRA` (una línea por regla), y **cada línea que se añade ahí debe venir de una denegación
|
||||
que el kernel denunció, nunca de una corazonada** — si se define de más, la política concede
|
||||
Alpine entero y la evidencia deja de valer.
|
||||
|
||||
```sh
|
||||
export BASE_EXTRA='ro /bin/sh
|
||||
ro /bin/busybox
|
||||
ro /lib/ld-musl-x86_64.so.1'
|
||||
HARKAQ_TIMEOUT=25 scripts/harkaq/q2-runtime-base.sh
|
||||
```
|
||||
|
||||
Resultado (2026-07-15, contra la dep `zlib` del store):
|
||||
|
||||
```
|
||||
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, no la dep declarada!
|
||||
```
|
||||
|
||||
### Dos rastrillos más, aprendidos acá
|
||||
|
||||
1. **El canario no puede depender de un binario.** Con `cat $CANARY` moría en `/bin/cat` (applet
|
||||
de busybox fuera de la clausura) y el probe no probaba nada. Ahora usa sólo builtins:
|
||||
`read _ < $CANARY || true`.
|
||||
2. **El canario no puede vivir en `/tmp`.** La política concede `rw /tmp` (es el HOME del build),
|
||||
así que un canario ahí cae DENTRO de la clausura, se lee sin problema y no deniega nada ⇒
|
||||
`SinEvidencia` eterno. Va en la raíz, donde la política no alcanza por construcción.
|
||||
|
||||
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ñó.
|
||||
|
||||
@@ -6,8 +6,20 @@
|
||||
//
|
||||
// Fichero de política (lo genera harkaq-policy a partir de la clausura; D1: la política se
|
||||
// DERIVA, no se escribe):
|
||||
// ro /store/<hash>-zlib
|
||||
// rw /out
|
||||
// ro /usr/include/zlib.h un fichero de la clausura: leer/ejecutar SÓLO él
|
||||
// ro /opt/zig un directorio: todo el subárbol
|
||||
// list /usr/include listar el directorio, SIN poder leer su contenido
|
||||
// rw /out
|
||||
//
|
||||
// El grano POR FICHERO no es capricho: el sandbox de hammer funde las deps y el rootfs Alpine
|
||||
// en el mismo `/usr` (`--overlay-src` × N + `--tmp-overlay /`), así que /usr/include/zlib.h
|
||||
// (dep declarada) y /usr/include/stdio.h (Alpine, no declarado) son el MISMO directorio. Una
|
||||
// regla sobre /usr concede los dos y la evidencia no vale nada. En el host sí sabemos qué
|
||||
// ficheros aporta cada dep ⇒ la clausura se enumera fichero a fichero.
|
||||
//
|
||||
// `list` existe por la misma razón: pkgconf/configure NECESITAN listar /usr/lib/pkgconfig, pero
|
||||
// listar no es leer. Landlock separa READ_DIR de READ_FILE, así que se concede el listado del
|
||||
// directorio y se deniega la lectura de los .pc que la receta no declaró.
|
||||
//
|
||||
// Estático a propósito: corre dentro del rootfs Alpine (musl) del sandbox.
|
||||
//
|
||||
@@ -24,6 +36,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -55,10 +68,16 @@ static int ll_restrict(int fd, __u32 f) {
|
||||
#define RO_RIGHTS \
|
||||
(LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_EXECUTE)
|
||||
|
||||
struct rule {
|
||||
char path[4096];
|
||||
int rw;
|
||||
};
|
||||
// Derechos que Landlock SÓLO admite sobre directorios. Añadir una regla con cualquiera de
|
||||
// éstos sobre un fichero regular hace que el kernel rechace la regla entera con EINVAL — y
|
||||
// como la clausura es mayormente ficheros sueltos (§4), sin esta máscara no arranca nada.
|
||||
#define DIR_ONLY_RIGHTS \
|
||||
(LANDLOCK_ACCESS_FS_READ_DIR | LANDLOCK_ACCESS_FS_REMOVE_DIR | \
|
||||
LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_CHAR | \
|
||||
LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG | \
|
||||
LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO | \
|
||||
LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM | \
|
||||
LANDLOCK_ACCESS_FS_REFER)
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *policy = NULL;
|
||||
@@ -92,15 +111,16 @@ int main(int argc, char **argv) {
|
||||
FILE *f = fopen(policy, "r");
|
||||
if (!f) { perror("[harkaq] abrir política"); return 4; }
|
||||
char line[4200];
|
||||
int n_ro = 0, n_rw = 0;
|
||||
int n_file = 0, n_dir = 0;
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl) *nl = 0;
|
||||
if (!line[0] || line[0] == '#') continue;
|
||||
int rw;
|
||||
__u64 grant;
|
||||
char *p;
|
||||
if (!strncmp(line, "ro ", 3)) { rw = 0; p = line + 3; }
|
||||
else if (!strncmp(line, "rw ", 3)) { rw = 1; p = line + 3; }
|
||||
if (!strncmp(line, "ro ", 3)) { grant = RO_RIGHTS; p = line + 3; }
|
||||
else if (!strncmp(line, "rw ", 3)) { grant = handled; p = line + 3; }
|
||||
else if (!strncmp(line, "list ", 5)) { grant = LANDLOCK_ACCESS_FS_READ_DIR; p = line + 5; }
|
||||
else {
|
||||
fprintf(stderr, "[harkaq] política inválida: %s\n", line);
|
||||
return 4;
|
||||
@@ -114,20 +134,26 @@ int main(int argc, char **argv) {
|
||||
fprintf(stderr, "[harkaq] la política nombra un path inexistente: %s (%m)\n", p);
|
||||
return 4;
|
||||
}
|
||||
struct landlock_path_beneath_attr pb = {.allowed_access = rw ? handled : RO_RIGHTS,
|
||||
.parent_fd = dfd};
|
||||
// REFER sólo se concede sobre directorios; sobre un fichero suelto el kernel rechaza
|
||||
// la regla entera con EINVAL. Reintentamos sin él antes de darlo por perdido.
|
||||
// Recortar los derechos de-sólo-directorio cuando el destino NO lo es: el kernel
|
||||
// rechazaría la regla entera con EINVAL. Es el caso MAYORITARIO — la clausura es una
|
||||
// lista de ficheros.
|
||||
struct stat st;
|
||||
int is_dir = (fstat(dfd, &st) == 0) && S_ISDIR(st.st_mode);
|
||||
if (!is_dir) grant &= ~(__u64)DIR_ONLY_RIGHTS;
|
||||
grant &= handled; // nunca conceder más de lo que el ruleset declara manejar
|
||||
if (!grant) {
|
||||
fprintf(stderr, "[harkaq] regla vacía tras recortar por tipo: %s\n", p);
|
||||
close(dfd);
|
||||
return 4;
|
||||
}
|
||||
struct landlock_path_beneath_attr pb = {.allowed_access = grant, .parent_fd = dfd};
|
||||
if (ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0) < 0) {
|
||||
pb.allowed_access &= ~(__u64)LANDLOCK_ACCESS_FS_REFER;
|
||||
if (ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0) < 0) {
|
||||
fprintf(stderr, "[harkaq] add_rule(%s): %m\n", p);
|
||||
close(dfd);
|
||||
return 4;
|
||||
}
|
||||
fprintf(stderr, "[harkaq] add_rule(%s): %m\n", p);
|
||||
close(dfd);
|
||||
return 4;
|
||||
}
|
||||
close(dfd);
|
||||
rw ? n_rw++ : n_ro++;
|
||||
is_dir ? n_dir++ : n_file++;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
@@ -150,8 +176,10 @@ int main(int argc, char **argv) {
|
||||
"ENJAULADO pero SIN EVIDENCIA; el veredicto no podrá afirmar hermeticidad.\n",
|
||||
abi);
|
||||
else
|
||||
fprintf(stderr, "[harkaq] jaula puesta: ABI %d, %d ro + %d rw, logging post-exec ON\n",
|
||||
abi, n_ro, n_rw);
|
||||
fprintf(stderr,
|
||||
"[harkaq] jaula puesta: ABI %d, clausura = %d ficheros + %d dirs, "
|
||||
"logging post-exec ON\n",
|
||||
abi, n_file, n_dir);
|
||||
|
||||
execvp(cmd[0], cmd);
|
||||
fprintf(stderr, "[harkaq] execvp %s: %m\n", cmd[0]);
|
||||
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
# harkaq-policy — deriva la política de la clausura declarada (SDD 16, D1).
|
||||
#
|
||||
# harkaq-policy.sh <dir-dep> [<dir-dep>...] > politica
|
||||
#
|
||||
# D1: la política NO se escribe, se DERIVA. La receta ya declara sus `deps` por hash; cada dep
|
||||
# es un artefacto sellado del store (`store/<hash>-<name>/usr/...`). La clausura es, exactamente,
|
||||
# el conjunto de ficheros que esas deps aportan — ni uno más.
|
||||
#
|
||||
# El sandbox de hammer apila cada dep con `--overlay-src` sobre el rootfs Alpine, así que el
|
||||
# fichero `store/<hash>-zlib/usr/include/zlib.h` aparece dentro como `/usr/include/zlib.h`. La
|
||||
# traducción es sólo quitar el prefijo del store: el resto del path YA es el path del sandbox.
|
||||
#
|
||||
# Por qué fichero a fichero y no por directorio: adentro, la dep declarada y el rootfs Alpine
|
||||
# comparten `/usr` (medido: /usr/include/zlib.h y /usr/include/stdio.h son el mismo directorio
|
||||
# overlayfs). Una regla sobre /usr concede los dos y la evidencia deja de significar algo. Los
|
||||
# `list` de los directorios se emiten para que las herramientas puedan ESCANEAR (pkgconf, los
|
||||
# tests de configure) sin poder LEER lo no declarado — Landlock separa READ_DIR de READ_FILE.
|
||||
set -eu
|
||||
|
||||
emit_dirs() {
|
||||
# Los directorios que contienen ficheros de la clausura: listables, no legibles.
|
||||
# Se emiten deduplicados y con sus ancestros, porque un `ls /usr` también es un escaneo
|
||||
# legítimo y su denegación sería un falso positivo, no deuda de de-Alpinización.
|
||||
while IFS= read -r d; do
|
||||
while [ "$d" != "/" ] && [ -n "$d" ]; do
|
||||
echo "list $d"
|
||||
d=$(dirname "$d")
|
||||
done
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "$TMP" "$TMP.d"' EXIT
|
||||
: > "$TMP"
|
||||
: > "$TMP.d"
|
||||
|
||||
for dep in "$@"; do
|
||||
real=$(readlink -f "$dep")
|
||||
# `.hammer/` es metadata del artefacto (recipe.toml), no parte de la clausura que el build
|
||||
# consume: si el build lo leyera sería una denegación legítima que queremos ver.
|
||||
find "$real" -type f -o -type l | grep -v "/\.hammer/" | while IFS= read -r f; do
|
||||
inside="${f#$real}"
|
||||
echo "ro $inside" >> "$TMP"
|
||||
dirname "$inside" >> "$TMP.d"
|
||||
done
|
||||
done
|
||||
|
||||
echo "# harkaq-policy: clausura derivada de $# dep(s)"
|
||||
sort -u "$TMP.d" | emit_dirs
|
||||
sort -u "$TMP"
|
||||
@@ -42,9 +42,12 @@ read -r _ < "$FIFO" || true # bloquea hasta que el lector esté escuchando
|
||||
# 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: `>/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).
|
||||
CMD="cat $CANARY; $*"
|
||||
# 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
|
||||
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user