Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 112 additions & 14 deletions crates/blockdev/src/blockdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ pub const ESP: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b";
/// BIOS boot partition type GUID for GPT
pub const BIOS_BOOT: &str = "21686148-6449-6e6f-744e-656564454649";

/// Extended Boot Loader Partition (XBOOTLDR) type GUID for GPT; see the
/// [Boot Loader Specification](https://uapi-group.org/specifications/specs/boot_loader_specification/).
pub const XBOOTLDR: &str = "bc13c2ff-59e6-4262-a352-b275fd6f7172";

/// MBR partition type ID of the XBOOTLDR partition.
pub const XBOOTLDR_ID_MBR: u8 = 0xEA;

/// Parse an MBR partition type as reported by lsblk, e.g. "0xef".
fn parse_mbr_parttype(pt: &str) -> Option<u8> {
u8::from_str_radix(pt.strip_prefix("0x").unwrap_or(pt), 16).ok()
}

#[derive(Debug, Deserialize)]
struct DevicesOutput {
blockdevices: Vec<Device>,
Expand Down Expand Up @@ -206,15 +218,42 @@ impl Device {
Ok((!esps.is_empty()).then_some(esps))
}

/// Find a single ESP partition among all root devices backing this device.
/// Find a single ESP partition among all root devices backing this device,
/// and the XBOOTLDR partition next to it, if there is one.
///
/// 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
/// partitions on each, and 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"))
///
/// Per the Boot Loader Specification, the XBOOTLDR partition must be on
/// the same disk as the ESP (systemd-boot only looks for it there), so an
/// XBOOTLDR partition on another disk is ignored.
pub fn find_first_colocated_esp_and_xbootldr(&self) -> Result<(Device, Option<Device>)> {
for root in self.find_all_roots()? {
if let Some(esp) = root.find_partition_of_esp_optional()? {
let xbootldr = root.find_xbootldr_next_to(esp).cloned();
return Ok((esp.clone(), xbootldr));
}
}
Err(anyhow!("No ESP partition found among backing devices"))
}

/// Find the XBOOTLDR partition (GPT or MBR) in the same partition table as `esp`,
/// recursing like [`Self::find_partition_of_esp_optional`] does (e.g.
/// into a firmware RAID array).
pub fn find_xbootldr_next_to(&self, esp: &Device) -> Option<&Device> {
let children = self.children.as_ref()?;
if children.iter().any(|child| child.name == esp.name) {
return match self.pttype.as_deref() {
Some("dos") => children.iter().find(|child| {
child.parttype.as_deref().and_then(parse_mbr_parttype) == Some(XBOOTLDR_ID_MBR)
}),
_ => self.find_partition_of_type(XBOOTLDR),
};
}
children
.iter()
.find_map(|child| child.find_xbootldr_next_to(esp))
}

/// Find all BIOS boot partitions across all root devices backing this device.
Expand Down Expand Up @@ -260,11 +299,8 @@ impl Device {
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()
})
.as_deref()
.and_then(parse_mbr_parttype)
.is_some_and(|pt| ESP_ID_MBR.contains(&pt))
}),
// When pttype is None (e.g. older lsblk or partition devices), default
Expand Down Expand Up @@ -358,14 +394,19 @@ impl Device {
// When udev is unavailable, lsblk can't populate parttype/pttype from
// the udev database. Fall back to blkid -p which probes the disk
// directly. See https://github.com/osbuild/osbuild/pull/2428
if !have_udev() && (self.parttype.is_none() || self.pttype.is_none()) {
if !have_udev()
&& (self.parttype.is_none() || self.pttype.is_none() || self.fstype.is_none())
{
let props = blkid_probe(&self.path())?;
if self.parttype.is_none() {
self.parttype = props.get("PART_ENTRY_TYPE").cloned();
}
if self.pttype.is_none() {
self.pttype = props.get("PTTYPE").cloned();
}
if self.fstype.is_none() {
self.fstype = props.get("TYPE").cloned();
}
}
// Recurse to child devices
for child in self.children.iter_mut().flatten() {
Expand Down Expand Up @@ -863,8 +904,20 @@ mod test {
assert_eq!(esp.partn, Some(1));
}

/// Linux root (x86-64) partition type GUID, per the Discoverable Partitions Specification.
const ROOT_X86_64: &str = "4f68bce3-e8cd-4db1-96e7-fbcaf984b709";

/// Helper to construct a minimal MBR disk Device with given child partition types.
fn make_mbr_disk(parttypes: &[&str]) -> Device {
make_disk("dos", parttypes)
}

/// Helper to construct a minimal GPT disk Device with given child partition types.
fn make_gpt_disk(parttypes: &[&str]) -> Device {
make_disk("gpt", parttypes)
}

fn make_disk(pttype: &str, parttypes: &[&str]) -> Device {
Device {
name: "vda".into(),
serial: None,
Expand All @@ -880,7 +933,7 @@ mod test {
fstype: None,
uuid: None,
path: Some("/dev/vda".into()),
pttype: Some("dos".into()),
pttype: Some(pttype.into()),
ro: None,
children: Some(
parttypes
Expand All @@ -901,7 +954,7 @@ mod test {
fstype: None,
uuid: None,
path: None,
pttype: Some("dos".into()),
pttype: Some(pttype.into()),
ro: None,
children: None,
})
Expand Down Expand Up @@ -973,6 +1026,51 @@ mod test {
assert_eq!(md0.fstype.as_deref().unwrap(), "ext4");
}

#[test]
fn test_find_xbootldr_next_to() {
let cases: &[(&[&str], Option<u32>)] = &[
// ESP and XBOOTLDR, in either order
(&[BIOS_BOOT, ESP, XBOOTLDR, ROOT_X86_64], Some(3)),
(&[XBOOTLDR, ESP, ROOT_X86_64], Some(1)),
// The XBOOTLDR type is matched case-insensitively, like the ESP's
(&[ESP, "BC13C2FF-59E6-4262-A352-B275FD6F7172"], Some(2)),
// No XBOOTLDR
(&[BIOS_BOOT, ESP, ROOT_X86_64], None),
];
for (parttypes, expected) in cases {
let dev = make_gpt_disk(parttypes);
let esp = dev.find_partition_of_esp().unwrap();
let xbootldr = dev.find_xbootldr_next_to(esp).map(|d| d.partn.unwrap());
assert_eq!(xbootldr, *expected, "{parttypes:?}");
}

// MBR: XBOOTLDR is type 0xea
let dev = make_mbr_disk(&["0xef", "0xea", "0x83"]);
let esp = dev.find_partition_of_esp().unwrap();
assert_eq!(dev.find_xbootldr_next_to(esp).unwrap().partn, Some(2));
let dev = make_mbr_disk(&["0xef", "0x83"]);
let esp = dev.find_partition_of_esp().unwrap();
assert!(dev.find_xbootldr_next_to(esp).is_none());

// An XBOOTLDR partition on another disk isn't next to the ESP
let other = make_gpt_disk(&[ESP]);
let dev = make_gpt_disk(&[XBOOTLDR]);
let esp = other.find_partition_of_esp().unwrap();
let esp = Device {
name: "vdb1".into(),
..esp.clone()
};
assert!(dev.find_xbootldr_next_to(&esp).is_none());

// Firmware RAID: the ESP is inside the md array, so is its XBOOTLDR
let fixture = include_str!("../tests/fixtures/lsblk-vroc.json");
let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
for nvme in &devs.blockdevices {
let esp = nvme.find_partition_of_esp().unwrap();
assert!(nvme.find_xbootldr_next_to(esp).is_none());
}
}

#[test]
fn test_mbr_esp_detection() {
// 0x06 (FAT16) is recognized as ESP
Expand Down
10 changes: 5 additions & 5 deletions crates/lib/src/bootc_composefs/backwards_compat/bcompat_boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ fn stage_bls_entry_changes(
}

BLSConfigType::EFI { key, .. } => {
// boot_dir in case of UKI is the ESP
// boot_dir in case of UKI is the ESP, or XBOOTLDR
plan_efi_binary_renames(&boot_dir, &digest, &mut rename_transaction)?;
let new_path = Utf8PathBuf::from("/")
.join(BOOTC_UKI_DIR)
Expand Down Expand Up @@ -290,10 +290,10 @@ fn handle_bls_conf(
create_staged_bls_entries(boot_dir, &new_bls_entries)?;

let binaries_dir = if is_uki {
let esp = storage.require_esp()?;
let uki_dir = esp.fd.open_dir(BOOTC_UKI_DIR).context("Opening UKI dir")?;

uki_dir
storage
.require_uki_partition()?
.open_dir(BOOTC_UKI_DIR)
.context("Opening UKI dir")?
} else {
storage.bls_boot_binaries_dir()?
};
Expand Down
Loading