18 Commits
Author SHA1 Message Date
sergioandClaude Opus 4.8 ec7ceb0915 harkaq: Q1-copy.fail RESPONDIDA — fs-verity mata el vector page-cache
El SDD 16 dejó abierto: '¿el vector page-cache de copy.fail sobrevive a un
ruleset que deniegue escritura a nivel de inode? Investigar antes de afirmar
nada'. Investigado en un fs AISLADO (loop en un worker efímero, nunca el fs del
usuario):

  escribir al artefacto sellado              → EPERM
  dd como ROOT                               → Operation not permitted (ni root)
  machacar los bytes POR DEBAJO (device)+leer→ Input/output error

El tercero ES el vector de copy.fail: modificar el fichero por debajo del fs. El
kernel detecta la corrupción AL LEER (el Merkle no cuadra) y rechaza la lectura.
⇒ fs-verity mata el vector. Landlock read-only no alcanzaba; fs-verity sí.

Costo medido, y corrige al SDD 17 §5: 'hash = identidad' es FALSO — fs-verity usa
Merkle SHA-256, hammer direcciona con BLAKE3 ⇒ son DOS hashes, no uno. Y ext4
exige -O verity Y blocksize = PAGE_SIZE (con 1024 el mount falla:
'Unsupported blocksize for fs-verity'). No es gratis, pero es barato para lo que da.

NO se tocó el fs del laptop (habilitar verity pediría tune2fs sobre la partición
del usuario): el experimento corrió en un loop device de un worker descartable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 22:17:01 -04:00
sergioandClaude Opus 4.8 1e0bc98ad4 matar gcc: rollout de cmake HECHO en la granja — y los consumidores no hacían falta
cmake reconstruido en un worker efímero (regla: la cadena GUI no se rebuildea en
el laptop, el zig-skew rompe cairo) y cosechado al hub:
    b3:834132e7…-cmake   NEEDED = sólo libc.musl-x86_64.so.1
El viejo (99864dcc…, con libstdc++.so.6 + libgcc_s.so.1) queda en el store por si
algo lo referencia.

LOS 10 CONSUMIDORES NO SE RECONSTRUYERON, Y NO HACE FALTA. El fix quita la
dependencia de RUNTIME del artefacto DE CMAKE. Los consumidores sólo lo usaron
como HERRAMIENTA DE BUILD — sus artefactos no linkean libstdc++. Rebuildearlos
sólo daría frescura de hash, y eso pasa solo en su próximo build. Confundir "usó
la herramienta" con "linkea la librería" habría costado un rebuild de gtk4 para
nada. (Yo mismo había propuesto los 11; el radio real es 1.)

REGALO DEL ROLLOUT: el worker (Ubuntu 24.04, ccx23) y el laptop (CachyOS)
produjeron cmake con el MISMO HASH, bit a bit — el invariante central de hammer
confirmándose entre dos máquinas distintas sin que nadie lo buscara. Y como los
bytes son idénticos, el NEEDED del worker queda verificado por transitividad: no
hizo falta readelf allá (que además no está instalado en la golden).

Limpieza: removido store/834132e7…-cmake-nogcc (mi artefacto experimental).
harkaq-suggest INDEXA el store, así que un artefacto huérfano aparecería como
"proveedor" de paths y ensuciaría los diagnósticos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:53:11 -04:00
sergioandClaude Opus 4.8 ee51e5d596 harkaq: el kernel de hammer ya trae Landlock — ABI 7 verificado BOOTEANDO (Fase 1 precond. 1)
recipes/linux.toml: -e SECURITY -e SECURITY_LANDLOCK -e AUDIT. Era lo único que
faltaba para que harkaq corra DENTRO de hammer (VM/metal) y no sólo en el laptop
y la granja. El CONFIG_LSM del defconfig ya lista `landlock` de primero ⇒ bastó
encenderlo, sin tocar la cadena de LSMs.

AUDIT se fija explícito aunque ya viniera =y por defconfig: sin él el kernel no
emite un solo registro y harkaq certificaría TODO como hermético en silencio
(§3.4). Es la precondición de la que cuelga la evidencia; no se deja al azar de
un default.

VERIFICADO BOOTEANDO, no leyendo el .config — que dice lo que se compiló, no lo
que el kernel hace al arrancar. Misma disciplina de §3.1 (el ABI se consulta por
syscall, jamás por versión) llevada a la verificación: el único que sabe si
Landlock está vivo es el kernel vivo. scripts/harkaq/vm-abi-probe.c es un /init
de initramfs mínimo que pregunta y apaga:

    ===== HARKAQ EN EL KERNEL DE HAMMER =====
    LANDLOCK ABI = 7
    audit de denegaciones (>=7): SI
    =========================================

Kernel nuevo: b3:f2583d61… (el hash cambia, como se esperaba; el viejo
34755ff2… decía "# CONFIG_SECURITY_LANDLOCK is not set").

Con esto las 4 precondiciones de la Fase 1 están cerradas y harkaq corre en las
tres máquinas del proyecto: laptop (ABI 10), granja (ABI 7 tras el bump de la
golden) y el kernel propio de hammer (ABI 7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:13:39 -04:00
sergioandClaude Opus 4.8 caa23f9ab7 harkaq: seccomp implementado (D4) — denylist, no allowlist; el hash no se mueve
D4 declaraba seccomp "obligatorio, no opcional" y harkaq-exec tenía CERO
seccomp: el documento afirmaba algo que el código no hacía. Cerrado.

CORRECCIÓN DELIBERADA A D4: pedía ALLOWLIST por syscall, se implementó DENYLIST.
Un allowlist para builds ARBITRARIOS (compiladores, make, shells, linkers, perl)
es un blanco móvil que cada herramienta nueva rompe — el riesgo del §7 ("los
falsos positivos matan proyectos de sandboxing") aplicado a las syscalls, y con
peor final: un build que muere por una syscall legítima no da un diagnóstico
útil, da un misterio. Lo PELIGROSO sí es enumerable y estable: no hay build
honesto que cargue módulos, haga kexec o attachee un ptrace.

Denegadas: io_uring_*, ptrace, process_vm_{readv,writev}, bpf, userfaultfd,
keyctl/add_key/request_key, {init,finit,delete}_module, kexec_*,
perf_event_open, mount/umount2, open_tree/move_mount/fs*, setns.
pivot_root NO: bwrap lo usa ANTES de llegar a harkaq-exec.

EPERM y no KILL: matar deja un cadáver sin explicación; EPERM deja al build
fallar donde corresponde y al log decir por qué. Se instala DESPUÉS de
no_new_privs y de Landlock, justo antes del exec, y se hereda por fork/exec como
el dominio (D5). Si el kernel lo rechaza NO se corre el build (D7: media jaula
creyéndose entera es peor que ninguna).

Chequeo de arquitectura antes del número de syscall: los nros son POR ARCH y sin
el check un binario i386 podría colar otra syscall con el mismo número — el error
clásico de los filtros seccomp a mano.

COMPROBADO: ptrace → EPERM bajo la jaula (control positivo), y un build REAL de
zlib sale Hermetico ×3 con el hash del artefacto IDÉNTICO al de antes de seccomp
(b3:adc5c251…). Añadir media jaula no movió un byte — como debe ser: esto recorta
superficie de escape, no cambia el build.

El --seccomp <fd> de bwrap quedó SIN USAR: harkaq-exec ya corre dentro y con
no_new_privs puesto, así que instala el filtro él mismo — una pieza menos de
plumbing y el filtro queda al lado de la política que lo justifica.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 19:31:58 -04:00
sergioandClaude Opus 4.8 8f7109f4e9 harkaq: el barrido queda en 0 IRREDUCIBLES — perl ya existía (§4.11)
La única deuda irreducible del barrido (§4.10) era /usr/bin/perl en
ca-certificates y curl. Dos hallazgos al abrirla:

1. EL COMENTARIO DE ca-certificates.toml YA LO DECÍA, escrito por quien la portó:
   "El resto del build (mk-ca-bundle.pl → cert.pem → split-ca-bundle.sh) usa el
   perl + coreutils del rootfs". La deuda estaba escrita EN PROSA y era invisible
   para el sistema; harkaq la convirtió en un veredicto. Es la tesis del §0 en una
   línea: no hace falta que nadie DESCUBRA nada, hace falta que la máquina pueda
   VER lo que ya se sabía.

2. LA RECETA TAMBIÉN EXISTÍA: recipes/incoming-kde/perl.toml, importada el 13/07
   para la campaña KDE (syntax-highlighting e intltool exigen perl). CUARTA
   convergencia independiente: dos campañas necesitaban el mismo binario y
   llegaron por caminos que no se hablan.

Construye tal cual (b3:c7a899bd…) y con el artefacto en el store:
    /usr/bin/perl → declarar dep: perl     exit=0 ⇒ CERO deuda irreducible

EL BARRIDO DE 24 RECETAS QUEDA EN 0 IRREDUCIBLES. La métrica del §7 (<5%) se
cumple midiendo lo que la métrica quería medir: cosas que no sabemos construir.
Resultado: ninguna.

Receta promovida a recipes/perl.toml (no había canónica ⇒ sin colisión). Lo que
queda es mecánico y no es diseño: declarar `make` en las ~10 recetas que lo usan
y `perl` en ca-certificates/curl. OJO: eso re-hashea sus artefactos y son
paquetes del base-system (índice firmado 748) ⇒ decisión de rollout, no un sed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 18:51:32 -04:00
sergioandClaude Opus 4.8 13233ee4e9 harkaq: primer barrido en la granja — la deuda irreducible de la muestra es perl (§4.10)
harkaq-farm-setup.sh: provisiona un worker para barrer (idempotente; gatea por
ABI>=7 y aborta si no llega — con la golden vieja el barrido daría SinEvidencia
en TODO). Deriva el runtime base DEL ROOTFS DEL WORKER, no copia el del laptop:
es por-rootfs (§4.3) y los sonames dependen de la versión de Alpine.

Barrido: 24 recetas, 14 con veredicto. Primer número crudo: 4 irreducibles (29%).
ERA FALSO, y por dos motivos propios:

1. GAP DE POLÍTICA. Los `list` salían sólo de los ancestros de la clausura ⇒ una
   receta SIN deps (bash) no generaba ni uno y todo escaneo del árbol caía como
   denegación (/usr/lib). Listar NO es leer: Landlock separa READ_DIR de
   READ_FILE ⇒ la estructura del árbol es CONTRATO. Se puede `ls /usr/lib` sin
   leer un solo fichero no declarado; la deuda es leer lo ajeno, no saber que
   existe. Idem /var/tmp: con --tmp-overlay / es scratch descartable, como /tmp.
2. La heurística del catálogo es por NOMBRE y un fichero no se llama como su
   paquete: /usr/bin/ranlib lo trae `binutils`, /usr/bin/diff lo trae
   `diffutils`. El único que sabe la verdad es el STORE (conoce la lista de
   ficheros de cada artefacto) — y el worker tiene 103 artefactos contra los
   cientos del hub.

⇒ EL WORKER MIDE, EL HUB CLASIFICA. Misma separación que lector/clasificador: el
que tiene el privilegio —o los datos— hace lo mínimo. Reclasificado en el hub:
    bash             ranlib,/usr/lib,/var/tmp → nada (gap de política)
    doas             /usr/bin/diff            → nada (diffutils lo provee)
    ca-certificates  /usr/bin/perl            → /usr/bin/perl
    curl             /usr/bin/perl            → /usr/bin/perl

RESULTADO: la deuda irreducible de toda la muestra es UN path — /usr/bin/perl
(2 de 14 = 14%). No hay recipes/perl.toml ni artefacto: deuda genuina y nombrada.
Todo lo demás era declarable (make ×10, ranlib→binutils, diff→diffutils).

Sigue por encima del <5% del §7, pero el perfil es el que importa: NO hay cola
larga de sorpresas. La deuda de un catálogo entero se resume en "declarar make" y
"no tenemos perl".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:50:34 -04:00
sergioandClaude Opus 4.8 b122ded31f matar gcc: cmake deja de arrastrar el runtime C++ de Alpine (§4.9) + /proc es contrato
harkaq (§4.6) dejó UN solo caso irreducible en el primer barrido: brotli pidiendo
libstdc++.so.6.0.34 + libgcc_s.so.1. Pero brotli es C, no C++ — la libstdc++ no
era suya: era de `cmake`, su dep de build.

  store/99864dcc…-cmake/usr/bin/cmake
      NEEDED: libstdc++.so.6, libgcc_s.so.1, libc.so

"gcc retenido para {kernel, cmake}" no era una concesión de BUILD-TIME como
sonaba: el artefacto de cmake arrastraba el runtime C++ de Alpine hacia dentro de
CADA build que lo declarara como dep. Un agujero de soberanía viajando por el
grafo de deps, invisible en la receta del consumidor. harkaq lo señaló desde el
consumidor, que es donde se ve.

EL ARREGLO NO TOCA EL COMPILADOR — gcc sigue compilando cmake (camino
conocido-bueno; zig c++ segfaultea el cmake mínimo). Sólo deja de enlazar su
runtime en dinámico:
    CXX='g++ -static-libstdc++ -static-libgcc'  LDFLAGS='-static-libstdc++ -static-libgcc'

Medido:
  antes   NEEDED: libstdc++.so.6, libgcc_s.so.1, libc.so
  después NEEDED: libc.musl-x86_64.so.1     (y corre: cmake version 3.31.6)

EL PAGO: brotli —el único irreducible del barrido— pasa a Hermetico ×3 fases,
artefacto sellado b3:bc900676…. El barrido queda 0 irreducibles de 6. El caso que
iba a contar contra el <5% no era una receta mal escrita: era una herramienta de
hammer filtrando Alpine.

+ /proc como superficie de CONTRATO (lo destapó el configure de brotli, que lee
/proc/cpuinfo y /proc/meminfo): bwrap monta un /proc FRESCO dentro del pidns, no
sale del rootfs Alpine y no ve al host ⇒ contrato, no deuda. Mismo caso que
/cache. El hash del artefacto es idéntico antes y después de añadirlo: cambia el
veredicto, no el build.

COSTO DEL ROLLOUT: cambiar recipes/cmake.toml re-hashea cmake y sus 3
consumidores (brotli, libjpeg-turbo, libtiff = 6 sellados). Radio chico, PERO
libjpeg-turbo y libtiff son la cadena GUI y la regla es no rebuildearla en el
laptop (zig-skew rompe cairo) ⇒ el rebuild va al worker.

LO QUE NO CIERRA: /usr/bin/gcc, c89, c99, ldd y el plugin LTO (§4.7) siguen
siendo SONDAS — la jaula las deniega, los builds completan igual, y denegarlas es
lo correcto. El gcc de Alpine sigue en el rootfs y sigue haciendo falta para
{kernel, cmake} en BUILD-TIME. Lo cerrado es la filtración a RUNTIME, que es la
que contaminaba artefactos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:19:50 -04:00
sergioandClaude Opus 4.8 993060630d harkaq: el bucle CERRADO — de Impuro a Hermetico sobre una receta real (§4.7)
Diagnosticar no vale nada si no se puede accionar. El bucle completo sobre
recipes/zlib.toml, cada paso guiado SÓLO por lo que dijo harkaq:

  zlib tal cual → Impuro: /usr/bin/make         → "declarar dep: make"
  + make        → Impuro: /usr/bin/ranlib       → "declarar dep: binutils"
  + binutils    → Impuro: liblto_plugin.so      → irreducible ⇒ clasificar
  final         → Hermetico ×3 fases, artefacto b3:adc5c251… SELLADO

Cada capa que se pela destapa la siguiente, y TODAS convergen en el gcc de
Alpine. El último hallazgo es el que no se encuentra a mano: los binutils DE
HAMMER —ya declarados como dep— cargan el plugin LTO del gcc DE ALPINE
(/usr/libexec/gcc/x86_64-alpine-linux-musl/15.2.0/liblto_plugin.so). No es un
fallo de declaración de la receta: es un agujero de soberanía DENTRO de un
artefacto que hammer construye.

Va a denegación esperada por la misma razón que /usr/bin/gcc (§4.2): el build
completa sin él ⇒ es una SONDA, no una necesidad, y bloquearla es DESEABLE —
impide que los binutils de hammer usen el plugin de Alpine por detrás. D3 otra
vez: no se calla, se clasifica.

Detalle que confirma el modelo: el hash del artefacto es IDÉNTICO antes y
después de clasificar la sonda (b3:adc5c251… las dos veces). Clasificar cambia
el VEREDICTO, no el BUILD. Es el principio de H1 sosteniéndose solo: la
evidencia es comportamiento, no identidad (recipe.rs:228, y por eso Evidence
está fuera de hash_inputs).

Y eso es lo que `Hermetico` significa ahora, con todo el peso: el kernel
certificó que ese build no usó NADA fuera de su clausura declarada, y el canario
prueba que el certificado no es el silencio de un lector roto.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:07:50 -04:00
sergioandClaude Opus 4.8 d0c9cdfa33 harkaq: deuda declarable vs irreducible (§4.5) + primer barrido de Fase 2 (§4.6)
La métrica del §7 (<5% de recetas necesitan excepción) medía otra cosa. zlib
salió Impuro por /usr/bin/make y parecía runtime base: NO lo es. recipes/make.toml
existe, store/fbad44ac…-make está sellado y SWAP_MAKE es uno de los swaps del
selfhost-verify ⇒ a zlib le falta una LÍNEA en [deps], no una excepción.

  declarable   el store ya provee el path ⇒ arreglo mecánico    → NO cuenta
  irreducible  nadie lo provee ⇒ receta nueva o runtime base    → SÍ cuenta

harkaq-suggest.py cruza cada path de deuda contra el store (el mapeo de §4.1 al
revés: el path relativo dentro del artefacto ES el path del sandbox):
    /usr/bin/make          → declarar dep: make
    /usr/lib/libz.so.1.3.2 → irreducible (la zlib de hammer da libz.a estático,
                             no .so ⇒ NO se arregla declarando)
El diagnóstico deja de ser "algo pasó" y pasa a ser "hacé esto".

PRIMER BARRIDO (6 recetas C, fuentes cacheadas):
  Hermetico ............... 0
  sólo deuda DECLARABLE ... 5  ← y la deuda es EL MISMO path en las 5: /usr/bin/make
  con deuda IRREDUCIBLE ... 1  (brotli)

Lo que importa: la deuda de 5/6 es UN SOLO path y se arregla con una línea. No
hay cola larga de excepciones — que era el riesgo de muerte del §7.

Y el caso irreducible es la frontera CONOCIDA: brotli (C++) pide
/usr/lib/libstdc++.so.6.0.34 y /usr/lib/libgcc_s.so.1 — el runtime C++ del gcc de
Alpine, que hammer no construye. Es exactamente lo que la campaña "matar gcc" ya
tenía identificado. TERCERA vez que harkaq llega por su cuenta a una lista que
otro frente ya tenía: los swaps del selfhost-verify (§4.3), los binutils (§4.4)
y ahora el runtime C++.

Honestidad: 6 recetas no son 750 y son las fáciles. 1/6 = 17%, muy por encima del
<5% — pero el único caso es un problema conocido, nombrado y compartido con otros
dos frentes, no una cola de sorpresas. El barrido grande es trabajo de granja.

Bug del barrido encontrado por el propio barrido: copiar la receta a otro
directorio rompe la resolución de deps.build (son relativas al dir de la receta)
⇒ brotli fallaba con "no pude cargar la dep 'cmake'" y se descartaba EN SILENCIO
— o sea que se descartaban justo las recetas CON deps, las interesantes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:02:27 -04:00
sergioandClaude Opus 4.8 47982d7c6a harkaq: HITO DE FASE 1 — wiring en Rust; veredicto por fase sobre una receta real
crates/hammer-build/src/harkaq.rs + Sandbox::bwrap_args_harkaq: harkaq sale de
`hammer build`, no de un script. Deriva la política de la clausura declarada
(D1), lanza el lector, mete harkaq-exec como último eslabón antes del `sh -c`, y
recoge el Verdict por FASE (configure/compile/install son bwraps distintos ⇒
dominios distintos ⇒ un veredicto cada uno).

EL HITO, sobre recipes/zlib.toml (receta real del catálogo):
  configure →  Hermetico   esperadas (14): /usr/bin/gcc   deuda: ninguna ✓
  compile   →  Impuro      DEUDA (1): /usr/bin/make       → rc=126, la jaula lo frenó

El diagnóstico completo de zlib cabe en una frase: lo único que toma de Alpine
sin declararlo es `make`.

INERTE sin HARKAQ=1: mismos args de bwrap, mismo entorno, ningún proceso extra.
Requisito duro, no cortesía — 700+ artefactos sellados no pueden cambiar de hash
por encender un diagnóstico. Los 57 tests previos del crate siguen verdes sin
tocar, que es la prueba.

harkaq-policy es PURO y se testea sin kernel (4 tests nuevos, como pide §4):
traduce store→sandbox, excluye la metadata `.hammer/` del artefacto, y deja
listables los ancestros de cada fichero de la clausura.

La integración se delató sola en su PRIMERA corrida real: faltaba /cache como
superficie de contrato (ZIG_GLOBAL_CACHE_DIR=/cache/zig — zig CREA dirs ahí) y
la fase moría con `fs.make_dir /cache/zig/tmp`. De ahí que las superficies de
contrato las decida el Sandbox (que sabe qué montó) y no harkaq: /cache sólo
existe si hay cache_dir, y una política que nombre un path inexistente aborta el
build a propósito.

libc en hammer-build: Child::kill() manda SIGKILL y el lector moriría MUDO, sin
emitir el Verdict — justo lo que no queremos de un componente cuyo producto ES
el veredicto. Hace falta SIGTERM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:55:29 -04:00
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
sergioandClaude Opus 4.8 77317a7291 harkaq: Verdict clasificado (base|esperada|deuda) + el runtime base es por forma de build
harkaq-verdict.py: clasifica las denegaciones crudas. Va DELIBERADAMENTE fuera
del lector — éste corre con CAP_AUDIT_READ y clasificar es POLÍTICA, no
privilegio (mismo argumento que Q1c). De yapa: se itera sin recompilar el
binario capabilitado y sin perder el setcap. Las expectativas viajan en la
propia política como `# expect <path>` (una sola fuente de verdad; harkaq-exec
las ignora como comentario). SinEvidencia manda sobre todo: si el lector no es
confiable no se clasifica nada — reinterpretarlo sería el falso Hermetico que el
canario existe para impedir.

  MODE=base  rc=0  Hermetico  esperadas: /usr/bin/gcc  deuda: ninguna ✓
  MODE=zlib  rc=1  Impuro     DEUDA: /usr/lib/libz.so.1.3.2

§4.3 — ¿aguantan los 5 paths en otra forma de build? Medido con un configure de
autotools REAL (libgpg-error, MODE=configure):

  ronda 1: /bin/bash, /bin/coreutils
  ronda 2: libacl, libattr, libcrypto, libreadline, libutmps
  ronda 3: libncursesw, libskarnet
  ronda 4: /usr/bin/c89, /usr/bin/c99, /usr/bin/ldd, /usr/bin/make

NO: el runtime base es POR FORMA DE BUILD (~16 entradas para autotools, no 5).
Pero converge en 3 rondas (34 denegaciones → 5) y se queda chico ⇒ la métrica de
<5% de la Fase 2 sigue siendo plausible.

Hallazgo: **el runtime base no es una lista de binarios, es su CIERRE de .so**
(bash arrastra libreadline+libncursesw; coreutils arrastra libacl+libattr). Es
computable con ldd, no adivinable ⇒ harkaq-policy debe derivarlo igual que
deriva la clausura de las deps. Misma idea, otro origen.

Y el residuo es todo señal, nada ruido: c89/c99/ldd son sondas de compilador de
Alpine (→ esperadas, como gcc) y /usr/bin/make es una decisión de diseño real —
hoy los builds de hammer usan el make de Alpine sin declararlo. Es EXACTAMENTE
lo que los swaps del selfhost-verify reemplazan uno a uno: la lista de deuda de
harkaq y la lista de swaps del bootstrap son la MISMA lista, descubierta por dos
caminos independientes. Que coincidan es la mejor validación externa del método.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:35:42 -04:00
sergioandClaude Opus 4.8 61171bfe7f 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>
2026-07-15 15:28:47 -04:00
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
sergioandClaude Opus 4.8 4eecba06f4 harkaq: Fase 1 — la cadena corre de punta a punta (Hermetico / Impuro sobre kernel real)
Tres piezas nuevas, las del §4 del SDD:
  - harkaq-audit.c  lector de evidencia; HOST junto a hammerd, CAP_AUDIT_READ;
                    espera el canario (que revela el domain=), junta las
                    denegaciones de ESE dominio, cruza contra el contador del
                    kernel al liberarse, y emite el Verdict JSON de 3 estados.
  - harkaq-exec.c   aplica la política y execea el builder; DENTRO de bwrap,
                    estático (rootfs musl). Negocia el ABI por syscall (D7:
                    <min rechaza, <7 avisa SIN EVIDENCIA) y pone
                    LOG_NEW_EXEC_ON + TSYNC.
  - harkaq-run.sh   encadena lector+bwrap+exec, pone el canario con nonce y
                    espera a que el lector confirme que escucha (--ready-fd)
                    antes de arrancar el build: sin esa barrera el canario se
                    emitiría sin nadie escuchando y daría SinEvidencia por una
                    carrera, no por un problema real.

El hito, con comandos sintéticos sobre el kernel real (ABI 10):
  limpio  {"estado":"Hermetico","canario_visto":true,"contador_kernel":1,"denials":[]}
  impuro  {"estado":"Impuro","canario_visto":true,"contador_kernel":2,
           "denials":[{"blockers":"fs.read_file","path":"…/README.md",…}]}
Canario visto y contador coherente en ambos ⇒ los 3 estados de D9 funcionan.

Q1c contestada en la práctica: el privilegio vive en un helper chico con
`setcap cap_audit_read,cap_audit_control`, NO en hammerd entero.

Sutileza medida: lo que DAC ya bloquea es INVISIBLE para harkaq (el hook del
LSM no llega a correr). Descubierto porque el escenario "impuro" con
/root/.bashrc (drwx------) dio Hermetico: DAC lo bloqueó antes que Landlock.
No rompe D2 (si DAC lo bloqueó, el build tampoco lo usó ⇒ Hermetico es cierto)
pero acota el diagnóstico, y al escribir tests hay que elegir paths que DAC
permita o el control no controla nada.

Falta para cerrar Fase 1: harkaq-policy derivando de la clausura real + el
contraste receta-de-Alpinizada vs. receta-que-no. El rebuild del kernel
(SECURITY_LANDLOCK) NO bloquea el hito: sólo hace falta para que harkaq corra
DENTRO de hammer; el laptop ya está en ABI 10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:04:27 -04:00
sergioandClaude Opus 4.8 6419f2a952 harkaq: Q1b CERRADA — la evidencia cruza el userns y el canario es la clave primaria
Banco: scripts/harkaq/q1b-attrib.c. Dos builds CONCURRENTES en bwrap
--unshare-all (los ns de Sandbox::bwrap_args), sin privilegio (uid 1000),
canarios con nonce distinto, lector en el host como root.

(a)  Los registros CRUZAN el userns/pidns/mountns (6 registros llegaron al
    lector del host) y cruzan con builds SIN privilegio, que es como corren de
    verdad. ⇒ harkaq-audit vive en el host junto a hammerd; la arquitectura
    del §4 se sostiene.
(b)  El canario con nonce ATRIBUYE: AAAA=1 BBBB=1, cero cruces con 2 builds
    en paralelo. ⇒ la granja puede construir en paralelo sin contaminarse la
    evidencia entre builds.

Tres hallazgos colaterales que valen más que el veredicto:
  - El contador quedó VALIDADO: dos `deallocated denials=1`, uno por dominio,
    cuadrando con 1 ACCESS cada uno ⇒ el chequeo anti-pérdida (nº ACCESS ==
    denials) está medido, no supuesto.
  - La identidad sobrevive a los ns: pid=8498 uid=1000 son del HOST (adentro
    del pidns la víctima es pid 1).
  - dev+ino identifican el OBJETO, no el BUILD: los 2 canarios dan el mismo
    ino=4457713 (ambos bindean /etc/hostname). Atribuir por dev/ino habría
    fundido dos builds leyendo el mismo fichero no declarado — el caso más
    común que harkaq va a ver. La clave es domain=, y el canario la revela.

⇒ D9 hace TRES trabajos con un mecanismo: honestidad (§3.4), clave primaria
(§3.5) y, vía el contador, detección de pérdida.

Nota de método: esta corrida también falló 2 veces más, y las 2 el banco
afirmó algo falso con total confianza. (1) `cat $canario >/dev/null` — /dev no
estaba en la clausura ⇒ sh fallaba en el REDIRECT y cat no corría; el rc=1 no
era del canario. (2) Como root, bwrap no pudo ni ejecutar el binario y el
veredicto imprimió "NADA cruzó ⇒ replantear la arquitectura" a partir de cero
estímulo (causa: --unshare-all mapea sólo uid 0→0; CAP_DAC_OVERRIDE en userns
sólo vale sobre uids MAPEADOS ⇒ root-en-userns no atraviesa un home
drwx------). Fix: drop_privs() + gate de estímulo (exit 4 si una víctima no
sale con status 0).

Van 3 veces en una sesión que un cero fue el instrumento y no el kernel, con
quien escribía el banco mirando de frente ese riesgo. harkaq sin canario no es
un riesgo de configuración: es el comportamiento por defecto.

Quedan Q1c (privilegio de harkaq-audit: mejor un helper acotado que hammerd
entero) y Q1d (qué ruta reporta exe= con --bind /src).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 14:42:12 -04:00
sergioandClaude Opus 4.8 ad0948cfb0 harkaq: Q1 CERRADA — la evidencia llega; + D9 (canario deliberado) y el banco de pruebas
Q1 era el gate del proyecto (¿llegan los registros AUDIT_LANDLOCK_* a un
lector?). Medido en el laptop (ABI 10) con scripts/harkaq/q1-audit.c:

   VIABLE. El multicast AUDIT_NLGRP_READLOG entrega
     blockers=fs.read_file path="/etc/passwd" dev="nvme1n1p6" ino=4458369
     + exe=/comm= del que lo intentó. Es el diagnóstico de de-Alpinización
     que el SDD promete, saliendo del kernel sin instrumentar nada.

Dos precondiciones que el Draft 2 no tenía, ambas medidas:
  - audit_enabled=1 NO viene de fábrica (sin audit=1 en cmdline, sin auditd)
    ⇒ cero registros de cualquier escenario. AUDIT_SET o audit=1 horneado.
  - LOG_NEW_EXEC_ON es OBLIGATORIO: same-exec=2 registros, new-exec=0,
    new-exec-logon=4. harkaq restringe y DESPUÉS execea el builder ⇒ sin el
    flag certificaría herméticos TODOS los builds, denials=[], sin un error.

D9 (nueva, no negociable): el canario lo fabrica harkaq. Se investigó si el
registro `deallocated denials=N` servía de verificación cruzada gratis: NO.
Medido con ventana de 6s, un dominio con flags default y denegación post-exec
no emite NADA (ni allocated, ni access, ni el contador); y el `allocated` es
perezoso (llega pegado al primer denial logueado) ⇒ su ausencia tampoco prueba
nada. Un build hermético y un lector ciego son bit-idénticos: denials=[]. El
Verdict pasa a 3 estados (Hermetico / Impuro / SinEvidencia).
El contador SÍ sirve para lo otro: nº ACCESS == denials caza registros
perdidos por backlog (riesgo real con la granja en paralelo).

Nota de método (§3.4): la 1ª corrida dio 0 en los 3 escenarios y "confirmaba"
la hipótesis. Era el instrumento roto (AUDIT_GET con NLM_F_ACK: el ACK llegaba
antes que la respuesta → enabled=-1 → nunca se encendía el audit). Se cazó
sólo porque el CONTROL (same-exec) también dio 0, y eso era imposible. Es el
modo de falla de harkaq en vivo sobre su propio banco. El test ahora aborta
(exit 4) si no confirma audit_enabled=1.

Nuevas Q1b (¿el lector ve a través del userns de bwrap? ¿cómo se atribuye un
registro a SU build con la granja en paralelo?) y Q1c (privilegio de
harkaq-audit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:23:19 -04:00
sergioandClaude Opus 4.8 9478f4c065 docs: SDD 16 harkaq — Draft 2 reescrito contra la realidad del builder (bwrap, no nix)
El Draft 1 estaba escrito contra el sandbox de nix. hammer no construye con
nix: construye con bubblewrap (ADR 0004, hammer-build/src/sandbox.rs). Eso
invalidaba por irrelevancia el argumento anti-nix (§0), las FOD y su red
abierta (§6 + D8), Q5, y la Fase 3 del fetcher mediado — que el propio doc
marcaba como el mayor riesgo de cronograma, de "meses".

Fase 0 ejecutada (5 minutos, no días):
  - ABI Landlock = 10 en el laptop (techo: audit/7, TSYNC/8, quiet/10).
  - Kernel de hammer 6.16.12-metal: CONFIG_SECURITY_LANDLOCK ausente, pero
    CONFIG_LSM ya lista landlock ⇒ falta un flag, no una versión. Q2 cerrada.
  - Cero FODs: download.rs baja con curl en el HOST y --unshare-all incluye
    --unshare-net ⇒ el fetcher mediado de D8 ya está construido. Fase 3 se
    elimina.

La brecha real es otra y paga mejor: el sandbox monta un rootfs Alpine ENTERO
como capa base ⇒ política ⊋ clausura ⇒ una receta usa Alpine sin declararlo y
pasa en verde. Es el bug que la campaña de de-Alpinización caza a mano. D1 +
audit de ABI 7 lo vuelve mecánico (path/dev/ino de lo no declarado).

Encaje en el modelo de confianza (recipe.rs:229): H1 garantiza que lo
declarado PASA; harkaq que lo declarado BASTA. Complementos simétricos.

Sobreviven intactos D1 (política derivada), D2 (evidencia negativa) y D3
(cero quieting). Nuevo Q1 = el riesgo técnico real: cómo llegan los registros
AUDIT_LANDLOCK_* al lector (CAP_AUDIT_READ, auditd, userns, flags de exec).

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