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. +# +# recetaparchehuellaveredicto (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 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") 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