1# ADR 0017 — Ciclo de vida del kernel: cambiarlo sin pagar el reboot
2
3- **Estado:** PROPUESTO, con **§1, §2 y §4 IMPLEMENTADOS y verificados en OVMF** (2026-09-12).
4 Siguen **las dos decisiones abiertas** (ver §Lo que no decide este ADR); ninguna bloquea lo hecho.
5- **Fecha:** 2026-09-11
6- **Frontera:** `recipes/linux-*.toml`, `crates/takana-cli/src/kernel_cmd.rs`, el GC del store.
7- **Hermano:** [ADR 0018](0018-soberania-del-arranque.md) — arrancar el kernel nuevo es otro
8 problema y tiene su propio fichero. **Relacionado:** [SDD 22](../22-configurador-kernel.md)
9 (el armador), [SDD 25](../25-tasas-del-kernel.md).
10
11## Contexto
12
13El problema de actualizar kernels en una distro normal no es técnico: es que **el kernel es un
14nombre mutable**. `/boot/vmlinuz-6.16.12` es un fichero que se sobreescribe, y conservar los N
15anteriores es una convención que se rompe sola cuando `/boot` se llena. De ahí sale todo lo demás —
16el rollback deja de ser una operación y pasa a ser un procedimiento con pasos que hay que recordar
17bajo presión.
18
19En takana el kernel es un artefacto de contenido: el viejo y el nuevo coexisten sin disputarse un
20nombre. **La mitad de este problema está resuelta por construcción y sin haberlo buscado.** Esto
21importa porque cambia dónde conviene invertir:
22
23> Una distro no se hace fuerte logrando que la actualización no falle.
24> Se hace fuerte logrando que **fallar no cueste nada**.
25
26## Lo medido (2026-09-11)
27
28Sobre los `.config` **sellados en `./store`**, no sobre las recetas — el `.config` producido es la
29identidad del artefacto (SDD 22 §1) y es lo único que dice qué kernel hay de verdad.
30
31| kernel (store) | `KEXEC` | `KEXEC_FILE` | `LIVEPATCH` | `MODULES` | `EFI_STUB` | `CMDLINE_BOOL` |
32|---|---|---|---|---|---|---|
33| `linux` `d2a03c29` | ✅ | — | — | **✅** | ✅ | — |
34| `linux-generic` `23f1cc43` | ✅ | — | — | — | ✅ | ✅ |
35| `linux-metal` `2ed8f54a` | ✅ | — | — | — | ✅ | ✅ |
36| `linux-metal-dual` `714f46d6` | ✅ | — | — | — | ✅ | ✅ |
37| `linux-gioser` `8ffd5710`, `b026fe14` | ✅ | — | — | — | ✅ | ✅ |
38
39Cuatro consecuencias, y ninguna es opinión:
40
411. **`CONFIG_KEXEC=y` en los 6.** La primitiva para cambiar de kernel sin pasar por el firmware
42 **ya está pagada en todo el corpus**, gratis, heredada del `defconfig` de x86. No hay que
43 re-hashear nada para empezar a usarla.
442. **`CONFIG_LIVEPATCH` en ninguno.** Lo que sí aparece es `CONFIG_HAVE_LIVEPATCH=y`, que es
45 *capacidad de la arquitectura*, no el feature — confundirlos es exactamente el modo de error que
46 este repo ya tiene nombrado: la etiqueta no es el hecho.
473. **`CONFIG_MODULES` sólo en `linux`.** Los cinco kernels que arrancan máquinas son monolíticos
48 (`recipes/linux-metal.toml:108` lo apaga a propósito). **Un livepatch *es* un `.ko`** ⇒ en esos
49 cinco kernels el livepatch está cerrado por construcción, no por falta de trabajo.
504. **`CONFIG_KEXEC_FILE` en ninguno.** Sólo existe el syscall viejo `kexec_load`. Con *lockdown*
51 activo (lo que trae Secure Boot) el kernel **rechaza `kexec_load`** y sólo acepta
52 `kexec_file_load` con imagen firmada. ⇒ **kexec y Secure Boot no coexisten hoy**, y eso ata este
53 ADR con el 0018 §Secure Boot.
54
55Y el estado de la herramienta: `takana kernel` tiene hoy nueve subcomandos —`closure`, `probe`,
56`bundles`, `plan`, `diff-back`, `gate`, `hw`, `contract`, `stats`— y **ninguno escribe nada**. SDD 22
57construyó deliberadamente un armador que sólo analiza. Los verbos que propone este ADR serían los
58primeros del namespace que actúan sobre la máquina, y esa frontera merece cruzarse a propósito.
59
60No existen en el catálogo `kexec-tools`, `efibootmgr`, `sbsigntool`, `shim`, `mokutil` ni `efivar`.
61
62## Decisión
63
64### 1. Tres verbos, nombrados por lo que cuestan
65
66Casi toda la industria vende «live kernel update» y entrega una de tres cosas distintas. Takana
67nombra las tres por separado y **no llama a ninguna lo que no es**:
68
69| verbo | qué hace | downtime | alcance | perfil |
70|---|---|---|---|---|
71| `takana kernel stage <hash>` | deja el kernel listo; entra en el próximo reboot natural | **cero** | total | escritorio |
72| `takana kernel kexec <hash>` | reboot **suave**: salta firmware y gestor de arranque | ~2 s, **mata el userspace** | total | servidor |
73| *(livepatch)* | parchea funciones del kernel vivo | cero real | **una función** | CVE en producción |
74
75`kexec` **no es «sin rebootear»** y la ayuda del comando tiene que decirlo con esas palabras. Lo que
76ahorra son los 20–30 s de POST/UEFI, que en un servidor remoto es la mitad del downtime y en un
77portátil no es nada. Ser la distro que **no confunde las tres** ya es una posición: la confusión es
78la norma del mercado.
79
80> **ENMIENDA 2026-09-12 (2ª) — `kernel kexec` implementado, y por `kexec_file_load`.**
81>
82> La diferencia entre los dos syscalls es de **orden de magnitud**, y decide el frente entero:
83>
84> - `kexec_load` (el que traen los seis kernels sellados) recibe los **segmentos ya armados** y el
85> *purgatory* —el código que corre entre los dos kernels—. Usarlo obliga a reimplementar
86> kexec-tools, o a empaquetarlo.
87> - `kexec_file_load` recibe los **descriptores** del kernel y del initrd y hace el trabajo adentro.
88> **~50 líneas**, sin dependencias ajenas.
89>
90> El syscall va con `asm!` en vez de agregar la crate `libc` al workspace: es **un** syscall, su
91> número y su ABI son contrato estable de Linux, y la alternativa era arrastrar una dependencia a un
92> workspace que comparten dos frentes. El cmdline viaja **con su NUL** — el kernel cuenta
93> `cmdline_len` incluyendo el terminador, y es el error clásico de esta llamada.
94>
95> **VERIFICADO SALTANDO DE VERDAD (2026-09-12), en OVMF:**
96>
97> ```
98> BdsDxe: starting Boot0002 "UEFI Misc Device" ← el firmware arranca UNA vez
99> KEXEC-PRUEBA: kernel = 6.16.12 ← linux-metal-kexec, con CONFIG_KEXEC_FILE
100> --- cargando el destino (7.1.2) y saltando:
101> ✓ kernel cargado en memoria
102> ⚡ saltando — el userspace muere ACÁ. Esto no vuelve.
103> KEXEC-PRUEBA: kernel = 7.1.2 ← linux-metal-dual, OTRO kernel
104> KEXEC-PRUEBA-LLEGADA: este kernel NO lo arrancó el firmware
105> ```
106>
107> **La prueba no es que aparezca el 7.1.2: es que `BdsDxe: starting` aparece UNA SOLA VEZ.** Dos
108> kernels distintos corrieron y el firmware sólo intervino en el primero. Y el 7.1.2 no está en
109> ninguna entrada de arranque —vive dentro del initramfs—, así que no hay forma de que el firmware lo
110> hubiera lanzado aunque hubiera querido.
111>
112> *(El banner `Linux version` sale una sola vez en el log porque el cmdline horneado del kernel de
113> arranque lleva `quiet loglevel=3`. No es que arrancara un solo kernel: los dos `uname -r` lo
114> desmienten.)*
115>
116> **Y esto responde media decisión abierta n.º 2.** `recipes/linux-metal-kexec.toml` es una variante
117> cuya única diferencia es `CONFIG_KEXEC_FILE=y`. Variante y no flag en la canónica porque el
118> `.config` **es** la identidad del artefacto (SDD 22 §1) y esa decisión es del usuario; comprobado
119> que la canónica **no se movió** (`b3:2ed8f54a…`, el que ya está sellado). Con ese flag, kexec y
120> Secure Boot **pueden** coexistir: lockdown prohíbe `kexec_load` y acepta `kexec_file_load` con
121> imagen firmada. Lo que queda por decidir es si se firma, no si es posible.
122>
123> **Un diagnóstico que corregí a los dos minutos de escribirlo, porque mandaba al lugar OPUESTO.**
124> El primer `match` de errno decía «falta `CONFIG_KEXEC_FILE`» para **EPERM**. Medido en este mismo
125> hub: el kernel de Artix trae `CONFIG_KEXEC_FILE=y` y aun así devolvió EPERM — **por no ser root**.
126> EPERM es falta de `CAP_SYS_BOOT` (o lockdown, si ya sos root); **ENOSYS** es el flag que falta.
127> Confundirlos manda a recompilar un kernel que estaba perfecto. Es la tercera vez en dos días que
128> el error caro no es el mecanismo sino **el mensaje que explica el mecanismo**.
129
130### 2. A/B con vuelta atrás automática — esto es lo que vale
131
132`kexec` solo no hace fuerte a nadie: hace *rápido*. Lo que hace fuerte es que **si el kernel nuevo
133no llega a “sistema sano” en T segundos, la máquina vuelve sola al anterior.**
134
135- systemd-boot aproxima esto con contadores en el *nombre del fichero* (`+3-0`). Takana puede
136 hacerlo mejor porque el estado al que volver es **un hash**, no un fichero que puede no estar.
137- Qué es «sano» lo declara el perfil, no el kernel: para `perfil.servidor` es que arje-zero levantó
138 sus servicios supervisados; para un escritorio, que el compositor pintó.
139- **Sin esto, actualizar el kernel de una máquina remota da miedo, y con razón.** Es la única
140 entrada de esta lista que yo pondría primero.
141
142> **ENMIENDA 2026-09-12 — el §2 implementado, y la vuelta atrás la hace el FIRMWARE.**
143>
144> `takana kernel {stage, boot-status, confirm, rollback}`. El hallazgo que lo vuelve barato: hay **dos
145> modos de fallo y sólo uno necesita código nuestro**.
146>
147> 1. **El candidato no arranca** (pánico temprano, EFI-stub que el firmware rechaza). Lo cubre
148> **`BootNext`**, y sale gratis: es una variable **de un solo uso** que el firmware **consume** al
149> leerla. Si el kernel muere, el siguiente arranque ya no la encuentra, cae en `BootOrder` y vuelve
150> al estable **sin que corra una sola línea nuestra**. No hay contador que mantener ni estado que
151> se pueda corromper — el mecanismo *es* el firmware, y por eso no se puede romper.
152> 2. **Arranca pero el sistema no queda sano.** Eso el firmware no lo puede saber ⇒ confirmación
153> explícita, y cada arranque sin confirmar gasta un intento.
154>
155> **Verificado con dos kernels REALES del store, cuatro arranques en OVMF:**
156>
157> | # | qué pasó | evidencia |
158> |---|---|---|
159> | 1 | estable 6.16.12, sin candidato → `stage` del 7.1.2 | `copiado (17142784 bytes)` · `BootNext = Boot0004` |
160> | 2 | **arranca el candidato** | `starting Boot0004 "takana (candidato)"` · `AB: kernel corriendo = 7.1.2` · `⏳ EN PRUEBA` → `confirm` → `✓ promovido` |
161> | 3 | arranque normal: el estable **cambió** | `AB: kernel corriendo = 7.1.2` · `sin candidato` |
162> | 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` |
163>
164> La fila 4 es la que justifica todo: **la máquina sobrevivió a que le instalaran un kernel que no
165> arranca, sin que nadie interviniera, y además sabe que pasó.**
166>
167> Tres decisiones que la prueba obligó a tomar:
168> - **El candidato va a una ranura propia de la ESP**, nunca encima del estable: un fallo a media
169> copia dejaría sin kernel al que volver. La copia es `tmp` + `rename`.
170> - **`stage` repone el `BootOrder`** después de crear la entrada. `reconcile` deja al candidato
171> primero —correcto para el ADR 0018, incorrecto acá—: un candidato tiene que arrancar **una vez**,
172> no volverse el default. Para eso está `BootNext`.
173> - **El estado A/B tiene que persistir.** El primer banco de pruebas lo tenía en un tmpfs y `stage` +
174> reinicio lo perdía. **Falló seguro** —dijo «sin candidato» y no promovió nada— pero dejaba la
175> entrada NVRAM huérfana sin que nadie lo supiera. Ahora `boot-status` se lo pregunta al firmware,
176> que es la fuente de verdad, y lo dice.
177>
178> Y un bug que cazó un **test**, no una revisión: `en_esp` normalizaba los backslashes *después* de
179> quitar la barra inicial, así que una ruta en formato EFI salía como `/EFI/…` —absoluta— y
180> `Path::join` con una ruta absoluta **descarta la base**: habría escrito en el `/EFI` de la raíz del
181> sistema en vez de dentro de la ESP.
182
183### 3. El kernel corriendo se identifica por **hash**, no por `uname -r`
184
185`uname -r` dice `6.16.12` y no dice qué `.config`, qué parches ni qué toolchain. Eso es el bug
186clásico de los livepatches y los módulos fuera de árbol: se construyen contra el kernel equivocado y
187el síntoma aparece meses después. Con el hash del artefacto viajando en el kernel vivo —el cmdline
188embebido ya lo permite en 5 de los 6— la pregunta «¿esto corresponde a este kernel?» pasa de
189heurística a **comparación exacta**.
190
191Es un cierre de invariante, no una comodidad.
192
193### 4. El kernel EN EJECUCIÓN es raíz del GC
194
195No el de la entrada de arranque: **el que está corriendo**. Si el GC poda el artefacto del kernel
196vivo, se pierden el rollback y la capacidad de diagnosticar, **y se pierden en silencio** — el
197sistema sigue andando perfecto hasta el día que lo necesitás. Es el mismo patrón que ya costó caro
198en el CAS: lo que no es alcanzable desde una raíz desaparece sin avisar.
199
200⇒ `scripts/store-gc.sh` y cualquier GC nuevo leen el hash del kernel vivo y lo tratan como raíz.
201Guardián con **rotura a propósito y control que tiene que pasar**, no sólo con el caso feliz.
202
203> **ENMIENDA 2026-09-11 — el §4 está implementado en `scripts/store-gc.sh` y probado en los tres
204> sentidos.** El GC suma ahora una raíz: el artefacto de kernel cuyo `boot/config-*` coincide byte a
205> byte con el `.config` del kernel vivo (`/proc/config.gz`, o `/boot/config-$(uname -r)`).
206>
207> | caso | esperado | resultado |
208> |---|---|---|
209> | 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» |
210> | **control:** un kernel con `.config` distinto, también marcado | **sigue condenado** | ✅ no protege de más |
211> | store real, en el hub (kernel ajeno de Artix) | no protege nada, y lo explica | ✅ «NINGUN artefacto del store lo tiene» |
212>
213> El control del medio es el que importa: sin él, una función que devolviera «protegido» siempre
214> pasaría la primera prueba y volvería inútil al GC sin que nadie lo notara.
215>
216> Identifica por **contenido del `.config`**, no por hash de artefacto: dos artefactos con el mismo
217> `.config` y distinta toolchain se protegen los dos. Es el lado correcto para un GC, y se vuelve
218> exacto el día que el §3 ponga el hash en el kernel vivo.
219
220### 5. Si alguna vez hace falta livepatch, es una **variante**, no un flag
221
222La salida correcta **no** es encender `MODULES` en el kernel general —eso revierte una decisión de
223reproducibilidad y superficie de ataque para todo el mundo—. Es una variante
224`linux-servidor-livepatch` con su propio hash, para el perfil que la pida. En takana las variantes
225son baratas; lo caro es descubrir dentro de dos años que la puerta se cerró sin que nadie supiera
226que la estaba cerrando.
227
228### 6. El límite que este ADR no cruza, dicho en voz alta
229
230Para «actualizar **sin interrumpir el trabajo**» la respuesta no está en el kernel: es CRIU o drenar
231la carga a otra máquina. **Ninguna distro resuelve eso**, y prometerlo es donde empiezan las
232mentiras de la categoría. `stage` + `kexec` + rollback automático es el techo honesto.
233
234## Lo que no decide este ADR
235
2361. **¿El perfil servidor quiere livepatch?** ⛔ **ABIERTA.**
237 Si **sí**: hay que prever la variante con `MODULES` ahora y aceptar su costo (superficie de
238 módulos cargables, `vermagic`, firma de módulos si además hay Secure Boot).
239 Si **no**: hay que escribirlo como decisión explícita, para que dentro de un año nadie lo
240 «arregle» encendiendo módulos en el kernel general creyendo que fue un olvido.
241
2422. **¿`kexec` es compatible con el destino de Secure Boot?** ✅ **RESPONDIDA (2026-09-12): sí, con
243 `KEXEC_FILE`.** Ya no es una incógnita técnica sino un costo conocido: existe
244 `recipes/linux-metal-kexec.toml` con el flag, y bajo lockdown ese es justamente el syscall que el
245 kernel acepta. Lo que queda abierto es **si el perfil que use kexec adopta esa variante** (un
246 artefacto de kernel más) o si se enciende el flag en la canónica (re-hashea el kernel de todas
247 las imágenes). Eso sí sigue siendo decisión del usuario, pero es una decisión de *coste*, no de
248 viabilidad.
249
250## Riesgos conocidos
251
252- **El cmdline está embebido** (`CONFIG_CMDLINE_BOOL`, `linux-metal.toml:150`) porque el arranque
253 normal es por EFI-STUB. `kexec` **no usa el stub**: carga por el protocolo de boot de Linux, así
254 que initrd y cmdline hay que pasárselos explícitos. El traspaso de la EFI memmap por `setup_data`
255 con `efi=novamap` puesto **es el punto a probar en QEMU antes de tocar metal** — no está
256 verificado y no debe darse por bueno.
257- **El monolítico es una ventaja acá**: sin módulos no hay `vermagic` ni `/lib/modules` que se
258 desincronicen. El modo de fallo clásico de «actualicé el kernel y no rebooteé» —enchufás un USB y
259 no pasa nada porque el módulo ya no está en disco— **no existe en takana**. Vale anotarlo del lado
260 del haber cuando se discuta el punto 1.
0017-ciclo-de-vida-del-kernel.md
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 — arrancar el kernel nuevo es otro
problema y tiene su propio fichero. Relacionado: SDD 22
(el armador), SDD 25.
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:
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.
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.
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.
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 <hash> | deja el kernel listo; entra en el próximo reboot natural | cero | total | escritorio |
takana kernel kexec <hash> | 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.
- 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.
- 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
-
¿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.
-
¿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.