1use std::env;
2use std::path::Path;
3use std::process::{Command, Stdio};
4
5use anyhow::{Context, Result, anyhow};
6use camino::{Utf8Path, Utf8PathBuf};
7use cap_std_ext::cap_std::fs::Dir;
8use fn_error_context::context;
9use serde::Deserialize;
10
11use bootc_utils::CommandRunExt;
12
13pub const ESP_ID_MBR: &[u8] = &[0x06, 0xEF];
18
19pub const ESP: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b";
21
22pub const BIOS_BOOT: &str = "21686148-6449-6e6f-744e-656564454649";
24
25#[derive(Debug, Deserialize)]
26struct DevicesOutput {
27 blockdevices: Vec<Device>,
28}
29
30#[allow(dead_code)]
31#[derive(Debug, Clone, Deserialize)]
32pub struct Device {
33 pub name: String,
34 pub serial: Option<String>,
35 pub model: Option<String>,
36 pub partlabel: Option<String>,
37 pub parttype: Option<String>,
38 pub partuuid: Option<String>,
39 pub partn: Option<u32>,
41 pub children: Option<Vec<Device>>,
42 pub size: u64,
43 #[serde(rename = "maj:min")]
44 pub maj_min: Option<String>,
45 pub start: Option<u64>,
48
49 pub label: Option<String>,
51 pub fstype: Option<String>,
52 pub uuid: Option<String>,
53 pub path: Option<String>,
54 pub pttype: Option<String>,
56}
57
58impl Device {
59 pub fn path(&self) -> String {
61 self.path.clone().unwrap_or(format!("/dev/{}", &self.name))
62 }
63
64 #[allow(dead_code)]
66 pub fn node(&self) -> String {
67 self.path()
68 }
69
70 #[allow(dead_code)]
71 pub fn has_children(&self) -> bool {
72 self.children.as_ref().is_some_and(|v| !v.is_empty())
73 }
74
75 pub fn is_mpath(&self) -> Result<bool> {
77 let dm_path = Utf8PathBuf::from_path_buf(std::fs::canonicalize(self.path())?)
78 .map_err(|_| anyhow::anyhow!("Non-UTF8 path"))?;
79 let dm_name = dm_path.file_name().unwrap_or("");
80 let uuid_path = Utf8PathBuf::from(format!("/sys/class/block/{dm_name}/dm/uuid"));
81
82 if uuid_path.exists() {
83 let uuid = std::fs::read_to_string(&uuid_path)
84 .with_context(|| format!("Failed to read {uuid_path}"))?;
85 if uuid.trim_start().starts_with("mpath-") {
86 return Ok(true);
87 }
88 }
89 Ok(false)
90 }
91
92 pub fn get_esp_partition_number(&self) -> Result<String> {
99 let esp_device = self.find_partition_of_esp()?;
100 let devname = &esp_device.name;
101
102 let partition_path = Utf8PathBuf::from(format!("/sys/class/block/{devname}/partition"));
103 if partition_path.exists() {
104 return std::fs::read_to_string(&partition_path)
105 .with_context(|| format!("Failed to read {partition_path}"));
106 }
107
108 if self.is_mpath()? {
110 if let Some(partn) = esp_device.partn {
111 return Ok(partn.to_string());
112 }
113 }
114 anyhow::bail!("Not supported for {devname}")
115 }
116
117 pub fn find_partition_of_bios_boot(&self) -> Option<&Device> {
119 self.find_partition_of_type(BIOS_BOOT)
120 }
121
122 pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> {
126 let esps: Vec<_> = self
127 .find_all_roots()?
128 .iter()
129 .flat_map(|root| root.find_partition_of_esp().ok())
130 .cloned()
131 .collect();
132 Ok((!esps.is_empty()).then_some(esps))
133 }
134
135 pub fn find_colocated_bios_boot(&self) -> Result<Option<Vec<Device>>> {
139 let bios_boots: Vec<_> = self
140 .find_all_roots()?
141 .iter()
142 .filter_map(|root| root.find_partition_of_bios_boot())
143 .cloned()
144 .collect();
145 Ok((!bios_boots.is_empty()).then_some(bios_boots))
146 }
147
148 pub fn find_partition_of_type(&self, parttype: &str) -> Option<&Device> {
150 self.children.as_ref()?.iter().find(|child| {
151 child
152 .parttype
153 .as_ref()
154 .is_some_and(|pt| pt.eq_ignore_ascii_case(parttype))
155 })
156 }
157
158 pub fn find_partition_of_esp(&self) -> Result<&Device> {
163 let children = self
164 .children
165 .as_ref()
166 .ok_or_else(|| anyhow!("Device has no children"))?;
167 match self.pttype.as_deref() {
168 Some("dos") => children
169 .iter()
170 .find(|child| {
171 child
172 .parttype
173 .as_ref()
174 .and_then(|pt| {
175 let pt = pt.strip_prefix("0x").unwrap_or(pt);
176 u8::from_str_radix(pt, 16).ok()
177 })
178 .is_some_and(|pt| ESP_ID_MBR.contains(&pt))
179 })
180 .ok_or_else(|| anyhow!("ESP not found in MBR partition table")),
181 Some("gpt") | None => self
184 .find_partition_of_type(ESP)
185 .ok_or_else(|| anyhow!("ESP not found in GPT partition table")),
186 Some(other) => Err(anyhow!("Unsupported partition table type: {other}")),
187 }
188 }
189
190 pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> {
192 self.children
193 .as_ref()
194 .ok_or_else(|| anyhow!("Device has no children"))?
195 .iter()
196 .find(|child| child.partn == Some(partno))
197 .ok_or_else(|| anyhow!("Missing partition for index {partno}"))
198 }
199
200 pub fn refresh(&mut self) -> Result<()> {
203 let path = self.path();
204 let new_device = list_dev(Utf8Path::new(&path))?;
205 *self = new_device;
206 Ok(())
207 }
208
209 fn read_sysfs_property<T>(&self, property: &str) -> Result<Option<T>>
211 where
212 T: std::str::FromStr,
213 T::Err: std::error::Error + Send + Sync + 'static,
214 {
215 let Some(majmin) = self.maj_min.as_deref() else {
216 return Ok(None);
217 };
218 let sysfs_path = format!("/sys/dev/block/{majmin}/{property}");
219 if !Utf8Path::new(&sysfs_path).try_exists()? {
220 return Ok(None);
221 }
222 let value = std::fs::read_to_string(&sysfs_path)
223 .with_context(|| format!("Reading {sysfs_path}"))?;
224 let parsed = value
225 .trim()
226 .parse()
227 .with_context(|| format!("Parsing sysfs {property} property"))?;
228 tracing::debug!("backfilled {property} to {value}");
229 Ok(Some(parsed))
230 }
231
232 pub fn backfill_missing(&mut self) -> Result<()> {
234 if self.start.is_none() {
237 self.start = self.read_sysfs_property("start")?;
238 }
239 if self.partn.is_none() {
242 self.partn = self.read_sysfs_property("partition")?;
243 }
244 for child in self.children.iter_mut().flatten() {
246 child.backfill_missing()?;
247 }
248 Ok(())
249 }
250
251 pub fn list_parents(&self) -> Result<Option<Vec<Device>>> {
258 let path = self.path();
259 let output: DevicesOutput = Command::new("lsblk")
260 .args(["-J", "-b", "-O", "--inverse"])
261 .arg(&path)
262 .log_debug()
263 .run_and_parse_json()?;
264
265 let device = output
266 .blockdevices
267 .into_iter()
268 .next()
269 .ok_or_else(|| anyhow!("no device output from lsblk --inverse for {path}"))?;
270
271 match device.children {
272 Some(mut children) if !children.is_empty() => {
273 for child in &mut children {
274 child.backfill_missing()?;
275 }
276 Ok(Some(children))
277 }
278 _ => Ok(None),
279 }
280 }
281
282 pub fn require_single_root(&self) -> Result<Device> {
288 let mut roots = self.find_all_roots()?;
289 match roots.len() {
290 1 => Ok(roots.remove(0)),
291 n => anyhow::bail!(
292 "Expected a single root device for {}, but found {n}",
293 self.path()
294 ),
295 }
296 }
297
298 pub fn find_all_roots(&self) -> Result<Vec<Device>> {
305 let Some(parents) = self.list_parents()? else {
306 return Ok(vec![list_dev(Utf8Path::new(&self.path()))?]);
308 };
309
310 let mut roots = Vec::new();
311 let mut queue = parents;
312 while let Some(mut device) = queue.pop() {
313 match device.children.take() {
314 Some(grandparents) if !grandparents.is_empty() => {
315 queue.extend(grandparents);
316 }
317 _ => {
318 roots.push(list_dev(Utf8Path::new(&device.path()))?);
320 }
321 }
322 }
323 Ok(roots)
324 }
325}
326
327#[context("Listing device {dev}")]
328pub fn list_dev(dev: &Utf8Path) -> Result<Device> {
329 let mut devs: DevicesOutput = Command::new("lsblk")
330 .args(["-J", "-b", "-O"])
331 .arg(dev)
332 .log_debug()
333 .run_and_parse_json()?;
334 for dev in devs.blockdevices.iter_mut() {
335 dev.backfill_missing()?;
336 }
337 devs.blockdevices
338 .into_iter()
339 .next()
340 .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
341}
342
343pub fn list_dev_by_dir(dir: &Dir) -> Result<Device> {
345 let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?;
346 list_dev(&Utf8PathBuf::from(&fsinfo.source))
347}
348
349pub struct LoopbackDevice {
350 pub dev: Option<Utf8PathBuf>,
351 cleanup_handle: Option<LoopbackCleanupHandle>,
353}
354
355struct LoopbackCleanupHandle {
357 child: std::process::Child,
359}
360
361impl LoopbackDevice {
362 pub fn new(path: &Path) -> Result<Self> {
364 let direct_io = match env::var("BOOTC_DIRECT_IO") {
365 Ok(val) => {
366 if val == "on" {
367 "on"
368 } else {
369 "off"
370 }
371 }
372 Err(_e) => "off",
373 };
374
375 let dev = Command::new("losetup")
376 .args([
377 "--show",
378 format!("--direct-io={direct_io}").as_str(),
379 "-P",
380 "--find",
381 ])
382 .arg(path)
383 .run_get_string()?;
384 let dev = Utf8PathBuf::from(dev.trim());
385 tracing::debug!("Allocated loopback {dev}");
386
387 let cleanup_handle = match Self::spawn_cleanup_helper(dev.as_str()) {
389 Ok(handle) => Some(handle),
390 Err(e) => {
391 tracing::warn!(
392 "Failed to spawn loopback cleanup helper for {}: {}. \
393 Loopback device may not be cleaned up if process is interrupted.",
394 dev,
395 e
396 );
397 None
398 }
399 };
400
401 Ok(Self {
402 dev: Some(dev),
403 cleanup_handle,
404 })
405 }
406
407 pub fn path(&self) -> &Utf8Path {
409 self.dev.as_deref().unwrap()
411 }
412
413 fn spawn_cleanup_helper(device_path: &str) -> Result<LoopbackCleanupHandle> {
416 let bootc_path = bootc_utils::reexec::executable_path()
418 .context("Failed to locate bootc binary for cleanup helper")?;
419
420 let mut cmd = Command::new(bootc_path);
422 cmd.args([
423 "internals",
424 "loopback-cleanup-helper",
425 "--device",
426 device_path,
427 ]);
428
429 cmd.env("BOOTC_LOOPBACK_CLEANUP_HELPER", "1");
431
432 cmd.stdin(Stdio::null());
434 cmd.stdout(Stdio::null());
435 let child = cmd
439 .spawn()
440 .context("Failed to spawn loopback cleanup helper")?;
441
442 Ok(LoopbackCleanupHandle { child })
443 }
444
445 fn impl_close(&mut self) -> Result<()> {
447 let Some(dev) = self.dev.take() else {
449 tracing::trace!("loopback device already deallocated");
450 return Ok(());
451 };
452
453 if let Some(mut cleanup_handle) = self.cleanup_handle.take() {
455 let _ = cleanup_handle.child.kill();
457 }
458
459 Command::new("losetup")
460 .args(["-d", dev.as_str()])
461 .run_capture_stderr()
462 }
463
464 pub fn close(mut self) -> Result<()> {
466 self.impl_close()
467 }
468}
469
470impl Drop for LoopbackDevice {
471 fn drop(&mut self) {
472 let _ = self.impl_close();
474 }
475}
476
477pub async fn run_loopback_cleanup_helper(device_path: &str) -> Result<()> {
480 if std::env::var("BOOTC_LOOPBACK_CLEANUP_HELPER").is_err() {
482 anyhow::bail!("This function should only be called as a cleanup helper");
483 }
484
485 rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM))
487 .context("Failed to set parent death signal")?;
488
489 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
491 .expect("Failed to create signal stream")
492 .recv()
493 .await;
494
495 let output = std::process::Command::new("losetup")
497 .args(["-d", device_path])
498 .output();
499
500 match output {
501 Ok(output) if output.status.success() => {
502 tracing::info!("Cleaned up leaked loopback device {}", device_path);
504 std::process::exit(0);
505 }
506 Ok(output) => {
507 let stderr = String::from_utf8_lossy(&output.stderr);
508 tracing::error!(
509 "Failed to clean up loopback device {}: {}. Stderr: {}",
510 device_path,
511 output.status,
512 stderr.trim()
513 );
514 std::process::exit(1);
515 }
516 Err(e) => {
517 tracing::error!(
518 "Error executing losetup to clean up loopback device {}: {}",
519 device_path,
520 e
521 );
522 std::process::exit(1);
523 }
524 }
525}
526
527pub fn parse_size_mib(mut s: &str) -> Result<u64> {
529 let suffixes = [
530 ("MiB", 1u64),
531 ("M", 1u64),
532 ("GiB", 1024),
533 ("G", 1024),
534 ("TiB", 1024 * 1024),
535 ("T", 1024 * 1024),
536 ];
537 let mut mul = 1u64;
538 for (suffix, imul) in suffixes {
539 if let Some((sv, rest)) = s.rsplit_once(suffix) {
540 if !rest.is_empty() {
541 anyhow::bail!("Trailing text after size: {rest}");
542 }
543 s = sv;
544 mul = imul;
545 }
546 }
547 let v = s.parse::<u64>()?;
548 Ok(v * mul)
549}
550
551#[cfg(test)]
552mod test {
553 use super::*;
554
555 #[test]
556 fn test_parse_size_mib() {
557 let ident_cases = [0, 10, 9, 1024].into_iter().map(|k| (k.to_string(), k));
558 let cases = [
559 ("0M", 0),
560 ("10M", 10),
561 ("10MiB", 10),
562 ("1G", 1024),
563 ("9G", 9216),
564 ("11T", 11 * 1024 * 1024),
565 ]
566 .into_iter()
567 .map(|(k, v)| (k.to_string(), v));
568 for (s, v) in ident_cases.chain(cases) {
569 assert_eq!(parse_size_mib(&s).unwrap(), v as u64, "Parsing {s}");
570 }
571 }
572
573 #[test]
574 fn test_parse_lsblk() {
575 let fixture = include_str!("../tests/fixtures/lsblk.json");
576 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
577 let dev = devs.blockdevices.into_iter().next().unwrap();
578 assert_eq!(dev.partn, None);
580 let children = dev.children.as_deref().unwrap();
581 assert_eq!(children.len(), 3);
582 let first_child = &children[0];
583 assert_eq!(first_child.partn, Some(1));
584 assert_eq!(
585 first_child.parttype.as_deref().unwrap(),
586 "21686148-6449-6e6f-744e-656564454649"
587 );
588 assert_eq!(
589 first_child.partuuid.as_deref().unwrap(),
590 "3979e399-262f-4666-aabc-7ab5d3add2f0"
591 );
592 let part2 = dev.find_device_by_partno(2).unwrap();
594 assert_eq!(part2.partn, Some(2));
595 assert_eq!(part2.parttype.as_deref().unwrap(), ESP);
596 let esp = dev.find_partition_of_esp().unwrap();
598 assert_eq!(esp.partn, Some(2));
599 let bios = dev.find_partition_of_bios_boot().unwrap();
601 assert_eq!(bios.partn, Some(1));
602 assert_eq!(bios.parttype.as_deref().unwrap(), BIOS_BOOT);
603 }
604
605 #[test]
606 fn test_parse_lsblk_mbr() {
607 let fixture = include_str!("../tests/fixtures/lsblk-mbr.json");
608 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
609 let dev = devs.blockdevices.into_iter().next().unwrap();
610 assert_eq!(dev.partn, None);
612 assert_eq!(dev.pttype.as_deref().unwrap(), "dos");
613 let children = dev.children.as_deref().unwrap();
614 assert_eq!(children.len(), 3);
615 let first_child = &children[0];
617 assert_eq!(first_child.partn, Some(1));
618 assert_eq!(first_child.parttype.as_deref().unwrap(), "0x06");
619 assert_eq!(first_child.partuuid.as_deref().unwrap(), "a1b2c3d4-01");
620 assert_eq!(first_child.fstype.as_deref().unwrap(), "vfat");
621 assert!(first_child.partlabel.is_none());
623 let second_child = &children[1];
625 assert_eq!(second_child.partn, Some(2));
626 assert_eq!(second_child.parttype.as_deref().unwrap(), "0x83");
627 assert_eq!(second_child.partuuid.as_deref().unwrap(), "a1b2c3d4-02");
628 let third_child = &children[2];
630 assert_eq!(third_child.partn, Some(3));
631 assert_eq!(third_child.parttype.as_deref().unwrap(), "0xef");
632 assert_eq!(third_child.partuuid.as_deref().unwrap(), "a1b2c3d4-03");
633 let part1 = dev.find_device_by_partno(1).unwrap();
635 assert_eq!(part1.partn, Some(1));
636 let esp = dev.find_partition_of_esp().unwrap();
638 assert_eq!(esp.partn, Some(1));
639 }
640
641 fn make_mbr_disk(parttypes: &[&str]) -> Device {
643 Device {
644 name: "vda".into(),
645 serial: None,
646 model: None,
647 partlabel: None,
648 parttype: None,
649 partuuid: None,
650 partn: None,
651 size: 10737418240,
652 maj_min: None,
653 start: None,
654 label: None,
655 fstype: None,
656 uuid: None,
657 path: Some("/dev/vda".into()),
658 pttype: Some("dos".into()),
659 children: Some(
660 parttypes
661 .iter()
662 .enumerate()
663 .map(|(i, pt)| Device {
664 name: format!("vda{}", i + 1),
665 serial: None,
666 model: None,
667 partlabel: None,
668 parttype: Some(pt.to_string()),
669 partuuid: None,
670 partn: Some(i as u32 + 1),
671 size: 1048576,
672 maj_min: None,
673 start: Some(2048),
674 label: None,
675 fstype: None,
676 uuid: None,
677 path: None,
678 pttype: Some("dos".into()),
679 children: None,
680 })
681 .collect(),
682 ),
683 }
684 }
685
686 #[test]
687 fn test_mbr_esp_detection() {
688 let dev = make_mbr_disk(&["0x06"]);
690 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(1));
691
692 let dev = make_mbr_disk(&["0x83", "0xef"]);
694 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(2));
695
696 let dev = make_mbr_disk(&["0x83", "0x82"]);
698 assert!(dev.find_partition_of_esp().is_err());
699 }
700}