#[cfg(target_family = "unix")]
use crate::disk_usage;
use crate::{
format_error,
image::{Block, Image},
};
use clap::ValueEnum;
use elf::{abi::PT_LOAD, endian::NativeEndian, segment::ProgramHeader};
#[cfg(not(target_family = "unix"))]
use std::env::consts::OS;
use std::{
fs::{metadata, OpenOptions},
num::NonZeroU64,
ops::Range,
path::{Path, PathBuf},
};
#[derive(thiserror::Error)]
pub enum Error {
#[error("unable to parse elf structures: {0}")]
Elf(elf::ParseError),
#[error("locked down /proc/kcore")]
LockedDownKcore,
#[error(
"estimated usage exceeds specified bounds: estimated size:{estimated} bytes. allowed:{allowed} bytes"
)]
DiskUsageEstimateExceeded { estimated: u64, allowed: u64 },
#[error("unable to create memory snapshot")]
UnableToCreateMemorySnapshot(#[from] crate::image::Error),
#[error("unable to create memory snapshot from source: {1}")]
UnableToCreateSnapshotFromSource(#[source] Box<Error>, Source),
#[error("unable to create memory snapshot: {0}")]
UnableToCreateSnapshot(String),
#[error("{0}: {1}")]
Other(&'static str, String),
#[error("disk error")]
Disk(#[source] std::io::Error),
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
format_error(self, f)
}
}
pub(crate) type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, ValueEnum)]
pub enum Source {
#[value(name = "/dev/crash")]
DevCrash,
#[value(name = "/dev/mem")]
DevMem,
#[value(name = "/proc/kcore")]
ProcKcore,
#[value(skip)]
Raw(PathBuf),
}
impl std::fmt::Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DevCrash => write!(f, "/dev/crash"),
Self::DevMem => write!(f, "/dev/mem"),
Self::ProcKcore => write!(f, "/proc/kcore"),
Self::Raw(path) => write!(f, "{}", path.display()),
}
}
}
#[must_use]
fn can_open(src: &Path) -> bool {
OpenOptions::new().read(true).open(src).is_ok()
}
#[must_use]
fn is_kcore_ok() -> bool {
metadata(Path::new("/proc/kcore"))
.map(|x| x.len() > 0x2000)
.unwrap_or(false)
&& can_open(Path::new("/proc/kcore"))
}
macro_rules! try_method {
($func:expr) => {{
match $func {
Ok(x) => return Ok(x),
Err(err) => {
if matches!(
err,
Error::UnableToCreateSnapshotFromSource(ref x, _) if matches!(x.as_ref(), Error::DiskUsageEstimateExceeded{..}),
) {
return Err(err);
}
crate::indent(format!("{:?}", err), 4)
}
}
}};
}
pub struct Snapshot<'a, 'b> {
source: Option<&'b Source>,
destination: &'a Path,
memory_ranges: Vec<Range<u64>>,
version: u32,
max_disk_usage: Option<NonZeroU64>,
max_disk_usage_percentage: Option<f64>,
}
impl<'a, 'b> Snapshot<'a, 'b> {
#[must_use]
pub fn new(destination: &'a Path, memory_ranges: Vec<Range<u64>>) -> Self {
Self {
source: None,
destination,
memory_ranges,
version: 1,
max_disk_usage: None,
max_disk_usage_percentage: None,
}
}
#[must_use]
pub fn max_disk_usage_percentage(self, max_disk_usage_percentage: Option<f64>) -> Self {
Self {
max_disk_usage_percentage,
..self
}
}
#[must_use]
pub fn max_disk_usage(self, max_disk_usage: Option<NonZeroU64>) -> Self {
Self {
max_disk_usage,
..self
}
}
#[must_use]
pub fn source(self, source: Option<&'b Source>) -> Self {
Self { source, ..self }
}
#[must_use]
pub fn version(self, version: u32) -> Self {
Self { version, ..self }
}
fn create_source(&self, src: &Source) -> Result<()> {
match src {
Source::ProcKcore => self.kcore(),
Source::DevCrash => self.phys(Path::new("/dev/crash")),
Source::DevMem => self.phys(Path::new("/dev/mem")),
Source::Raw(s) => self.phys(s),
}
.map_err(|e| Error::UnableToCreateSnapshotFromSource(Box::new(e), src.clone()))
}
pub fn create(&self) -> Result<()> {
if let Some(src) = self.source {
self.create_source(src)?;
} else if self.destination == Path::new("/dev/stdout") {
if is_kcore_ok() {
self.create_source(&Source::ProcKcore)?;
} else if can_open(Path::new("/dev/crash")) {
self.create_source(&Source::DevCrash)?;
} else if can_open(Path::new("/dev/mem")) {
self.create_source(&Source::DevMem)?;
} else {
return Err(Error::UnableToCreateSnapshot(
"no source available".to_string(),
));
}
} else {
let crash_err = try_method!(self.create_source(&Source::DevCrash));
let kcore_err = try_method!(self.create_source(&Source::ProcKcore));
let devmem_err = try_method!(self.create_source(&Source::DevMem));
let reason = vec![String::new(), crash_err, kcore_err, devmem_err].join("\n");
return Err(Error::UnableToCreateSnapshot(crate::indent(reason, 4)));
}
Ok(())
}
fn find_kcore_blocks(ranges: &[Range<u64>], headers: &[Block]) -> Vec<Block> {
let mut result = vec![];
'outer: for range in ranges {
let mut range = range.clone();
'inner: for header in headers {
match (
header.range.contains(&range.start),
header.range.contains(&(range.end - 1)),
) {
(true, true) => {
let block = Block {
offset: header.offset + range.start - header.range.start,
range: range.clone(),
};
result.push(block);
continue 'outer;
}
(true, false) => {
let block = Block {
offset: header.offset + range.start - header.range.start,
range: range.start..header.range.end,
};
result.push(block);
range.start = header.range.end;
}
_ => {
continue 'inner;
}
};
}
}
result
}
#[cfg(target_family = "unix")]
fn check_disk_usage(&self, _: &Image) -> Result<()> {
disk_usage::check(
self.destination,
&self.memory_ranges,
self.max_disk_usage,
self.max_disk_usage_percentage,
)
}
#[cfg(not(target_family = "unix"))]
fn check_disk_usage(&self, _: &Image) -> Result<()> {
if self.max_disk_usage.is_some() || self.max_disk_usage_percentage.is_some() {
return Err(Error::Other(
"unable to check disk usage on this platform",
format!("os:{OS}"),
));
}
Ok(())
}
fn kcore(&self) -> Result<()> {
if !is_kcore_ok() {
return Err(Error::LockedDownKcore);
}
let mut image = Image::new(self.version, Path::new("/proc/kcore"), self.destination)?;
self.check_disk_usage(&image)?;
let file =
elf::ElfStream::<NativeEndian, _>::open_stream(&mut image.src).map_err(Error::Elf)?;
let mut segments: Vec<&ProgramHeader> = file
.segments()
.iter()
.filter(|x| x.p_type == PT_LOAD)
.collect();
segments.sort_by(|a, b| a.p_vaddr.cmp(&b.p_vaddr));
let first_vaddr = segments
.get(0)
.ok_or_else(|| Error::UnableToCreateSnapshot("no initial addresses".to_string()))?
.p_vaddr;
let first_start = self
.memory_ranges
.get(0)
.ok_or_else(|| Error::UnableToCreateSnapshot("no initial memory range".to_string()))?
.start;
let start = first_vaddr - first_start;
let mut physical_ranges = vec![];
for phdr in segments {
let entry_start = phdr.p_vaddr - start;
let entry_end = entry_start + phdr.p_memsz;
physical_ranges.push(Block {
range: entry_start..entry_end,
offset: phdr.p_offset,
});
}
let blocks = Self::find_kcore_blocks(&self.memory_ranges, &physical_ranges);
image.write_blocks(&blocks)?;
Ok(())
}
fn phys(&self, mem: &Path) -> Result<()> {
let is_crash = mem == Path::new("/dev/crash");
let blocks = self
.memory_ranges
.iter()
.map(|x| Block {
offset: x.start,
range: if is_crash {
x.start..((x.end >> 12) << 12)
} else {
x.start..x.end
},
})
.collect::<Vec<_>>();
let mut image = Image::new(self.version, mem, self.destination)?;
self.check_disk_usage(&image)?;
image.write_blocks(&blocks)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn translate_ranges() {
let ranges = [10..20, 30..35, 45..55];
let core_ranges = [
Block {
range: 10..20,
offset: 0,
},
Block {
range: 25..35,
offset: 10,
},
Block {
range: 40..50,
offset: 20,
},
Block {
range: 50..55,
offset: 35,
},
];
let expected = vec![
Block {
offset: 0,
range: 10..20,
},
Block {
offset: 10 + 5,
range: 30..35,
},
Block {
offset: 25,
range: 45..50,
},
Block {
offset: 35,
range: 50..55,
},
];
let result = Snapshot::find_kcore_blocks(&ranges, &core_ranges);
assert_eq!(result, expected);
}
}