1use std::collections::HashSet;
2use std::env;
3use std::path::Path;
4use std::process::{Command, Stdio};
5
6use anyhow::{Context, Result, anyhow};
7use camino::{Utf8Path, Utf8PathBuf};
8use cap_std_ext::cap_std::fs::Dir;
9use fn_error_context::context;
10use serde::Deserialize;
11
12use bootc_utils::CommandRunExt;
13
14pub const ESP_ID_MBR: &[u8] = &[0x06, 0xEF];
19
20pub const ESP: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b";
22
23pub const BIOS_BOOT: &str = "21686148-6449-6e6f-744e-656564454649";
25
26#[derive(Debug, Deserialize)]
27struct DevicesOutput {
28 blockdevices: Vec<Device>,
29}
30
31#[allow(dead_code)]
32#[derive(Debug, Clone, Deserialize)]
33pub struct Device {
34 pub name: String,
35 pub serial: Option<String>,
36 pub model: Option<String>,
37 pub partlabel: Option<String>,
38 pub parttype: Option<String>,
39 pub partuuid: Option<String>,
40 pub partn: Option<u32>,
42 pub children: Option<Vec<Device>>,
43 pub size: u64,
44 #[serde(rename = "maj:min")]
45 pub maj_min: Option<String>,
46 pub start: Option<u64>,
49
50 pub label: Option<String>,
52 pub fstype: Option<String>,
53 pub uuid: Option<String>,
54 pub path: Option<String>,
55 pub pttype: Option<String>,
57}
58
59impl Device {
60 pub fn path(&self) -> String {
62 self.path.clone().unwrap_or(format!("/dev/{}", &self.name))
63 }
64
65 #[allow(dead_code)]
67 pub fn node(&self) -> String {
68 self.path()
69 }
70
71 #[allow(dead_code)]
72 pub fn has_children(&self) -> bool {
73 self.children.as_ref().is_some_and(|v| !v.is_empty())
74 }
75
76 pub fn is_mpath(&self) -> Result<bool> {
78 let dm_path = Utf8PathBuf::from_path_buf(std::fs::canonicalize(self.path())?)
79 .map_err(|_| anyhow::anyhow!("Non-UTF8 path"))?;
80 let dm_name = dm_path.file_name().unwrap_or("");
81 let uuid_path = Utf8PathBuf::from(format!("/sys/class/block/{dm_name}/dm/uuid"));
82
83 if uuid_path.exists() {
84 let uuid = std::fs::read_to_string(&uuid_path)
85 .with_context(|| format!("Failed to read {uuid_path}"))?;
86 if uuid.trim_start().starts_with("mpath-") {
87 return Ok(true);
88 }
89 }
90 Ok(false)
91 }
92
93 pub fn get_esp_partition_number(&self) -> Result<String> {
100 let esp_device = self.find_partition_of_esp()?;
101 let devname = &esp_device.name;
102
103 let partition_path = Utf8PathBuf::from(format!("/sys/class/block/{devname}/partition"));
104 if partition_path.exists() {
105 return std::fs::read_to_string(&partition_path)
106 .with_context(|| format!("Failed to read {partition_path}"));
107 }
108
109 if self.is_mpath()? {
111 if let Some(partn) = esp_device.partn {
112 return Ok(partn.to_string());
113 }
114 }
115 anyhow::bail!("Not supported for {devname}")
116 }
117
118 pub fn find_partition_of_bios_boot(&self) -> Option<&Device> {
120 self.find_partition_of_type(BIOS_BOOT)
121 }
122
123 pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> {
127 let mut esps = Vec::new();
128 for root in &self.find_all_roots()? {
129 if let Some(esp) = root.find_partition_of_esp_optional()? {
130 esps.push(esp.clone());
131 }
132 }
133 Ok((!esps.is_empty()).then_some(esps))
134 }
135
136 pub fn find_first_colocated_esp(&self) -> Result<Device> {
142 self.find_colocated_esps()?
143 .and_then(|mut v| Some(v.remove(0)))
144 .ok_or_else(|| anyhow!("No ESP partition found among backing devices"))
145 }
146
147 pub fn find_colocated_bios_boot(&self) -> Result<Option<Vec<Device>>> {
151 let bios_boots: Vec<_> = self
152 .find_all_roots()?
153 .iter()
154 .filter_map(|root| root.find_partition_of_bios_boot())
155 .cloned()
156 .collect();
157 Ok((!bios_boots.is_empty()).then_some(bios_boots))
158 }
159
160 pub fn find_partition_of_type(&self, parttype: &str) -> Option<&Device> {
162 self.children.as_ref()?.iter().find(|child| {
163 child
164 .parttype
165 .as_ref()
166 .is_some_and(|pt| pt.eq_ignore_ascii_case(parttype))
167 })
168 }
169
170 pub fn find_partition_of_esp_optional(&self) -> Result<Option<&Device>> {
183 let Some(children) = self.children.as_ref() else {
184 return Ok(None);
185 };
186 let direct = match self.pttype.as_deref() {
187 Some("dos") => children.iter().find(|child| {
188 child
189 .parttype
190 .as_ref()
191 .and_then(|pt| {
192 let pt = pt.strip_prefix("0x").unwrap_or(pt);
193 u8::from_str_radix(pt, 16).ok()
194 })
195 .is_some_and(|pt| ESP_ID_MBR.contains(&pt))
196 }),
197 Some("gpt") | None => self.find_partition_of_type(ESP),
200 Some(other) => return Err(anyhow!("Unsupported partition table type: {other}")),
201 };
202 if direct.is_some() {
203 return Ok(direct);
204 }
205 for child in children {
208 if child.pttype.is_some() {
209 if let Some(esp) = child.find_partition_of_esp_optional()? {
210 return Ok(Some(esp));
211 }
212 }
213 }
214 Ok(None)
215 }
216
217 pub fn find_partition_of_esp(&self) -> Result<&Device> {
222 self.find_partition_of_esp_optional()?
223 .ok_or_else(|| anyhow!("ESP partition not found on {}", self.path()))
224 }
225
226 pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> {
228 self.children
229 .as_ref()
230 .ok_or_else(|| anyhow!("Device has no children"))?
231 .iter()
232 .find(|child| child.partn == Some(partno))
233 .ok_or_else(|| anyhow!("Missing partition for index {partno}"))
234 }
235
236 pub fn refresh(&mut self) -> Result<()> {
239 let path = self.path();
240 let new_device = list_dev(Utf8Path::new(&path))?;
241 *self = new_device;
242 Ok(())
243 }
244
245 fn read_sysfs_property<T>(&self, property: &str) -> Result<Option<T>>
247 where
248 T: std::str::FromStr,
249 T::Err: std::error::Error + Send + Sync + 'static,
250 {
251 let Some(majmin) = self.maj_min.as_deref() else {
252 return Ok(None);
253 };
254 let sysfs_path = format!("/sys/dev/block/{majmin}/{property}");
255 if !Utf8Path::new(&sysfs_path).try_exists()? {
256 return Ok(None);
257 }
258 let value = std::fs::read_to_string(&sysfs_path)
259 .with_context(|| format!("Reading {sysfs_path}"))?;
260 let parsed = value
261 .trim()
262 .parse()
263 .with_context(|| format!("Parsing sysfs {property} property"))?;
264 tracing::debug!("backfilled {property} to {value}");
265 Ok(Some(parsed))
266 }
267
268 pub fn backfill_missing(&mut self) -> Result<()> {
270 if self.start.is_none() {
273 self.start = self.read_sysfs_property("start")?;
274 }
275 if self.partn.is_none() {
278 self.partn = self.read_sysfs_property("partition")?;
279 }
280 for child in self.children.iter_mut().flatten() {
282 child.backfill_missing()?;
283 }
284 Ok(())
285 }
286
287 pub fn list_parents(&self) -> Result<Option<Vec<Device>>> {
294 let path = self.path();
295 let output: DevicesOutput = Command::new("lsblk")
296 .args(["-J", "-b", "-O", "--inverse"])
297 .arg(&path)
298 .log_debug()
299 .run_and_parse_json()?;
300
301 let device = output
302 .blockdevices
303 .into_iter()
304 .next()
305 .ok_or_else(|| anyhow!("no device output from lsblk --inverse for {path}"))?;
306
307 match device.children {
308 Some(mut children) if !children.is_empty() => {
309 for child in &mut children {
310 child.backfill_missing()?;
311 }
312 Ok(Some(children))
313 }
314 _ => Ok(None),
315 }
316 }
317
318 pub fn require_single_root(&self) -> Result<Device> {
324 let mut roots = self.find_all_roots()?;
325 match roots.len() {
326 1 => Ok(roots.remove(0)),
327 n => anyhow::bail!(
328 "Expected a single root device for {}, but found {n}",
329 self.path()
330 ),
331 }
332 }
333
334 pub fn find_all_roots(&self) -> Result<Vec<Device>> {
341 let Some(parents) = self.list_parents()? else {
342 return Ok(vec![list_dev(Utf8Path::new(&self.path()))?]);
344 };
345
346 let mut roots = Vec::new();
347 let mut seen = HashSet::new();
348 let mut queue = parents;
349 while let Some(mut device) = queue.pop() {
350 match device.children.take() {
351 Some(grandparents) if !grandparents.is_empty() => {
352 queue.extend(grandparents);
353 }
354 _ => {
355 let name = device.name.clone();
358 if seen.insert(name) {
359 roots.push(list_dev(Utf8Path::new(&device.path()))?);
361 }
362 }
363 }
364 }
365 Ok(roots)
366 }
367}
368
369#[context("Listing device {dev}")]
370pub fn list_dev(dev: &Utf8Path) -> Result<Device> {
371 let mut devs: DevicesOutput = Command::new("lsblk")
372 .args(["-J", "-b", "-O"])
373 .arg(dev)
374 .log_debug()
375 .run_and_parse_json()?;
376 for dev in devs.blockdevices.iter_mut() {
377 dev.backfill_missing()?;
378 }
379 devs.blockdevices
380 .into_iter()
381 .next()
382 .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
383}
384
385pub fn list_dev_by_dir(dir: &Dir) -> Result<Device> {
387 let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?;
388 list_dev(&Utf8PathBuf::from(&fsinfo.source))
389}
390
391pub struct LoopbackDevice {
392 pub dev: Option<Utf8PathBuf>,
393 cleanup_handle: Option<LoopbackCleanupHandle>,
395}
396
397struct LoopbackCleanupHandle {
399 child: std::process::Child,
401}
402
403impl LoopbackDevice {
404 pub fn new(path: &Path) -> Result<Self> {
406 let direct_io = match env::var("BOOTC_DIRECT_IO") {
407 Ok(val) => {
408 if val == "on" {
409 "on"
410 } else {
411 "off"
412 }
413 }
414 Err(_e) => "off",
415 };
416
417 let dev = Command::new("losetup")
418 .args([
419 "--show",
420 format!("--direct-io={direct_io}").as_str(),
421 "-P",
422 "--find",
423 ])
424 .arg(path)
425 .run_get_string()?;
426 let dev = Utf8PathBuf::from(dev.trim());
427 tracing::debug!("Allocated loopback {dev}");
428
429 let cleanup_handle = match Self::spawn_cleanup_helper(dev.as_str()) {
431 Ok(handle) => Some(handle),
432 Err(e) => {
433 tracing::warn!(
434 "Failed to spawn loopback cleanup helper for {}: {}. \
435 Loopback device may not be cleaned up if process is interrupted.",
436 dev,
437 e
438 );
439 None
440 }
441 };
442
443 Ok(Self {
444 dev: Some(dev),
445 cleanup_handle,
446 })
447 }
448
449 pub fn path(&self) -> &Utf8Path {
451 self.dev.as_deref().unwrap()
453 }
454
455 fn spawn_cleanup_helper(device_path: &str) -> Result<LoopbackCleanupHandle> {
458 let bootc_path = bootc_utils::reexec::executable_path()
460 .context("Failed to locate bootc binary for cleanup helper")?;
461
462 let mut cmd = Command::new(bootc_path);
464 cmd.args([
465 "internals",
466 "loopback-cleanup-helper",
467 "--device",
468 device_path,
469 ]);
470
471 cmd.env("BOOTC_LOOPBACK_CLEANUP_HELPER", "1");
473
474 cmd.stdin(Stdio::null());
476 cmd.stdout(Stdio::null());
477 let child = cmd
481 .spawn()
482 .context("Failed to spawn loopback cleanup helper")?;
483
484 Ok(LoopbackCleanupHandle { child })
485 }
486
487 fn impl_close(&mut self) -> Result<()> {
489 let Some(dev) = self.dev.take() else {
491 tracing::trace!("loopback device already deallocated");
492 return Ok(());
493 };
494
495 if let Some(mut cleanup_handle) = self.cleanup_handle.take() {
497 let _ = cleanup_handle.child.kill();
499 }
500
501 Command::new("losetup")
502 .args(["-d", dev.as_str()])
503 .run_capture_stderr()
504 }
505
506 pub fn close(mut self) -> Result<()> {
508 self.impl_close()
509 }
510}
511
512impl Drop for LoopbackDevice {
513 fn drop(&mut self) {
514 let _ = self.impl_close();
516 }
517}
518
519pub async fn run_loopback_cleanup_helper(device_path: &str) -> Result<()> {
522 if std::env::var("BOOTC_LOOPBACK_CLEANUP_HELPER").is_err() {
524 anyhow::bail!("This function should only be called as a cleanup helper");
525 }
526
527 rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM))
529 .context("Failed to set parent death signal")?;
530
531 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
533 .expect("Failed to create signal stream")
534 .recv()
535 .await;
536
537 let output = std::process::Command::new("losetup")
539 .args(["-d", device_path])
540 .output();
541
542 match output {
543 Ok(output) if output.status.success() => {
544 tracing::info!("Cleaned up leaked loopback device {}", device_path);
546 std::process::exit(0);
547 }
548 Ok(output) => {
549 let stderr = String::from_utf8_lossy(&output.stderr);
550 tracing::error!(
551 "Failed to clean up loopback device {}: {}. Stderr: {}",
552 device_path,
553 output.status,
554 stderr.trim()
555 );
556 std::process::exit(1);
557 }
558 Err(e) => {
559 tracing::error!(
560 "Error executing losetup to clean up loopback device {}: {}",
561 device_path,
562 e
563 );
564 std::process::exit(1);
565 }
566 }
567}
568
569pub fn parse_size_mib(mut s: &str) -> Result<u64> {
571 let suffixes = [
572 ("MiB", 1u64),
573 ("M", 1u64),
574 ("GiB", 1024),
575 ("G", 1024),
576 ("TiB", 1024 * 1024),
577 ("T", 1024 * 1024),
578 ];
579 let mut mul = 1u64;
580 for (suffix, imul) in suffixes {
581 if let Some((sv, rest)) = s.rsplit_once(suffix) {
582 if !rest.is_empty() {
583 anyhow::bail!("Trailing text after size: {rest}");
584 }
585 s = sv;
586 mul = imul;
587 }
588 }
589 let v = s.parse::<u64>()?;
590 Ok(v * mul)
591}
592
593#[cfg(test)]
594mod test {
595 use super::*;
596
597 #[test]
598 fn test_parse_size_mib() {
599 let ident_cases = [0, 10, 9, 1024].into_iter().map(|k| (k.to_string(), k));
600 let cases = [
601 ("0M", 0),
602 ("10M", 10),
603 ("10MiB", 10),
604 ("1G", 1024),
605 ("9G", 9216),
606 ("11T", 11 * 1024 * 1024),
607 ]
608 .into_iter()
609 .map(|(k, v)| (k.to_string(), v));
610 for (s, v) in ident_cases.chain(cases) {
611 assert_eq!(parse_size_mib(&s).unwrap(), v as u64, "Parsing {s}");
612 }
613 }
614
615 #[test]
616 fn test_parse_lsblk() {
617 let fixture = include_str!("../tests/fixtures/lsblk.json");
618 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
619 let dev = devs.blockdevices.into_iter().next().unwrap();
620 assert_eq!(dev.partn, None);
622 let children = dev.children.as_deref().unwrap();
623 assert_eq!(children.len(), 3);
624 let first_child = &children[0];
625 assert_eq!(first_child.partn, Some(1));
626 assert_eq!(
627 first_child.parttype.as_deref().unwrap(),
628 "21686148-6449-6e6f-744e-656564454649"
629 );
630 assert_eq!(
631 first_child.partuuid.as_deref().unwrap(),
632 "3979e399-262f-4666-aabc-7ab5d3add2f0"
633 );
634 let part2 = dev.find_device_by_partno(2).unwrap();
636 assert_eq!(part2.partn, Some(2));
637 assert_eq!(part2.parttype.as_deref().unwrap(), ESP);
638 let esp = dev.find_partition_of_esp().unwrap();
640 assert_eq!(esp.partn, Some(2));
641 let bios = dev.find_partition_of_bios_boot().unwrap();
643 assert_eq!(bios.partn, Some(1));
644 assert_eq!(bios.parttype.as_deref().unwrap(), BIOS_BOOT);
645 }
646
647 #[test]
648 fn test_parse_lsblk_mbr() {
649 let fixture = include_str!("../tests/fixtures/lsblk-mbr.json");
650 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
651 let dev = devs.blockdevices.into_iter().next().unwrap();
652 assert_eq!(dev.partn, None);
654 assert_eq!(dev.pttype.as_deref().unwrap(), "dos");
655 let children = dev.children.as_deref().unwrap();
656 assert_eq!(children.len(), 3);
657 let first_child = &children[0];
659 assert_eq!(first_child.partn, Some(1));
660 assert_eq!(first_child.parttype.as_deref().unwrap(), "0x06");
661 assert_eq!(first_child.partuuid.as_deref().unwrap(), "a1b2c3d4-01");
662 assert_eq!(first_child.fstype.as_deref().unwrap(), "vfat");
663 assert!(first_child.partlabel.is_none());
665 let second_child = &children[1];
667 assert_eq!(second_child.partn, Some(2));
668 assert_eq!(second_child.parttype.as_deref().unwrap(), "0x83");
669 assert_eq!(second_child.partuuid.as_deref().unwrap(), "a1b2c3d4-02");
670 let third_child = &children[2];
672 assert_eq!(third_child.partn, Some(3));
673 assert_eq!(third_child.parttype.as_deref().unwrap(), "0xef");
674 assert_eq!(third_child.partuuid.as_deref().unwrap(), "a1b2c3d4-03");
675 let part1 = dev.find_device_by_partno(1).unwrap();
677 assert_eq!(part1.partn, Some(1));
678 let esp = dev.find_partition_of_esp().unwrap();
680 assert_eq!(esp.partn, Some(1));
681 }
682
683 fn make_mbr_disk(parttypes: &[&str]) -> Device {
685 Device {
686 name: "vda".into(),
687 serial: None,
688 model: None,
689 partlabel: None,
690 parttype: None,
691 partuuid: None,
692 partn: None,
693 size: 10737418240,
694 maj_min: None,
695 start: None,
696 label: None,
697 fstype: None,
698 uuid: None,
699 path: Some("/dev/vda".into()),
700 pttype: Some("dos".into()),
701 children: Some(
702 parttypes
703 .iter()
704 .enumerate()
705 .map(|(i, pt)| Device {
706 name: format!("vda{}", i + 1),
707 serial: None,
708 model: None,
709 partlabel: None,
710 parttype: Some(pt.to_string()),
711 partuuid: None,
712 partn: Some(i as u32 + 1),
713 size: 1048576,
714 maj_min: None,
715 start: Some(2048),
716 label: None,
717 fstype: None,
718 uuid: None,
719 path: None,
720 pttype: Some("dos".into()),
721 children: None,
722 })
723 .collect(),
724 ),
725 }
726 }
727
728 #[test]
729 fn test_parse_lsblk_vroc() {
730 let fixture = include_str!("../tests/fixtures/lsblk-vroc.json");
731 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
732 assert_eq!(devs.blockdevices.len(), 2);
733
734 for nvme in &devs.blockdevices {
738 let esp = nvme.find_partition_of_esp().unwrap();
739 assert_eq!(esp.name, "md126p1");
740 assert_eq!(esp.partn, Some(1));
741 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
742 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
743 }
744 }
745
746 #[test]
747 fn test_parse_lsblk_swraid() {
748 let fixture = include_str!("../tests/fixtures/lsblk-swraid.json");
749 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
750 assert_eq!(devs.blockdevices.len(), 2);
751
752 let sda = &devs.blockdevices[0];
758 let esp = sda.find_partition_of_esp().unwrap();
759 assert_eq!(esp.name, "sda1");
760 assert_eq!(esp.partn, Some(1));
761 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
762 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
763
764 let sdb = &devs.blockdevices[1];
765 let esp = sdb.find_partition_of_esp().unwrap();
766 assert_eq!(esp.name, "sdb1");
767 assert_eq!(esp.partn, Some(1));
768 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
769 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
770
771 let sda3 = sda
774 .children
775 .as_ref()
776 .unwrap()
777 .iter()
778 .find(|c| c.name == "sda3")
779 .unwrap();
780 assert_eq!(sda3.fstype.as_deref().unwrap(), "linux_raid_member");
781 let md0 = sda3
782 .children
783 .as_ref()
784 .unwrap()
785 .iter()
786 .find(|c| c.name == "md0")
787 .unwrap();
788 assert_eq!(md0.fstype.as_deref().unwrap(), "ext4");
789 }
790
791 #[test]
792 fn test_mbr_esp_detection() {
793 let dev = make_mbr_disk(&["0x06"]);
795 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(1));
796
797 let dev = make_mbr_disk(&["0x83", "0xef"]);
799 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(2));
800
801 let dev = make_mbr_disk(&["0x83", "0x82"]);
803 assert!(dev.find_partition_of_esp().is_err());
804 }
805}