Skip to content
83 changes: 54 additions & 29 deletions crates/blockdev/src/blockdev.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::env;
use std::path::Path;
use std::process::{Command, Stdio};
Expand Down Expand Up @@ -123,15 +124,26 @@ impl Device {
/// Calls find_all_roots() to discover physical disks, then searches each for an ESP.
/// Returns None if no ESPs are found.
pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> {
let esps: Vec<_> = self
.find_all_roots()?
.iter()
.flat_map(|root| root.find_partition_of_esp().ok())
.cloned()
.collect();
let mut esps = Vec::new();
for root in &self.find_all_roots()? {
if let Some(esp) = root.find_partition_of_esp_optional()? {
esps.push(esp.clone());
}
}
Ok((!esps.is_empty()).then_some(esps))
}

/// Find a single ESP partition among all root devices backing this device.
///
/// Walks the parent chain to find all backing disks, then looks for ESP
/// partitions on each. Returns the first ESP found. This is the common
/// case for composefs/UKI boot paths where exactly one ESP is expected.
pub fn find_first_colocated_esp(&self) -> Result<Device> {
self.find_colocated_esps()?
.and_then(|mut v| Some(v.remove(0)))
.ok_or_else(|| anyhow!("No ESP partition found among backing devices"))
}

/// Find all BIOS boot partitions across all root devices backing this device.
/// Calls find_all_roots() to discover physical disks, then searches each for a BIOS boot partition.
/// Returns None if no BIOS boot partitions are found.
Expand Down Expand Up @@ -159,34 +171,41 @@ impl Device {
///
/// For GPT disks, this matches by the ESP partition type GUID.
/// For MBR (dos) disks, this matches by the MBR partition type IDs (0x06 or 0xEF).
pub fn find_partition_of_esp(&self) -> Result<&Device> {
let children = self
.children
.as_ref()
.ok_or_else(|| anyhow!("Device has no children"))?;
///
/// Returns `Ok(None)` when there are no children or no ESP partition
/// is present. Returns `Err` only for genuinely unexpected conditions
/// (e.g. an unsupported partition table type).
pub fn find_partition_of_esp_optional(&self) -> Result<Option<&Device>> {
let Some(children) = self.children.as_ref() else {
return Ok(None);
};
match self.pttype.as_deref() {
Some("dos") => children
.iter()
.find(|child| {
child
.parttype
.as_ref()
.and_then(|pt| {
let pt = pt.strip_prefix("0x").unwrap_or(pt);
u8::from_str_radix(pt, 16).ok()
})
.is_some_and(|pt| ESP_ID_MBR.contains(&pt))
})
.ok_or_else(|| anyhow!("ESP not found in MBR partition table")),
Some("dos") => Ok(children.iter().find(|child| {
child
.parttype
.as_ref()
.and_then(|pt| {
let pt = pt.strip_prefix("0x").unwrap_or(pt);
u8::from_str_radix(pt, 16).ok()
})
.is_some_and(|pt| ESP_ID_MBR.contains(&pt))
})),
// When pttype is None (e.g. older lsblk or partition devices), default
// to GPT UUID matching which will simply not match MBR hex types.
Some("gpt") | None => self
.find_partition_of_type(ESP)
.ok_or_else(|| anyhow!("ESP not found in GPT partition table")),
Some("gpt") | None => Ok(self.find_partition_of_type(ESP)),
Some(other) => Err(anyhow!("Unsupported partition table type: {other}")),
}
}

/// Find the EFI System Partition (ESP) among children, or error if absent.
///
/// This is a convenience wrapper around [`find_partition_of_esp_optional`]
/// for callers that require an ESP to be present.
pub fn find_partition_of_esp(&self) -> Result<&Device> {
self.find_partition_of_esp_optional()?
.ok_or_else(|| anyhow!("ESP partition not found on {}", self.path()))
}

/// Find a child partition by partition number (1-indexed).
pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> {
self.children
Expand Down Expand Up @@ -308,15 +327,21 @@ impl Device {
};

let mut roots = Vec::new();
let mut seen = HashSet::new();
let mut queue = parents;
while let Some(mut device) = queue.pop() {
match device.children.take() {
Some(grandparents) if !grandparents.is_empty() => {
queue.extend(grandparents);
}
_ => {
// Found a root; re-query to populate its actual children
roots.push(list_dev(Utf8Path::new(&device.path()))?);
// Deduplicate: in complex topologies (e.g. multipath)
// multiple branches can converge on the same physical disk.
let name = device.name.clone();
if seen.insert(name) {
// Found a new root; re-query to populate its actual children
roots.push(list_dev(Utf8Path::new(&device.path()))?);
}
}
}
}
Expand Down
63 changes: 38 additions & 25 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ use crate::{
use crate::{
composefs_consts::{
BOOT_LOADER_ENTRIES, ORIGIN_KEY_BOOT, ORIGIN_KEY_BOOT_DIGEST, STAGED_BOOT_LOADER_ENTRIES,
STATE_DIR_ABS, USER_CFG, USER_CFG_STAGED,
STATE_DIR_RELATIVE, USER_CFG, USER_CFG_STAGED,
},
spec::{Bootloader, Host},
};
Expand Down Expand Up @@ -354,22 +354,21 @@ pub(crate) fn compute_boot_digest_uki(uki: &[u8]) -> Result<String> {

/// Given the SHA256 sum of current VMlinuz + Initrd combo, find boot entry with the same SHA256Sum
///
/// `deployments_dir` should be the composefs state/deploy directory opened
/// relative to the target physical root. This avoids using ambient absolute
/// paths, which would be wrong during install (where `/sysroot/state/deploy`
/// belongs to the host, not the target).
///
/// # Returns
/// Returns the verity of all deployments that have a boot digest same as the one passed in
#[context("Checking boot entry duplicates")]
pub(crate) fn find_vmlinuz_initrd_duplicates(digest: &str) -> Result<Option<Vec<String>>> {
let deployments = Dir::open_ambient_dir(STATE_DIR_ABS, ambient_authority());

let deployments = match deployments {
Ok(d) => d,
// The first ever deployment
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => anyhow::bail!(e),
};

pub(crate) fn find_vmlinuz_initrd_duplicates(
deployments_dir: &Dir,
digest: &str,
) -> Result<Option<Vec<String>>> {
let mut symlink_to: Option<Vec<String>> = None;

for depl in deployments.entries()? {
for depl in deployments_dir.entries()? {
let depl = depl?;

let depl_file_name = depl.file_name();
Expand Down Expand Up @@ -518,6 +517,11 @@ pub(crate) fn setup_composefs_bls_boot(
) -> Result<String> {
let id_hex = id.to_hex();

let physical_root = match &setup_type {
BootSetupType::Setup((root_setup, ..)) => &root_setup.physical_root,
BootSetupType::Upgrade((storage, ..)) => &storage.physical_root,
};

let (root_path, esp_device, mut cmdline_refs, fs, bootloader) = match setup_type {
BootSetupType::Setup((root_setup, state, postfetch, fs)) => {
// root_setup.kargs has [root=UUID=<UUID>, "rw"]
Expand All @@ -529,8 +533,8 @@ pub(crate) fn setup_composefs_bls_boot(
ComposefsCmdline::build(&id_hex, state.composefs_options.allow_missing_verity);
cmdline_options.extend(&Cmdline::from(&composefs_cmdline.to_string()));

// Locate ESP partition device
let esp_part = root_setup.device_info.find_partition_of_esp()?;
// Locate ESP partition device by walking up to the root disk(s)
let esp_part = root_setup.device_info.find_first_colocated_esp()?;

(
root_setup.physical_root_path.clone(),
Expand Down Expand Up @@ -567,13 +571,12 @@ pub(crate) fn setup_composefs_bls_boot(
.context("Failed to create 'composefs=' parameter")?;
cmdline.add_or_modify(&param);

// Locate ESP partition device
let root_dev =
bootc_blockdev::list_dev_by_dir(&storage.physical_root)?.require_single_root()?;
let esp_dev = root_dev.find_partition_of_esp()?;
// Locate ESP partition device by walking up to the root disk(s)
let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
let esp_dev = root_dev.find_first_colocated_esp()?;

(
Utf8PathBuf::from("/sysroot"),
storage.physical_root_path.clone(),
esp_dev.path(),
cmdline,
fs,
Expand Down Expand Up @@ -687,7 +690,17 @@ pub(crate) fn setup_composefs_bls_boot(
options: Some(cmdline_refs),
});

match find_vmlinuz_initrd_duplicates(&boot_digest)? {
// Check for shared boot binaries with existing deployments.
// On fresh install the state dir won't exist yet, so this is
// naturally a no-op.
let shared_boot_binaries =
match physical_root.open_dir_optional(STATE_DIR_RELATIVE)? {
Some(deploy_dir) => {
find_vmlinuz_initrd_duplicates(&deploy_dir, &boot_digest)?
}
None => None,
};
match shared_boot_binaries {
Some(shared_entries) => {
// Multiple deployments could be using the same kernel + initrd, but there
// would be only one available
Expand Down Expand Up @@ -1103,7 +1116,8 @@ pub(crate) fn setup_composefs_uki_boot(
BootSetupType::Setup((root_setup, state, postfetch, ..)) => {
state.require_no_kargs_for_uki()?;

let esp_part = root_setup.device_info.find_partition_of_esp()?;
// Locate ESP partition device by walking up to the root disk(s)
let esp_part = root_setup.device_info.find_first_colocated_esp()?;

(
root_setup.physical_root_path.clone(),
Expand All @@ -1118,10 +1132,9 @@ pub(crate) fn setup_composefs_uki_boot(
let sysroot = Utf8PathBuf::from("/sysroot"); // Still needed for root_path
let bootloader = host.require_composefs_booted()?.bootloader.clone();

// Locate ESP partition device
let root_dev =
bootc_blockdev::list_dev_by_dir(&storage.physical_root)?.require_single_root()?;
let esp_dev = root_dev.find_partition_of_esp()?;
// Locate ESP partition device by walking up to the root disk(s)
let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
let esp_dev = root_dev.find_first_colocated_esp()?;

(
sysroot,
Expand Down
Loading
Loading