From 64fe4e744574c8e0273dd1e5d54ceae9714748a0 Mon Sep 17 00:00:00 2001 From: Wei Date: Wed, 24 Jun 2026 19:42:45 +0800 Subject: [PATCH] fix(fde): fall back to default GRUB menuentry when grubenv has no saved_entry show-reference-value (load_kernel_artifacts) aborted with "saved_entry not found in GRUB environment" on freshly built / never-booted images, whose GRUB environment block is empty. GRUB's default selection order is next_entry > saved_entry > `set default`. On such images GRUB boots the default entry (`set default="0"`, i.e. the first menuentry), so fall back to resolving the first menuentry id from grub.cfg instead of failing. This lets reference values be computed for images that have not been booted yet, without mutating the image to fabricate a saved_entry. --- cryptpilot-fde/src/disk/grub.rs | 52 +++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/cryptpilot-fde/src/disk/grub.rs b/cryptpilot-fde/src/disk/grub.rs index a1bdc1ff..bfb8b44b 100644 --- a/cryptpilot-fde/src/disk/grub.rs +++ b/cryptpilot-fde/src/disk/grub.rs @@ -131,6 +131,30 @@ impl BootArtifacts for GrubBootArtifacts { } } +/// Resolve the id of GRUB's default menuentry from a `grub.cfg`. +/// +/// Used when the GRUB environment block has no `saved_entry` (e.g. an image that +/// has never been booted). The default selection on such images is +/// `set default="0"`, i.e. the first menuentry, so its id is returned. The id is +/// the quoted token right before the entry's opening brace, e.g. +/// `menuentry 'Ubuntu' --class os $menuentry_id_option 'gnulinux-simple-' {`. +fn default_menuentry_id(grub_cfg: &str) -> Option { + for line in grub_cfg.lines() { + let line = line.trim(); + // Match an actual menuentry definition (not e.g. `menuentry_id_option=...`). + if !line.starts_with("menuentry ") || !line.contains('{') { + continue; + } + let head = line.split('{').next().unwrap_or(line).trim_end(); + if let Some(end) = head.rfind('\'') { + if let Some(start) = head[..end].rfind('\'') { + return Some(head[start + 1..end].to_string()); + } + } + } + None +} + fn parse_pe(bytes: &[u8]) -> Result, object::read::Error> { if let Ok(pe) = PeFile64::parse(bytes) { Ok(Box::new(pe)) @@ -382,12 +406,30 @@ pub(super) trait FdeDiskGrubExt: Disk { grub_vars: &HashMap, grub_cfg: &str, ) -> Result { - let saved_entry = grub_vars - .get("saved_entry") - .ok_or_else(|| anyhow::anyhow!("saved_entry not found in GRUB environment"))?; + // GRUB's default selection order is: next_entry > saved_entry > `set default`. + // Freshly built / never-booted images have an empty GRUB environment block + // (no `saved_entry`); GRUB then boots the default entry (`set default="0"`, + // i.e. the first menuentry). Fall back to that instead of failing, so that + // reference values can be computed for images that have never been booted. + let saved_entry = match grub_vars.get("saved_entry") { + Some(entry) => entry.clone(), + None => { + let entry = default_menuentry_id(grub_cfg).ok_or_else(|| { + anyhow::anyhow!( + "saved_entry not found in GRUB environment and no menuentry found in grub.cfg" + ) + })?; + tracing::warn!( + %entry, + "saved_entry not set in GRUB environment (image likely never booted); \ + falling back to the default grub.cfg menuentry" + ); + entry + } + }; let (mut kernel_path, mut initrd_path, cmdline) = match self - .load_from_loader_entry_file(saved_entry, grub_vars) + .load_from_loader_entry_file(&saved_entry, grub_vars) .await { Ok(v) => v, @@ -397,7 +439,7 @@ pub(super) trait FdeDiskGrubExt: Disk { "Failed to parse kernel artifacts info from loader entry file, fallback to parse from grub.cfg" ); - self.load_from_grub_cfg(saved_entry, grub_cfg).await? + self.load_from_grub_cfg(&saved_entry, grub_cfg).await? } };