// 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 ] [--dir ]... [--prefix ]... [-o ] -- ... // // --fs marca el FILESYSTEM que contiene (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 marca un directorio suelto, sólo hijos directos (modo self-test sin // privilegios — suficiente para probar el ciclo, NO para un build real). // --prefix

filtra la salida a paths bajo

(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 #include #include #include #include #include #include #include #include #include #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; }