From 2b87ecb50f1b0253661a39bd813922e85b9ccc27 Mon Sep 17 00:00:00 2001 From: Sergio Date: Sat, 12 Sep 2026 00:37:13 +0000 Subject: [PATCH] =?UTF-8?q?respaldo=20del=20arranque:=20lista=20blanca,=20?= =?UTF-8?q?porque=20BootCurrent=20romp=C3=ADa=20la=20idempotencia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01HHAKWoBqof9XvsYCvDicEj --- crates/takana-cli/src/efi_boot.rs | 37 +++++++++++++++++++++++++ crates/takana-cli/src/main.rs | 11 ++++++-- docs/adr/0018-soberania-del-arranque.md | 12 ++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) 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 { 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-` y `Boot####-`. 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> { 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