Skip to content

Commit c9b50cb

Browse files
Johan-Liebert1cgwalters
authored andcommitted
boot/config: Add method to get boot artifact name
Add a method in BLSConfig and Grub Menuconfig to get the boot artifact name, i.e. get the name of the UKI or the name of the directory containing the Kernel + Initrd. The names are stripped of all our custom prefixes and suffixes, so basically they return the verity digest part of the name. This is useful for GC-ing Kernel + Initrd that are shared among multiple deployments since we can't rely on the composefs= parameter in the options as the cmdline verity digest might be different than the verity digest of the shared Kernel + Initrd. Tests written by Claude Code (Opus) Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent ce44df9 commit c9b50cb

2 files changed

Lines changed: 246 additions & 4 deletions

File tree

crates/lib/src/parsers/bls_config.rs

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
//!
33
//! This module parses the config files for the spec.
44
5-
#![allow(dead_code)]
6-
75
use anyhow::{Result, anyhow};
86
use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
97
use camino::Utf8PathBuf;
@@ -15,7 +13,7 @@ use std::fmt::Display;
1513
use uapi_version::Version;
1614

1715
use crate::bootc_composefs::status::ComposefsCmdline;
18-
use crate::composefs_consts::UKI_NAME_PREFIX;
16+
use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, UKI_NAME_PREFIX};
1917

2018
#[derive(Debug, PartialEq, Eq, Default)]
2119
pub enum BLSConfigType {
@@ -173,6 +171,9 @@ impl BLSConfig {
173171
self
174172
}
175173

174+
/// Get the fs-verity digest from a BLS config
175+
/// For EFI BLS entries, this returns the name of the UKI
176+
/// For Non-EFI BLS entries, this returns the fs-verity digest in the "options" field
176177
pub(crate) fn get_verity(&self) -> Result<String> {
177178
match &self.cfg_type {
178179
BLSConfigType::EFI { efi } => {
@@ -205,6 +206,56 @@ impl BLSConfig {
205206
}
206207
}
207208

209+
/// Returns name of UKI in case of EFI config
210+
/// Returns name of the directory containing Kernel + Initrd in case of Non-EFI config
211+
///
212+
/// The names are stripped of our custom prefix and suffixes, so this returns the
213+
/// verity digest part of the name
214+
#[allow(dead_code)]
215+
pub(crate) fn boot_artifact_name(&self) -> Result<&str> {
216+
match &self.cfg_type {
217+
BLSConfigType::EFI { efi } => {
218+
let file_name = efi
219+
.file_name()
220+
.ok_or_else(|| anyhow::anyhow!("EFI path missing file name: {}", efi))?;
221+
222+
let without_suffix = file_name.strip_suffix(EFI_EXT).ok_or_else(|| {
223+
anyhow::anyhow!(
224+
"EFI file name missing expected suffix '{}': {}",
225+
EFI_EXT,
226+
file_name
227+
)
228+
})?;
229+
230+
// For backwards compatibility, we don't make this prefix mandatory
231+
match without_suffix.strip_prefix(UKI_NAME_PREFIX) {
232+
Some(no_prefix) => Ok(no_prefix),
233+
None => Ok(without_suffix),
234+
}
235+
}
236+
237+
BLSConfigType::NonEFI { linux, .. } => {
238+
let parent_dir = linux.parent().ok_or_else(|| {
239+
anyhow::anyhow!("Linux kernel path has no parent directory: {}", linux)
240+
})?;
241+
242+
let dir_name = parent_dir.file_name().ok_or_else(|| {
243+
anyhow::anyhow!("Parent directory has no file name: {}", parent_dir)
244+
})?;
245+
246+
// For backwards compatibility, we don't make this prefix mandatory
247+
match dir_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX) {
248+
Some(dir_name_no_prefix) => Ok(dir_name_no_prefix),
249+
None => Ok(dir_name),
250+
}
251+
}
252+
253+
BLSConfigType::Unknown => {
254+
anyhow::bail!("Cannot extract boot artifact name from unknown config type")
255+
}
256+
}
257+
}
258+
208259
/// Gets the `options` field from the config
209260
/// Returns an error if the field doesn't exist
210261
/// or if the config is of type `EFI`
@@ -585,4 +636,99 @@ mod tests {
585636
assert!(config_final < config_rc1);
586637
Ok(())
587638
}
639+
640+
#[test]
641+
fn test_boot_artifact_name_efi_success() -> Result<()> {
642+
use camino::Utf8PathBuf;
643+
644+
let efi_path = Utf8PathBuf::from("bootc_composefs-abcd1234.efi");
645+
let config = BLSConfig {
646+
cfg_type: BLSConfigType::EFI { efi: efi_path },
647+
version: "1".to_string(),
648+
..Default::default()
649+
};
650+
651+
let artifact_name = config.boot_artifact_name()?;
652+
assert_eq!(artifact_name, "abcd1234");
653+
Ok(())
654+
}
655+
656+
#[test]
657+
fn test_boot_artifact_name_non_efi_success() -> Result<()> {
658+
use camino::Utf8PathBuf;
659+
660+
let linux_path = Utf8PathBuf::from("/boot/bootc_composefs-xyz5678/vmlinuz");
661+
let config = BLSConfig {
662+
cfg_type: BLSConfigType::NonEFI {
663+
linux: linux_path,
664+
initrd: vec![],
665+
options: None,
666+
},
667+
version: "1".to_string(),
668+
..Default::default()
669+
};
670+
671+
let artifact_name = config.boot_artifact_name()?;
672+
assert_eq!(artifact_name, "xyz5678");
673+
Ok(())
674+
}
675+
676+
#[test]
677+
fn test_boot_artifact_name_efi_missing_prefix() {
678+
use camino::Utf8PathBuf;
679+
680+
let efi_path = Utf8PathBuf::from("/EFI/Linux/abcd1234.efi");
681+
let config = BLSConfig {
682+
cfg_type: BLSConfigType::EFI { efi: efi_path },
683+
version: "1".to_string(),
684+
..Default::default()
685+
};
686+
687+
let artifact_name = config
688+
.boot_artifact_name()
689+
.expect("Should extract artifact name");
690+
assert_eq!(artifact_name, "abcd1234");
691+
}
692+
693+
#[test]
694+
fn test_boot_artifact_name_efi_missing_suffix() {
695+
use camino::Utf8PathBuf;
696+
697+
let efi_path = Utf8PathBuf::from("bootc_composefs-abcd1234");
698+
let config = BLSConfig {
699+
cfg_type: BLSConfigType::EFI { efi: efi_path },
700+
version: "1".to_string(),
701+
..Default::default()
702+
};
703+
704+
let result = config.boot_artifact_name();
705+
assert!(result.is_err());
706+
assert!(
707+
result
708+
.unwrap_err()
709+
.to_string()
710+
.contains("missing expected suffix")
711+
);
712+
}
713+
714+
#[test]
715+
fn test_boot_artifact_name_efi_no_filename() {
716+
use camino::Utf8PathBuf;
717+
718+
let efi_path = Utf8PathBuf::from("/");
719+
let config = BLSConfig {
720+
cfg_type: BLSConfigType::EFI { efi: efi_path },
721+
version: "1".to_string(),
722+
..Default::default()
723+
};
724+
725+
let result = config.boot_artifact_name();
726+
assert!(result.is_err());
727+
assert!(
728+
result
729+
.unwrap_err()
730+
.to_string()
731+
.contains("missing file name")
732+
);
733+
}
588734
}

crates/lib/src/parsers/grub_menuconfig.rs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ impl<'a> MenuEntry<'a> {
114114
let name = to_path
115115
.components()
116116
.last()
117-
.ok_or(anyhow::anyhow!("Empty efi field"))?
117+
.ok_or_else(|| anyhow::anyhow!("Empty efi field"))?
118118
.to_string()
119119
.strip_prefix(UKI_NAME_PREFIX)
120120
.ok_or_else(|| anyhow::anyhow!("efi does not start with custom prefix"))?
@@ -124,6 +124,35 @@ impl<'a> MenuEntry<'a> {
124124

125125
Ok(name)
126126
}
127+
128+
/// Returns name of UKI in case of EFI config
129+
///
130+
/// The names are stripped of our custom prefix and suffixes, so this returns
131+
/// the verity digest part of the name
132+
pub(crate) fn boot_artifact_name(&self) -> Result<String> {
133+
let chainloader_path = Utf8PathBuf::from(&self.body.chainloader);
134+
135+
let file_name = chainloader_path.file_name().ok_or_else(|| {
136+
anyhow::anyhow!(
137+
"Chainloader path missing file name: {}",
138+
&self.body.chainloader
139+
)
140+
})?;
141+
142+
let without_suffix = file_name.strip_suffix(EFI_EXT).ok_or_else(|| {
143+
anyhow::anyhow!(
144+
"EFI file name missing expected suffix '{}': {}",
145+
EFI_EXT,
146+
file_name
147+
)
148+
})?;
149+
150+
// For backwards compatibility, we don't make this prefix mandatory
151+
match without_suffix.strip_prefix(UKI_NAME_PREFIX) {
152+
Some(no_prefix) => Ok(no_prefix.into()),
153+
None => Ok(without_suffix.into()),
154+
}
155+
}
127156
}
128157

129158
/// Parser that takes content until balanced brackets, handling nested brackets and escapes.
@@ -547,4 +576,71 @@ mod test {
547576
assert_eq!(result[1].body.chainloader, "/EFI/Linux/second.efi");
548577
assert_eq!(result[1].body.search, "--set=root --fs-uuid \"some-uuid\"");
549578
}
579+
580+
#[test]
581+
fn test_menuentry_boot_artifact_name_success() {
582+
let body = MenuentryBody {
583+
insmod: vec!["fat", "chain"],
584+
chainloader: "/EFI/bootc_composefs/bootc_composefs-abcd1234.efi".to_string(),
585+
search: "--no-floppy --set=root --fs-uuid test",
586+
version: 0,
587+
extra: vec![],
588+
};
589+
590+
let entry = MenuEntry {
591+
title: "Test Entry".to_string(),
592+
body,
593+
};
594+
595+
let artifact_name = entry
596+
.boot_artifact_name()
597+
.expect("Should extract artifact name");
598+
assert_eq!(artifact_name, "abcd1234");
599+
}
600+
601+
#[test]
602+
fn test_menuentry_boot_artifact_name_missing_prefix() {
603+
let body = MenuentryBody {
604+
insmod: vec!["fat", "chain"],
605+
chainloader: "/EFI/Linux/abcd1234.efi".to_string(),
606+
search: "--no-floppy --set=root --fs-uuid test",
607+
version: 0,
608+
extra: vec![],
609+
};
610+
611+
let entry = MenuEntry {
612+
title: "Test Entry".to_string(),
613+
body,
614+
};
615+
616+
let artifact_name = entry
617+
.boot_artifact_name()
618+
.expect("Should extract artifact name");
619+
assert_eq!(artifact_name, "abcd1234");
620+
}
621+
622+
#[test]
623+
fn test_menuentry_boot_artifact_name_missing_suffix() {
624+
let body = MenuentryBody {
625+
insmod: vec!["fat", "chain"],
626+
chainloader: "/EFI/bootc_composefs/bootc_composefs-abcd1234".to_string(),
627+
search: "--no-floppy --set=root --fs-uuid test",
628+
version: 0,
629+
extra: vec![],
630+
};
631+
632+
let entry = MenuEntry {
633+
title: "Test Entry".to_string(),
634+
body,
635+
};
636+
637+
let result = entry.boot_artifact_name();
638+
assert!(result.is_err());
639+
assert!(
640+
result
641+
.unwrap_err()
642+
.to_string()
643+
.contains("missing expected suffix")
644+
);
645+
}
550646
}

0 commit comments

Comments
 (0)