diff --git a/docs/adr/0017-ciclo-de-vida-del-kernel.md b/docs/adr/0017-ciclo-de-vida-del-kernel.md index 3cc470a2..0568cad7 100644 --- a/docs/adr/0017-ciclo-de-vida-del-kernel.md +++ b/docs/adr/0017-ciclo-de-vida-del-kernel.md @@ -1,7 +1,7 @@ # ADR 0017 — Ciclo de vida del kernel: cambiarlo sin pagar el reboot -- **Estado:** PROPUESTO — **dos decisiones abiertas** (ver §Lo que no decide este ADR). No - implementar los verbos antes de cerrar la primera. +- **Estado:** PROPUESTO, con **§1, §2 y §4 IMPLEMENTADOS y verificados en OVMF** (2026-09-12). + Siguen **las dos decisiones abiertas** (ver §Lo que no decide este ADR); ninguna bloquea lo hecho. - **Fecha:** 2026-09-11 - **Frontera:** `recipes/linux-*.toml`, `crates/takana-cli/src/kernel_cmd.rs`, el GC del store. - **Hermano:** [ADR 0018](0018-soberania-del-arranque.md) — arrancar el kernel nuevo es otro @@ -89,6 +89,47 @@ no llega a “sistema sano” en T segundos, la máquina vuelve sola al anterior - **Sin esto, actualizar el kernel de una máquina remota da miedo, y con razón.** Es la única entrada de esta lista que yo pondría primero. +> **ENMIENDA 2026-09-12 — el §2 implementado, y la vuelta atrás la hace el FIRMWARE.** +> +> `takana kernel {stage, boot-status, confirm, rollback}`. El hallazgo que lo vuelve barato: hay **dos +> modos de fallo y sólo uno necesita código nuestro**. +> +> 1. **El candidato no arranca** (pánico temprano, EFI-stub que el firmware rechaza). Lo cubre +> **`BootNext`**, y sale gratis: es una variable **de un solo uso** que el firmware **consume** al +> leerla. Si el kernel muere, el siguiente arranque ya no la encuentra, cae en `BootOrder` y vuelve +> al estable **sin que corra una sola línea nuestra**. No hay contador que mantener ni estado que +> se pueda corromper — el mecanismo *es* el firmware, y por eso no se puede romper. +> 2. **Arranca pero el sistema no queda sano.** Eso el firmware no lo puede saber ⇒ confirmación +> explícita, y cada arranque sin confirmar gasta un intento. +> +> **Verificado con dos kernels REALES del store, cuatro arranques en OVMF:** +> +> | # | qué pasó | evidencia | +> |---|---|---| +> | 1 | estable 6.16.12, sin candidato → `stage` del 7.1.2 | `copiado (17142784 bytes)` · `BootNext = Boot0004` | +> | 2 | **arranca el candidato** | `starting Boot0004 "takana (candidato)"` · `AB: kernel corriendo = 7.1.2` · `⏳ EN PRUEBA` → `confirm` → `✓ promovido` | +> | 3 | arranque normal: el estable **cambió** | `AB: kernel corriendo = 7.1.2` · `sin candidato` | +> | 4 | **`stage` de un candidato ROTO** (4 KiB de basura) y reinicio | `starting Boot0002` —la ruta normal— · `AB: kernel corriendo = 7.1.2` · `✗ el candidato Boot0004 NO arrancó: esta sesión vino del estable` | +> +> La fila 4 es la que justifica todo: **la máquina sobrevivió a que le instalaran un kernel que no +> arranca, sin que nadie interviniera, y además sabe que pasó.** +> +> Tres decisiones que la prueba obligó a tomar: +> - **El candidato va a una ranura propia de la ESP**, nunca encima del estable: un fallo a media +> copia dejaría sin kernel al que volver. La copia es `tmp` + `rename`. +> - **`stage` repone el `BootOrder`** después de crear la entrada. `reconcile` deja al candidato +> primero —correcto para el ADR 0018, incorrecto acá—: un candidato tiene que arrancar **una vez**, +> no volverse el default. Para eso está `BootNext`. +> - **El estado A/B tiene que persistir.** El primer banco de pruebas lo tenía en un tmpfs y `stage` + +> reinicio lo perdía. **Falló seguro** —dijo «sin candidato» y no promovió nada— pero dejaba la +> entrada NVRAM huérfana sin que nadie lo supiera. Ahora `boot-status` se lo pregunta al firmware, +> que es la fuente de verdad, y lo dice. +> +> Y un bug que cazó un **test**, no una revisión: `en_esp` normalizaba los backslashes *después* de +> quitar la barra inicial, así que una ruta en formato EFI salía como `/EFI/…` —absoluta— y +> `Path::join` con una ruta absoluta **descarta la base**: habría escrito en el `/EFI` de la raíz del +> sistema en vez de dentro de la ESP. + ### 3. El kernel corriendo se identifica por **hash**, no por `uname -r` `uname -r` dice `6.16.12` y no dice qué `.config`, qué parches ni qué toolchain. Eso es el bug