/
githubmirror
/
coreos-installer
Обзор
Документация
Войти
/
githubmirror
/
coreos-installer
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/blockdev.rs
1 841 строка
66 KB
Steven Presti
unique_fs: detect unassembled RAID arrays in verify-unique-fs-label
18 май 2026, 23:19
18 май 2026, 23:19
95d56cd
Код
Авторство
О чём код?
// Copyright 2019 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use anyhow::{anyhow, bail, Context, Result}; use gptman::{GPTPartitionEntry, GPT}; use lazy_static::lazy_static; use nix::sys::stat::{major, minor}; use nix::{errno::Errno, mount, sched}; use regex::Regex; use std::collections::{HashMap, HashSet}; use std::fs::{ canonicalize, metadata, read_dir, read_to_string, remove_dir, symlink_metadata, File, OpenOptions, }; use std::io::{Read, Seek, SeekFrom, Write}; use std::num::{NonZeroU32, NonZeroU64}; use std::os::linux::fs::MetadataExt; use std::os::raw::c_int; use std::os::unix::fs::FileTypeExt; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::process::Command; use std::thread::sleep; use std::time::Duration; use uuid::Uuid; use crate::cmdline::PartitionFilter; use crate::util::*; use crate::{runcmd, runcmd_output}; #[derive(Debug)] pub struct Disk { path: String, } impl Disk { pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> { let path = path.as_ref(); let canon_path = path .canonicalize() .with_context(|| format!("canonicalizing {}", path.display()))?; let canon_path = canon_path .to_str() .with_context(|| { format!( "path {} canonicalized from {} is not UTF-8", canon_path.display(), path.display() ) })? .to_string(); Ok(Disk { path: canon_path }) } pub fn mount_partition_by_label(&self, label: &str, flags: mount::MsFlags) -> Result<Mount> { // get partition list let partitions = self.get_partitions()?; if partitions.is_empty() { bail!("couldn't find any partitions on {}", self.path); } // find the partition with the matching label let matching_partitions = partitions .iter() .filter(|d| d.label.as_ref().unwrap_or(&"".to_string()) == label) .collect::<Vec<&Partition>>(); let part = match matching_partitions.len() { 0 => bail!("couldn't find {} device for {}", label, self.path), 1 => matching_partitions[0], _ => bail!( "found multiple devices on {} with label \"{}\"", self.path, label ), }; // mount it match &part.fstype { Some(fstype) => Mount::try_mount(&part.path, fstype, flags), None => bail!( "couldn't get filesystem type of {} device for {}", label, self.path ), } } fn get_partitions(&self) -> Result<Vec<Partition>> { // walk each device in the output let mut result: Vec<Partition> = Vec::new(); for devinfo in lsblk(Path::new(&self.path), true)? { if let Some(name) = devinfo.get("NAME") { // Only return partitions. Skip the whole-disk device, as well // as holders like LVM or RAID devices using one of the partitions. if devinfo.get("TYPE").map(|s| s.as_str()) != Some("part") { continue; } // only trust lsblk output for the following fields if we have udev if have_udev() { let (mountpoint, swap) = match devinfo.get("MOUNTPOINT") { Some(mp) if mp == "[SWAP]" => (None, true), Some(mp) => (Some(mp.to_string()), false), None => (None, false), }; result.push(Partition { path: name.to_owned(), label: devinfo.get("LABEL").map(<_>::to_string), fstype: devinfo.get("FSTYPE").map(<_>::to_string), parent: self.path.to_owned(), mountpoint, swap, }); } else { let devinfo = blkid_single(Path::new(name))?; // note TYPE here: blkid uses TYPE instead of FSTYPE let fstype = devinfo.get("TYPE").map(<_>::to_string); result.push(Partition { path: name.to_owned(), label: devinfo.get("LABEL").map(<_>::to_string), fstype: fstype.to_owned(), parent: self.path.to_owned(), mountpoint: None, swap: fstype.is_some_and(|s| s == "swap"), }); } } } Ok(result) } /// Return an empty list if we have exclusive access to the device, or /// a list of partitions preventing us from gaining exclusive access. pub fn get_busy_partitions(self) -> Result<Vec<Partition>> { // Try rereading the partition table. This is the most complete // check, but it only works on partitionable devices. let rereadpt_result = { let mut f = OpenOptions::new() .write(true) .open(&self.path) .with_context(|| format!("opening {}", &self.path))?; reread_partition_table(&mut f, false).map(|_| Vec::new()) }; if rereadpt_result.is_ok() { return rereadpt_result; } // Walk partitions, record the ones that are reported in use, // and return the list if any let mut busy: Vec<Partition> = Vec::new(); for d in self.get_partitions()? { if d.mountpoint.is_some() || d.swap || !d.get_holders()?.is_empty() { busy.push(d) } } if !busy.is_empty() { return Ok(busy); } // Our investigation found nothing. If the device is expected to be // partitionable but reread failed, we evidently missed something, // so error out for safety if !self.is_dm_device() { return rereadpt_result; } Ok(Vec::new()) } /// Get a handle to the set of device nodes for individual partitions /// of the device. pub fn get_partition_table(&self) -> Result<Box<dyn PartTable>> { if self.is_dm_device() { Ok(Box::new(PartTableKpartx::new(&self.path)?)) } else { Ok(Box::new(PartTableKernel::new(&self.path)?)) } } pub fn is_dm_device(&self) -> bool { self.path.starts_with("/dev/dm-") } pub fn is_luks_integrity(&self) -> Result<bool> { if !self.is_dm_device() { return Ok(false); } let dminfo = runcmd_output!( "dmsetup", "info", "--columns", "--noheadings", "-o", "uuid", &self.path ) .with_context(|| format!("checking if device {} is type LUKS integrity", self.path))?; let uuid = dminfo.trim(); // since cryptsetup-2.8.0 SUBDEV is used Ok(uuid.starts_with("CRYPT-INTEGRITY-") || uuid.starts_with("CRYPT-SUBDEV-")) } } /// A handle to the set of device nodes for individual partitions of a /// device. Must be held as long as the device nodes are needed; they might /// be removed upon drop. pub trait PartTable { /// Update device nodes for the current state of the partition table fn reread(&mut self) -> Result<()>; } /// Device nodes for partitionable kernel devices, managed by the kernel. #[derive(Debug)] pub struct PartTableKernel { file: File, } impl PartTableKernel { fn new(path: &str) -> Result<Self> { let file = OpenOptions::new() .write(true) .open(path) .with_context(|| format!("opening {path}"))?; Ok(Self { file }) } } impl PartTable for PartTableKernel { fn reread(&mut self) -> Result<()> { reread_partition_table(&mut self.file, true)?; udev_settle() } } /// Device nodes for non-partitionable kernel devices, managed by running /// kpartx to parse the partition table and create device-mapper devices for /// each partition. #[derive(Debug)] pub struct PartTableKpartx { path: String, need_teardown: bool, } impl PartTableKpartx { fn new(path: &str) -> Result<Self> { let mut table = Self { path: path.to_string(), need_teardown: !Self::already_set_up(path)?, }; // create/sync partition devices if missing table.reread()?; Ok(table) } // We only want to kpartx -d on drop if we're the one initially // creating the partition devices. There's no good way to detect // this. fn already_set_up(path: &str) -> Result<bool> { let re = Regex::new(r"^p[0-9]+$").expect("compiling RE"); let expected = Path::new(path) .file_name() .with_context(|| format!("getting filename of {path}"))? .to_os_string() .into_string() .map_err(|_| anyhow!("converting filename of {}", path))?; for ent in read_dir("/dev/mapper").context("listing /dev/mapper")? { let ent = ent.context("reading /dev/mapper entry")?; let found = ent.file_name().into_string().map_err(|_| { anyhow!( "converting filename of {}", Path::new(&ent.file_name()).display() ) })?; if found.starts_with(&expected) && re.is_match(&found[expected.len()..]) { return Ok(true); } } Ok(false) } fn run_kpartx(&self, flag: &str) -> Result<()> { // Swallow stderr on success. Avoids spurious warnings: // GPT:Primary header thinks Alt. header is not at the end of the disk. // GPT:Alternate GPT header not at the end of the disk. // GPT: Use GNU Parted to correct GPT errors. // // By default, kpartx waits for udev to settle before returning, // but this blocks indefinitely inside a container. See e.g. // https://github.com/moby/moby/issues/22025 // Use -n to skip blocking on udev, and then manually settle. runcmd_output!("kpartx", flag, "-n", &self.path)?; udev_settle()?; Ok(()) } } impl PartTable for PartTableKpartx { fn reread(&mut self) -> Result<()> { let delay = 1; for _ in 0..4 { match self.run_kpartx("-u") { Ok(()) => return Ok(()), Err(e) => eprintln!("Error: {e}"), } eprintln!("Retrying in {delay} second"); sleep(Duration::from_secs(delay)); } self.run_kpartx("-u") } } impl Drop for PartTableKpartx { /// If we created the partition devices (rather than finding them /// already existing), delete them afterward so we don't leave DM /// devices attached to the specified disk. fn drop(&mut self) { if self.need_teardown { if let Err(e) = self.run_kpartx("-d") { eprintln!("{e}") } } } } #[derive(Debug)] pub struct Partition { pub path: String, pub label: Option<String>, pub fstype: Option<String>, pub parent: String, pub mountpoint: Option<String>, pub swap: bool, } impl Partition { /// Return start and end offsets within the disk. pub fn get_offsets(path: &str) -> Result<(u64, u64)> { let dev = metadata(path) .with_context(|| format!("getting metadata for {path}"))? .st_rdev(); let maj: u64 = major(dev); let min: u64 = minor(dev); let start = read_sysfs_dev_block_value_u64(maj, min, "start")?; let size = read_sysfs_dev_block_value_u64(maj, min, "size")?; // We multiply by 512 here: the kernel values are always in 512 blocks, regardless of the // actual sector size of the block device. We keep the values as bytes to make things // easier. let start_offset: u64 = start .checked_mul(512) .context("start offset mult overflow")?; let end_offset: u64 = start_offset .checked_add(size.checked_mul(512).context("end offset mult overflow")?) .context("end offset add overflow")?; Ok((start_offset, end_offset)) } pub fn get_holders(&self) -> Result<Vec<String>> { let holders = self.get_sysfs_dir()?.join("holders"); let mut ret: Vec<String> = Vec::new(); for ent in read_dir(&holders).with_context(|| format!("reading {}", &holders.display()))? { let ent = ent.with_context(|| format!("reading {} entry", &holders.display()))?; ret.push(format!("/dev/{}", ent.file_name().to_string_lossy())); } Ok(ret) } // Try to locate the device directory in sysfs. fn get_sysfs_dir(&self) -> Result<PathBuf> { let basedir = Path::new("/sys/block"); // First assume we have a regular partition. // /sys/block/sda/sda1 let devdir = basedir .join( Path::new(&self.parent) .file_name() .with_context(|| format!("parent {} has no filename", self.parent))?, ) .join( Path::new(&self.path) .file_name() .with_context(|| format!("path {} has no filename", self.path))?, ); if devdir.exists() { return Ok(devdir); } // Now assume a kpartx "partition", where the path is a symlink to // an unpartitioned DM device node. // /sys/block/dm-1 let is_link = symlink_metadata(&self.path) .with_context(|| format!("reading metadata for {}", self.path))? .file_type() .is_symlink(); if is_link { let target = canonicalize(&self.path) .with_context(|| format!("getting absolute path to {}", self.path))?; let devdir = basedir.join( target .file_name() .with_context(|| format!("target {} has no filename", target.display()))?, ); if devdir.exists() { return Ok(devdir); } } // Give up bail!( "couldn't find /sys/block directory for partition {} of {}", &self.path, &self.parent ); } } #[derive(Debug)] pub struct Mount { device: String, mountpoint: PathBuf, /// Whether we own this mount. owned: bool, } impl Mount { pub fn try_mount(device: &str, fstype: &str, flags: mount::MsFlags) -> Result<Mount> { let tempdir = tempfile::Builder::new() .prefix("coreos-installer-") .tempdir() .context("creating temporary directory")?; // avoid auto-cleanup of tempdir, which could recursively remove // the partition contents if umount failed let mountpoint = tempdir.keep(); // Ensure we're in a private mount namespace so the mount isn't // visible to the rest of the system. Multiple unshare calls // should be safe. sched::unshare(sched::CloneFlags::CLONE_NEWNS).context("unsharing mount namespace")?; mount::mount::<str, Path, str, str>(Some(device), &mountpoint, Some(fstype), flags, None) .with_context(|| format!("mounting device {} on {}", device, mountpoint.display()))?; Ok(Mount { device: device.to_string(), mountpoint, owned: true, }) } pub fn from_existing<P: AsRef<Path>>(path: P) -> Result<Mount> { let mounts = read_to_string("/proc/self/mounts").context("reading mount table")?; for line in mounts.lines() { let mount: Vec<&str> = line.split_whitespace().collect(); // see https://man7.org/linux/man-pages/man5/fstab.5.html if mount.len() != 6 { bail!("invalid line in /proc/self/mounts: {}", line); } if Path::new(mount[1]) == path.as_ref() { return Ok(Mount { device: mount[0].to_string(), mountpoint: path.as_ref().into(), owned: false, }); } } bail!("mountpoint {} not found", path.as_ref().display()); } pub fn device(&self) -> &str { self.device.as_str() } pub fn mountpoint(&self) -> &Path { self.mountpoint.as_path() } pub fn get_partition_offsets(&self) -> Result<(u64, u64)> { Partition::get_offsets(&self.device) } pub fn get_filesystem_uuid(&self) -> Result<String> { // We used to use lsblk_single, but its cache may be stale after mkfs. // blkid_single doesn't use cache. let devinfo = blkid_single(Path::new(&self.device))?; devinfo .get("UUID") .map(String::from) .with_context(|| format!("filesystem {} has no UUID", self.device)) } } impl Drop for Mount { fn drop(&mut self) { if !self.owned { return; } // Unmount sometimes fails immediately after closing the last open // file on the partition. Retry several times before giving up. for retries in (0..20).rev() { match mount::umount(&self.mountpoint) { Ok(_) => break, Err(err) => { if retries == 0 { eprintln!("umounting {}: {}", self.device, err); return; } else { sleep(Duration::from_millis(100)); } } } } if let Err(err) = remove_dir(&self.mountpoint) { eprintln!("removing {}: {}", self.mountpoint.display(), err); } } } #[derive(Debug)] pub struct SavedPartitions { sector_size: u64, partitions: Vec<(u32, GPTPartitionEntry)>, } impl SavedPartitions { /// Create a SavedPartitions for a block device with a sector size. pub fn new_from_disk(disk: &mut File, filters: &[PartitionFilter]) -> Result<Self> { if !disk .metadata() .context("getting disk metadata")? .file_type() .is_block_device() { bail!("specified file is not a block device"); } Self::new(disk, get_sector_size(disk)?.get() as u64, filters) } /// Create a SavedPartitions for a file with a specified imputed sector /// size. Useful for unit tests, and fails on a real disk. #[cfg(test)] pub fn new_from_file( disk: &mut File, sector_size: u64, filters: &[PartitionFilter], ) -> Result<Self> { if disk .metadata() .context("getting disk metadata")? .file_type() .is_block_device() { bail!("called new_from_file() on a block device"); } match sector_size { 512 | 4096 => (), _ => bail!("specified unreasonable sector size {}", sector_size), } Self::new(disk, sector_size, filters) } fn new(disk: &mut File, sector_size: u64, filters: &[PartitionFilter]) -> Result<Self> { // if there are no filters, ignore existing GPT, since we're going to // overwrite it if filters.is_empty() { return Ok(Self { sector_size, partitions: Vec::new(), }); } // read GPT let gpt = match GPT::find_from(disk) { Ok(gpt) => gpt, Err(gptman::Error::InvalidSignature) => { // ensure no indexes are listed to be saved from a MBR disk // we don't need to check for labels since MBR does not support them if filters .iter() .any(|f| matches!(f, PartitionFilter::Index(_, _))) && disk_has_mbr(disk).context("checking if disk has an MBR")? { bail!("saving partitions from an MBR disk is not yet supported"); } // no GPT on this disk, so no partitions to save return Ok(Self { sector_size, partitions: Vec::new(), }); } Err(e) => return Err(e).context("reading partition table"), }; // cross-check GPT sector size Self::verify_gpt_sector_size(&gpt, sector_size)?; // save partitions accepted by filters let mut partitions = Vec::new(); for (i, p) in gpt.iter() { if Self::matches_filters(i, p, filters) { partitions.push((i, p.clone())); } } let result = Self { sector_size, partitions, }; // Test restoring the saved partitions to a temporary file. If the // resulting partition table contains invalid data (e.g. duplicate // partition GUIDs) we need to know now, before the caller // overwrites the partition table. Otherwise we could fail to // restore, clear the table, and fail to restore _again_ to the // empty table. if !result.partitions.is_empty() { let len = disk.seek(SeekFrom::End(0)).context("getting disk size")?; let mut temp = tempfile::tempfile().context("creating dry run image")?; temp.set_len(len) .with_context(|| format!("setting test image size to {len}"))?; result.overwrite(&mut temp).context( "failed dry run restoring saved partitions; input partition table may be invalid", )?; } Ok(result) } fn verify_disk_sector_size(&self, disk: &File) -> Result<()> { if !disk .metadata() .context("getting disk metadata")? .file_type() .is_block_device() { return Ok(()); } let disk_sector_size = get_sector_size(disk)?.get() as u64; if disk_sector_size != self.sector_size { bail!( "disk sector size {} doesn't match expected {}", disk_sector_size, self.sector_size ); } Ok(()) } fn verify_gpt_sector_size(gpt: &GPT, sector_size: u64) -> Result<()> { if gpt.sector_size != sector_size { bail!( "GPT sector size {} doesn't match expected {}", gpt.sector_size, sector_size ); } Ok(()) } fn matches_filters(i: u32, p: &GPTPartitionEntry, filters: &[PartitionFilter]) -> bool { use PartitionFilter::*; if !p.is_used() { return false; } filters.iter().any(|f| match f { Index(Some(first), _) if first.get() > i => false, Index(_, Some(last)) if last.get() < i => false, Index(_, _) => true, Label(glob) if glob.matches(p.partition_name.as_str()) => true, _ => false, }) } /// Unconditionally write the saved partitions, and only the saved /// partitions, to the disk. Write a protective MBR and overwrite any /// MBR boot code. Updating the kernel partition table is the caller's /// responsibility. pub fn overwrite(&self, disk: &mut File) -> Result<()> { // create GPT self.verify_disk_sector_size(disk)?; let mut gpt = GPT::new_from(disk, self.sector_size, *Uuid::new_v4().as_bytes()) .context("creating new GPT")?; // add partitions for (i, p) in &self.partitions { gpt[*i] = p.clone(); } // write GPT gpt.write_into(disk).context("writing new GPT")?; // Overwrite only the parts of the MBR that don't contain the // partition table, then write protective MBR. This ensures that // there's no time window without an MBR, during which the kernel // would refuse to read the GPT. disk.rewind().context("seeking to MBR")?; disk.write(&[0u8; 446]) .context("overwriting MBR boot code")?; if self.sector_size > 512 { disk.seek(SeekFrom::Start(512)) .context("seeking to end of MBR")?; disk.write(&vec![0u8; self.sector_size as usize - 512]) .context("overwriting end of MBR")?; } GPT::write_protective_mbr_into(disk, self.sector_size).context("writing protective MBR")?; Ok(()) } /// If any partitions are saved, merge them into the GPT from source, /// which must be valid, and write a protective MBR with the correct /// protective partition size. Updating the kernel partition table is /// the caller's responsibility. pub fn merge(&self, source: &mut (impl Read + Seek), disk: &mut File) -> Result<()> { if self.partitions.is_empty() { return Ok(()); } // read GPT self.verify_disk_sector_size(disk)?; let mut gpt = GPT::find_from(source).context("couldn't read partition table from source")?; Self::verify_gpt_sector_size(&gpt, self.sector_size)?; // The GPT thinks the disk is the size of the install image. // Update sizing. gpt.header .update_from(disk, self.sector_size) .context("updating GPT header")?; // merge saved partitions into partition table // find partition number one larger than the largest used one let mut next = gpt .iter() .fold(1, |prev, (i, e)| if e.is_used() { i + 1 } else { prev }); for (i, p) in &self.partitions { // use the next partition number in the sequence if we have to, // or the partition's original number if it's larger next = next.max(*i); eprintln!( "Saving partition {} (\"{}\") to new partition {}", i, p.partition_name, next ); gpt[next] = p.clone(); next += 1; } // write gpt.write_into(disk).context("writing updated GPT")?; // update protective partition size GPT::write_protective_mbr_into(disk, self.sector_size).context("writing protective MBR")?; Ok(()) } /// Get the sector size in use for this partition table. pub fn get_sector_size(&self) -> u64 { self.sector_size } /// Get the byte offset of the first byte not to be overwritten, if any, /// plus a description of the partition at that offset. pub fn get_offset(&self) -> Result<Option<(u64, String)>> { match self.partitions.iter().min_by_key(|(_, p)| p.starting_lba) { None => Ok(None), Some((i, p)) => Ok(Some(( p.starting_lba .checked_mul(self.sector_size) .context("overflow calculating partition start")?, format!("partition {} (\"{}\")", i, p.partition_name.as_str()), ))), } } pub fn is_saved(&self) -> bool { !self.partitions.is_empty() } } fn read_sysfs_dev_block_value_u64(maj: u64, min: u64, field: &str) -> Result<u64> { let s = read_sysfs_dev_block_value(maj, min, field) .with_context(|| format!("reading partition {maj}:{min} {field} value from sysfs"))?; s.parse().with_context(|| { format!( "parsing partition {}:{} {} value \"{}\" as u64", maj, min, field, &s ) }) } fn read_sysfs_dev_block_value(maj: u64, min: u64, field: &str) -> Result<String> { let path = PathBuf::from(format!("/sys/dev/block/{maj}:{min}/{field}")); Ok(read_to_string(path)?.trim_end().into()) } pub fn lsblk_single(dev: &Path) -> Result<HashMap<String, String>> { let mut devinfos = lsblk(Path::new(dev), false)?; if devinfos.is_empty() { // this should never happen because `lsblk` itself would've failed bail!("no lsblk results for {}", dev.display()); } Ok(devinfos.remove(0)) } pub fn blkid_single(dev: &Path) -> Result<HashMap<String, String>> { let mut devinfos = blkid(Some(vec![dev]))?; if devinfos.len() != 1 { bail!( "expected one blkid result for {}, got: {:?}", dev.display(), devinfos ); } Ok(devinfos.remove(0)) } /// Returns all available filesystems. /// rereadpt mitigates possible issue with outdated UUIDs on different /// paths to the same disk: after 'ignition-ostree-firstboot-uuid' /// '/dev/sdaX' path gets new UUID, but '/dev/sdbX/' path has an old one fn get_all_filesystems(rereadpt: bool) -> Result<Vec<HashMap<String, String>>> { if rereadpt { let mut cmd = Command::new("lsblk"); cmd.arg("--noheadings") .arg("--nodeps") .arg("--list") .arg("--paths") .arg("--output") .arg("NAME"); let output = cmd_output(&mut cmd)?; for dev in output.lines() { if let Ok(mut fd) = std::fs::File::open(dev) { // best-effort reread of disk that may have busy partitions; don't retry let _ = reread_partition_table(&mut fd, false); } } udev_settle()?; } blkid(None) } /// Returns filesystems with given label. /// If multiple filesystems with the label have the same UUID, we only return one of them. /// RAID member devices are excluded since they carry the assembled array's label /// but are not directly usable filesystems. pub fn get_filesystems_with_label(label: &str, rereadpt: bool) -> Result<Vec<String>> { let mut uuids = HashSet::new(); let result = get_all_filesystems(rereadpt)? .iter() .filter(|v| v.get("LABEL").map(|l| l.as_str()) == Some(label)) // Exclude RAID member devices whose superblocks carry the // assembled array's filesystem label (e.g. /dev/sdb1 vs /dev/md127). .filter(|v| v.get("USAGE").map(|u| u.as_str()) != Some("raid")) .filter(|v| match v.get("UUID") { Some(uuid) => { if !uuid.is_empty() { uuids.insert(uuid) } else { true } } None => true, }) .filter_map(|v| v.get("NAME").map(<_>::to_owned)) .collect(); Ok(result) } /// Returns RAID member devices with the given filesystem label. /// Used to produce better diagnostics when an expected filesystem is missing /// but RAID members carrying its label exist (e.g. array failed to assemble). pub fn get_raid_members_with_label(label: &str, rereadpt: bool) -> Result<Vec<String>> { let result = get_all_filesystems(rereadpt)? .iter() .filter(|v| v.get("LABEL").map(|l| l.as_str()) == Some(label)) .filter(|v| v.get("USAGE").map(|u| u.as_str()) == Some("raid")) .filter_map(|v| v.get("NAME").map(<_>::to_owned)) .collect(); Ok(result) } pub fn lsblk(dev: &Path, with_deps: bool) -> Result<Vec<HashMap<String, String>>> { let mut cmd = Command::new("lsblk"); // Older lsblk, e.g. in CentOS 7.6, doesn't support PATH, but --paths option cmd.arg("--pairs") .arg("--paths") .arg("--output") .arg("NAME,LABEL,FSTYPE,TYPE,MOUNTPOINT,UUID") .arg(dev); if !with_deps { cmd.arg("--nodeps"); } let output = cmd_output(&mut cmd)?; let mut result: Vec<HashMap<String, String>> = Vec::new(); for line in output.lines() { // parse key-value pairs result.push(split_lsblk_line(line)); } Ok(result) } /// Parse key-value pairs from blkid. fn split_blkid_line(line: &str) -> HashMap<String, String> { let (name, data) = match line.find(':') { Some(n) => line.split_at(n), None => return HashMap::new(), }; let (name, data) = (name.trim(), data[1..].trim()); if name.is_empty() { return HashMap::new(); } let mut fields = split_lsblk_line(data); fields.insert("NAME".to_string(), name.to_string()); fields } fn blkid(devices: Option<Vec<&Path>>) -> Result<Vec<HashMap<String, String>>> { // Run blkid with a clean cache to avoid collecting old devices which no // longer exist. // https://github.com/coreos/coreos-installer/pull/1288#discussion_r1312008111 // Run once to gather the list of devices, which we need to specify for // the blkid -p below, which we use to probe the devices to not rely on // the blkid cache: // https://github.com/coreos/fedora-coreos-config/pull/2181#issuecomment-1397386896 let found_devices; // need this for &Path refs to outlive the else block let devices: Vec<&Path> = if let Some(paths) = devices { paths } else { found_devices = { let mut cmd = Command::new("blkid"); cmd.arg("--cache-file"); cmd.arg("/dev/null"); cmd.arg("-o"); cmd.arg("device"); cmd_output(&mut cmd)? }; found_devices .lines() .map(|line| Path::new(line.trim())) .collect() }; let output = { let mut cmd = Command::new("blkid"); cmd.arg("-p"); cmd.args(devices); cmd_output(&mut cmd)? }; let mut result: Vec<HashMap<String, String>> = Vec::new(); for line in output.lines() { result.push(split_blkid_line(line)); } Ok(result) } /// This is a bit fuzzy, but... this function will return every block device in the parent /// hierarchy of `device` capable of containing other partitions. So e.g. parent devices of type /// "part" doesn't match, but "disk" and "mpath" does. pub fn find_parent_devices(device: &str) -> Result<Vec<String>> { let mut cmd = Command::new("lsblk"); // Older lsblk, e.g. in CentOS 7.6, doesn't support PATH, but --paths option cmd.arg("--pairs") .arg("--paths") .arg("--inverse") .arg("--output") .arg("NAME,TYPE") .arg(device); let output = cmd_output(&mut cmd)?; let mut parents = Vec::new(); // skip first line, which is the device itself for line in output.lines().skip(1) { let dev = split_lsblk_line(line); let name = dev .get("NAME") .with_context(|| format!("device in hierarchy of {device} missing NAME"))?; let kind = dev .get("TYPE") .with_context(|| format!("device in hierarchy of {device} missing TYPE"))?; if kind == "disk" { parents.push(name.clone()); } else if kind == "mpath" { parents.push(name.clone()); // we don't need to know what disks back the multipath break; } } if parents.is_empty() { bail!("no parent devices found for {}", device); } Ok(parents) } /// Find ESP partitions which sit at the same hierarchy level as `device`. pub fn find_colocated_esps(device: &str) -> Result<Vec<String>> { const ESP_TYPE_GUID: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"; // first, get the parent device let parent_devices = find_parent_devices(device) .with_context(|| format!("while looking for colocated ESPs of '{device}'"))?; // now, look for all ESPs on those devices let mut esps = Vec::new(); for parent_device in parent_devices { let mut cmd = Command::new("lsblk"); // Older lsblk, e.g. in CentOS 7.6, doesn't support PATH, but --paths option cmd.arg("--pairs") .arg("--paths") .arg("--output") .arg("NAME,PARTTYPE") .arg(parent_device); for line in cmd_output(&mut cmd)?.lines() { let dev = split_lsblk_line(line); if dev.get("PARTTYPE").map(|t| t.as_str()) == Some(ESP_TYPE_GUID) { esps.push( dev.get("NAME") .cloned() .context("ESP device with missing NAME")?, ) } } } Ok(esps) } /// This is basically a Rust version of: /// https://github.com/coreos/coreos-assembler/blob/d3c7ec094a02/src/cmd-buildextend-live#L492-L495 pub fn find_efi_vendor_dir(efi_mount: &Mount) -> Result<PathBuf> { let p = efi_mount.mountpoint().join("EFI"); let mut vendor_dir: Vec<PathBuf> = Vec::new(); for ent in p.read_dir()? { let ent = ent.with_context(|| format!("reading directory entry in {}", p.display()))?; if !ent.file_type()?.is_dir() { continue; } let path = ent.path(); if path.join("grub.cfg").is_file() { vendor_dir.push(path); } } if vendor_dir.len() != 1 { bail!( "Expected one vendor dir on {}, got {} ({:?})", efi_mount.device(), vendor_dir.len(), vendor_dir, ); } Ok(vendor_dir.pop().unwrap()) } /// Parse key-value pairs from lsblk --pairs. /// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't. fn split_lsblk_line(line: &str) -> HashMap<String, String> { lazy_static! { static ref REGEX: Regex = Regex::new(r#"([A-Z-_]+)="([^"]+)""#).unwrap(); } let mut fields: HashMap<String, String> = HashMap::new(); for cap in REGEX.captures_iter(line) { fields.insert(cap[1].to_string(), cap[2].to_string()); } fields } pub fn get_blkdev_deps(device: &Path) -> Result<Vec<PathBuf>> { let deps = { let mut p = PathBuf::from("/sys/block"); p.push( device .canonicalize() .with_context(|| format!("canonicalizing {}", device.display()))? .file_name() .with_context(|| format!("path {} has no filename", device.display()))?, ); p.push("slaves"); p }; let mut ret: Vec<PathBuf> = Vec::new(); let dir_iter = match read_dir(&deps) { Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(ret), Err(e) => return Err(e).with_context(|| format!("reading dir {}", &deps.display())), Ok(it) => it, }; for ent in dir_iter { let ent = ent.with_context(|| format!("reading {} entry", &deps.display()))?; ret.push(Path::new("/dev").join(ent.file_name())); } Ok(ret) } pub fn get_blkdev_deps_recursing(device: &Path) -> Result<Vec<PathBuf>> { let mut ret: Vec<PathBuf> = Vec::new(); for dep in get_blkdev_deps(device)? { ret.extend(get_blkdev_deps_recursing(&dep)?); ret.push(dep); } Ok(ret) } fn reread_partition_table(file: &mut File, retry: bool) -> Result<()> { let fd = file.as_raw_fd(); // Reread sometimes fails inexplicably. Retry several times before // giving up. let max_tries = if retry { 20 } else { 1 }; for retries in (0..max_tries).rev() { let result = unsafe { ioctl::blkrrpart(fd) }; match result { Ok(_) => break, Err(err) if retries == 0 && err == Errno::EINVAL => { return Err(err) .context("couldn't reread partition table: device may not support partitions") } Err(err) if retries == 0 && err == Errno::EBUSY => { return Err(err).context("couldn't reread partition table: device is in use") } Err(err) if retries == 0 => return Err(err).context("couldn't reread partition table"), Err(_) => sleep(Duration::from_millis(100)), } } Ok(()) } /// Get the sector size of the block device at a given path. pub fn get_sector_size_for_path(device: &Path) -> Result<NonZeroU32> { let dev = OpenOptions::new() .read(true) .open(device) .with_context(|| format!("opening {device:?}"))?; if !dev .metadata() .with_context(|| format!("getting metadata for {device:?}"))? .file_type() .is_block_device() { bail!("{:?} is not a block device", device); } get_sector_size(&dev) } /// Get the logical sector size of a block device. pub fn get_sector_size(file: &File) -> Result<NonZeroU32> { let fd = file.as_raw_fd(); let mut size: c_int = 0; match unsafe { ioctl::blksszget(fd, &mut size) } { Ok(_) => { let size_u32: u32 = size .try_into() .with_context(|| format!("sector size {size} doesn't fit in u32"))?; NonZeroU32::new(size_u32).context("found sector size of zero") } Err(e) => Err(anyhow!(e).context("getting sector size")), } } /// Get the size of a block device. pub fn get_block_device_size(file: &File) -> Result<NonZeroU64> { let fd = file.as_raw_fd(); let mut size: libc::size_t = 0; match unsafe { ioctl::blkgetsize64(fd, &mut size) } { // just cast using `as`: there is no platform we care about today where size_t > 64bits Ok(_) => NonZeroU64::new(size as u64).context("found block size of zero"), Err(e) => Err(anyhow!(e).context("getting block size")), } } /// Get the size of the GPT metadata at the start of the disk. pub fn get_gpt_size(file: &mut (impl Read + Seek)) -> Result<u64> { let gpt = GPT::find_from(file).context("reading GPT")?; Ok(gpt.header.first_usable_lba * gpt.sector_size) } fn disk_has_mbr(file: &mut (impl Read + Seek)) -> Result<bool> { let mut sig = [0u8; 2]; file.seek(SeekFrom::Start(510)) .context("seeking to MBR signature")?; file.read_exact(&mut sig).context("reading MBR signature")?; Ok(sig == [0x55, 0xaa]) } pub fn have_udev() -> bool { Path::new("/run/udev/control").exists() } pub fn udev_settle() -> Result<()> { // "udevadm settle" silently no-ops if the udev socket is missing, and // then lsblk can't find partition labels. Catch this early. if !have_udev() { bail!("udevd socket missing; are we running in a container without /run/udev mounted?"); } // There's a potential window after rereading the partition table where // udevd hasn't yet received updates from the kernel, settle will return // immediately, and lsblk won't pick up partition labels. Try to sleep // our way out of this. sleep(Duration::from_millis(200)); runcmd!("udevadm", "settle")?; Ok(()) } /// Inspect a buffer from the start of a disk image and return its formatted /// sector size, if any can be determined. pub fn detect_formatted_sector_size(buf: &[u8]) -> Option<NonZeroU32> { let gpt_magic: &[u8; 8] = b"EFI PART"; if buf.len() >= 520 && buf[512..520] == gpt_magic[..] { // GPT at offset 512 NonZeroU32::new(512) } else if buf.len() >= 4104 && buf[4096..4104] == gpt_magic[..] { // GPT at offset 4096 NonZeroU32::new(4096) } else { // Unknown None } } /// Checks if underlying device is IBM DASD disk pub fn is_dasd(device: &str, fd: Option<&mut File>) -> Result<bool> { let target = canonicalize(device).with_context(|| format!("getting absolute path to {device}"))?; if target.to_string_lossy().starts_with("/dev/dasd") { return Ok(true); } let read_magic = |device: &str, disk: &mut File| -> Result<[u8; 4]> { let offset = disk .stream_position() .with_context(|| format!("saving offset {device}"))?; disk.seek(SeekFrom::Start(8194)) .with_context(|| format!("seeking {device}"))?; let mut lbl = [0u8; 4]; disk.read_exact(&mut lbl) .with_context(|| format!("reading label {device}"))?; disk.seek(SeekFrom::Start(offset)) .with_context(|| format!("restoring offset {device}"))?; Ok(lbl) }; if target.to_string_lossy().starts_with("/dev/vd") { let cdl_magic = [0xd3, 0xf1, 0xe5, 0xd6]; let lbl = if let Some(t) = fd { read_magic(device, t)? } else { let mut disk = File::open(device).with_context(|| format!("opening {device}"))?; read_magic(device, &mut disk)? }; return Ok(cdl_magic == lbl); } Ok(false) } // create unsafe ioctl wrappers #[allow(clippy::missing_safety_doc)] mod ioctl { use super::c_int; use nix::{ioctl_none, ioctl_read, ioctl_read_bad, request_code_none}; ioctl_none!(blkrrpart, 0x12, 95); ioctl_read_bad!(blksszget, request_code_none!(0x12, 104), c_int); ioctl_read!(blkgetsize64, 0x12, 114, libc::size_t); } #[cfg(test)] mod tests { use super::*; use maplit::hashmap; use std::io::copy; use tempfile::tempfile; use xz2::read::XzDecoder; #[test] fn lsblk_split() { assert_eq!( split_lsblk_line(r#"NAME="sda" LABEL="" FSTYPE="""#), hashmap! { String::from("NAME") => String::from("sda"), } ); assert_eq!( split_lsblk_line(r#"NAME="sda1" LABEL="" FSTYPE="vfat""#), hashmap! { String::from("NAME") => String::from("sda1"), String::from("FSTYPE") => String::from("vfat") } ); assert_eq!( split_lsblk_line(r#"NAME="sda2" LABEL="boot" FSTYPE="ext4""#), hashmap! { String::from("NAME") => String::from("sda2"), String::from("LABEL") => String::from("boot"), String::from("FSTYPE") => String::from("ext4"), } ); assert_eq!( split_lsblk_line(r#"NAME="sda3" LABEL="foo=\x22bar\x22 baz" FSTYPE="ext4""#), hashmap! { String::from("NAME") => String::from("sda3"), // for now, we don't care about resolving lsblk's hex escapes, // so we just pass them through String::from("LABEL") => String::from(r#"foo=\x22bar\x22 baz"#), String::from("FSTYPE") => String::from("ext4"), } ); } #[test] fn blkid_split() { assert_eq!(split_blkid_line(r#""#), std::collections::HashMap::new()); assert_eq!(split_blkid_line(r#" : "#), std::collections::HashMap::new()); assert_eq!( split_blkid_line(r#": UUID="0000""#), std::collections::HashMap::new() ); assert_eq!( split_blkid_line(r#"/dev/empty:"#), hashmap! { String::from("NAME") => String::from("/dev/empty") } ); assert_eq!( split_blkid_line( r#"/dev/mapper/luks-f022921b-0100-4d48-9812-cfa6c225060a: UUID="2ff16ac3-103f-41d4-8e02-03686e255270" BLOCK_SIZE="4096" TYPE="ext4""# ), hashmap! { String::from("NAME") => String::from("/dev/mapper/luks-f022921b-0100-4d48-9812-cfa6c225060a"), String::from("UUID") => String::from("2ff16ac3-103f-41d4-8e02-03686e255270"), String::from("TYPE") => String::from("ext4"), String::from("BLOCK_SIZE") => String::from("4096") } ); assert_eq!( split_blkid_line( r#"/dev/vdb4: UUID="fdc69fb1-d7f3-4696-846e-b2275504f63c" LABEL="crypt_rootfs" TYPE="crypto_LUKS" PARTLABEL="root" PARTUUID="835753cb-d7f0-465e-84db-07860d3da2f6""# ), hashmap! { String::from("NAME") => String::from("/dev/vdb4"), String::from("LABEL") => String::from("crypt_rootfs"), String::from("UUID") => String::from("fdc69fb1-d7f3-4696-846e-b2275504f63c"), String::from("TYPE") => String::from("crypto_LUKS"), String::from("PARTLABEL") => String::from("root"), String::from("PARTUUID") => String::from("835753cb-d7f0-465e-84db-07860d3da2f6"), } ); } #[test] fn blkid_split_raid_member() { assert_eq!( split_blkid_line( r#"/dev/sdb1: LABEL="boot" UUID="12345678-1234-1234-1234-123456789abc" TYPE="linux_raid_member" USAGE="raid""# ), hashmap! { String::from("NAME") => String::from("/dev/sdb1"), String::from("LABEL") => String::from("boot"), String::from("UUID") => String::from("12345678-1234-1234-1234-123456789abc"), String::from("TYPE") => String::from("linux_raid_member"), String::from("USAGE") => String::from("raid"), } ); } #[test] fn blkid_split_filesystem_usage() { assert_eq!( split_blkid_line( r#"/dev/md127: LABEL="boot" UUID="abcdef01-2345-6789-abcd-ef0123456789" BLOCK_SIZE="4096" TYPE="ext4" USAGE="filesystem""# ), hashmap! { String::from("NAME") => String::from("/dev/md127"), String::from("LABEL") => String::from("boot"), String::from("UUID") => String::from("abcdef01-2345-6789-abcd-ef0123456789"), String::from("BLOCK_SIZE") => String::from("4096"), String::from("TYPE") => String::from("ext4"), String::from("USAGE") => String::from("filesystem"), } ); } #[test] fn disk_sector_size_reader() { struct Test { name: &'static str, data: &'static [u8], compressed: bool, result: Option<NonZeroU32>, } let tests = vec![ Test { name: "zero-length", data: b"", compressed: false, result: None, }, Test { name: "empty-disk", data: include_bytes!("../fixtures/empty.xz"), compressed: true, result: None, }, Test { name: "gpt-512", data: include_bytes!("../fixtures/gpt-512.xz"), compressed: true, result: NonZeroU32::new(512), }, Test { name: "gpt-4096", data: include_bytes!("../fixtures/gpt-4096.xz"), compressed: true, result: NonZeroU32::new(4096), }, ]; for test in tests { let data = if test.compressed { let mut decoder = XzDecoder::new(test.data); let mut data: Vec<u8> = Vec::new(); decoder.read_to_end(&mut data).expect("decompress failed"); data } else { test.data.to_vec() }; assert_eq!( detect_formatted_sector_size(&data), test.result, "{}", test.name ); } } #[test] fn test_saved_partitions() { use PartitionFilter::*; let make_part = |i: u32, name: &str, start: u64, end: u64| { ( i, GPTPartitionEntry { partition_type_guid: make_guid("type"), unique_partition_guid: make_guid(&format!("{name} {start} {end}")), starting_lba: start * 2048, ending_lba: end * 2048 - 1, attribute_bits: 0, partition_name: name.into(), }, ) }; let base_parts = vec![ make_part(1, "one", 1, 1024), make_part(2, "two", 1024, 2048), make_part(3, "three", 2048, 3072), make_part(4, "four", 3072, 4096), make_part(5, "five", 4096, 5120), make_part(7, "seven", 5120, 6144), make_part(8, "eight", 6144, 7168), make_part(9, "nine", 7168, 8192), make_part(10, "", 8192, 8193), make_part(11, "", 8193, 8194), ]; let image_parts = vec![ make_part(1, "boot", 1, 384), make_part(2, "EFI-SYSTEM", 384, 512), make_part(4, "root", 1024, 2200), ]; let merge_base_parts = vec![make_part(2, "unused", 500, 3500)]; let index = |i| Some(NonZeroU32::new(i).unwrap()); let label = |l| Label(glob::Pattern::new(l).unwrap()); let tests = vec![ // Partition range ( vec![Index(index(5), None)], vec![ make_part(5, "five", 4096, 5120), make_part(7, "seven", 5120, 6144), make_part(8, "eight", 6144, 7168), make_part(9, "nine", 7168, 8192), make_part(10, "", 8192, 8193), make_part(11, "", 8193, 8194), ], vec![ make_part(1, "boot", 1, 384), make_part(2, "EFI-SYSTEM", 384, 512), make_part(4, "root", 1024, 2200), make_part(5, "five", 4096, 5120), make_part(7, "seven", 5120, 6144), make_part(8, "eight", 6144, 7168), make_part(9, "nine", 7168, 8192), make_part(10, "", 8192, 8193), make_part(11, "", 8193, 8194), ], ), // Glob ( vec![label("*i*")], vec![ make_part(5, "five", 4096, 5120), make_part(8, "eight", 6144, 7168), make_part(9, "nine", 7168, 8192), ], vec![ make_part(1, "boot", 1, 384), make_part(2, "EFI-SYSTEM", 384, 512), make_part(4, "root", 1024, 2200), make_part(5, "five", 4096, 5120), make_part(8, "eight", 6144, 7168), make_part(9, "nine", 7168, 8192), ], ), // Missing label, single partition, irrelevant range ( vec![ label("six"), Index(index(7), index(7)), Index(index(15), None), ], vec![make_part(7, "seven", 5120, 6144)], vec![ make_part(1, "boot", 1, 384), make_part(2, "EFI-SYSTEM", 384, 512), make_part(4, "root", 1024, 2200), make_part(7, "seven", 5120, 6144), ], ), // Empty label match, multiple results ( vec![label("")], vec![make_part(10, "", 8192, 8193), make_part(11, "", 8193, 8194)], vec![ make_part(1, "boot", 1, 384), make_part(2, "EFI-SYSTEM", 384, 512), make_part(4, "root", 1024, 2200), make_part(10, "", 8192, 8193), make_part(11, "", 8193, 8194), ], ), // Partition renumbering ( vec![Index(index(4), None)], vec![ make_part(4, "four", 3072, 4096), make_part(5, "five", 4096, 5120), make_part(7, "seven", 5120, 6144), make_part(8, "eight", 6144, 7168), make_part(9, "nine", 7168, 8192), make_part(10, "", 8192, 8193), make_part(11, "", 8193, 8194), ], vec![ make_part(1, "boot", 1, 384), make_part(2, "EFI-SYSTEM", 384, 512), make_part(4, "root", 1024, 2200), make_part(5, "four", 3072, 4096), make_part(6, "five", 4096, 5120), make_part(7, "seven", 5120, 6144), make_part(8, "eight", 6144, 7168), make_part(9, "nine", 7168, 8192), make_part(10, "", 8192, 8193), make_part(11, "", 8193, 8194), ], ), // No saved partitions ( vec![Index(index(15), None)], vec![], merge_base_parts.clone(), ), // No filters (vec![], vec![], merge_base_parts.clone()), ]; let mut base = make_disk(512, &base_parts); let mut image = make_disk(512, &image_parts); for (testnum, (filter, expected_blank, expected_image)) in tests.iter().enumerate() { // try overwriting on blank disk let saved = SavedPartitions::new_from_file(&mut base, 512, filter).unwrap(); let mut disk = make_unformatted_disk(); saved.overwrite(&mut disk).unwrap(); assert!(disk_has_mbr(&mut disk).unwrap(), "test {testnum}"); let result = GPT::find_from(&mut disk).unwrap(); assert_eq!( get_gpt_size(&mut disk).unwrap(), 512 * result.header.first_usable_lba ); assert_partitions_eq(expected_blank, &result, &format!("test {testnum} blank")); // try merging with image disk onto merge_base disk let mut disk = make_disk(512, &merge_base_parts); saved.merge(&mut image, &mut disk).unwrap(); assert!( disk_has_mbr(&mut disk).unwrap() != expected_blank.is_empty(), "test {testnum}" ); let result = GPT::find_from(&mut disk).unwrap(); assert_eq!( get_gpt_size(&mut disk).unwrap(), 512 * result.header.first_usable_lba ); assert_partitions_eq(expected_image, &result, &format!("test {testnum} image")); assert_eq!( saved.get_offset().unwrap(), match expected_blank.is_empty() { true => None, false => { let (i, p) = &expected_blank[0]; Some(( p.starting_lba * 512, format!("partition {} (\"{}\")", i, p.partition_name.as_str()), )) } }, "test {testnum}" ); } // ensure overwrite clobbers every byte of MBR for sector_size in [512_usize, 4096_usize].iter() { let mut disk = make_unformatted_disk(); disk.write_all(&vec![0xdau8; *sector_size]).unwrap(); let saved = SavedPartitions::new_from_file(&mut disk, *sector_size as u64, &[]).unwrap(); saved.overwrite(&mut disk).unwrap(); assert!(disk_has_mbr(&mut disk).unwrap(), "{}", *sector_size); disk.rewind().unwrap(); let mut buf = vec![0u8; *sector_size + 1]; disk.read_exact(&mut buf).unwrap(); assert_eq!( buf.iter().position(|v| *v == 0xda), None, "{}", *sector_size ); // verify the first byte of the GPT magic number is intact assert_eq!(buf[*sector_size], 0x45u8, "{}", *sector_size); } // test merging with unformatted initial disk let mut disk = make_unformatted_disk(); let saved = SavedPartitions::new_from_file(&mut disk, 512, &[label("z")]).unwrap(); let mut disk = make_disk(512, &merge_base_parts); saved.merge(&mut image, &mut disk).unwrap(); let result = GPT::find_from(&mut disk).unwrap(); assert_partitions_eq(&merge_base_parts, &result, "unformatted disk"); // test overlapping partitions let saved = SavedPartitions::new_from_file(&mut base, 512, &[Index(index(1), index(1))]).unwrap(); let mut disk = make_disk(512, &merge_base_parts); let err = saved.merge(&mut image, &mut disk).unwrap_err(); assert!( format!("{err:#}").contains(&gptman::Error::InvalidPartitionBoundaries.to_string()), "incorrect error: {err:#}" ); // test trying to save partitions from a MBR disk let mut disk = make_unformatted_disk(); gptman::GPT::write_protective_mbr_into(&mut disk, 512).unwrap(); // label only SavedPartitions::new(&mut disk, 512, &[label("*i*")]).unwrap(); // index only assert_eq!( SavedPartitions::new(&mut disk, 512, &[Index(index(1), index(1))]) .unwrap_err() .to_string(), "saving partitions from an MBR disk is not yet supported" ); // label and index assert_eq!( SavedPartitions::new(&mut disk, 512, &[Index(index(1), index(1)), label("*i*")]) .unwrap_err() .to_string(), "saving partitions from an MBR disk is not yet supported" ); // test sector size mismatch let saved = SavedPartitions::new_from_file(&mut base, 512, &[label("*i*")]).unwrap(); let mut image_4096 = make_disk(4096, &image_parts); assert_eq!( get_gpt_size(&mut image_4096).unwrap(), 4096 * GPT::find_from(&mut image_4096) .unwrap() .header .first_usable_lba ); let mut disk = make_disk(4096, &merge_base_parts); assert_eq!( saved .merge(&mut image_4096, &mut disk) .unwrap_err() .to_string(), "GPT sector size 4096 doesn't match expected 512" ); // test copying invalid partitions let mut disk = make_unformatted_disk(); let data = include_bytes!("../fixtures/gpt-512-duplicate-partition-guids.xz"); copy(&mut XzDecoder::new(&data[..]), &mut disk).unwrap(); assert_eq!( SavedPartitions::new_from_file(&mut disk, 512, &[label("*")]) .unwrap_err() .to_string(), "failed dry run restoring saved partitions; input partition table may be invalid" ); // test corrupt input partition table for sector_size in &[512, 4096] { let sector_size: u64 = *sector_size; // backup corrupt let mut disk = make_damaged_disk(sector_size, &base_parts, false, true); let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &[]).unwrap(); assert!(!saved.is_saved()); let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &[label("one")]).unwrap(); assert!(saved.is_saved()); // primary corrupt let mut disk = make_damaged_disk(sector_size, &base_parts, true, false); let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &[]).unwrap(); assert!(!saved.is_saved()); let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &[label("one")]).unwrap(); assert!(saved.is_saved()); // both corrupt let mut disk = make_damaged_disk(sector_size, &base_parts, true, true); let saved = SavedPartitions::new_from_file(&mut disk, sector_size, &[]).unwrap(); assert!(!saved.is_saved()); let err = SavedPartitions::new_from_file(&mut disk, sector_size, &[label("one")]) .unwrap_err(); assert!( format!("{err:#}").contains("could not read primary header"), "incorrect error: {err:#}" ); } } // TODO: The partitions array assumes 512-byte sectors and we don't // scale the start/end values for 4096. This doesn't matter right now // because the only use of 4096-byte sectors is in an error test. fn make_disk(sector_size: u64, partitions: &Vec<(u32, GPTPartitionEntry)>) -> File { let mut disk = make_unformatted_disk(); // Make the disk just large enough for its partitions, then resize // it back up afterward. This tests that we properly handle copying // saved partitions from the larger base disk into the smaller // install image. let len = if partitions.is_empty() { 1024 * 1024 } else { partitions[partitions.len() - 1].1.ending_lba * sector_size + 1024 * 1024 }; disk.set_len(len).unwrap(); let mut gpt = GPT::new_from(&mut disk, sector_size, make_guid("disk")).unwrap(); for (partnum, entry) in partitions { gpt[*partnum] = entry.clone(); } gpt.write_into(&mut disk).unwrap(); disk.set_len(10 * 1024 * 1024 * 1024).unwrap(); disk } fn make_unformatted_disk() -> File { let disk = tempfile().unwrap(); disk.set_len(10 * 1024 * 1024 * 1024).unwrap(); disk } fn make_damaged_disk( sector_size: u64, partitions: &Vec<(u32, GPTPartitionEntry)>, damage_primary: bool, damage_backup: bool, ) -> File { let mut disk = make_unformatted_disk(); // don't use make_disk() because it intentionally misaligns the // backup GPT let mut gpt = GPT::new_from(&mut disk, sector_size, make_guid("disk")).unwrap(); for (partnum, entry) in partitions { gpt[*partnum] = entry.clone(); gpt[*partnum].starting_lba /= sector_size / 512; gpt[*partnum].ending_lba /= sector_size / 512; } gpt.write_into(&mut disk).unwrap(); if damage_primary { // write garbage to the HeaderCRC32 disk.seek(SeekFrom::Start(gpt.header.primary_lba * sector_size + 16)) .unwrap(); disk.write_all(&[0x15, 0xcd, 0x5b, 0x07]).unwrap(); } if damage_backup { // write garbage to the HeaderCRC32 disk.seek(SeekFrom::Start(gpt.header.backup_lba * sector_size + 16)) .unwrap(); disk.write_all(&[0xb1, 0x68, 0xde, 0x3a]).unwrap(); } disk } fn make_guid(seed: &str) -> [u8; 16] { let mut guid = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; for (i, b) in seed.as_bytes().iter().enumerate() { guid[i % guid.len()] ^= *b; } guid } fn assert_partitions_eq(expected: &[(u32, GPTPartitionEntry)], found: &GPT, message: &str) { assert_eq!( expected .iter() .map(|(i, p)| (*i, p)) .collect::<Vec<(u32, &GPTPartitionEntry)>>(), found .iter() .filter(|(_, p)| p.is_used()) .collect::<Vec<(u32, &GPTPartitionEntry)>>(), "{message}" ); } }