diff --git a/docs/22-configurador-kernel.md b/docs/22-configurador-kernel.md new file mode 100644 index 00000000..61662ad2 --- /dev/null +++ b/docs/22-configurador-kernel.md @@ -0,0 +1,223 @@ +# SDD 22 — Configurador de kernel por niveles: respuesta al handoff de tawasuyu + +Escrito 2026-08-07, en respuesta a `tawasuyu/HANDOFF-KERNEL-CONFIG-A-HAMMER.md`. + +El handoff avisa de su propio método: *«nada de lo que sigue está verificado contra el código de +hammer — si un dato de hammer lo contradice, manda hammer»*. Este documento es esa verificación. **No +es una aprobación ni un plan de implementación**: contesta las seis preguntas de su §9 con evidencia, +corrige lo que estaba mal, y señala dos objeciones técnicas que cambian el diseño. + +**Veredicto corto:** la idea se sostiene y el sustrato está más listo de lo que el handoff supone — +el mecanismo de §6 (*«la app nunca escribe un `.config`»*) **ya es exactamente como funciona hoy**. +Pero hay **un hecho estructural que el handoff no podía conocer y que reordena todo**, y **una +objeción técnica a §2.1** que hay que resolver antes de escribir una línea. + +--- + +## 1. El hecho que reordena todo: **el config ES la identidad del artefacto** + +En hammer, `Recipe::hash_inputs` es una lista blanca, y **las fases de build entran en ella**. El +config del kernel vive dentro de la fase `configure` de `recipes/linux*.toml`. Por lo tanto: + +> **Cambiar un solo símbolo de config cambia el `ArtifactHash` del kernel.** + +Esto no es un detalle de implementación: es la bisagra del diseño, y corta en los dos sentidos. + +**A favor, y fuerte:** §2.2 (config atestado por huella de hardware) **sale casi gratis**. No hay que +inventar un mecanismo para «atestar un config»: cada config ya *es* un artefacto direccionado por +contenido, reproducible bit a bit y firmable con la maquinaria que existe. Un «config que booteó en +esta huella» es, literalmente, un `ArtifactHash` + una firma. El handoff intuyó que hammer tenía el +sustrato; lo tiene más de lo que creía. + +**En contra, y también fuerte:** la explosión de builds que §2.2 quiere evitar **no es hipotética, +es aritmética visible en el disco**. Cada variante de config es un artefacto nuevo en el store, y el +store ya crece 24 G por ciclo de trabajo sólo con las variantes que producimos hoy sin querer. Con +N1 (~40–60 bundles) el espacio combinatorio es 2⁴⁰; con «perfiles publicados» —que es lo que el +handoff propone en §8— es un puñado. **La restricción de §8 no es una preferencia de diseño: es una +condición de supervivencia del disco**, y conviene decirlo así en la UI. + +**Consecuencia práctica que hay que aceptar desde el día uno:** una «perilla» de la UI **no es un +parámetro de runtime, es una edición de receta**. No hay forma de tener un config variable sin +generar recetas. El diseño correcto es que `hammer kernel plan` **emita una receta derivada** (con su +propio hash) en vez de fingir que el kernel es un binario parametrizable. + +--- + +## 2. Las seis preguntas del §9, contestadas con evidencia + +### P1 — ¿Las recetas de kernel aceptan fragmentos o sólo defconfig completo? +**Ya son fragmentos, y con la forma que §6 prescribe.** `recipes/linux.toml`, fase `configure`: + +``` +make ARCH=x86_64 defconfig && \ +scripts/config -d MODULE_SIG -d DEBUG_INFO_BTF -e SECURITY_LANDLOCK -e AUDIT \ + -d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL … && \ +make ARCH=x86_64 olddefconfig +``` + +Base + deltas por símbolo + resolución por el `olddefconfig` **del propio kernel**. La app nunca +escribe un `.config`. La única diferencia con el handoff es el vehículo: `scripts/config -e/-d` en +vez de ficheros de fragmento y `merge_config.sh`. Son equivalentes y `scripts/config` es más fácil de +generar programáticamente. **Media implementación de §6 ya está en producción.** + +Falta la otra mitad, que es la que da la UI veraz: **el diff-back**. Hoy nada comprueba que los +símbolos pedidos hayan sobrevivido a `olddefconfig`. Un `-e FOO` cuya dependencia no se cumple se +pierde en silencio — el mismo patrón de fallo que el `license` que caía dentro de `[deps]`. + +### P2 — ¿El build es bit-reproducible con config variable? +**Sí, y por construcción.** El config no es un parámetro externo: es parte de la entrada hasheada +(§1). Cada config es su propia receta con su propio hash y su propia garantía de reproducción. No hay +un «kernel» cuya reproducibilidad dependa de congelar el config; hay N kernels, cada uno congelado. +⇒ **§2.2 puede prefirmar artefactos, no sólo configs.** + +### P3 — ¿Cuánto tarda un kernel completo? 🚨 +Declarado en las propias recetas: **`linux` ~35 min, `linux-metal` ~45 min, `linux-generic` ~60 min** +en la máquina de referencia. En la más lenta del parque, más. + +**Esto mata la bisección ingenua de §5.** ~6 rebuilds × 45 min = **4,5 horas** de la máquina del +usuario para encontrar un bundle culpable. Nadie espera eso, y menos con la máquina que acaba de no +arrancar. El handoff ya preveía el riesgo («si es caro, tope duro de iteraciones»); la respuesta es +que **es caro**. Ver §4, donde propongo una bisección que no recompila. + +### P4 — ¿harkaq puede prestar su motor de clausura? +**El patrón sí; el código no.** `crates/hammer-build/src/harkaq.rs` deriva su política enumerando +**fichero a fichero** los artefactos de las deps declaradas — es una clausura sobre el grafo de +paquetes materializado en el store, con semántica de rutas de filesystem. Kconfig es otro grafo, con +otra semántica y ~15 000 símbolos. Reusar la *idea* («política = clausura, no lista») es correcto y +está validado; reusar la *implementación* no. Hace falta un lector de Kconfig — y ahí está la +objeción del §3. + +### P5 — ¿El índice firmado admite un eje más (la huella)? +**Sí, y sin romper nada.** `PackageEntry` (`crates/hammer-core/src/repo.rs`) es un struct serde cuyos +campos opcionales llevan `#[serde(default, skip_serializing_if = ...)]`. Añadir `fingerprint: +Option` es compatible hacia atrás: los índices viejos siguen parseando y los clientes viejos +ignoran el campo. **Es exactamente el truco que hizo pagable el campo `license` hoy mismo.** + +### P6 — El prerrequisito de PLAN-KIKIN §4.bis +**Estaba dado por pendiente y está hecho en dos de tres.** Medido: + +| receta | `SECURITY_LANDLOCK` | `AUDIT` | `IO_URING` | `BPF_SYSCALL` | +|---|---|---|---|---| +| `linux-metal` | ✅ `-e` | ✅ `-e` | ✅ `-e` | ✅ `-e` | +| `linux-generic` | ✅ `-e` | ✅ `-e` | ✅ `-e` | ✅ `-e` | +| `linux` | ✅ `-e` | ✅ `-e` | ❌ sólo en un comentario | ❌ sólo en un comentario | + +⚠️ Y la forma de medirlo importa: `grep IO_URING recipes/linux.toml` **da positivo** porque la palabra +aparece en un comentario. Hay que mirar los `-e`/`-d` **dentro del bloque `configure`**. Es el mismo +error que hoy infló el conteo de licencias (`grep -l license` contaba comentarios y nombres de +paquete) y el que hace mentir a la regla del `.a` no-PIC. **En este repo, `grep` sobre ficheros con +comentarios densos no es una medición.** + +⇒ Queda pendiente sólo `linux`, que es el kernel de QEMU/serial. + +--- + +## 3. Objeción técnica a §2.1: «clausura» sobre Kconfig no está bien definida todavía + +La propuesta —«todo lo alcanzable desde `CONFIG_WIRELESS` menos lo alcanzable desde lo que sí +quiero»— asume un grafo con aristas de un solo tipo. Kconfig tiene **al menos tres, con semánticas +incompatibles**: + +- `depends on` — arista hacia arriba: *no puedo existir sin X*. +- `select` — arista hacia abajo y **forzada**: *si yo entro, X entra*, ignorando los `depends on` de + X. Es la fuente clásica de configs inválidos. +- `imply` — sugerencia blanda. + +«Alcanzable» por `depends on` y «alcanzable» por `select` dan conjuntos distintos y **ninguno de los +dos es la respuesta**. Un driver de otro subsistema puede `select` un helper de wireless sin ser +wireless: cae dentro de la clausura ingenua y apagarlo rompe algo que el usuario sí quería. Y al +revés, `select` puede meter símbolos wireless desde fuera de la clausura. + +**Esto no invalida §2.1 — lo convierte en la parte con contenido técnico real**, tal como el propio +handoff dice en su §10. Pero hay que definir la semántica de arista antes de escribir el predicado, y +la definición hay que **validarla contra algo**. + +### Y tenemos contra qué validarla, hoy y gratis +El config actual de `linux.toml` **ya es un juego de bundles N1 escrito a mano**. Literalmente: + +``` +-d WLAN -d WIRELESS -d CFG80211 -d MAC80211 -d RFKILL ← el bundle «no necesito wifi» +-d SOUND -d SND ← «no necesito audio» +-d DRM -d DRM_I915 -d DRM_AMDGPU -d DRM_NOUVEAU -d FB ← «sin gráficos» +-d XFS_FS -d BTRFS_FS -d F2FS_FS -d JFS_FS -d REISERFS_FS ← «sólo ext4» +``` + +Es un corpus de verdad-de-campo: alguien decidió a mano qué símbolos componen «no necesito wifi», y +ese kernel **arranca**. La prueba de §2.1 es directa y barata: *¿la clausura calculada de +`CONFIG_WIRELESS` reproduce ese conjunto?* Si sobra o falta mucho, la semántica de arista está mal y +nos enteramos en una tarde, sin compilar nada. **Refuerza el §10 del handoff: eso se hace antes que +nada, junto al modo reversa.** + +--- + +## 4. Lo que agrego: bisección **sin recompilar** + +Dado P3 (35–60 min por kernel), la bisección de §5 cuesta horas. Pero la mayoría de los bundles N1 +son **drivers**, y un driver puede ser `=m` en vez de `=y`. + +Propuesta: **un kernel «superset» con todo lo bisecable como módulo**. Los bundles N1 de tipo +hardware no se resuelven entonces recompilando, sino con una **lista de bloqueo de módulos en la +línea de arranque**. La bisección deja de ser 6 rebuilds (4,5 h) y pasa a ser **6 reinicios** +(minutos), sobre un kernel que ya está en el árbol de generaciones. + +Consecuencias: +- El «kernel a medida» pasa a ser una optimización de tamaño/arranque, no un requisito de + funcionalidad ⇒ el usuario puede probar bundles antes de pagar un build. +- Encaja con el árbol de generaciones y el rollback E4, que ya existen. +- **Limitación honesta:** no cubre lo que debe ser `=y` (LSM, preempción, unwinder, `-march`, LTO, + o sea buena parte de N2). Para esos sí hay rebuild y sí hay que topar iteraciones. +- **Y un coste que hay que decir:** el superset es exactamente el kernel gordo que el nivel 1 quiere + evitar. Es una herramienta de diagnóstico, no el kernel de todos los días. + +## 5. Lo que agrego: el gate de no-regresión (§6) debe ser **por objetivo**, no global + +El handoff lo llama la mejor relación valor/esfuerzo y estoy de acuerdo. Pero el config actual de +`linux.toml` apaga `USB_SUPPORT`, `HID`, `HID_SUPPORT`, `INPUT_MOUSE`, `INPUT_TOUCHSCREEN`. Ese +kernel **no puede recibir teclado en metal** — y es correcto, porque es el kernel de QEMU con consola +serie. Un gate global «todo dispositivo en uso debe seguir teniendo driver» **rechazaría una receta +sana**. + +⇒ El gate necesita el objetivo declarado (`qemu-serial` / `metal` / `servidor`) y una noción de +«dispositivos en uso **de la máquina destino**», que no siempre es la máquina donde se construye. +Sigue siendo la propuesta más barata de las nueve; sólo que el portón se pone en el sitio correcto. + +## 6. Dos huecos del handoff que conviene cerrar antes + +**«Booteó» no es una aserción suficiente para atestar (§2.2).** Un kernel puede arrancar y dejarte +sin red, sin wifi o sin teclado — que es justo el desastre que §6 quiere evitar. Si la atestación +dice sólo «booteó en esta huella», estamos firmando la clase de fallo que más duele. La atestación +debería llevar **qué se comprobó**: qué dispositivos de la huella quedaron con driver bindeado +después de arrancar. Es el mismo dato que ya calcula el gate de §6 ⇒ una implementación, dos usos. + +**El catálogo de bundles necesita las tres etiquetas de nivel, no dos.** El handoff avisa en §5 que +«la mitad de N2 no es Kconfig sino variables de receta». Con §1 en la mano eso es más grave de lo que +parece: las perillas de receta (LTO, `-march`, `zig_version`) **cambian el hash igual que los +símbolos**, pero se aplican en otra fase y fallan de otra forma. La UI tiene que saber de qué lado +cae cada perilla o prometerá diffs que no puede explicar. + +--- + +## 7. Qué contesto al reparto (§7) + +De acuerdo con el reparto tal como está. Dos precisiones desde este lado: + +- El contrato JSON: de acuerdo, y el precedente que cita (`/run/hammer/boot-graph.json`) es el + correcto. Añadiría que el JSON de `hammer kernel plan` **debe incluir el `ArtifactHash` resultante**, + porque por §1 el plan *determina* el artefacto: la UI puede decir «esto ya está construido y + firmado» sin construir nada. +- Los verbos propuestos me parecen bien salvo `hammer kernel build --plan`, que esconde dos cosas muy + distintas (construir, y dar de alta en el árbol de generaciones). Separarlos deja que la sonda en + VM de §4 se meta entre medio como un paso propio y auditable. + +## 8. Orden que propongo (modifica el §10 del handoff) + +1. **Modo reversa (#9)** — tal cual lo propone: cero riesgo, valida el catálogo contra un kernel real. +2. **Validar la clausura contra el config actual** (§3 de este documento) — sale gratis, usa un corpus + que ya existe, y decide si §2.1 es viable. **Va antes que el gate**, porque si la clausura no + reproduce los bundles hechos a mano, el diseño de N1 cambia. +3. **Gate de no-regresión (#6), por objetivo** (§5 de este documento). +4. **Diff-back** (la mitad que falta de §6 del handoff): barato, y sin él la UI miente. +5. Recién después, #1 (clausuras) con la semántica ya decidida. + +**No implementar todavía.** Este documento existe para que la decisión se tome con los seis datos +verificados, no con las suposiciones del handoff — que su autor, correctamente, marcó como tales.