> 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
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