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 1163Rust 61Shell 52Markdown 381349 Definitions

atuq §6.10: el tercer escalón — qué NO puede hacer el navegador, y por qué

hammer-9f nombró el punto ciego que ni su vigía ni mi guardián podían cubrir:
una librería que sólo aparece como CADENA LITERAL dentro de un dlopen(). No hay
NEEDED en ningún ELF, así que ningún auditor de readelf la encuentra. La
jerarquía queda:

    NEEDED del ejecutable          -> lo vemos los dos
    NEEDED de un .so dlopeado      -> lo vemos los dos
    dlopen("libfoo.so.1") literal  -> NO LO VE NINGUNO

Y un navegador vive de eso. Firefox sondea ffmpeg, VA-API, vulkan y libnotify
por nombre, y cuando no están NO FALLA: apaga la función y sigue. No hay línea
roja; hay una función que nadie ofrece y nadie reclama. Es la forma que ya costó
caro con OBS y su dlopen("libGL.so.1").

`--dlopen` busca esas cadenas y las cruza contra el rootfs. Es un HEURÍSTICO y se
declara como tal —una cadena no prueba un dlopen y su ausencia no prueba que no
lo haya—, así que no falla nunca: imprime un mapa triado. Lo afirmable es lo
contrario, que es lo útil: si la cadena está y el fichero no, esa función no
existe en esta imagen.

De 85 cadenas, 47 sin proveedor. Siete son huecos de verdad:

    códecs del sistema (H.264/AAC)  sin receta de ffmpeg — el más caro
    notificaciones web              sin receta
    llavero (libsecret)             RECETA YA EXISTE en incoming-gnome
    sonidos (libcanberra)           receta en incoming-kde
    WebGPU (vulkan-loader)          receta en incoming-kde
    vídeo por hardware (VA-API)     sin receta
    lectura en voz alta             sin receta

Tres de los siete son promoción, no autoría. Ocho son decisiones ya tomadas
(libGL por Wayland-only sin GLX; libcurl porque sólo lo usa el pingsender de
telemetría) y siete son ruido de musl o sonames viejos. El triaje va en una tabla
del propio script, no escondido en un `if`, porque es criterio y no medición: ahí
se puede discutir.

Sin triar: 0. Si aparece una cadena nueva, el informe la marca «SIN TRIAR» en vez
de tragársela.

Lo que esto cambia de fondo: hasta hoy la pregunta era «¿arranca?» y la respuesta
era sí. La que faltaba era «¿y qué NO puede hacer?», que ninguna métrica del repo
respondía porque todas miran presencia y ésta mira ausencia declarada por el
propio binario.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ

7a81b6480c946cc47bdd393e656943a4668b0d33 · by Sergio · (1 month ago) · parent a20d1b745 · browse the tree at this commit

Semantic delta · 3

scripts/test-atuq-rootfs.py

2 files · +161 −1

diff --git a/docs/26-atuq-envoltorio-gecko.md b/docs/26-atuq-envoltorio-gecko.md
index 3c5af0bd..9f72516a 100644
--- a/docs/26-atuq-envoltorio-gecko.md
+++ b/docs/26-atuq-envoltorio-gecko.md
@@ -449,6 +449,51 @@ directas, nadie llamando al proxy—. La única diferencia entre las dos corrida
 configuración y el observable se da vuelta entero. Sin ese modo, una prueba que se hubiera vuelto
 ciega se vería idéntica a una que funciona.
 
+### 6.10 Lo que `atuq` NO PUEDE hacer hoy, y por qué (2026-09-07)
+
+Sale de un punto ciego que nombramos entre los dos frentes y que **ningún auditor de `readelf`
+puede cubrir**. La jerarquía de lo que se ve:
+
+    NEEDED del ejecutable          → lo ve `test-atuq-rootfs.py` y lo ve `vigia-sonames.py`
+    NEEDED de un .so dlopeado      → lo ven los dos, si el fichero está en el artefacto
+    dlopen("libfoo.so.1") LITERAL  → NO LO VE NINGUNO
+
+Y un navegador vive del tercer escalón: Firefox sondea ffmpeg, VA-API, vulkan, libnotify y una
+docena más **por nombre**, y cuando no están **no falla: apaga la función y sigue**. No hay línea
+roja; hay una función que nadie ofrece y nadie reclama. Es la misma forma que ya nos costó caro con
+OBS (`dlopen("libGL.so.1")` desde su glad).
+
+`scripts/test-atuq-rootfs.py --dlopen` busca esas cadenas y las cruza contra el rootfs. **Es un
+heurístico y se declara como tal** —una cadena no prueba un `dlopen`, y su ausencia no prueba que no
+lo haya—, así que no falla nunca: imprime un mapa triado. Lo que sí se puede afirmar es lo
+contrario, que es lo útil: **si la cadena está y el fichero no, esa función no existe en esta
+imagen.**
+
+De 85 cadenas con forma de soname, **47 no tienen quién las provea**. Triadas:
+
+| Qué se pierde | Estado |
+|---|---|
+| **Códecs del sistema (H.264/AAC)** | hueco — no hay receta de `ffmpeg`. Lo bundleado cubre VP9/AV1/Opus, no H.264 |
+| **Notificaciones web** (`libnotify`) | hueco — sin receta |
+| **Llavero** (`libsecret`) | hueco — pero **la receta ya existe** en `incoming-gnome` |
+| **Sonidos del sistema** (`libcanberra`) | hueco — receta en `incoming-kde` |
+| **WebGPU / vulkan** (`libvulkan`) | hueco — receta `vulkan-loader` en `incoming-kde` |
+| **Vídeo por hardware** (VA-API) | hueco — sin receta |
+| **Lectura en voz alta** (`libspeechd`) | hueco — sin receta |
+| `libGL` | **decisión**: la distro es Wayland-only sin GLX y Firefox va por EGL |
+| `libcurl` | **decisión**: sólo lo usa `pingsender`, que manda telemetría — apagada |
+| Kerberos, menú global, ML | fuera de alcance |
+| `libdl/libpthread/librt/libudev.so.0/libfreebl3` | ruido: musl los funde, o el soname es viejo |
+
+**Tres de los siete huecos son baratos** —`libsecret`, `libcanberra` y `vulkan-loader` ya tienen
+receta y están en colas `incoming`—, así que son promoción y no autoría. Los otros cuatro piden
+receta nueva, y el más caro para el usuario es `ffmpeg`: sin él, un sitio que sirva H.264 no
+reproduce.
+
+**Lo que esta sección cambia de fondo:** hasta hoy la pregunta era «¿arranca?», y la respuesta era
+sí. La pregunta que faltaba era «¿y qué NO puede hacer?», que ninguna métrica del repo respondía
+porque todas miran presencia y ésta mira ausencia declarada por el propio binario.
+
 ### 2.ter Dónde vive la fuente de `atuq` — hizo falta un modo de `[source]` nuevo
 
 Escribir la receta destapó un muro que este documento no había visto: `[source]` era
diff --git a/scripts/test-atuq-rootfs.py b/scripts/test-atuq-rootfs.py
index 1409d53b..a86c805d 100755
--- a/scripts/test-atuq-rootfs.py
+++ b/scripts/test-atuq-rootfs.py
@@ -1,8 +1,9 @@
 #!/usr/bin/env python3
 """¿El rootfs de `atuq-nested.sh` resuelve TODOS los NEEDED, o alguno se cae al LAB?
 
-    python3 scripts/test-atuq-rootfs.py                    # sale 0 si el rootfs se basta solo
+    python3 scripts/test-atuq-rootfs.py                     # sale 0 si el rootfs se basta solo
     python3 scripts/test-atuq-rootfs.py --negative-control  # esconde una lib y exige que se note
+    python3 scripts/test-atuq-rootfs.py --dlopen            # el TERCER escalón: cadenas literales
 
 POR QUÉ EXISTE. `scripts/atuq-nested.sh` hidrata una **lista de raíces escrita a mano**, y la jaula
 monta `.dev-fs/alpine` como capa de abajo. Esas dos cosas juntas hacen que una raíz que falte no se
@@ -17,6 +18,23 @@ que es un grafo de recetas. Esto mira el otro objeto: **el directorio hidratado
 de verdad**. Son preguntas distintas y la segunda es la que corresponde al runner, porque su lista
 no sale del grafo.
 
+EL TERCER ESCALÓN, QUE NO LO VE NINGÚN AUDITOR DE `readelf` (`--dlopen`). La jerarquía es:
+
+    NEEDED del ejecutable          → lo ve este guardián y lo ve vigia-sonames
+    NEEDED de un .so dlopeado      → lo ven los dos, si el fichero está en el artefacto
+    dlopen("libfoo.so.1") LITERAL  → no hay NEEDED en ningún ELF ⇒ NO LO VE NINGUNO
+
+El repo ya tiene el caso caro escrito: OBS hace `dlopen("libGL.so.1")` desde su glad, y por eso
+«glvnd sin GLX no lo destraba» aunque todos los NEEDED estén satisfechos. Un navegador vive de eso:
+Firefox sondea ffmpeg, VA-API, vulkan, libnotify y una docena más por nombre, y **cuando no están no
+falla: apaga la función y sigue**. No hay línea roja; hay una función que nadie ofrece.
+
+`--dlopen` busca las cadenas con forma de soname dentro de los ELF del artefacto y las cruza contra
+el rootfs. **Es un HEURÍSTICO y se declara como tal**: una cadena no prueba un `dlopen` —puede estar
+en un mensaje de error— y la ausencia de cadena no prueba que no lo haya. Por eso no falla nunca:
+imprime un mapa triado para mirar, no un veredicto. Lo que sí se puede afirmar es lo contrario, que
+es lo útil: **si la cadena está y el fichero no, esa función no existe en esta imagen.**
+
 LO QUE SÍ VIENE DEL LAB, A PROPÓSITO. El intérprete `/lib/ld-musl-x86_64.so.1` y `libc.so` no los
 provee NINGÚN artefacto del corpus —`musl` sella cabeceras y estáticas— y las imágenes lo copian del
 devfs (`scripts/mirada-usb.sh`). Así que van en una lista explícita de excepciones: si algún día
@@ -36,6 +54,52 @@ HAMMER = os.path.join(ROOT, "target/release/hammer")
 DEL_LAB = {"libc.so", "libc.musl-x86_64.so.1", "ld-musl-x86_64.so.1"}
 
 
+# ── Triaje del tercer escalón ─────────────────────────────────────────────────────────────────
+# La clasificación es criterio, no medición, así que va acá donde se puede discutir y no escondida
+# en un `if`. «decisión» = la distro eligió no tenerlo; «hueco» = falta y se nota; «ruido» = la
+# cadena existe pero no corresponde a una función real en musl o en este build.
+TRIAJE = {
+    # decisiones ya tomadas por la distro, con su documento
+    "libGL.so": ("decisión", "Wayland-only sin GLX: ninguna cola publica libGL (ver sin-opengl-de-escritorio)"),
+    "libGL.so.1": ("decisión", "idem; Firefox va por EGL. `glxtest` sondea y se apaga"),
+    "libcurl.so": ("decisión", "sólo lo usa `pingsender`, que manda telemetría — apagada a propósito"),
+    "libcurl.so.3": ("decisión", "idem pingsender"),
+    "libcurl.so.4": ("decisión", "idem pingsender"),
+    "libcurl-gnutls.so": ("decisión", "idem pingsender"),
+    "libcurl-gnutls.so.3": ("decisión", "idem pingsender"),
+    "libcurl-gnutls.so.4": ("decisión", "idem pingsender"),
+    "libgcompat.so.0": ("ruido", "shim glibc-en-musl de Alpine; no lo queremos"),
+    "libdl.so.2": ("ruido", "musl funde dl/pthread/rt dentro de libc"),
+    "libpthread.so.0": ("ruido", "idem musl"),
+    "librt.so.1": ("ruido", "idem musl"),
+    "libc.so": ("ruido", "el nombre musl de la libc; viene del lab por diseño"),
+    "libudev.so.0": ("ruido", "soname viejo; el corpus provee libudev.so.1 (libudev-zero)"),
+    "libfreebl3.so": ("ruido", "NSS de este build expone libfreeblpriv3.so, que SÍ está"),
+    # huecos de verdad: la función no existe en esta imagen
+    "libnotify.so.4": ("hueco", "NOTIFICACIONES WEB. Sin receta en el corpus"),
+    "libnotify.so.1": ("hueco", "idem, soname viejo"),
+    "libsecret-1.so.0": ("hueco", "llavero: contraseñas al keyring. HAY receta en incoming-gnome"),
+    "libcanberra.so.0": ("hueco", "sonidos del sistema. HAY receta en incoming-kde"),
+    "libvulkan.so.1": ("hueco", "WebGPU/vulkan. HAY receta (vulkan-loader) en incoming-kde"),
+    "libvulkan.so": ("hueco", "idem"),
+    "libva.so.2": ("hueco", "decodificación de vídeo por HARDWARE. Sin receta"),
+    "libva-drm.so.2": ("hueco", "idem VA-API"),
+    "libspeechd.so.2": ("hueco", "lectura en voz alta. Sin receta"),
+    "libpci.so": ("hueco", "sólo para el sondeo de `glxtest`; irrelevante sin GLX"),
+    "libpci.so.3": ("hueco", "idem"),
+    "libonnxruntime.so": ("fuera de alcance", "funciones de ML del navegador"),
+    "libaudit.so.1": ("fuera de alcance", "auditoría de NSS"),
+    "libdbusmenu-glib.so.4": ("fuera de alcance", "menú global estilo Unity"),
+    "libdbusmenu-gtk3.so.4": ("fuera de alcance", "idem"),
+    "libgssapi.so.1": ("fuera de alcance", "Kerberos de empresa"),
+    "libgssapi.so.4": ("fuera de alcance", "idem"),
+    "libgssapi_krb5.so.2": ("fuera de alcance", "idem"),
+}
+# `libavcodec*` es una familia entera de sonames: Firefox sondea de la 53 a la 63 y todas las
+# variantes `-ffmpeg`. Se trata junta o el informe es una lista de once líneas iguales.
+FAMILIA_FFMPEG = ("hueco", "CÓDECS DEL SISTEMA (H.264/AAC): sin receta de ffmpeg en el corpus")
+
+
 def fatal(msg):
     print(f"✗ {msg}", file=sys.stderr)
     raise SystemExit(1)
@@ -99,6 +163,50 @@ def dinamico(paths):
     return pide, provee
 
 
+LITERAL = re.compile(rb"\blib[A-Za-z0-9_+\-]{1,30}\.so(?:\.[0-9]+){0,3}\b")
+
+
+def literales(paths):
+    """Cadenas con forma de soname dentro de los ELF. Heurístico declarado, no prueba."""
+    cand = {}
+    for p in paths:
+        try:
+            with open(p, "rb") as fh:
+                datos = fh.read()
+        except OSError:
+            continue
+        for m in set(LITERAL.findall(datos)):
+            cand.setdefault(m.decode(), set()).add(os.path.basename(p))
+    return cand
+
+
+def informe_dlopen(paths_artefacto, provee):
+    cand = literales(paths_artefacto)
+    faltan = {k: v for k, v in cand.items() if k not in provee}
+    grupos = {}
+    for s, quien in faltan.items():
+        clase, por_que = (FAMILIA_FFMPEG if s.startswith("libavcodec")
+                          else TRIAJE.get(s, ("SIN TRIAR", "nadie lo miró todavía")))
+        grupos.setdefault(clase, []).append((s, por_que, quien))
+    print(f"cadenas con forma de soname: {len(cand)} · sin fichero que las provea: {len(faltan)}\n")
+    for clase in ("hueco", "SIN TRIAR", "decisión", "fuera de alcance", "ruido"):
+        filas = sorted(grupos.get(clase, []))
+        if not filas:
+            continue
+        print(f"── {clase} ({len(filas)})")
+        vistos = set()
+        for s, por_que, quien in filas:
+            if clase == "hueco" and por_que in vistos and s.startswith("libavcodec"):
+                continue
+            vistos.add(por_que)
+            print(f"   {s:24s} {por_que}")
+            print(f"   {'':24s} ← {', '.join(sorted(quien))[:64]}")
+    sin_triar = len(grupos.get("SIN TRIAR", []))
+    print(f"\nRESULTADO: mapa impreso. Sin triar: {sin_triar}"
+          + ("  (agregalos a TRIAJE)" if sin_triar else ""))
+    return 0
+
+
 def main():
     negativo = "--negative-control" in sys.argv[1:]
     if not os.path.isdir(RFS):
@@ -118,6 +226,13 @@ def main():
             nombres.update(fs)
     provee |= nombres
 
+    if "--dlopen" in sys.argv[1:]:
+        nombres_todo = set()
+        for base in (RFS, atuq):
+            for b, _, fs in os.walk(base):
+                nombres_todo.update(fs)
+        return informe_dlopen([p for p in paths if p.startswith(atuq)], nombres_todo)
+
     if negativo:
         # SABOTAJE: se hace de cuenta que el rootfs no trae libstdc++. Si el guardián no lo caza,
         # es que no está mirando — que es justo el modo de fallo que este fichero existe para negar.