tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Sutichaykuna

⎇ 7c794568a963b18b68d747959e6bbd07cf2ebff7 scripts/harkaq/q1b-attrib.c C · 260 siq'ikuna · 11.7 KB Ñawpa willay Pi imata llamkarqan ch'uya

harkaq: la granja VPS ya puede ser el compilador continuo — golden bumpeada a 6.17 (ABI 7) cba59e829 · sergio ·

1// Q1b de harkaq (SDD 16 §8): ¿el lector del host ve las denegaciones de ADENTRO de bwrap,
2// y se puede atribuir cada registro a SU build con la granja corriendo en paralelo?
3//
4// Q1 probó que la evidencia llega, pero con la víctima corriendo en el host. El caso real es
5// otro: `Sandbox::run` lanza bwrap con --unshare-all (user/pid/mount/net/ipc/uts ns) y el
6// lector (harkaq-audit) vive AFUERA, junto a hammerd. Dos preguntas:
7//
8//   (a) ¿Cruzan los registros el userns/pidns? El audit no está namespaceado, así que
9//       DEBERÍA — pero medirlo es el punto.
10//   (b) ATRIBUCIÓN: con N builds en paralelo hay N dominios. ¿Cuál es de quién?
11//       Hipótesis: el canario de D9 lo resuelve gratis. Si cada build usa un canario con
12//       nonce ÚNICO, el primer registro logueado trae path="/harkaq-canary-<nonce>" ⇒
13//       identifica el build sin depender del pid (que el pidns reescribe) ⇒ y de paso
14//       revela su domain=, que ata todos los registros siguientes de ese build.
15//
16// Uso (como root):  ./q1b-attrib            → lanza 2 víctimas concurrentes y atribuye
17//                   ./q1b-attrib victim <canario>   (uso interno, dentro de bwrap)
18
19#define _GNU_SOURCE
20#include <errno.h>
21#include <fcntl.h>
22#include "harkaq-uapi.h"
23#include <linux/netlink.h>
24#include <stdio.h>
25#include <stdlib.h>
26#include <string.h>
27#include <grp.h>
28#include <sys/prctl.h>
29#include <sys/socket.h>
30#include <sys/syscall.h>
31#include <sys/wait.h>
32#include <time.h>
33#include <unistd.h>
34
35static int ll_create(const struct landlock_ruleset_attr *a, size_t n, __u32 f) {
36    return syscall(SYS_landlock_create_ruleset, a, n, f);
37}
38static int ll_add(int fd, enum landlock_rule_type t, const void *a, __u32 f) {
39    return syscall(SYS_landlock_add_rule, fd, t, a, f);
40}
41static int ll_restrict(int fd, __u32 f) {
42    return syscall(SYS_landlock_restrict_self, fd, f);
43}
44
45#define ACCESS_READ                                               \
46    (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR | \
47     LANDLOCK_ACCESS_FS_EXECUTE)
48
49// ------------------------------------------------------------------ víctima (dentro de bwrap)
50
51// Política: todo lo que sh/cat necesitan, y NADA más. El canario queda deliberadamente fuera
52// ⇒ exactamente UNA denegación esperada, con un path que identifica a este build.
53static int victim(const char *canary) {
54    struct landlock_ruleset_attr attr = {.handled_access_fs = ACCESS_READ};
55    int rs = ll_create(&attr, sizeof(attr), 0);
56    if (rs < 0) { perror("[victima] create_ruleset"); return 2; }
57    // /tmp NO va en la clausura: es donde vive el canario (único punto de montaje escribible
58    // bajo `--ro-bind / /`, ver README). Todo lo demás es lo que sh/cat necesitan para correr.
59    const char *allowed[] = {"/usr", "/lib", "/lib64", "/bin", "/etc", "/proc", NULL};
60    for (int i = 0; allowed[i]; i++) {
61        int dfd = open(allowed[i], O_PATH | O_CLOEXEC);
62        if (dfd < 0) continue;
63        struct landlock_path_beneath_attr pb = {.allowed_access = ACCESS_READ, .parent_fd = dfd};
64        ll_add(rs, LANDLOCK_RULE_PATH_BENEATH, &pb, 0);
65        close(dfd);
66    }
67    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { perror("[victima] nnp"); return 2; }
68    // LOG_NEW_EXEC_ON: sin esto no se loguea nada tras el execve (Q1/§3.4). Obligatorio.
69    if (ll_restrict(rs, LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON) < 0) {
70        perror("[victima] restrict_self");
71        return 2;
72    }
73    close(rs);
74    // Post-exec, como el builder real (bwrap → sh -c → make).
75    // Sin redirects: `>/dev/null` necesita /dev, que NO está en la clausura ⇒ sh fallaba en la
76    // redirección y `cat` no llegaba a correr nunca. El rc=1 venía del redirect, no del canario:
77    // el test habría medido la denegación equivocada y "pasado" igual. Mantener el comando
78    // mínimo, y que el único acceso fuera de la clausura sea EL CANARIO.
79    char cmd[512];
80    snprintf(cmd, sizeof(cmd), "cat %s; echo \"[victima] canario=%s cat rc=$?\" >&2", canary,
81             canary);
82    execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
83    perror("[victima] execl");
84    return 2;
85}
86
87// ------------------------------------------------------------------ netlink
88
89static int nl_send(int fd, int type, const void *payload, size_t len) {
90    struct { struct nlmsghdr h; char data[256]; } req = {0};
91    req.h.nlmsg_len = NLMSG_LENGTH(len);
92    req.h.nlmsg_type = type;
93    req.h.nlmsg_flags = NLM_F_REQUEST;
94    req.h.nlmsg_seq = 1;
95    req.h.nlmsg_pid = getpid();
96    if (payload) memcpy(req.data, payload, len);
97    struct sockaddr_nl sa = {.nl_family = AF_NETLINK};
98    return sendto(fd, &req, req.h.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa));
99}
100
101static int audit_status(int *enabled) {
102    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
103    if (fd < 0) return -1;
104    if (nl_send(fd, AUDIT_GET, NULL, 0) < 0) { close(fd); return -1; }
105    struct timeval tv = {.tv_sec = 1};
106    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
107    char buf[8192];
108    for (int i = 0; i < 8; i++) {
109        ssize_t n = recv(fd, buf, sizeof(buf), 0);
110        if (n <= 0) break;
111        for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n); h = NLMSG_NEXT(h, n))
112            if (h->nlmsg_type == AUDIT_GET) {
113                *enabled = ((struct audit_status *)NLMSG_DATA(h))->enabled;
114                close(fd);
115                return 0;
116            }
117    }
118    close(fd);
119    return -1;
120}
121
122static int audit_enable(void) {
123    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
124    if (fd < 0) return -1;
125    struct audit_status st = {.mask = AUDIT_STATUS_ENABLED, .enabled = 1};
126    int r = nl_send(fd, AUDIT_SET, &st, sizeof(st));
127    close(fd);
128    return r < 0 ? -1 : 0;
129}
130
131// ------------------------------------------------------------------ driver
132
133// Baja a un uid sin privilegio antes de lanzar bwrap. Dos razones:
134//  1. Corrección: bwrap con --unshare-all mapea SÓLO uid 0→0. Los ficheros de otro uid quedan
135//     sin mapear (aparecen como `nobody`) y CAP_DAC_OVERRIDE dentro de un userns sólo vale
136//     sobre uids MAPEADOS ⇒ root-en-userns no puede atravesar un home drwx------ y el exec
137//     del binario falla con EACCES. Costó una corrida entera diagnosticarlo.
138//  2. Fidelidad: modela el despliegue real — harkaq-audit privilegiado (CAP_AUDIT_READ),
139//     los builds sin privilegio. Si la evidencia cruzara sólo con builds root, no serviría.
140static void drop_privs(void) {
141    const char *su = getenv("SUDO_UID"), *sg = getenv("SUDO_GID");
142    if (!su) return;  // no venimos de sudo: nada que bajar
143    uid_t uid = (uid_t)atoi(su);
144    gid_t gid = sg ? (gid_t)atoi(sg) : uid;
145    if (setgroups(0, NULL) < 0) perror("[q1b] setgroups");
146    if (setgid(gid) < 0) { perror("[q1b] setgid"); _exit(2); }
147    if (setuid(uid) < 0) { perror("[q1b] setuid"); _exit(2); }
148}
149
150static void spawn_victim(const char *self, const char *canary_src, const char *canary_path) {
151    // bwrap con los mismos ns que Sandbox::bwrap_args (--unshare-all). El canario se bindea
152    // en una ruta fija que la política de landlock NO concede.
153    execlp("bwrap", "bwrap", "--unshare-all", "--ro-bind", "/", "/", "--tmpfs", "/tmp",
154           "--ro-bind", canary_src, canary_path, "--die-with-parent", self, "victim",
155           canary_path, (char *)NULL);
156    perror("execlp bwrap");
157    _exit(2);
158}
159
160int main(int argc, char **argv) {
161    if (argc >= 3 && !strcmp(argv[1], "victim")) return victim(argv[2]);
162
163    char self[4096];
164    ssize_t sl = readlink("/proc/self/exe", self, sizeof(self) - 1);
165    if (sl < 0) { perror("readlink"); return 1; }
166    self[sl] = 0;
167
168    int enabled = -1;
169    audit_status(&enabled);
170    if (enabled != 1) {
171        fprintf(stderr, "[q1b] audit enabled=%d → AUDIT_SET\n", enabled);
172        audit_enable();
173        usleep(100000);
174        audit_status(&enabled);
175    }
176    // Mismo gate que q1-audit: sin audit_enabled=1 un 0 no significa "sin denegaciones",
177    // significa "el instrumento no mide". Abortar, nunca reportar.
178    if (enabled != 1) {
179        fprintf(stderr, "[q1b] ABORTO: no pude confirmar audit_enabled=1 (¿root?)\n");
180        return 4;
181    }
182
183    int nl = socket(AF_NETLINK, SOCK_RAW, NETLINK_AUDIT);
184    struct sockaddr_nl sa = {.nl_family = AF_NETLINK,
185                             .nl_groups = 1 << (AUDIT_NLGRP_READLOG - 1)};
186    if (nl < 0 || bind(nl, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
187        perror("[q1b] bind multicast (¿CAP_AUDIT_READ?)");
188        return 1;
189    }
190    fprintf(stderr, "[q1b] lector del HOST escuchando; audit enabled=%d\n", enabled);
191
192    // DOS builds concurrentes, canarios con nonce distinto. Si la atribución por canario
193    // funciona, cada registro cae inequívocamente en su build sin mirar el pid.
194    const char *canaries[] = {"/tmp/harkaq-canary-AAAA", "/tmp/harkaq-canary-BBBB"};
195    pid_t kids[2];
196    for (int i = 0; i < 2; i++) {
197        if ((kids[i] = fork()) == 0) {
198            usleep(300000);
199            drop_privs();
200            spawn_victim(self, "/etc/hostname", canaries[i]);
201        }
202        fprintf(stderr, "[q1b] build %d: bwrap pid=%d canario=%s\n", i, kids[i], canaries[i]);
203    }
204
205    struct timeval tv = {.tv_sec = 1};
206    setsockopt(nl, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
207    char buf[16384];
208    int seen[2] = {0, 0}, other = 0, total = 0;
209    time_t deadline = time(NULL) + 8;
210    while (time(NULL) < deadline) {
211        ssize_t n = recv(nl, buf, sizeof(buf), 0);
212        if (n <= 0) continue;
213        for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, n); h = NLMSG_NEXT(h, n)) {
214            if (h->nlmsg_type != AUDIT_LANDLOCK_ACCESS && h->nlmsg_type != AUDIT_LANDLOCK_DOMAIN)
215                continue;
216            total++;
217            const char *rec = (const char *)NLMSG_DATA(h);
218            printf("[%s] %.*s\n",
219                   h->nlmsg_type == AUDIT_LANDLOCK_ACCESS ? "ACCESS" : "DOMAIN",
220                   (int)NLMSG_PAYLOAD(h, 0), rec);
221            int matched = 0;
222            for (int i = 0; i < 2; i++)
223                if (strstr(rec, canaries[i])) { seen[i]++; matched = 1; }
224            if (!matched && h->nlmsg_type == AUDIT_LANDLOCK_ACCESS) other++;
225        }
226    }
227    // GATE DEL INSTRUMENTO: si la víctima no corrió, "0 registros" no significa "nada cruzó",
228    // significa "no medí nada". La corrida anterior imprimió "NADA cruzó ⇒ replantear la
229    // arquitectura" cuando bwrap ni siquiera había podido ejecutar el binario (EACCES). Un
230    // veredicto que no verifica su propio estímulo es exactamente el falso 'hermético' de D9,
231    // pero del lado del banco de pruebas. Tercera vez en esta sesión: por eso es un gate.
232    int bad = 0;
233    for (int i = 0; i < 2; i++) {
234        int st = 0;
235        waitpid(kids[i], &st, 0);
236        if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) {
237            fprintf(stderr, "[q1b] build %d NO corrió limpio (status=%d)\n", i, st);
238            bad = 1;
239        }
240    }
241    if (bad) {
242        fprintf(stderr,
243                "\n[q1b] ABORTO: alguna víctima no llegó a ejecutarse ⇒ el estímulo no existió.\n"
244                "      Cualquier conteo de registros sería del instrumento, no del kernel.\n");
245        return 4;
246    }
247
248    fprintf(stderr, "\n[q1b] ── VEREDICTO ──\n");
249    fprintf(stderr, "[q1b] (a) ¿cruzan el userns de bwrap?  %s (%d registros landlock)\n",
250            total > 0 ? "SÍ" : "NO", total);
251    fprintf(stderr, "[q1b] (b) atribución por canario: AAAA=%d  BBBB=%d  (otros ACCESS=%d)\n",
252            seen[0], seen[1], other);
253    if (seen[0] >= 1 && seen[1] >= 1)
254        fprintf(stderr, "[q1b] ⇒ el canario con nonce ATRIBUYE: 2 builds paralelos, sin ambigüedad.\n");
255    else if (total == 0)
256        fprintf(stderr, "[q1b] ⇒ NADA cruzó. harkaq-audit no puede vivir en el host: replantear.\n");
257    else
258        fprintf(stderr, "[q1b] ⇒ registros SÍ, atribución NO. Hace falta otra clave (¿domain=?).\n");
259    return 0;
260}