From d244c0748354e168f407583c8a183e38b4850453 Mon Sep 17 00:00:00 2001 From: Bruce Leidl Date: Thu, 17 Jan 2019 09:10:28 -0500 Subject: [PATCH] refactor boot selection algorithm --- citadel-mount/src/boot_select.rs | 140 ----------------- citadel-mount/src/main.rs | 9 +- citadel-mount/src/rootfs.rs | 251 ++++++++++++++++++++----------- 3 files changed, 168 insertions(+), 232 deletions(-) delete mode 100644 citadel-mount/src/boot_select.rs diff --git a/citadel-mount/src/boot_select.rs b/citadel-mount/src/boot_select.rs deleted file mode 100644 index 3807fda..0000000 --- a/citadel-mount/src/boot_select.rs +++ /dev/null @@ -1,140 +0,0 @@ - -use libcitadel::{Partition,Result,ImageHeader}; - -pub struct BootSelection { - partitions: Vec, -} - -impl BootSelection { - pub fn choose_install_partition() -> Result { - let bs = BootSelection::load_partitions()?; - match bs._choose_install_partition() { - Some(p) => Ok(p.clone()), - None => bail!("no partition found for installation"), - } - } - - pub fn choose_boot_partition() -> Result { - let bs = BootSelection::load_partitions()?; - match bs._choose_boot_partition() { - Some(p) => Ok(p.clone()), - None => bail!("no partition found to boot from"), - } - } - - fn load_partitions() -> Result { - let partitions = Partition::rootfs_partitions() - .map_err(|e| format_err!("Could not load rootfs partition info: {}", e))?; - - Ok(BootSelection { - partitions - }) - } - - fn _choose_install_partition(&self) -> Option<&Partition> { - self.choose(|p| { - // first pass, if there is a partition which is not mounted and - // not initialized use that one - !p.is_mounted() && !p.is_initialized() - }).or_else(|| self.choose(|p| { - // second pass, just find one that's not mounted - !p.is_mounted() - })) - } - - fn choose(&self, pred: F) -> Option<&Partition> - where F: Sized + Fn(&&Partition) -> bool - { - self.partitions.iter().find(pred) - } - - /// Find the best rootfs partition to boot from - fn _choose_boot_partition(&self) -> Option<&Partition> { - let mut best: Option<&Partition> = None; - - for p in &self.partitions { - if is_better(&best, p) { - best = Some(p); - } - } - best - } - - - /// Perform checks for error states at boot time. - pub fn scan_boot_partitions(&mut self) -> Result<()> { - for mut p in &mut self.partitions { - if let Err(e) = boot_scan_partition(&mut p) { - warn!("error in bootscan of partition {}: {}", p.path().display(), e); - } - } - Ok(()) - } -} - -/// Called at boot to perform various checks and possibly -/// update the status field to an error state. -/// -/// Mark `STATUS_TRY_BOOT` partition as `STATUS_FAILED`. -/// -/// If metainfo cannot be parsed, mark as `STATUS_BAD_META`. -/// -/// Verify metainfo signature and mark `STATUS_BAD_SIG` if -/// signature verification fails. -/// -fn boot_scan_partition(p: &mut Partition) -> Result<()> { - if !p.is_initialized() { - return Ok(()) - } - if p.header().status() == ImageHeader::STATUS_TRY_BOOT { - warn!("Partition {} has STATUS_TRY_BOOT, assuming it failed boot attempt and marking STATUS_FAILED", p.path().display()); - p.write_status(ImageHeader::STATUS_FAILED)?; - } - p.header().verify_signature()?; - Ok(()) -} - -fn is_better<'a>(current_best: &Option<&'a Partition>, other: &'a Partition) -> bool { - - if !other.is_initialized() { - return false; - } - - // Only consider partitions in state NEW or state GOOD - if !other.is_good() && !other.is_new() { - return false; - } - // If metainfo is broken, then no, it's not better - //if !other.metainfo().is_ok() { - // return false; - //} - - let best = match *current_best { - Some(p) => p, - // No current 'best', so 'other' is better, whatever it is. - None => return true, - }; - - // First parition with PREFER flag trumps everything else - if best.is_preferred() { - return false; - } - - let best_version = best.metainfo().version(); - let other_version = other.metainfo().version(); - - if best_version > other_version { - return false; - } - - if other_version > best_version { - return true; - } - - // choose NEW over GOOD if versions are the same - if other.is_new() && best.is_good() { - return true; - } - // ... but if all things otherwise match, return first match - false -} diff --git a/citadel-mount/src/main.rs b/citadel-mount/src/main.rs index ca46698..a831a8f 100644 --- a/citadel-mount/src/main.rs +++ b/citadel-mount/src/main.rs @@ -10,11 +10,7 @@ use std::fs; use libcitadel::{Result,CommandLine,set_verbose,format_error,ResourceImage,util}; -mod boot_select; mod rootfs; -pub use boot_select::BootSelection; -use rootfs::Rootfs; - /// mount command supports 4 subcommands /// @@ -27,7 +23,7 @@ use rootfs::Rootfs; /// /// 'kernel' mounts a resource bundle containing kernel modules /// 'extra' mounts a resource bundle containing extra files -/// +/// 'overlay' mounts a tmpfs overlay over rootfs filesystem only if citadel.overlay is set /// fn main() { @@ -54,8 +50,7 @@ fn main() { fn mount_rootfs() -> Result<()> { info!("citadel-mount rootfs"); - let rootfs = Rootfs::new(); - rootfs.setup() + rootfs::setup_rootfs() } fn mount_kernel() -> Result<()> { diff --git a/citadel-mount/src/rootfs.rs b/citadel-mount/src/rootfs.rs index 39ffce2..3d53318 100644 --- a/citadel-mount/src/rootfs.rs +++ b/citadel-mount/src/rootfs.rs @@ -1,93 +1,174 @@ use std::process::Command; -use libcitadel::{BlockDev,CommandLine,Partition,Result,verity}; +use libcitadel::{BlockDev,CommandLine,ImageHeader,Partition,Result,verity}; use std::path::Path; use std::process::Stdio; -use BootSelection; use ResourceImage; -pub struct Rootfs { -} - -impl Rootfs { - pub fn new() -> Rootfs { - Rootfs {} - } - - pub fn setup(&self) -> Result<()> { - if CommandLine::install_mode() || CommandLine::live_mode() { - self.setup_rootfs_resource() - } else { - let partition = BootSelection::choose_boot_partition()?; - self.setup_partition(partition) - } - } - - fn setup_partition(&self, partition: Partition) -> Result<()> { - if CommandLine::noverity() { - self.setup_partition_unverified(&partition) - } else { - self.setup_partition_verified(&partition) - } - } - - fn setup_rootfs_resource(&self) -> Result<()> { - info!("Searching for rootfs resource image"); - - let img = ResourceImage::find_rootfs()?; - - if CommandLine::noverity() { - self.setup_resource_unverified(&img) - } else { - self.setup_resource_verified(&img) - } - } - - fn setup_resource_unverified(&self, img: &ResourceImage) -> Result<()> { - if img.is_compressed() { - img.decompress()?; - } - let loopdev = img.create_loopdev()?; - info!("Loop device created: {}", loopdev.display()); - self.setup_linear_mapping(&loopdev) - } - - fn setup_resource_verified(&self, img: &ResourceImage) -> Result<()> { - let _ = img.setup_verity_device()?; - Ok(()) - } - - fn setup_partition_unverified(&self, partition: &Partition) -> Result<()> { - info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)"); - self.setup_linear_mapping(partition.path()) - } - - fn setup_partition_verified(&self, partition: &Partition) -> Result<()> { - info!("Creating /dev/mapper/rootfs dm-verity device"); - if !CommandLine::nosignatures() { - partition.header().verify_signature()?; - info!("Image signature is valid for channel {}", partition.metainfo().channel()); - } - verity::setup_partition_device(partition)?; - Ok(()) - } - - fn setup_linear_mapping(&self, blockdev: &Path) -> Result<()> { - let dev = BlockDev::open_ro(blockdev)?; - let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display()); - - info!("/usr/sbin/dmsetup create rootfs --table '{}'", table); - - let ok = Command::new("/usr/sbin/dmsetup") - .args(&["create", "rootfs", "--table", &table]) - .stderr(Stdio::inherit()) - .status() - .expect("unable to execute /usr/sbin/dmsetup") - .success(); - - if !ok { - bail!("Failed to set up linear identity mapping with /usr/sbin/dmsetup"); - } - Ok(()) +pub fn setup_rootfs() -> Result<()> { + if CommandLine::install_mode() || CommandLine::live_mode() { + setup_rootfs_resource() + } else { + let p = choose_boot_partiton(true)?; + setup_partition(p) } } + +fn setup_partition(mut p: Partition) -> Result<()> { + if CommandLine::noverity() { + setup_partition_unverified(&p) + } else { + setup_partition_verified(&mut p) + } +} + +fn setup_rootfs_resource() -> Result<()> { + info!("Searching for rootfs resource image"); + + let img = ResourceImage::find_rootfs()?; + + if CommandLine::noverity() { + setup_resource_unverified(&img) + } else { + setup_resource_verified(&img) + } +} + +fn setup_resource_unverified(img: &ResourceImage) -> Result<()> { + if img.is_compressed() { + img.decompress()?; + } + let loopdev = img.create_loopdev()?; + info!("Loop device created: {}", loopdev.display()); + setup_linear_mapping(&loopdev) +} + +fn setup_resource_verified(img: &ResourceImage) -> Result<()> { + let _ = img.setup_verity_device()?; + Ok(()) +} + +fn setup_partition_unverified(p: &Partition) -> Result<()> { + info!("Creating /dev/mapper/rootfs device with linear device mapping of partition (no verity)"); + setup_linear_mapping(p.path()) +} + +fn setup_partition_verified(p: &mut Partition) -> Result<()> { + info!("Creating /dev/mapper/rootfs dm-verity device"); + if !CommandLine::nosignatures() { + if !p.has_public_key() { + bail!("No public key available for channel {}", p.metainfo().channel()) + } + if !p.is_signature_valid() { + p.write_status(ImageHeader::STATUS_BAD_SIG)?; + bail!("Signature verification failed on partition"); + } + info!("Image signature is valid for channel {}", p.metainfo().channel()); + } + verity::setup_partition_device(p)?; + Ok(()) +} + +fn setup_linear_mapping(blockdev: &Path) -> Result<()> { + let dev = BlockDev::open_ro(blockdev)?; + let table = format!("0 {} linear {} 0", dev.nsectors()?, blockdev.display()); + + info!("/usr/sbin/dmsetup create rootfs --table '{}'", table); + + let ok = Command::new("/usr/sbin/dmsetup") + .args(&["create", "rootfs", "--table", &table]) + .stderr(Stdio::inherit()) + .status() + .expect("unable to execute /usr/sbin/dmsetup") + .success(); + + if !ok { + bail!("Failed to set up linear identity mapping with /usr/sbin/dmsetup"); + } + Ok(()) +} + +fn choose_boot_partiton(scan: bool) -> Result { + let mut partitions = Partition::rootfs_partitions()?; + + if scan { + for p in &mut partitions { + p.boot_scan()?; + } + } + + let mut best = None; + for p in partitions { + best = compare_boot_partitions(best, p); + } + best.ok_or(format_err!("No partition found to boot from")) +} + +fn compare_boot_partitions(a: Option, b: Partition) -> Option { + if !is_bootable(&b) { + return a; + } + + // b is bootable, so if a is None, then just return b + let a = match a { + Some(partition) => partition, + None => return Some(b), + }; + + // First partition with FLAG_PREFER_BOOT trumps everything + if a.is_preferred() { + return Some(a); + } + + if b.is_preferred() { + return Some(b); + } + + // Compare versions and channels + let a_v = a.metainfo().version(); + let b_v = b.metainfo().version(); + + // Compare versions only if channels match + if a.metainfo().channel() == b.metainfo().channel() { + if a_v > b_v { + return Some(a); + } else if b_v > a_v { + return Some(b); + } + } + + // choose NEW over GOOD if versions are the same or + // if versions cannot be compared because channels differ + if b.is_new() && a.is_good() { + return Some(b); + } + + Some(a) +} + +fn is_bootable(p: &Partition) -> bool { + if !p.is_initialized() { + return false; + } + + // signatures enabled so not bootable without pubkey + if signatures_enabled() && !p.has_public_key() { + return false; + } + + if p.is_new() || p.is_good() { + return true; + } + + // If signatures are disabled then don't disqualify an + // image which failed a prior signature verification + if !signatures_enabled() && p.is_sig_failed() { + return true; + } + + false +} + +fn signatures_enabled() -> bool { + !(CommandLine::nosignatures() || CommandLine::noverity()) +}