Skip to content

Commit efd3f2d

Browse files
committed
blockdev: Fall back to blkid -p when udev is unavailable
See osbuild/osbuild#2428 When running inside a container or sandbox without /run/udev bind-mounted (e.g. osbuild's bwrap), lsblk returns null for partition metadata fields like parttype (partition type GUID) and pttype (partition table type), because these come from the udev database. This causes partition discovery to fail -- most visibly on ppc64le where the PReP partition can't be found by GUID, but also affecting ESP discovery on all architectures. On x86_64 UEFI, bootupd silently falls back to mounted-ESP detection, masking the problem; on ppc64le there is no fallback and the install fails hard. Add a blkid -p fallback in backfill_missing() that probes partition metadata directly from the disk when udev is absent. The udev check tests for /run/udev/data (the actual database directory) rather than /run/udev, because bootc's ensure_mirrored_host_mount() creates an empty /run/udev directory on the bwrap tmpfs that is_same_as_host() then considers 'already mounted'. Tested end-to-end via the BIB tmt test (plan-33-bib-build) with a patched bootupd that removes the mounted-ESP fallback entirely. This follows the same approach coreos-installer uses (PRs bootc-dev#1511, sfdisk-based workaround originally added in PR bootc-dev#688. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 2829403 commit efd3f2d

4 files changed

Lines changed: 465 additions & 3 deletions

File tree

crates/blockdev/src/blockdev.rs

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use std::collections::HashSet;
1+
use std::collections::{HashMap, HashSet};
22
use std::env;
33
use std::path::Path;
44
use std::process::{Command, Stdio};
5+
use std::sync::OnceLock;
56

67
use anyhow::{Context, Result, anyhow};
78
use camino::{Utf8Path, Utf8PathBuf};
@@ -11,6 +12,64 @@ use serde::Deserialize;
1112

1213
use bootc_utils::CommandRunExt;
1314

15+
/// Check whether the udev database is accessible (cached for the process lifetime).
16+
///
17+
/// When running inside a container or sandbox without `/run/udev`
18+
/// bind-mounted, tools like `lsblk` that depend on the udev database
19+
/// will return null for fields like `parttype` and `fstype`.
20+
///
21+
/// We check for `/run/udev/data` (the actual database directory) rather
22+
/// than just `/run/udev` because the parent directory can exist as an
23+
/// empty mount point without the database being populated.
24+
fn have_udev() -> bool {
25+
static HAVE_UDEV: OnceLock<bool> = OnceLock::new();
26+
*HAVE_UDEV.get_or_init(|| {
27+
let r = Path::new("/run/udev/data").exists();
28+
if !r {
29+
tracing::debug!(
30+
"udev database not available, will use blkid -p for partition metadata"
31+
);
32+
}
33+
r
34+
})
35+
}
36+
37+
/// Probe a device with `blkid -p` and return all discovered properties
38+
/// as key-value pairs.
39+
///
40+
/// This uses the `export` output format (`KEY=value`, one per line) to
41+
/// retrieve all tags in a single invocation, rather than spawning blkid
42+
/// once per property.
43+
///
44+
/// Returns `Ok(empty map)` if blkid exits with code 2 (no tags found,
45+
/// e.g. the device is a whole disk). Other non-zero exits are propagated
46+
/// as errors.
47+
fn blkid_probe(dev: &str) -> Result<HashMap<String, String>> {
48+
let mut cmd = Command::new("blkid");
49+
cmd.args(["-p", "-o", "export"]).arg(dev);
50+
cmd.log_debug();
51+
let output = cmd.output().context("Failed to run blkid")?;
52+
if !output.status.success() {
53+
// blkid exits with 2 when no tags are found (e.g. whole disk)
54+
if output.status.code() == Some(2) {
55+
return Ok(HashMap::new());
56+
}
57+
let stderr = String::from_utf8_lossy(&output.stderr);
58+
anyhow::bail!(
59+
"blkid -p failed on {dev} (exit status {}): {stderr}",
60+
output.status
61+
);
62+
}
63+
let text = String::from_utf8(output.stdout).context("blkid output is not UTF-8")?;
64+
let mut props = HashMap::new();
65+
for line in text.lines() {
66+
if let Some((key, value)) = line.split_once('=') {
67+
props.insert(key.to_string(), value.to_string());
68+
}
69+
}
70+
Ok(props)
71+
}
72+
1473
/// MBR partition type IDs that indicate an EFI System Partition.
1574
/// 0x06 is FAT16 (used as ESP on some MBR systems), 0xEF is the
1675
/// explicit EFI System Partition type.
@@ -29,7 +88,7 @@ struct DevicesOutput {
2988
}
3089

3190
#[allow(dead_code)]
32-
#[derive(Debug, Clone, Deserialize)]
91+
#[derive(Debug, Clone, serde::Serialize, Deserialize)]
3392
pub struct Device {
3493
pub name: String,
3594
pub serial: Option<String>,
@@ -265,7 +324,12 @@ impl Device {
265324
Ok(Some(parsed))
266325
}
267326

268-
/// Older versions of util-linux may be missing some properties. Backfill them if they're missing.
327+
/// Backfill properties that may be missing from lsblk output.
328+
///
329+
/// Older versions of util-linux may lack `start` and `partn`; these are
330+
/// backfilled from sysfs. When the udev database is unavailable (e.g.
331+
/// inside a container sandbox), `parttype` and `pttype` are backfilled
332+
/// via `blkid -p` which reads directly from the disk.
269333
pub fn backfill_missing(&mut self) -> Result<()> {
270334
// The "start" parameter was only added in a version of util-linux that's only
271335
// in Fedora 40 as of this writing.
@@ -277,6 +341,18 @@ impl Device {
277341
if self.partn.is_none() {
278342
self.partn = self.read_sysfs_property("partition")?;
279343
}
344+
// When udev is unavailable, lsblk can't populate parttype/pttype from
345+
// the udev database. Fall back to blkid -p which probes the disk
346+
// directly. See https://github.com/osbuild/osbuild/pull/2428
347+
if !have_udev() && (self.parttype.is_none() || self.pttype.is_none()) {
348+
let props = blkid_probe(&self.path())?;
349+
if self.parttype.is_none() {
350+
self.parttype = props.get("PART_ENTRY_TYPE").cloned();
351+
}
352+
if self.pttype.is_none() {
353+
self.pttype = props.get("PTTYPE").cloned();
354+
}
355+
}
280356
// Recurse to child devices
281357
for child in self.children.iter_mut().flatten() {
282358
child.backfill_missing()?;
@@ -673,6 +749,26 @@ mod test {
673749
assert_eq!(bios.parttype.as_deref().unwrap(), BIOS_BOOT);
674750
}
675751

752+
/// Verify that without the udev database, partition type fields are null
753+
/// and partition discovery fails. This simulates what happens when bootc
754+
/// runs inside a sandbox (like osbuild's bwrap) without /run/udev.
755+
#[test]
756+
fn test_parse_lsblk_no_udev() {
757+
let fixture = include_str!("../tests/fixtures/lsblk-no-udev.json");
758+
let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
759+
let dev = devs.blockdevices.into_iter().next().unwrap();
760+
// Without udev, parttype and pttype are null
761+
assert!(dev.pttype.is_none());
762+
let children = dev.children.as_deref().unwrap();
763+
assert_eq!(children.len(), 3);
764+
assert!(children[0].parttype.is_none());
765+
assert!(children[1].parttype.is_none());
766+
assert!(children[2].parttype.is_none());
767+
// ESP and BIOS boot discovery should fail (no parttype to match)
768+
assert!(dev.find_partition_of_esp_optional().unwrap().is_none());
769+
assert!(dev.find_partition_of_bios_boot().is_none());
770+
}
771+
676772
#[test]
677773
fn test_parse_lsblk_mbr() {
678774
let fixture = include_str!("../tests/fixtures/lsblk-mbr.json");

0 commit comments

Comments
 (0)