gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

respaldo del arranque: lista blanca, porque BootCurrent rompía la idempotencia

Dos arranques reales seguidos, sin tocar nada, daban dos respaldos con
nombre distinto: boot-27b952c9….json y boot-f999e1e8….json. La promesa
de "un arranque que no cambió produce el mismo fichero" era falsa.

La causa: el filtro era «empieza por Boot», y eso deja entrar
BootCurrent, que dice por dónde arrancó ESTA vez y cambia en cada
arranque. Y encima es de SÓLO LECTURA, así que restore habría intentado
escribirla.

Ahora es lista blanca —BootOrder y Boot#### y nada más—, que también
deja fuera BootNext (de un solo uso: restaurarla dispararía un arranque
que nadie pidió) y BootOptionSupport (informativa). Ninguna de las tres
describe cómo debe arrancar la máquina.

Comprobado: cambiando BootCurrent a mano, el respaldo da el mismo
fichero y lo dice — "idéntico a uno que ya estaba: el arranque no
cambió".

Esto sólo se veía ARRANCANDO DOS VECES. Un test del respaldo contra un
efivarfs fabricado habría pasado en verde: la variable que rompía la
propiedad la pone el firmware, no el código.

80/80 del CLI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj

2b87ecb50f1b0253661a39bd813922e85b9ccc27 · por Sergio · (hace 1 mes) · padre bd1fa8bf8 · ver el árbol en este commit

Delta semántico · 3

crates/takana-cli/src/efi_boot.rs

crates/takana-cli/src/main.rs

  • cambió por dentro fn main

3 archivos · +58 −2

diff --git a/crates/takana-cli/src/efi_boot.rs b/crates/takana-cli/src/efi_boot.rs
index 3bae81e9..186e8e29 100644
--- a/crates/takana-cli/src/efi_boot.rs
+++ b/crates/takana-cli/src/efi_boot.rs
@@ -404,6 +404,27 @@ pub fn first_free_slot(root: &Path) -> io::Result<u16> {
     Err(io::Error::new(io::ErrorKind::AlreadyExists, "no queda ni un Boot#### libre (0000-FFFF)"))
 }
 
+/// ¿Es una variable de **configuración** del arranque, y no estado de la sesión?
+///
+/// Lista blanca a propósito: sólo `BootOrder-<guid>` y `Boot####-<guid>`. Un «empieza por Boot» deja
+/// entrar `BootCurrent` (por dónde arrancó esta vez — cambia en cada arranque y es de sólo lectura),
+/// `BootOptionSupport` (informativa) y `BootNext` (de un solo uso: restaurarla dispararía un arranque
+/// que nadie pidió). Ninguna de las tres describe cómo debe arrancar la máquina.
+pub fn es_variable_de_config(nombre: &str) -> bool {
+    let Some((base, guid)) = nombre.rsplit_once('-') else { return false };
+    // El GUID lleva guiones: `rsplit_once` sólo corta el último trozo, así que se rearma mirando
+    // el sufijo completo.
+    let _ = (base, guid);
+    let Some(resto) = nombre.strip_suffix(&format!("-{GLOBAL_GUID}")) else { return false };
+    if resto == "BootOrder" {
+        return true;
+    }
+    match resto.strip_prefix("Boot") {
+        Some(n) => n.len() == 4 && n.chars().all(|c| c.is_ascii_hexdigit()),
+        None => false,
+    }
+}
+
 /// Lista las entradas `Boot####` presentes, en orden numérico, con su descripción ya decodificada.
 pub fn list_entries(root: &Path) -> io::Result<Vec<(u16, LoadOption)>> {
     let mut out = Vec::new();
@@ -736,6 +757,22 @@ mod tests {
         std::fs::remove_dir_all(&tmp).ok();
     }
 
+    #[test]
+    fn la_lista_blanca_deja_fuera_el_estado_de_la_sesion() {
+        let g = GLOBAL_GUID;
+        assert!(es_variable_de_config(&format!("BootOrder-{g}")));
+        assert!(es_variable_de_config(&format!("Boot0004-{g}")));
+        assert!(es_variable_de_config(&format!("BootFFFF-{g}")));
+        // Las tres que rompían la idempotencia del respaldo o son de sólo lectura:
+        assert!(!es_variable_de_config(&format!("BootCurrent-{g}")), "cambia en CADA arranque");
+        assert!(!es_variable_de_config(&format!("BootNext-{g}")), "es de un solo uso");
+        assert!(!es_variable_de_config(&format!("BootOptionSupport-{g}")), "informativa");
+        // Y nada de otro namespace de GUID, aunque se llame parecido.
+        assert!(!es_variable_de_config("Boot0004-11111111-2222-3333-4444-555555555555"));
+        assert!(!es_variable_de_config("Boot004-{g}"), "3 dígitos no es un slot");
+        assert!(!es_variable_de_config("BootOrder"), "sin GUID no es una variable EFI");
+    }
+
     #[test]
     fn boot_order_ida_y_vuelta() {
         let o = vec![0x0001u16, 0x0000, 0x2001];
diff --git a/crates/takana-cli/src/main.rs b/crates/takana-cli/src/main.rs
index b20d10b9..00f8ed04 100644
--- a/crates/takana-cli/src/main.rs
+++ b/crates/takana-cli/src/main.rs
@@ -1645,8 +1645,15 @@ fn main() -> anyhow::Result<()> {
                             for ent in std::fs::read_dir(&efivars)? {
                                 let ent = ent?;
                                 let n = ent.file_name().to_string_lossy().to_string();
-                                let interesa = n.starts_with("Boot") && n.contains(efi_boot::GLOBAL_GUID);
-                                if !interesa {
+                                // LISTA BLANCA, no «empieza con Boot». Medido en dos arranques
+                                // seguidos: `BootCurrent` dice por dónde arrancó ESTA vez y cambia
+                                // aunque la configuración no haya cambiado, así que el respaldo
+                                // salía con un hash distinto cada arranque y la promesa de «un
+                                // arranque que no cambió produce el mismo fichero» era falsa. Y
+                                // encima es de SÓLO LECTURA: `restore` intentaría escribirla.
+                                // Lo mismo `BootOptionSupport` (informativa) y `BootNext` (de un
+                                // solo uso: restaurarla dispararía un arranque que nadie pidió).
+                                if !efi_boot::es_variable_de_config(&n) {
                                     continue;
                                 }
                                 if let Ok(raw) = std::fs::read(ent.path()) {
diff --git a/docs/adr/0018-soberania-del-arranque.md b/docs/adr/0018-soberania-del-arranque.md
index 731d44b3..95bb328e 100644
--- a/docs/adr/0018-soberania-del-arranque.md
+++ b/docs/adr/0018-soberania-del-arranque.md
@@ -264,6 +264,18 @@ conocido**. Barato, y es exactamente la forma de takana.
 > Y `restore` **no escribe por defecto**: hay que pasar `--apply`. La NVRAM es lo único de la máquina
 > que no se puede rehacer desde el store.
 >
+> **Y la promesa de «un arranque que no cambió produce el mismo fichero» empezó siendo falsa.** Dos
+> arranques reales seguidos, sin tocar nada, dieron `boot-27b952c9….json` y `boot-f999e1e8….json`.
+> La causa: el filtro era «empieza por `Boot`», y eso deja entrar **`BootCurrent`**, que dice por
+> dónde arrancó **esta** vez y cambia en cada arranque — además de ser de **sólo lectura**, así que
+> `restore` habría intentado escribirla. Ahora es lista blanca (`BootOrder` y `Boot####` y nada más),
+> que también deja fuera `BootNext` (de un solo uso: restaurarla dispararía un arranque que nadie
+> pidió) y `BootOptionSupport` (informativa). Comprobado: cambiando `BootCurrent` a mano, el respaldo
+> da el mismo fichero y lo dice — *«idéntico a uno que ya estaba: el arranque no cambió»*.
+>
+> Sólo se veía **arrancando dos veces**. Un test del respaldo contra un `efivarfs` fabricado habría
+> pasado en verde: la variable que rompía la propiedad la pone el firmware, no el código.
+>
 > El lector FAT ganó lectura de ficheros para el manifiesto, con su trampa propia: **hay que truncar
 > al tamaño declarado en el directorio**, no al final del último cluster. Un fichero de 1,5 MB en
 > clusters de 1 KiB termina con relleno, y hashear el relleno daría un hash distinto al del mismo