use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::Mutex;
use crate::{DumpMetadata, Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
const KDUMP_SIG: &[u8; 8] = b"KDUMP ";
const DISKDUMP_SIG: &[u8; 8] = b"DISKDUMP";
const COMPRESS_ZLIB: u32 = 0x01;
const COMPRESS_LZO: u32 = 0x02;
const COMPRESS_SNAPPY: u32 = 0x04;
const COMPRESS_ZSTD: u32 = 0x20;
const PAGE_DESC_SIZE: usize = 24;
const CACHE_CAPACITY: usize = 1024;
#[derive(Debug, Clone)]
struct PageDesc {
offset: i64,
size: u32,
flags: u32,
}
pub struct KdumpProvider {
data: Vec<u8>,
block_size: u32,
max_mapnr: u32,
bitmap2_offset: usize,
bitmap2_len: usize,
desc_offset: usize,
num_descs: usize,
ranges: Vec<PhysicalRange>,
cache: Mutex<lru::LruCache<u64, Vec<u8>>>,
}
fn read_i32(data: &[u8], offset: usize) -> Result<i32> {
data.get(offset..offset + 4)
.and_then(|s| s.try_into().ok())
.map(i32::from_le_bytes)
.ok_or_else(|| Error::Corrupt(format!("read_i32 out of bounds at offset {offset}")))
}
fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
data.get(offset..offset + 4)
.and_then(|s| s.try_into().ok())
.map(u32::from_le_bytes)
.ok_or_else(|| Error::Corrupt(format!("read_u32 out of bounds at offset {offset}")))
}
fn read_i64(data: &[u8], offset: usize) -> Result<i64> {
data.get(offset..offset + 8)
.and_then(|s| s.try_into().ok())
.map(i64::from_le_bytes)
.ok_or_else(|| Error::Corrupt(format!("read_i64 out of bounds at offset {offset}")))
}
fn is_kdump_signature(header: &[u8]) -> bool {
if header.len() < 8 {
return false;
}
&header[0..8] == KDUMP_SIG || &header[0..8] == DISKDUMP_SIG
}
fn parse_page_desc(data: &[u8], offset: usize) -> Result<PageDesc> {
Ok(PageDesc {
offset: read_i64(data, offset)?,
size: read_u32(data, offset + 8)?,
flags: read_u32(data, offset + 12)?,
})
}
fn bitmap_test(bitmap: &[u8], bit: usize) -> bool {
let byte_idx = bit / 8;
let bit_idx = bit % 8;
if byte_idx >= bitmap.len() {
return false;
}
(bitmap[byte_idx] >> bit_idx) & 1 != 0
}
fn bitmap_popcount_before(bitmap: &[u8], bit: usize) -> usize {
let full_bytes = bit / 8;
let remaining_bits = bit % 8;
let mut count = 0usize;
for &b in &bitmap[..full_bytes.min(bitmap.len())] {
count += b.count_ones() as usize;
}
if remaining_bits > 0 && full_bytes < bitmap.len() {
let mask = (1u8 << remaining_bits) - 1;
count += (bitmap[full_bytes] & mask).count_ones() as usize;
}
count
}
fn ranges_from_bitmap(bitmap: &[u8], max_pfn: u32, block_size: u32) -> Vec<PhysicalRange> {
let mut ranges = Vec::new();
let mut run_start: Option<u64> = None;
let bs = u64::from(block_size);
for pfn in 0..max_pfn as usize {
if bitmap_test(bitmap, pfn) {
if run_start.is_none() {
run_start = Some(pfn as u64 * bs);
}
} else if let Some(start) = run_start.take() {
ranges.push(PhysicalRange {
start,
end: pfn as u64 * bs,
});
}
}
if let Some(start) = run_start {
ranges.push(PhysicalRange {
start,
end: u64::from(max_pfn) * bs,
});
}
ranges
}
fn decompress_page(compressed: &[u8], flags: u32, block_size: u32) -> Result<Vec<u8>> {
let bs = block_size as usize;
match flags {
0 => {
if compressed.len() == bs {
Ok(compressed.to_vec())
} else {
Err(Error::Corrupt(format!(
"uncompressed page size {} != block_size {bs}",
compressed.len()
)))
}
}
COMPRESS_ZLIB => {
use std::io::Read as _;
let mut decoder = flate2::read::ZlibDecoder::new(compressed);
let mut out = vec![0u8; bs];
decoder
.read_exact(&mut out)
.map_err(|e| Error::Decompression(format!("zlib: {e}")))?;
Ok(out)
}
COMPRESS_LZO => Err(Error::Decompression("LZO not yet supported".into())),
COMPRESS_SNAPPY => {
let mut decoder = snap::raw::Decoder::new();
decoder
.decompress_vec(compressed)
.map_err(|e| Error::Decompression(format!("snappy: {e}")))
}
COMPRESS_ZSTD => {
use std::io::Read as _;
let cursor = std::io::Cursor::new(compressed);
let mut decoder = ruzstd::decoding::StreamingDecoder::new(cursor)
.map_err(|e| Error::Decompression(format!("zstd init: {e}")))?;
let mut out = vec![0u8; bs];
decoder
.read_exact(&mut out)
.map_err(|e| Error::Decompression(format!("zstd: {e}")))?;
Ok(out)
}
other => Err(Error::Decompression(format!(
"unknown compression flags: 0x{other:02X}"
))),
}
}
impl KdumpProvider {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
Self::parse(bytes.to_vec())
}
pub fn from_path(path: &Path) -> Result<Self> {
let data = std::fs::read(path)?;
Self::parse(data)
}
fn parse(data: Vec<u8>) -> Result<Self> {
if !is_kdump_signature(&data) {
return Err(Error::Corrupt("not a kdump/diskdump file".into()));
}
let fields_off = (0x0C + 390 + 3) & !3;
let block_size_raw = read_i32(&data, fields_off)?;
let sub_hdr_size_raw = read_i32(&data, fields_off + 4)?;
let block_size = u32::try_from(block_size_raw)
.map_err(|_| Error::Corrupt(format!("negative block_size: {block_size_raw}")))?;
let sub_hdr_size = u32::try_from(sub_hdr_size_raw)
.map_err(|_| Error::Corrupt(format!("negative sub_hdr_size: {sub_hdr_size_raw}")))?;
let bitmap_blocks = read_u32(&data, fields_off + 8)?;
let max_mapnr = read_u32(&data, fields_off + 12)?;
let bs = block_size as usize;
if bs == 0 {
return Err(Error::Corrupt("block_size is 0".into()));
}
let bitmap_start_block = 1 + sub_hdr_size as usize;
let bm1_offset = bitmap_start_block * bs;
let bm_byte_len = bitmap_blocks as usize * bs;
let bm2_offset = bm1_offset + bm_byte_len;
if bm2_offset + bm_byte_len > data.len() {
return Err(Error::Corrupt("bitmaps extend beyond file".into()));
}
let bitmap2 = &data[bm2_offset..bm2_offset + bm_byte_len];
let mut num_descs = 0usize;
for pfn in 0..max_mapnr as usize {
if bitmap_test(bitmap2, pfn) {
num_descs += 1;
}
}
let desc_offset = bm2_offset + bm_byte_len;
let descs_raw_size = num_descs * PAGE_DESC_SIZE;
if desc_offset + descs_raw_size > data.len() {
return Err(Error::Corrupt("page descriptors extend beyond file".into()));
}
let ranges = ranges_from_bitmap(bitmap2, max_mapnr, block_size);
let cache = Mutex::new(lru::LruCache::new(
NonZeroUsize::new(CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN),
));
Ok(Self {
data,
block_size,
max_mapnr,
bitmap2_offset: bm2_offset,
bitmap2_len: bm_byte_len,
desc_offset,
num_descs,
ranges,
cache,
})
}
fn bitmap2(&self) -> &[u8] {
&self.data[self.bitmap2_offset..self.bitmap2_offset + self.bitmap2_len]
}
fn load_page(&self, pfn: u64) -> Result<Vec<u8>> {
let bitmap2 = self.bitmap2();
if !bitmap_test(bitmap2, pfn as usize) {
return Ok(vec![]);
}
let desc_idx = bitmap_popcount_before(bitmap2, pfn as usize);
if desc_idx >= self.num_descs {
return Err(Error::Corrupt(format!(
"descriptor index {desc_idx} out of range (max {})",
self.num_descs
)));
}
let desc = parse_page_desc(&self.data, self.desc_offset + desc_idx * PAGE_DESC_SIZE)?;
let file_offset = usize::try_from(desc.offset)
.map_err(|_| Error::Corrupt(format!("negative page offset: {}", desc.offset)))?;
let size = desc.size as usize;
if file_offset + size > self.data.len() {
return Err(Error::Corrupt(format!(
"page data at offset {file_offset} + size {size} extends beyond file"
)));
}
let compressed = &self.data[file_offset..file_offset + size];
decompress_page(compressed, desc.flags, self.block_size)
}
}
impl PhysicalMemoryProvider for KdumpProvider {
fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let bs = u64::from(self.block_size);
let pfn = addr / bs;
let page_offset = (addr % bs) as usize;
{
let mut cache = self
.cache
.lock()
.map_err(|_| crate::Error::Corrupt("cache lock poisoned".into()))?;
if let Some(page) = cache.get(&pfn) {
let avail = page.len().saturating_sub(page_offset);
let to_read = buf.len().min(avail);
buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]);
return Ok(to_read);
}
}
if pfn >= u64::from(self.max_mapnr) || !bitmap_test(self.bitmap2(), pfn as usize) {
return Ok(0);
}
let page = self.load_page(pfn)?;
let avail = page.len().saturating_sub(page_offset);
let to_read = buf.len().min(avail);
buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]);
{
let mut cache = self
.cache
.lock()
.map_err(|_| crate::Error::Corrupt("cache lock poisoned".into()))?;
cache.put(pfn, page);
}
Ok(to_read)
}
fn ranges(&self) -> &[PhysicalRange] {
&self.ranges
}
fn format_name(&self) -> &str {
"kdump"
}
fn metadata(&self) -> Option<DumpMetadata> {
Some(DumpMetadata {
dump_type: Some("kdump".into()),
..DumpMetadata::default()
})
}
}
pub struct KdumpPlugin;
impl FormatPlugin for KdumpPlugin {
fn name(&self) -> &str {
"kdump"
}
fn probe(&self, header: &[u8]) -> u8 {
if is_kdump_signature(header) {
90
} else {
0
}
}
fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
Ok(Box::new(KdumpProvider::from_path(path)?))
}
}
inventory::submit!(&KdumpPlugin as &dyn FormatPlugin);
#[cfg(test)]
mod tests {
use super::*;
use crate::test_builders::KdumpBuilder;
#[test]
fn probe_kdump_signature() {
let dump = KdumpBuilder::new().add_page(0, &[0xAAu8; 4096]).build();
let plugin = KdumpPlugin;
assert_eq!(plugin.probe(&dump), 90);
}
#[test]
fn probe_diskdump_signature() {
let mut dump = KdumpBuilder::new().add_page(0, &[0xAAu8; 4096]).build();
dump[0..8].copy_from_slice(b"DISKDUMP");
let plugin = KdumpPlugin;
assert_eq!(plugin.probe(&dump), 90);
}
#[test]
fn probe_non_kdump() {
let zeros = vec![0u8; 4096];
let plugin = KdumpPlugin;
assert_eq!(plugin.probe(&zeros), 0);
}
#[test]
fn probe_short_header_returns_zero() {
let plugin = KdumpPlugin;
assert_eq!(plugin.probe(&[0u8; 4]), 0);
assert_eq!(plugin.probe(&[]), 0);
}
#[test]
fn single_page_snappy_read() {
let mut page = vec![0u8; 4096];
page[0] = 0xDE;
page[1] = 0xAD;
page[2] = 0xBE;
page[3] = 0xEF;
let dump = KdumpBuilder::new()
.compression(0x04)
.add_page(1, &page)
.build();
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let mut buf = [0u8; 4];
let n = provider.read_phys(4096, &mut buf).unwrap();
assert_eq!(n, 4);
assert_eq!(buf, [0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn single_page_zlib_read() {
let mut page = vec![0u8; 4096];
page[100] = 0x42;
page[101] = 0x43;
let dump = KdumpBuilder::new()
.compression(0x01)
.add_page(2, &page)
.build();
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let mut buf = [0u8; 2];
let n = provider.read_phys(2 * 4096 + 100, &mut buf).unwrap();
assert_eq!(n, 2);
assert_eq!(buf, [0x42, 0x43]);
}
#[test]
fn uncompressed_page_read() {
let mut page = vec![0u8; 4096];
page[0] = 0xFF;
page[4095] = 0x01;
let dump = KdumpBuilder::new()
.compression(0x00)
.add_page(0, &page)
.build();
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let mut buf = [0u8; 1];
let n = provider.read_phys(0, &mut buf).unwrap();
assert_eq!(n, 1);
assert_eq!(buf, [0xFF]);
let n = provider.read_phys(4095, &mut buf).unwrap();
assert_eq!(n, 1);
assert_eq!(buf, [0x01]);
}
#[test]
fn multi_page_read() {
let mut page_a = vec![0xAAu8; 4096];
page_a[0] = 0x11;
let mut page_b = vec![0xBBu8; 4096];
page_b[0] = 0x22;
let dump = KdumpBuilder::new()
.add_page(2, &page_a)
.add_page(5, &page_b)
.build();
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let mut buf = [0u8; 1];
let n = provider.read_phys(2 * 4096, &mut buf).unwrap();
assert_eq!(n, 1);
assert_eq!(buf, [0x11]);
let n = provider.read_phys(5 * 4096, &mut buf).unwrap();
assert_eq!(n, 1);
assert_eq!(buf, [0x22]);
}
#[test]
fn read_gap_returns_zero() {
let page = vec![0xAAu8; 4096];
let dump = KdumpBuilder::new().add_page(1, &page).build();
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let mut buf = [0u8; 4];
let n = provider.read_phys(0, &mut buf).unwrap();
assert_eq!(n, 0);
}
#[test]
fn read_empty_buffer() {
let page = vec![0xAAu8; 4096];
let dump = KdumpBuilder::new().add_page(0, &page).build();
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let mut buf = [0u8; 0];
let n = provider.read_phys(0, &mut buf).unwrap();
assert_eq!(n, 0);
}
#[test]
fn metadata_extraction() {
let page = vec![0u8; 4096];
let dump = KdumpBuilder::new().add_page(0, &page).build();
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let meta = provider.metadata().expect("should return metadata");
assert_eq!(meta.dump_type.as_deref(), Some("kdump"));
}
#[test]
fn lru_cache_hit() {
let mut page = vec![0u8; 4096];
page[0] = 0xCA;
page[100] = 0xFE;
let dump = KdumpBuilder::new().add_page(0, &page).build();
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let mut buf = [0u8; 1];
let n = provider.read_phys(0, &mut buf).unwrap();
assert_eq!(n, 1);
assert_eq!(buf, [0xCA]);
let n = provider.read_phys(100, &mut buf).unwrap();
assert_eq!(n, 1);
assert_eq!(buf, [0xFE]);
}
#[test]
fn lzo_returns_error() {
let page = vec![0xAAu8; 4096];
let mut dump = KdumpBuilder::new()
.compression(0x04)
.add_page(0, &page)
.build();
let desc_start = 4 * 4096;
let flags_off = desc_start + 12;
dump[flags_off..flags_off + 4].copy_from_slice(&0x02u32.to_le_bytes());
let provider = KdumpProvider::from_bytes(&dump).unwrap();
let mut buf = [0u8; 4];
let result = provider.read_phys(0, &mut buf);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("LZO"),
"error should mention LZO: {err}"
);
}
#[test]
fn plugin_name() {
let plugin = KdumpPlugin;
assert_eq!(plugin.name(), "kdump");
}
#[test]
fn from_path_roundtrip() {
let mut page = vec![0u8; 4096];
page[0] = 0x99;
let dump = KdumpBuilder::new().add_page(0, &page).build();
let path = std::env::temp_dir().join("memf_test_kdump.bin");
std::fs::write(&path, &dump).unwrap();
let provider = KdumpProvider::from_path(&path).unwrap();
let mut buf = [0u8; 1];
let n = provider.read_phys(0, &mut buf).unwrap();
assert_eq!(n, 1);
assert_eq!(buf, [0x99]);
std::fs::remove_file(&path).ok();
}
#[test]
fn builder_produces_kdump_signature() {
let dump = KdumpBuilder::new().add_page(0, &[0u8; 4096]).build();
assert_eq!(&dump[0..8], b"KDUMP ");
}
#[test]
fn from_bytes_tiny_input_returns_error_not_panic() {
let result = KdumpProvider::from_bytes(&[0u8; 4]);
assert!(result.is_err(), "4 bytes is too short for kdump header");
}
}