harkaq-trace: la traza POSITIVA de la clausura (plan-freebsd T1.1-T1.2, prior art filemon/META MODE)
- harkaq-trace.c: fanotify FAN_MARK_FILESYSTEM — agnóstico de mount-ns, el tracer en el HOST ve los open() de dentro del bwrap (ptrace lo deniega el seccomp D4; audit sólo emite denegaciones). Crudo y tonto a propósito (reparto Q1c); D9: sin fanotify ⇒ exit 3 SinEvidencia, jamás traza vacía. - harkaq-trace-norm.py: canoniza (drop /proc /sys /dev /tmp /out /src, dedupe, orden estable — la lección nº1 de META MODE) + atribución por dep con --resumen = la señal de PODA (T1.4): dep declarada con 0 toques = grasa. - VALIDADO: modo --fs como root (worker, 5s, sin tocar colas): capturó exactamente zlib.h + 3×libz.a de un cat/head; laptop sin privilegio ⇒ SinEvidencia honesto. Piloto completo (traza de un hammer build real + resumen de poda) pendiente de un rebuild natural en el worker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""harkaq-trace-norm — canoniza la traza cruda de harkaq-trace (plan-freebsd T1.2).
|
||||
|
||||
harkaq-trace-norm.py <traza-cruda> [--store <dir>] [--drop <prefijo>]... [--resumen]
|
||||
|
||||
Las reglas de normalización son la parte que FreeBSD tardó años en iterar (los `.meta` de
|
||||
META MODE): el ruido no se CALLA, se clasifica con reglas explícitas — D3 aplica acá igual
|
||||
que en el verdict. Reglas:
|
||||
|
||||
1. DROP por prefijo: /proc /sys /dev /tmp /out /src (el build escribe /out y lee /src por
|
||||
contrato; lo interesante es lo demás). Ampliable con --drop.
|
||||
2. DEDUPE + ORDEN ESTABLE: la traza cruda trae un evento por open(); el mismo path N veces
|
||||
no es señal (el orden de llegada tampoco: readdir no garantiza orden — la lección
|
||||
número uno de META MODE). Salida: paths únicos, ordenados.
|
||||
3. ATRIBUCIÓN por dep (--store): un path store/<hash>-<nombre>/resto se atribuye a la dep
|
||||
<nombre>. Con --resumen emite el conteo por dep — la señal de PODA (T1.4): una dep
|
||||
declarada con 0 toques es grasa de la clausura.
|
||||
|
||||
El sellado (T1.3) NO es de este script: la salida canónica es determinista a propósito
|
||||
(orden estable, sin timestamps) para que `b3sum` del worker la selle tal cual y viaje junto
|
||||
al of_tree. Un script canoniza, el store sella — cada pieza tonta en lo suyo.
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
DROP_DEFAULT = ["/proc", "/sys", "/dev", "/tmp", "/out", "/src", "/run"]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("traza")
|
||||
ap.add_argument("--store", default=None,
|
||||
help="raíz del store para atribuir paths a deps (store/<hash>-<nombre>/…)")
|
||||
ap.add_argument("--drop", action="append", default=[],
|
||||
help="prefijo adicional a descartar (repetible)")
|
||||
ap.add_argument("--resumen", action="store_true",
|
||||
help="emite conteo por dep en vez de la lista canónica")
|
||||
args = ap.parse_args()
|
||||
|
||||
drops = DROP_DEFAULT + args.drop
|
||||
store = str(Path(args.store).resolve()) if args.store else None
|
||||
# store/<hash-hex>-<nombre>/… — el nombre puede traer guiones; el hash no.
|
||||
re_dep = re.compile(re.escape(store) + r"/[0-9a-f]{16,}-([^/]+)/") if store else None
|
||||
|
||||
unicos = set()
|
||||
por_dep = Counter()
|
||||
with open(args.traza) as f:
|
||||
for linea in f:
|
||||
p = linea.strip()
|
||||
if not p or any(p == d or p.startswith(d + "/") for d in drops):
|
||||
continue
|
||||
if p not in unicos and re_dep:
|
||||
m = re_dep.match(p)
|
||||
if m:
|
||||
por_dep[m.group(1)] += 1
|
||||
unicos.add(p)
|
||||
|
||||
if args.resumen:
|
||||
for dep, n in sorted(por_dep.items()):
|
||||
print(f"{n:6d} {dep}")
|
||||
else:
|
||||
for p in sorted(unicos):
|
||||
print(p)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,184 @@
|
||||
// harkaq-trace — la traza POSITIVA de la clausura (plan-freebsd T1.1; prior art: el
|
||||
// filemon/META MODE de FreeBSD, 12 años de builds midiendo "qué tocó este target").
|
||||
//
|
||||
// harkaq-audit lee lo DENEGADO (evidencia negativa bajo política=clausura). Esto lee lo
|
||||
// ABIERTO (evidencia positiva). Juntas cierran el bucle en las dos direcciones que el SDD 16
|
||||
// sólo cierra en una: la deuda (usó sin declarar → audit) y la GRASA (declaró y jamás tocó →
|
||||
// esta traza). La grasa hoy es invisible y se hereda a la política runtime del cierre §3.
|
||||
//
|
||||
// harkaq-trace [--fs <path>] [--dir <path>]... [--prefix <pfx>]... [-o <out>] -- <cmd>...
|
||||
//
|
||||
// --fs <path> marca el FILESYSTEM que contiene <path> (FAN_MARK_FILESYSTEM; pide
|
||||
// CAP_SYS_ADMIN — el modo del piloto en el worker, que corre como root).
|
||||
// La marca de filesystem es AGNÓSTICA de mount-namespaces: el tracer vive
|
||||
// en el HOST y ve los open() que el build hace DENTRO del bwrap sin cruzar
|
||||
// ningún ns — la razón por la que fanotify le gana a ptrace acá (ptrace
|
||||
// además lo deniega el seccomp D4 del propio sandbox) y a audit (que sólo
|
||||
// emite DENEGACIONES, nunca concesiones usadas).
|
||||
// --dir <path> marca un directorio suelto, sólo hijos directos (modo self-test sin
|
||||
// privilegios — suficiente para probar el ciclo, NO para un build real).
|
||||
// --prefix <p> filtra la salida a paths bajo <p> (repetible). En el worker: el store —
|
||||
// exactamente la pregunta "¿qué parte de la clausura concedida se usó?".
|
||||
//
|
||||
// Salida: un path por línea, CRUDO y con duplicados. Clasificar, deduplicar y canonizar es
|
||||
// del normalizador (harkaq-trace-norm.py) — el mismo reparto que harkaq-audit/harkaq-verdict:
|
||||
// el proceso con privilegio se mantiene lo más tonto posible (argumento Q1c).
|
||||
//
|
||||
// D9 (canario): si fanotify no arranca (sin privilegio en --fs, kernel sin soporte), el exit
|
||||
// es 3 = SinEvidencia. Jamás una traza vacía que se lea como "el build no usó nada".
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <poll.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/fanotify.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define MAX_MARKS 32
|
||||
#define MAX_PREFIJOS 32
|
||||
|
||||
static const char *marcas_fs[MAX_MARKS];
|
||||
static const char *marcas_dir[MAX_MARKS];
|
||||
static const char *prefijos[MAX_PREFIJOS];
|
||||
static int n_fs = 0, n_dir = 0, n_pref = 0;
|
||||
|
||||
static int pasa_filtro(const char *path) {
|
||||
if (n_pref == 0)
|
||||
return 1;
|
||||
for (int i = 0; i < n_pref; i++)
|
||||
if (strncmp(path, prefijos[i], strlen(prefijos[i])) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *salida = NULL;
|
||||
int i = 1;
|
||||
for (; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--fs") == 0 && i + 1 < argc && n_fs < MAX_MARKS)
|
||||
marcas_fs[n_fs++] = argv[++i];
|
||||
else if (strcmp(argv[i], "--dir") == 0 && i + 1 < argc && n_dir < MAX_MARKS)
|
||||
marcas_dir[n_dir++] = argv[++i];
|
||||
else if (strcmp(argv[i], "--prefix") == 0 && i + 1 < argc && n_pref < MAX_PREFIJOS)
|
||||
prefijos[n_pref++] = argv[++i];
|
||||
else if (strcmp(argv[i], "-o") == 0 && i + 1 < argc)
|
||||
salida = argv[++i];
|
||||
else if (strcmp(argv[i], "--") == 0) {
|
||||
i++;
|
||||
break;
|
||||
} else {
|
||||
fprintf(stderr, "harkaq-trace: arg desconocido: %s\n", argv[i]);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (i >= argc || (n_fs + n_dir) == 0) {
|
||||
fprintf(stderr,
|
||||
"uso: harkaq-trace [--fs p] [--dir p]... [--prefix p]... [-o out] -- cmd...\n");
|
||||
return 2;
|
||||
}
|
||||
char **cmd = &argv[i];
|
||||
|
||||
FILE *out = stdout;
|
||||
if (salida) {
|
||||
out = fopen(salida, "w");
|
||||
if (!out) {
|
||||
perror("harkaq-trace: fopen salida");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// FAN_CLASS_NOTIF: sólo observamos, no autorizamos — la autorización es de Landlock
|
||||
// (harkaq-exec). FAN_CLOEXEC: el hijo no hereda el grupo (se auto-trazaría el exec).
|
||||
int fan = fanotify_init(FAN_CLASS_NOTIF | FAN_CLOEXEC, O_RDONLY | O_CLOEXEC);
|
||||
if (fan < 0) {
|
||||
// D9: sin grupo fanotify no hay evidencia. 3 = SinEvidencia, mismo código que audit.
|
||||
fprintf(stderr, "harkaq-trace: SinEvidencia — fanotify_init: %s\n", strerror(errno));
|
||||
return 3;
|
||||
}
|
||||
for (int m = 0; m < n_fs; m++)
|
||||
if (fanotify_mark(fan, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, FAN_OPEN, AT_FDCWD,
|
||||
marcas_fs[m]) < 0) {
|
||||
fprintf(stderr, "harkaq-trace: SinEvidencia — mark fs %s: %s\n", marcas_fs[m],
|
||||
strerror(errno));
|
||||
return 3;
|
||||
}
|
||||
for (int m = 0; m < n_dir; m++)
|
||||
if (fanotify_mark(fan, FAN_MARK_ADD, FAN_OPEN | FAN_EVENT_ON_CHILD, AT_FDCWD,
|
||||
marcas_dir[m]) < 0) {
|
||||
fprintf(stderr, "harkaq-trace: SinEvidencia — mark dir %s: %s\n", marcas_dir[m],
|
||||
strerror(errno));
|
||||
return 3;
|
||||
}
|
||||
|
||||
pid_t hijo = fork();
|
||||
if (hijo < 0) {
|
||||
perror("harkaq-trace: fork");
|
||||
return 2;
|
||||
}
|
||||
if (hijo == 0) {
|
||||
execvp(cmd[0], cmd);
|
||||
perror("harkaq-trace: execvp");
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
// Bucle: drenar eventos mientras el hijo vive, y un último drenado tras su muerte
|
||||
// (la cola puede traer eventos rezagados del propio build).
|
||||
int rc_hijo = -1;
|
||||
int vivo = 1;
|
||||
char buf[16384], path[PATH_MAX + 1], link[64];
|
||||
while (1) {
|
||||
if (vivo) {
|
||||
int st;
|
||||
pid_t w = waitpid(hijo, &st, WNOHANG);
|
||||
if (w == hijo) {
|
||||
vivo = 0;
|
||||
rc_hijo = WIFEXITED(st) ? WEXITSTATUS(st) : 128 + WTERMSIG(st);
|
||||
}
|
||||
}
|
||||
struct pollfd p = {.fd = fan, .events = POLLIN};
|
||||
int pr = poll(&p, 1, vivo ? 200 : 150);
|
||||
if (pr < 0 && errno != EINTR) {
|
||||
perror("harkaq-trace: poll");
|
||||
break;
|
||||
}
|
||||
if (pr <= 0) {
|
||||
if (!vivo)
|
||||
break; // hijo muerto y cola seca: terminamos
|
||||
continue;
|
||||
}
|
||||
ssize_t n = read(fan, buf, sizeof(buf));
|
||||
if (n <= 0)
|
||||
continue;
|
||||
for (struct fanotify_event_metadata *ev = (void *)buf; FAN_EVENT_OK(ev, n);
|
||||
ev = FAN_EVENT_NEXT(ev, n)) {
|
||||
if (ev->vers != FANOTIFY_METADATA_VERSION) {
|
||||
fprintf(stderr, "harkaq-trace: version fanotify no casa\n");
|
||||
return 3;
|
||||
}
|
||||
if (ev->fd < 0)
|
||||
continue;
|
||||
// El propio tracer (y su fichero de salida) generan eventos: fuera.
|
||||
if (ev->pid == getpid()) {
|
||||
close(ev->fd);
|
||||
continue;
|
||||
}
|
||||
snprintf(link, sizeof(link), "/proc/self/fd/%d", ev->fd);
|
||||
ssize_t l = readlink(link, path, PATH_MAX);
|
||||
close(ev->fd);
|
||||
if (l <= 0)
|
||||
continue;
|
||||
path[l] = '\0';
|
||||
if (pasa_filtro(path))
|
||||
fprintf(out, "%s\n", path);
|
||||
}
|
||||
}
|
||||
if (out != stdout)
|
||||
fclose(out);
|
||||
// El rc del build viaja intacto: la traza es un side-channel, no un juez.
|
||||
return rc_hijo < 0 ? 2 : rc_hijo;
|
||||
}
|
||||
Reference in New Issue
Block a user