> 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…
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
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.