From 4374d2b5918bb4454ed1d36d710fddfb7c613ed7 Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 16 Jul 2026 22:09:59 -0400 Subject: [PATCH] =?UTF-8?q?cierres=20=C2=A74:=20CVE=20por=20grafo=20?= =?UTF-8?q?=E2=80=94=20frontera=20m=C3=ADnima=20de=20rebuild=20(dirty=5Fco?= =?UTF-8?q?ne=20+=20topo=5Forder)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `scripts/affected.py [--agenda] [--sellados]`. Un CVE no obliga a reconstruir el catálogo: obliga a reconstruir el CONO de lo que depende del paquete, y nada más. El grafo declarado ya lo sabe. CVE en zlib → 65 de 761 recetas (8%) — el resto NO se toca CVE en expat → 20 de 761 (2%), las 20 selladas = el trabajo real agenda: fontconfig → fcft → cairo → pango → dbus → wayland… Es el `dirty_cone` de `dataflow` (crate no_std de tawasuyu) + `topo_order` como agenda (una dep antes que su consumidor), calculados sobre las deps declaradas. `--sellados` separa lo que hay que reconstruir de verdad (tiene artefacto) de lo que no existe aún. CAE SOLO DEL INVARIANTE: las deps están declaradas por hash ⇒ el cono es computable y EXACTO. Una distro sin clausura declarada tiene que adivinar o reconstruir todo por las dudas. Ésa es la tesis del SDD 17. Honestidad grabada en el propio output: la frontera es exacta respecto de lo DECLARADO. Lo que una receta usa sin declarar queda FUERA del cono — y ese es el agujero que harkaq cierra (SDD 16). La campaña de esta semana declaró ~84 deps que el kernel midió y nadie había escrito: sin eso, este cono habría sido optimista. **El cono es tan bueno como la clausura**, y por eso los dos frentes se necesitan. Co-Authored-By: Claude Opus 4.8 --- scripts/affected.py | 114 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100755 scripts/affected.py diff --git a/scripts/affected.py b/scripts/affected.py new file mode 100755 index 00000000..04603d81 --- /dev/null +++ b/scripts/affected.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""affected — frontera MÍNIMA de rebuild ante un CVE (SDD 17 §4). + + affected.py zlib # ¿qué hay que reconstruir si zlib tiene un CVE? + affected.py zlib --agenda # + el orden topológico (qué primero) + affected.py zlib --sellados # sólo lo que YA está en el store (el trabajo real) + +Un CVE en `zlib` no obliga a reconstruir el catálogo: obliga a reconstruir **el cono de lo que +depende de zlib**, y nada más. El grafo de dependencias declarado ya lo sabe — es el `dirty_cone` +de `dataflow` (crate no_std de tawasuyu), y el `topo_order` es la agenda. + +Que esto sea exacto (y no "reconstruí todo por las dudas") **cae solo del invariante**: las deps +están declaradas por hash, así que el cono es computable. Un distro sin clausura declarada tiene +que adivinar. + +Nota honesta: la frontera es exacta respecto de **lo declarado**. Si una receta usa algo sin +declararlo, queda fuera del cono — y ese es justo el agujero que harkaq cierra (SDD 16): la +campaña de esta semana declaró ~84 deps que el kernel midió y nadie había escrito. **El cono es +tan bueno como la clausura**, y por eso los dos frentes se necesitan. +""" +import collections +import glob +import os +import re +import sys + + +def grafo(): + """(deps, rev): receta → sus deps declaradas, y su inverso.""" + deps = collections.defaultdict(set) + for f in glob.glob("recipes/*.toml"): + n = os.path.basename(f)[:-5] + s = open(f, errors="ignore").read() + m = re.search(r"\[deps\](.*?)(\n\[|\Z)", s, re.S) + if m: + for d in re.findall(r'"([^"]+)"', m.group(1)): + deps[n].add(d) + rev = collections.defaultdict(set) + for n, ds in deps.items(): + for d in ds: + rev[d].add(n) + return deps, rev + + +def cono(rev, semilla): + """dirty_cone: todo lo que depende TRANSITIVAMENTE de `semilla`. La frontera de rebuild.""" + vistos, frontera = set(), [semilla] + while frontera: + x = frontera.pop() + for c in rev.get(x, ()): + if c not in vistos: + vistos.add(c) + frontera.append(c) + return vistos + + +def agenda(deps, afectadas): + """topo_order dentro del cono: qué reconstruir primero (una dep antes que su consumidor).""" + pend = {n: {d for d in deps.get(n, ()) if d in afectadas} for n in afectadas} + orden, listos = [], [n for n, p in pend.items() if not p] + while listos: + n = listos.pop() + orden.append(n) + for m in afectadas: + if n in pend[m]: + pend[m].discard(n) + if not pend[m]: + listos.append(m) + ciclo = [n for n in afectadas if n not in orden] + return orden, ciclo + + +def main(): + args = [a for a in sys.argv[1:] if not a.startswith("--")] + if not args: + print(__doc__.split("\n")[2].strip(), file=sys.stderr) + return 2 + pkg = args[0] + if not os.path.exists(f"recipes/{pkg}.toml"): + print(f"no existe recipes/{pkg}.toml", file=sys.stderr) + return 2 + + deps, rev = grafo() + afectadas = cono(rev, pkg) + total = len(glob.glob("recipes/*.toml")) + + print(f"══ CVE en «{pkg}» — frontera mínima de rebuild") + print(f" {len(afectadas)} de {total} recetas afectadas ({len(afectadas)*100//max(total,1)}%)" + f" — el resto NO se toca") + + if "--sellados" in sys.argv or "--agenda" in sys.argv: + sell = {n for n in afectadas if glob.glob(f"store/*-{n}")} + print(f" de ésas, {len(sell)} ya están selladas en el store ⇒ ése es el trabajo real") + + if "--agenda" in sys.argv: + orden, ciclo = agenda(deps, afectadas) + print("\n ── agenda (topo_order: una dep antes que su consumidor):") + for i, n in enumerate(orden, 1): + marca = "●" if glob.glob(f"store/*-{n}") else "○" + print(f" {i:3}. {marca} {n}") + if ciclo: + print(f" ⚠ {len(ciclo)} en ciclo (no ordenables): {', '.join(sorted(ciclo))}") + print(" ● = sellado (hay que reconstruir) ○ = sin artefacto") + else: + for n in sorted(afectadas): + print(f" {n}") + + print("\n La frontera es exacta respecto de lo DECLARADO. Lo no declarado queda fuera del" + "\n cono — es el agujero que harkaq cierra (SDD 16).") + return 0 + + +if __name__ == "__main__": + sys.exit(main())