Skip to content

Commit 0c0fe42

Browse files
composefs/boot: Change the way we find sharable vmlinuz + initrd
Instead of looking in the ".origin" files and trying to match the boot_digest to digests in other origin files, we now simply re-compute the sha256sum for vmlinuz + initrd for all boot entries present. This fixes the bug that arises after mutiple upgrades where the original deployment that created the boot entry has been garbage collected, so we end up linking to another deployment that does have the same boot digest, but the verity digest doesn't match the verity digest used for the name of the directory where we store the kernel + initrd pair. Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
1 parent d33ddd2 commit 0c0fe42

2 files changed

Lines changed: 121 additions & 76 deletions

File tree

commits

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
commit f074ee3087538faa1aca0e9a6d906d1d1bfbc25a
2+
Author: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
3+
Date: Tue Mar 31 14:24:27 2026 +0530
4+
5+
test: Add test for GC-ing shared Type1 entries
6+
7+
Add a test to make sure we do not GC shared Type1 entries when they're
8+
still referenced
9+
10+
Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
11+
12+
commit b3acc1eacf1c64bd0f3027a0f5a9254e52935e80
13+
Author: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
14+
Date: Tue Mar 31 13:18:59 2026 +0530
15+
16+
composefs/gc: Fix shared entry GC even when they're in use
17+
18+
This fixes a bug where a shared Type1 entry would get GCd even when it's
19+
in use due to the original image that created it being deleted. Combined
20+
with the fact that we were comparing the fsverity digest in the options
21+
field of the BLS config (which will be different than the name of the
22+
directory containing the vmlinuz + initrd pair).
23+
24+
Now, we compare against the directory name when GC-ing boot binaries
25+
26+
Fixes: https://github.com/bootc-dev/bootc/issues/2102
27+
28+
Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
29+
30+
commit ce4dcd35ded9e801364613e6c86a9fa27ac25f65
31+
Author: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
32+
Date: Tue Mar 31 13:04:44 2026 +0530
33+
34+
composefs/boot: Change the way we find sharable vmlinuz + initrd
35+
36+
Instead of looking in the ".origin" files and trying to match the
37+
boot_digest to digests in other origin files, we now simply re-compute
38+
the sha256sum for vmlinuz + initrd for all boot entries present.
39+
40+
This fixes the bug that arises after mutiple upgrades where the original
41+
deployment that created the boot entry has been garbage collected, so we
42+
end up linking to another deployment that does have the same boot
43+
digest, but the verity digest doesn't match the verity digest used for
44+
the name of the directory where we store the kernel + initrd pair.
45+
46+
Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
47+
48+
commit d33ddd2cda789e52884534bf2ec074a6648e097c
49+
Author: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>
50+
Date: Tue Mar 31 10:38:22 2026 +0530
51+
52+
boot/config: Add method to get boot artifact name
53+
54+
Add a method in BLSConfig and Grub Menuconfig to get the boot artifact
55+
name, i.e. get the name of the UKI or the name of the directory
56+
containing the Kernel + Initrd. The names are stripped of all our custom
57+
prefixes and suffixes, so basically they return the verity digest part
58+
of the name.
59+
60+
This is useful for GC-ing Kernel + Initrd that are shared among multiple
61+
deployments since we can't rely on the composefs= parameter in the
62+
options as the cmdline verity digest might be different than the verity
63+
digest of the shared Kernel + Initrd.
64+
65+
Tests written by Claude Code (Opus)
66+
67+
Signed-off-by: Pragyan Poudyal <pragyanpoudyal41999@gmail.com>

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 54 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@ use crate::{
116116
};
117117
use crate::{
118118
composefs_consts::{
119-
BOOT_LOADER_ENTRIES, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, STAGED_BOOT_LOADER_ENTRIES,
120-
STATE_DIR_ABS, USER_CFG, USER_CFG_STAGED,
119+
BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, USER_CFG, USER_CFG_STAGED,
121120
},
122121
spec::{Bootloader, Host},
123122
};
@@ -328,6 +327,27 @@ fn compute_boot_digest(
328327
Ok(hex::encode(digest))
329328
}
330329

330+
#[context("Computing boot digest for Type1 entries")]
331+
fn compute_boot_digest_type1(dir: &Dir) -> Result<String> {
332+
let mut vmlinuz = dir
333+
.open(VMLINUZ)
334+
.with_context(|| format!("Opening {VMLINUZ}"))?;
335+
336+
let mut initrd = dir
337+
.open(INITRD)
338+
.with_context(|| format!("Opening {INITRD}"))?;
339+
340+
let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
341+
.context("Creating hasher")?;
342+
343+
std::io::copy(&mut vmlinuz, &mut hasher)?;
344+
std::io::copy(&mut initrd, &mut hasher)?;
345+
346+
let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
347+
348+
Ok(hex::encode(digest))
349+
}
350+
331351
/// Compute SHA256Sum of .linux + .initrd section of the UKI
332352
///
333353
/// # Arguments
@@ -355,52 +375,35 @@ pub(crate) fn compute_boot_digest_uki(uki: &[u8]) -> Result<String> {
355375
/// Given the SHA256 sum of current VMlinuz + Initrd combo, find boot entry with the same SHA256Sum
356376
///
357377
/// # Returns
358-
/// Returns the verity of all deployments that have a boot digest same as the one passed in
378+
/// Returns the directory name that has the same sha256 digest for vmlinuz + initrd as the one
379+
/// that's passed in
359380
#[context("Checking boot entry duplicates")]
360-
pub(crate) fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result<Option<Vec<String>>> {
361-
let deployments = Dir::open_ambient_dir(STATE_DIR_ABS, ambient_authority());
362-
363-
let deployments = match deployments {
364-
Ok(d) => d,
365-
// The first ever deployment
366-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
367-
Err(e) => anyhow::bail!(e),
368-
};
369-
370-
let mut symlink_to: Option<Vec<String>> = None;
371-
372-
for depl in deployments.entries()? {
373-
let depl = depl?;
374-
375-
let depl_file_name = depl.file_name();
376-
let depl_file_name = depl_file_name.as_str()?;
377-
378-
let config = depl
379-
.open_dir()
380-
.with_context(|| format!("Opening {depl_file_name}"))?
381-
.read_to_string(format!("{depl_file_name}.origin"))
382-
.context("Reading origin file")?;
381+
pub(crate) fn find_vmlinuz_initrd_duplicate(
382+
storage: &Storage,
383+
digest: &str,
384+
) -> Result<Option<String>> {
385+
let boot_dir = storage.bls_boot_binaries_dir()?;
386+
387+
for entry in boot_dir.entries_utf8()? {
388+
let entry = entry?;
389+
let dir_name = entry.file_name()?;
390+
391+
if !entry.file_type()?.is_dir() {
392+
continue;
393+
}
383394

384-
let ini = tini::Ini::from_string(&config)
385-
.with_context(|| format!("Failed to parse file {depl_file_name}.origin as ini"))?;
395+
let Some(..) = dir_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX) else {
396+
continue;
397+
};
386398

387-
match ini.get::<String>(ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST) {
388-
Some(hash) => {
389-
if hash == digest {
390-
match symlink_to {
391-
Some(ref mut prev) => prev.push(depl_file_name.to_string()),
392-
None => symlink_to = Some(vec![depl_file_name.to_string()]),
393-
}
394-
}
395-
}
399+
let entry_digest = compute_boot_digest_type1(&boot_dir.open_dir(&dir_name)?)?;
396400

397-
// No SHASum recorded in origin file
398-
// `symlink_to` is already none, but being explicit here
399-
None => symlink_to = None,
400-
};
401+
if entry_digest == digest {
402+
return Ok(Some(dir_name));
403+
}
401404
}
402405

403-
Ok(symlink_to)
406+
Ok(None)
404407
}
405408

406409
#[context("Writing BLS entries to disk")]
@@ -687,45 +690,20 @@ pub(crate) fn setup_composefs_bls_boot(
687690
options: Some(cmdline_refs),
688691
});
689692

690-
match find_vmlinuz_initrd_duplicates(&boot_digest)? {
691-
Some(shared_entries) => {
693+
let shared_entry = match setup_type {
694+
BootSetupType::Setup(_) => None,
695+
BootSetupType::Upgrade((storage, ..)) => {
696+
find_vmlinuz_initrd_duplicate(storage, &boot_digest)?
697+
}
698+
};
699+
700+
match shared_entry {
701+
Some(shared_entry) => {
692702
// Multiple deployments could be using the same kernel + initrd, but there
693703
// would be only one available
694704
//
695705
// Symlinking directories themselves would be better, but vfat does not support
696706
// symlinks
697-
698-
let mut shared_entry: Option<String> = None;
699-
700-
let entries =
701-
Dir::open_ambient_dir(entry_paths.entries_path, ambient_authority())
702-
.context("Opening entries path")?
703-
.entries_utf8()
704-
.context("Getting dir entries")?;
705-
706-
for ent in entries {
707-
let ent = ent?;
708-
// We shouldn't error here as all our file names are UTF-8 compatible
709-
let ent_name = ent.file_name()?;
710-
711-
let Some(entry_verity_part) = ent_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX)
712-
else {
713-
// Not our directory
714-
continue;
715-
};
716-
717-
if shared_entries
718-
.iter()
719-
.any(|shared_ent| shared_ent == entry_verity_part)
720-
{
721-
shared_entry = Some(ent_name);
722-
break;
723-
}
724-
}
725-
726-
let shared_entry = shared_entry
727-
.ok_or_else(|| anyhow::anyhow!("Shared boot binaries not found"))?;
728-
729707
match bls_config.cfg_type {
730708
BLSConfigType::NonEFI {
731709
ref mut linux,

0 commit comments

Comments
 (0)