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 1463Shell 157Markdown 91Rust 853029 Definitions

⎇ main scripts/harkaq/harkaq-base-closure.py Python · 115 lines · 4.5 KB History Who touched what raw

harkaq: el runtime base DERIVADO (§4.3) + el diagnóstico sobre un configure real (§4.4) ed614e689 · sergio ·

1#!/usr/bin/env python3
2"""harkaq-base-closure — deriva el cierre dinámico del runtime base (SDD 16 §4.3).
3
4    harkaq-base-closure.py <rootfs> <binario-dentro> [<binario-dentro>...]
5
6Emite reglas `ro <path>` para cada binario y **todo su cierre transitivo de `.so`**, en las rutas
7tal como se ven DENTRO del sandbox.
8
9Por qué existe: §4.3 midió que el runtime base no es una lista de binarios sino su cierre.
10Conceder /bin/bash arrastra libreadline+libncursesw; /bin/coreutils arrastra libacl+libattr. Se
11descubrió a mano, en rondas, mirando denegaciones — pero es **computable**, así que mantenerlo a
12mano sería exactamente el "alguien mantiene un perfil de permisos" que D1 dice que mata a todos
13los sandboxes. Es D1 aplicado al runtime base: se deriva, no se escribe.
14
15No se usa `ldd`: resolvería contra el host, no contra el rootfs Alpine. Se leen los DT_NEEDED del
16ELF (readelf funciona sobre cualquier ELF, sea cual sea su libc) y se resuelve dentro del rootfs.
17"""
18import os
19import subprocess
20import sys
21
22# Rutas de búsqueda de ld-musl. El default de musl es /lib:/usr/local/lib:/usr/lib; Alpine no
23# suele traer ld-musl-*.path, pero si estuviera, mandaría — se lee si existe.
24SEARCH_POR_DEFECTO = ["/lib", "/usr/local/lib", "/usr/lib"]
25
26
27def rutas_de_busqueda(rootfs):
28    conf = os.path.join(rootfs, "etc/ld-musl-x86_64.path")
29    if os.path.exists(conf):
30        with open(conf) as f:
31            rutas = [l.strip() for l in f if l.strip()]
32        if rutas:
33            return rutas
34    return SEARCH_POR_DEFECTO
35
36
37def needed(ruta_host):
38    """DT_NEEDED + el intérprete. Devuelve ([sonames], interp|None)."""
39    try:
40        out = subprocess.run(["readelf", "-dl", ruta_host], capture_output=True, text=True).stdout
41    except FileNotFoundError:
42        print("hace falta readelf (binutils)", file=sys.stderr)
43        sys.exit(2)
44    sonames, interp = [], None
45    for linea in out.splitlines():
46        if "(NEEDED)" in linea and "Shared library:" in linea:
47            sonames.append(linea.split("[")[1].split("]")[0])
48        elif "program interpreter:" in linea:
49            interp = linea.split("program interpreter:")[1].strip().rstrip("]")
50    return sonames, interp
51
52
53def resolver(rootfs, soname, search):
54    for d in search:
55        cand = os.path.join(rootfs, d.lstrip("/"), soname)
56        if os.path.exists(cand):
57            return cand
58    return None
59
60
61def dentro(rootfs, ruta_host):
62    """Path del sandbox = path del host sin el prefijo del rootfs."""
63    return "/" + os.path.relpath(ruta_host, rootfs)
64
65
66def main():
67    if len(sys.argv) < 3:
68        print(__doc__.split("\n")[2].strip(), file=sys.stderr)
69        return 2
70    rootfs = os.path.realpath(sys.argv[1])
71    search = rutas_de_busqueda(rootfs)
72
73    pendientes = list(sys.argv[2:])
74    vistos, salida = set(), set()
75
76    while pendientes:
77        p = pendientes.pop()
78        host = os.path.join(rootfs, p.lstrip("/"))
79        if not os.path.exists(host):
80            print(f"# AVISO: no existe en el rootfs: {p}", file=sys.stderr)
81            continue
82        # Resolver symlinks: /bin/sh → /bin/busybox, /usr/lib/libz.so.1 → libz.so.1.3.2. El
83        # kernel denuncia el fichero REAL (medido: `libz.so.1.3.2`, no el symlink), así que la
84        # regla tiene que caer sobre el destino. Emitimos AMBOS: el symlink porque es el nombre
85        # por el que se abre, y el destino porque es el que Landlock evalúa.
86        real = os.path.realpath(host)
87        for h in {host, real}:
88            if h.startswith(rootfs) and os.path.exists(h):
89                salida.add(dentro(rootfs, h))
90        if real in vistos:
91            continue
92        vistos.add(real)
93
94        sonames, interp = needed(real)
95        if interp:
96            pendientes.append(interp)
97        for so in sonames:
98            hallado = resolver(rootfs, so, search)
99            if hallado:
100                pendientes.append(dentro(rootfs, hallado))
101            else:
102                # Un soname irresoluble NO se calla: o el rootfs está roto, o la búsqueda está
103                # mal, y en cualquier caso la política saldría más chica que el runtime real ⇒
104                # falsos positivos, que es lo que mata a estos proyectos (§7).
105                print(f"# AVISO: soname sin resolver: {so} (de {dentro(rootfs, real)})",
106                      file=sys.stderr)
107
108    print(f"# runtime base: cierre dinámico de {len(sys.argv) - 2} binario(s) → {len(salida)} rutas")
109    for r in sorted(salida):
110        print(f"ro {r}")
111    return 0
112
113
114if __name__ == "__main__":
115    sys.exit(main())