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 Definiciones

parches: los 8 FUZZ del catálogo, mirados uno por uno — y un libro que se auto-invalida

`vigia-parches.py --all` sobre las 99 aplicaciones de parche del catálogo: **0 FALLA**, y 14 líneas
FUZZ (8 pares parche/fuente distintos). FUZZ significa que `patch` metió el cambio ADIVINANDO dónde
porque el contexto no casaba, y si cayó en el sitio correcto sólo lo dice el diff. Nadie los había
mirado. Los miré todos:

  · **libxml2 / CVE-2026-6732** — el que más importaba, un parche de seguridad con fuzz 2 en sus dos
    hunks. Cayó bien: los dos dentro de `xmlParseReference()`, las 4 llamadas pasan `ctxt->userData`
    y no sobrevive ningún `sax->characters(ctxt,` en el fichero. El fuzz era por un offset de 226
    líneas, no por el sitio.
  · **doas / rowhammer** — el otro sensible, toca la decisión de privilegio. Cae dentro de
    `checkconfig()` y queda `rv=permit(...); if(rv==0)→permit`, coherente con el `if(rv!=0)→EPERM`
    de `main()`. Y el fuzz lo causa un parche ANTERIOR de la propia cadena (el `#ifdef DOAS_CONFDIR`
    que inserta `configuration-directory.patch`), no un cambio de upstream — que es una causa que no
    se me habría ocurrido sin abrirlo.
  · **wayland**, **mesa** (×3), **cairo** (×2), **firefox** (time64 y fix-rust-target): todos en su
    sitio, cada uno comprobado contra lo que el propio parche declara querer.
  · **gnupg / 0001-include-unistd** — hallazgo: el parche está OBSOLETO. Añade `#include <unistd.h>`
    y upstream YA lo trae dos líneas más abajo, así que sólo lo duplica. Inocuo (el header tiene
    guardas) y por eso el fuzz 2: cambió el contexto porque upstream lo incorporó. Quitarlo re-hashea
    gnupg, así que se paga cuando se re-selle por otro motivo.

Y para que esto no se repregunte en cada corrida —lo que vuelve ruido al vigía, y así es como se
pierde el FALLA del día que aparezca— los veredictos van a `docs/state/fuzz-verificado.tsv` con un
cuarto estado, FUZZ✓.

La clave del libro NO es (receta, parche) sino (receta, parche, HUELLA), donde la huella resume el
texto del parche MÁS el pin de la fuente. Tocá el parche o subí la versión y la huella cambia, la
entrada deja de casar y el vigía vuelve a preguntar. Es lo contrario de una lista de excepciones: no
hay forma de silenciar algo y que siga silenciado cuando cambió. Y el vigía imprime la línea lista
para pegar debajo de cada FUZZ sin verificar, porque calcular la huella a mano es justo la fricción
que hace que nadie lo anote.

Los dos FUZZ de waterfox quedan FUERA del libro a propósito: no los verifiqué en esta ronda y
waterfox está fuera de alcance. Van a seguir saliendo como FUZZ, que es lo honesto — el libro dice
lo que se miró, no lo que se supone.

db7fd81236a725a489c5b5dcae2e34d705579a01 · por Sergio · (hace 1 mes) · padre 2a62c44b1 · ver el árbol en este commit

Delta semántico · 4

scripts/vigia-parches.py

2 archivos · +85 −5

diff --git a/docs/state/fuzz-verificado.tsv b/docs/state/fuzz-verificado.tsv
new file mode 100644
index 00000000..6435e983
--- /dev/null
+++ b/docs/state/fuzz-verificado.tsv
@@ -0,0 +1,29 @@
+# fuzz-verificado.tsv — los FUZZ de `scripts/vigia-parches.py` que YA se miraron, con su veredicto.
+#
+# Un FUZZ no se puede resolver automáticamente: `patch` metió el cambio ADIVINANDO dónde, y si cayó
+# en el sitio correcto sólo lo dice el diff. Pero una vez mirado, el veredicto no caduca solo:
+# depende del TEXTO del parche y de la FUENTE, y las dos están pineadas.
+#
+# Por eso la clave no es (receta, parche) sino (receta, parche, HUELLA), donde la huella resume el
+# parche + el pin de la fuente. Tocá el parche o subí la versión y la huella cambia, la entrada deja
+# de casar y el vigía VUELVE A PREGUNTAR. No es una lista de excepciones: no hay forma de silenciar
+# algo y que siga silenciado cuando cambió.
+#
+# La línea lista para pegar la imprime el propio vigía debajo de cada FUZZ sin verificar.
+#
+# receta<TAB>parche<TAB>huella<TAB>veredicto (qué se comprobó, no «ok»)
+#
+# ⚠ Los dos FUZZ de waterfox NO están acá a propósito: no los verifiqué en esta ronda y waterfox está
+# fuera de alcance. Van a seguir saliendo como FUZZ, que es lo honesto.
+cairo	cairo-ctime-r.patch	053bd7e1c4b4051f	2026-09-05: los 3 hunks exactos — conf.set('HAVE_CTIME_R',1) queda DESPUÉS del bucle check_funcs; el `if false` envuelve subdir('cairo-trace') y NO subdir('cairo-script'); el tercero va tras meson.override_dependency ⇒ la .pc sobrevive sin los exe csi-*
+cairo-shared	cairo-ctime-r.patch	053bd7e1c4b4051f	2026-09-05: misma huella que `cairo` (mismo parche, misma fuente pineada) ⇒ mismo veredicto
+doas	rowhammer.patch	22d6f35cf2e6fa57	2026-09-05: el hunk cae dentro de checkconfig() y queda rv=permit(...); if(rv==0)→permit, coherente con main() que hace if(rv!=0)→EPERM. El fuzz lo causa un parche ANTERIOR de la cadena (el #ifdef DOAS_CONFDIR de configuration-directory.patch), no un cambio de upstream
+firefox	time64.patch	66bc1630adc67e1b	2026-09-05: now_including_suspend() y now_awake() quedan con libc::timespec::default(); no sobrevive ningún literal `tv_sec: 0` en zeitstempel/src/unix.rs
+firefox	fix-rust-target.patch	cd6f96abf42122e4	2026-09-05: la asignación cae justo tras el `return None` de find_candidate y el return usa ensure_unicode; no queda ninguna LLAMADA a find_candidate(candidates), sólo su def
+gnupg	0001-include-unistd.patch	d688ed86408ec74d	2026-09-05: entra en el bloque de includes de scd/app.c — pero upstream YA trae <unistd.h> dos líneas más abajo ⇒ el parche está OBSOLETO y sólo lo duplica (inocuo: el header tiene guardas). Quitarlo re-hashea gnupg, así que se paga cuando se re-selle por otro motivo
+libxml2	CVE-2026-6732.patch	d67f9694dcad492f	2026-09-05: los 2 hunks dentro de xmlParseReference(); las 4 llamadas pasan ctxt->userData y NO queda ninguna sax->characters(ctxt, ni sax->cdataBlock(ctxt, en todo el fichero. El fuzz es por el offset de 226 líneas, no por el sitio
+libxml2-shared	CVE-2026-6732.patch	d67f9694dcad492f	2026-09-05: misma huella que `libxml2` ⇒ mismo veredicto
+mesa	mesa-version-script-comma.patch	6054348f4a56ac2c	2026-09-05: version-script y dynamic-list quedan en forma COMA ('-Wl,--version-script,'+ruta), que es exactamente lo que el parche declara querer
+mesa-llvmpipe	mesa-version-script-comma.patch	6054348f4a56ac2c	2026-09-05: misma huella que `mesa` ⇒ mismo veredicto
+mesa-swrast	mesa-version-script-comma.patch	6054348f4a56ac2c	2026-09-05: misma huella que `mesa` ⇒ mismo veredicto
+wayland	wayland-scanner-dup2-output.patch	b56341a45c5e7b1d	2026-09-05: el dup2(outfd, STDOUT_FILENO) cae dentro de main() de src/scanner.c y no queda NINGÚN freopen en el fichero
diff --git a/scripts/vigia-parches.py b/scripts/vigia-parches.py
index fb5a39ef..2f5ccc71 100755
--- a/scripts/vigia-parches.py
+++ b/scripts/vigia-parches.py
@@ -27,6 +27,12 @@
 #               que el fichero creció por arriba, no que el contexto haya cambiado.
 #     FUZZ      entra, pero adivinando. NO es un fallo y no rompe el build — es la señal de que hay
 #               que abrir el diff y confirmar a mano que el cambio cayó donde debía.
+#     FUZZ✓     entra adivinando, PERO alguien ya abrió el diff y anotó dónde cayó, en
+#               `docs/state/fuzz-verificado.tsv`. La entrada está indexada por una huella del parche
+#               MÁS el pin de la fuente: si cualquiera de los dos cambia, deja de casar y vuelve a
+#               salir como FUZZ. Un fuzz mirado no debería preguntarse en cada corrida —eso vuelve
+#               ruido al vigía y así es como se pierde el FALLA del día que aparezca— pero tampoco
+#               debe poder silenciarse para siempre.
 #     FALLA     no entra. Esto sí es el muro, y es el que queremos ver en dos minutos y no en cuatro
 #               horas. El arreglo es rebasar el parche que se queje, no bajar el listón del vigía.
 #
@@ -55,6 +61,7 @@
 
 import argparse
 import os
+import hashlib
 import re
 import shutil
 import subprocess
@@ -70,6 +77,41 @@ ROOT = Path(__file__).resolve().parent.parent
 # `patch` reporta el desplazamiento y el fuzz por hunk, en stdout, sólo con --verbose.
 RE_HUNK = re.compile(r"^Hunk #(\d+) (succeeded|FAILED) at (\d+)(.*)$", re.M)
 RE_FUZZ = re.compile(r"with fuzz (\d+)")
+
+# ── EL LIBRO DE FUZZ YA MIRADOS ────────────────────────────────────────────────────────────────
+# Un FUZZ no se puede resolver automáticamente: hay que abrir el diff y ver si el cambio cayó donde
+# debía. Pero una vez mirado, el veredicto NO caduca solo — depende de dos cosas y las dos están
+# pineadas: el TEXTO del parche y la FUENTE. Repreguntarlo en cada corrida convierte al vigía en
+# ruido y se acaba ignorando (que es como se pierde el FALLA del día que aparezca).
+#
+# Por eso el libro se indexa por una HUELLA de las dos cosas. Si alguien toca el parche, o sube la
+# versión de la receta, la huella cambia, la entrada deja de casar y el vigía vuelve a preguntar.
+# Es lo contrario de una lista de excepciones: no hay forma de silenciar algo y que se quede
+# silenciado cuando cambió.
+LIBRO = Path(__file__).resolve().parent.parent / "docs/state/fuzz-verificado.tsv"
+
+
+def huella(texto_parche: str, src: dict) -> str:
+    """Identidad de un (parche, fuente) pineados. Cambiá cualquiera de los dos y cambia."""
+    pin = src.get("sha256") or src.get("commit") or ""
+    h = hashlib.sha256()
+    h.update(texto_parche.encode())
+    h.update(b"\n")
+    h.update(pin.encode())
+    return h.hexdigest()[:16]
+
+
+def cargar_libro() -> dict:
+    if not LIBRO.exists():
+        return {}
+    out = {}
+    for linea in LIBRO.read_text(encoding="utf8").splitlines():
+        if linea.startswith("#") or not linea.strip():
+            continue
+        campos = linea.split("\t")
+        if len(campos) >= 4:
+            out[(campos[0], campos[1], campos[2])] = campos[3]
+    return out
 RE_OFFSET = re.compile(r"offset (-?\d+) lines?")
 
 
@@ -136,7 +178,7 @@ def traer_tarball(url: str, ficheros: list[str], dest: Path) -> None:
                     destino.write_bytes(src.read())
 
 
-def probar(receta: Path, keep: bool, git_only: bool) -> tuple[str, list[str]]:
+def probar(receta: Path, keep: bool, git_only: bool, libro: dict) -> tuple[str, list[str]]:
     """Devuelve (estado, líneas del informe) para una receta. Estado ∈ ok|fuzz|falla|saltada."""
     datos = tomllib.loads(receta.read_text(encoding="utf8"))
     src = datos.get("source", {})
@@ -192,10 +234,18 @@ def probar(receta: Path, keep: bool, git_only: bool) -> tuple[str, list[str]]:
                     if "FAILED" in l or "can't find file" in l or "Reversed" in l:
                         lineas.append(f"          {l.strip()}")
             elif fuzz:
-                if peor != "falla":
-                    peor = "fuzz"
                 det = ", ".join(f"hunk #{h} fuzz {f}" for h, f in fuzz)
-                lineas.append(f"   FUZZ   {base}  ({det}) — entra ADIVINANDO, mirá el diff")
+                hu = huella(textos[p], src)
+                visto = libro.get((nombre, base, hu))
+                if visto:
+                    lineas.append(f"   FUZZ✓  {base}  ({det}) — ya mirado: {visto}")
+                else:
+                    if peor != "falla":
+                        peor = "fuzz"
+                    lineas.append(f"   FUZZ   {base}  ({det}) — entra ADIVINANDO, mirá el diff")
+                    # La línea lista para pegar en el libro DESPUÉS de mirarlo. Se da acá porque
+                    # calcular la huella a mano es justo la fricción que hace que nadie lo anote.
+                    lineas.append(f"          libro: {nombre}\t{base}\t{hu}\t<tu veredicto>")
             else:
                 extra = f"  ({len(offs)} hunks desplazados)" if offs else ""
                 lineas.append(f"   ok     {base}{extra}")
@@ -225,10 +275,11 @@ def main() -> int:
         ap.error("dame una receta o --all")
 
     peor = "ok"
+    libro = cargar_libro()
     orden = {"saltada": 0, "ok": 1, "fuzz": 2, "falla": 3}
     for r in objetivos:
         try:
-            estado, lineas = probar(r, a.keep, a.git_only)
+            estado, lineas = probar(r, a.keep, a.git_only, libro)
         except subprocess.CalledProcessError as e:
             estado, lineas = "falla", [f"{r}: ✗ no pude traer la fuente ({e})"]
         except Exception as e:  # una receta rota no debe tumbar el barrido entero