# ADR 0017 — Ciclo de vida del kernel: cambiarlo sin pagar el reboot - **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 problema y tiene su propio fichero. **Relacionado:** [SDD 22](../22-configurador-kernel.md) (el armador), [SDD 25](../25-tasas-del-kernel.md). ## Contexto El problema de actualizar kernels en una distro normal no es técnico: es que **el kernel es un nombre mutable**. `/boot/vmlinuz-6.16.12` es un fichero que se sobreescribe, y conservar los N anteriores es una convención que se rompe sola cuando `/boot` se llena. De ahí sale todo lo demás — el rollback deja de ser una operación y pasa a ser un procedimiento con pasos que hay que recordar bajo presión. En takana el kernel es un artefacto de contenido: el viejo y el nuevo coexisten sin disputarse un nombre. **La mitad de este problema está resuelta por construcción y sin haberlo buscado.** Esto importa porque cambia dónde conviene invertir: > Una distro no se hace fuerte logrando que la actualización no falle. > Se hace fuerte logrando que **fallar no cueste nada**. ## Lo medido (2026-09-11) Sobre los `.config` **sellados en `./store`**, no sobre las recetas — el `.config` producido es la identidad del artefacto (SDD 22 §1) y es lo único que dice qué kernel hay de verdad. | kernel (store) | `KEXEC` | `KEXEC_FILE` | `LIVEPATCH` | `MODULES` | `EFI_STUB` | `CMDLINE_BOOL` | |---|---|---|---|---|---|---| | `linux` `d2a03c29` | ✅ | — | — | **✅** | ✅ | — | | `linux-generic` `23f1cc43` | ✅ | — | — | — | ✅ | ✅ | | `linux-metal` `2ed8f54a` | ✅ | — | — | — | ✅ | ✅ | | `linux-metal-dual` `714f46d6` | ✅ | — | — | — | ✅ | ✅ | | `linux-gioser` `8ffd5710`, `b026fe14` | ✅ | — | — | — | ✅ | ✅ | Cuatro consecuencias, y ninguna es opinión: 1. **`CONFIG_KEXEC=y` en los 6.** La primitiva para cambiar de kernel sin pasar por el firmware **ya está pagada en todo el corpus**, gratis, heredada del `defconfig` de x86. No hay que re-hashear nada para empezar a usarla. 2. **`CONFIG_LIVEPATCH` en ninguno.** Lo que sí aparece es `CONFIG_HAVE_LIVEPATCH=y`, que es *capacidad de la arquitectura*, no el feature — confundirlos es exactamente el modo de error que este repo ya tiene nombrado: la etiqueta no es el hecho. 3. **`CONFIG_MODULES` sólo en `linux`.** Los cinco kernels que arrancan máquinas son monolíticos (`recipes/linux-metal.toml:108` lo apaga a propósito). **Un livepatch *es* un `.ko`** ⇒ en esos cinco kernels el livepatch está cerrado por construcción, no por falta de trabajo. 4. **`CONFIG_KEXEC_FILE` en ninguno.** Sólo existe el syscall viejo `kexec_load`. Con *lockdown* activo (lo que trae Secure Boot) el kernel **rechaza `kexec_load`** y sólo acepta `kexec_file_load` con imagen firmada. ⇒ **kexec y Secure Boot no coexisten hoy**, y eso ata este ADR con el 0018 §Secure Boot. Y el estado de la herramienta: `takana kernel` tiene hoy nueve subcomandos —`closure`, `probe`, `bundles`, `plan`, `diff-back`, `gate`, `hw`, `contract`, `stats`— y **ninguno escribe nada**. SDD 22 construyó deliberadamente un armador que sólo analiza. Los verbos que propone este ADR serían los primeros del namespace que actúan sobre la máquina, y esa frontera merece cruzarse a propósito. No existen en el catálogo `kexec-tools`, `efibootmgr`, `sbsigntool`, `shim`, `mokutil` ni `efivar`. ## Decisión ### 1. Tres verbos, nombrados por lo que cuestan Casi toda la industria vende «live kernel update» y entrega una de tres cosas distintas. Takana nombra las tres por separado y **no llama a ninguna lo que no es**: | verbo | qué hace | downtime | alcance | perfil | |---|---|---|---|---| | `takana kernel stage ` | deja el kernel listo; entra en el próximo reboot natural | **cero** | total | escritorio | | `takana kernel kexec ` | reboot **suave**: salta firmware y gestor de arranque | ~2 s, **mata el userspace** | total | servidor | | *(livepatch)* | parchea funciones del kernel vivo | cero real | **una función** | CVE en producción | `kexec` **no es «sin rebootear»** y la ayuda del comando tiene que decirlo con esas palabras. Lo que ahorra son los 20–30 s de POST/UEFI, que en un servidor remoto es la mitad del downtime y en un portátil no es nada. Ser la distro que **no confunde las tres** ya es una posición: la confusión es la norma del mercado. > **ENMIENDA 2026-09-12 (2ª) — `kernel kexec` implementado, y por `kexec_file_load`.** > > La diferencia entre los dos syscalls es de **orden de magnitud**, y decide el frente entero: > > - `kexec_load` (el que traen los seis kernels sellados) recibe los **segmentos ya armados** y el > *purgatory* —el código que corre entre los dos kernels—. Usarlo obliga a reimplementar > kexec-tools, o a empaquetarlo. > - `kexec_file_load` recibe los **descriptores** del kernel y del initrd y hace el trabajo adentro. > **~50 líneas**, sin dependencias ajenas. > > El syscall va con `asm!` en vez de agregar la crate `libc` al workspace: es **un** syscall, su > número y su ABI son contrato estable de Linux, y la alternativa era arrastrar una dependencia a un > workspace que comparten dos frentes. El cmdline viaja **con su NUL** — el kernel cuenta > `cmdline_len` incluyendo el terminador, y es el error clásico de esta llamada. > > **VERIFICADO SALTANDO DE VERDAD (2026-09-12), en OVMF:** > > ``` > BdsDxe: starting Boot0002 "UEFI Misc Device" ← el firmware arranca UNA vez > KEXEC-PRUEBA: kernel = 6.16.12 ← linux-metal-kexec, con CONFIG_KEXEC_FILE > --- cargando el destino (7.1.2) y saltando: > ✓ kernel cargado en memoria > ⚡ saltando — el userspace muere ACÁ. Esto no vuelve. > KEXEC-PRUEBA: kernel = 7.1.2 ← linux-metal-dual, OTRO kernel > KEXEC-PRUEBA-LLEGADA: este kernel NO lo arrancó el firmware > ``` > > **La prueba no es que aparezca el 7.1.2: es que `BdsDxe: starting` aparece UNA SOLA VEZ.** Dos > kernels distintos corrieron y el firmware sólo intervino en el primero. Y el 7.1.2 no está en > ninguna entrada de arranque —vive dentro del initramfs—, así que no hay forma de que el firmware lo > hubiera lanzado aunque hubiera querido. > > *(El banner `Linux version` sale una sola vez en el log porque el cmdline horneado del kernel de > arranque lleva `quiet loglevel=3`. No es que arrancara un solo kernel: los dos `uname -r` lo > desmienten.)* > > **Y esto responde media decisión abierta n.º 2.** `recipes/linux-metal-kexec.toml` es una variante > cuya única diferencia es `CONFIG_KEXEC_FILE=y`. Variante y no flag en la canónica porque el > `.config` **es** la identidad del artefacto (SDD 22 §1) y esa decisión es del usuario; comprobado > que la canónica **no se movió** (`b3:2ed8f54a…`, el que ya está sellado). Con ese flag, kexec y > Secure Boot **pueden** coexistir: lockdown prohíbe `kexec_load` y acepta `kexec_file_load` con > imagen firmada. Lo que queda por decidir es si se firma, no si es posible. > > **Un diagnóstico que corregí a los dos minutos de escribirlo, porque mandaba al lugar OPUESTO.** > El primer `match` de errno decía «falta `CONFIG_KEXEC_FILE`» para **EPERM**. Medido en este mismo > hub: el kernel de Artix trae `CONFIG_KEXEC_FILE=y` y aun así devolvió EPERM — **por no ser root**. > EPERM es falta de `CAP_SYS_BOOT` (o lockdown, si ya sos root); **ENOSYS** es el flag que falta. > Confundirlos manda a recompilar un kernel que estaba perfecto. Es la tercera vez en dos días que > el error caro no es el mecanismo sino **el mensaje que explica el mecanismo**. ### 2. A/B con vuelta atrás automática — esto es lo que vale `kexec` solo no hace fuerte a nadie: hace *rápido*. Lo que hace fuerte es que **si el kernel nuevo no llega a “sistema sano” en T segundos, la máquina vuelve sola al anterior.** - systemd-boot aproxima esto con contadores en el *nombre del fichero* (`+3-0`). Takana puede hacerlo mejor porque el estado al que volver es **un hash**, no un fichero que puede no estar. - Qué es «sano» lo declara el perfil, no el kernel: para `perfil.servidor` es que arje-zero levantó sus servicios supervisados; para un escritorio, que el compositor pintó. - **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 clásico de los livepatches y los módulos fuera de árbol: se construyen contra el kernel equivocado y el síntoma aparece meses después. Con el hash del artefacto viajando en el kernel vivo —el cmdline embebido ya lo permite en 5 de los 6— la pregunta «¿esto corresponde a este kernel?» pasa de heurística a **comparación exacta**. Es un cierre de invariante, no una comodidad. ### 4. El kernel EN EJECUCIÓN es raíz del GC No el de la entrada de arranque: **el que está corriendo**. Si el GC poda el artefacto del kernel vivo, se pierden el rollback y la capacidad de diagnosticar, **y se pierden en silencio** — el sistema sigue andando perfecto hasta el día que lo necesitás. Es el mismo patrón que ya costó caro en el CAS: lo que no es alcanzable desde una raíz desaparece sin avisar. ⇒ `scripts/store-gc.sh` y cualquier GC nuevo leen el hash del kernel vivo y lo tratan como raíz. Guardián con **rotura a propósito y control que tiene que pasar**, no sólo con el caso feliz. > **ENMIENDA 2026-09-11 — el §4 está implementado en `scripts/store-gc.sh` y probado en los tres > sentidos.** El GC suma ahora una raíz: el artefacto de kernel cuyo `boot/config-*` coincide byte a > byte con el `.config` del kernel vivo (`/proc/config.gz`, o `/boot/config-$(uname -r)`). > > | caso | esperado | resultado | > |---|---|---| > | store de juguete con el kernel **vivo** marcado «superado» | se rescata | ✅ movido a vigentes, y lo **dice**: «1 estaban marcados para BORRAR y se rescataron» | > | **control:** un kernel con `.config` distinto, también marcado | **sigue condenado** | ✅ no protege de más | > | store real, en el hub (kernel ajeno de Artix) | no protege nada, y lo explica | ✅ «NINGUN artefacto del store lo tiene» | > > El control del medio es el que importa: sin él, una función que devolviera «protegido» siempre > pasaría la primera prueba y volvería inútil al GC sin que nadie lo notara. > > Identifica por **contenido del `.config`**, no por hash de artefacto: dos artefactos con el mismo > `.config` y distinta toolchain se protegen los dos. Es el lado correcto para un GC, y se vuelve > exacto el día que el §3 ponga el hash en el kernel vivo. ### 5. Si alguna vez hace falta livepatch, es una **variante**, no un flag La salida correcta **no** es encender `MODULES` en el kernel general —eso revierte una decisión de reproducibilidad y superficie de ataque para todo el mundo—. Es una variante `linux-servidor-livepatch` con su propio hash, para el perfil que la pida. En takana las variantes son baratas; lo caro es descubrir dentro de dos años que la puerta se cerró sin que nadie supiera que la estaba cerrando. ### 6. El límite que este ADR no cruza, dicho en voz alta Para «actualizar **sin interrumpir el trabajo**» la respuesta no está en el kernel: es CRIU o drenar la carga a otra máquina. **Ninguna distro resuelve eso**, y prometerlo es donde empiezan las mentiras de la categoría. `stage` + `kexec` + rollback automático es el techo honesto. ## Lo que no decide este ADR 1. **¿El perfil servidor quiere livepatch?** ⛔ **ABIERTA.** Si **sí**: hay que prever la variante con `MODULES` ahora y aceptar su costo (superficie de módulos cargables, `vermagic`, firma de módulos si además hay Secure Boot). Si **no**: hay que escribirlo como decisión explícita, para que dentro de un año nadie lo «arregle» encendiendo módulos en el kernel general creyendo que fue un olvido. 2. **¿`kexec` es compatible con el destino de Secure Boot?** ✅ **RESPONDIDA (2026-09-12): sí, con `KEXEC_FILE`.** Ya no es una incógnita técnica sino un costo conocido: existe `recipes/linux-metal-kexec.toml` con el flag, y bajo lockdown ese es justamente el syscall que el kernel acepta. Lo que queda abierto es **si el perfil que use kexec adopta esa variante** (un artefacto de kernel más) o si se enciende el flag en la canónica (re-hashea el kernel de todas las imágenes). Eso sí sigue siendo decisión del usuario, pero es una decisión de *coste*, no de viabilidad. ## Riesgos conocidos - **El cmdline está embebido** (`CONFIG_CMDLINE_BOOL`, `linux-metal.toml:150`) porque el arranque normal es por EFI-STUB. `kexec` **no usa el stub**: carga por el protocolo de boot de Linux, así que initrd y cmdline hay que pasárselos explícitos. El traspaso de la EFI memmap por `setup_data` con `efi=novamap` puesto **es el punto a probar en QEMU antes de tocar metal** — no está verificado y no debe darse por bueno. - **El monolítico es una ventaja acá**: sin módulos no hay `vermagic` ni `/lib/modules` que se desincronicen. El modo de fallo clásico de «actualicé el kernel y no rebooteé» —enchufás un USB y no pasa nada porque el módulo ya no está en disco— **no existe en takana**. Vale anotarlo del lado del haber cuando se discuta el punto 1.