1use std::num::NonZeroUsize;
9use std::path::Path;
10use std::sync::Mutex;
11
12use crate::{DumpMetadata, Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
13
14const KDUMP_SIG: &[u8; 8] = b"KDUMP ";
16const DISKDUMP_SIG: &[u8; 8] = b"DISKDUMP";
18
19const COMPRESS_ZLIB: u32 = 0x01;
21const COMPRESS_LZO: u32 = 0x02;
23const COMPRESS_SNAPPY: u32 = 0x04;
25const COMPRESS_ZSTD: u32 = 0x20;
27
28const PAGE_DESC_SIZE: usize = 24;
30
31const CACHE_CAPACITY: usize = 1024;
33
34#[derive(Debug, Clone)]
36struct PageDesc {
37 offset: i64,
39 size: u32,
41 flags: u32,
43}
44
45pub struct KdumpProvider {
47 data: Vec<u8>,
49 block_size: u32,
51 max_mapnr: u32,
53 bitmap2_offset: usize,
55 bitmap2_len: usize,
56 desc_offset: usize,
58 num_descs: usize,
60 ranges: Vec<PhysicalRange>,
62 cache: Mutex<lru::LruCache<u64, Vec<u8>>>,
64}
65
66fn read_i32(data: &[u8], offset: usize) -> Result<i32> {
68 data.get(offset..offset + 4)
69 .and_then(|s| s.try_into().ok())
70 .map(i32::from_le_bytes)
71 .ok_or_else(|| Error::Corrupt(format!("read_i32 out of bounds at offset {offset}")))
72}
73
74fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
76 data.get(offset..offset + 4)
77 .and_then(|s| s.try_into().ok())
78 .map(u32::from_le_bytes)
79 .ok_or_else(|| Error::Corrupt(format!("read_u32 out of bounds at offset {offset}")))
80}
81
82fn read_i64(data: &[u8], offset: usize) -> Result<i64> {
84 data.get(offset..offset + 8)
85 .and_then(|s| s.try_into().ok())
86 .map(i64::from_le_bytes)
87 .ok_or_else(|| Error::Corrupt(format!("read_i64 out of bounds at offset {offset}")))
88}
89
90fn is_kdump_signature(header: &[u8]) -> bool {
92 if header.len() < 8 {
93 return false;
94 }
95 &header[0..8] == KDUMP_SIG || &header[0..8] == DISKDUMP_SIG
96}
97
98fn parse_page_desc(data: &[u8], offset: usize) -> Result<PageDesc> {
100 Ok(PageDesc {
101 offset: read_i64(data, offset)?,
102 size: read_u32(data, offset + 8)?,
103 flags: read_u32(data, offset + 12)?,
104 })
105}
106
107fn bitmap_test(bitmap: &[u8], bit: usize) -> bool {
109 let byte_idx = bit / 8;
110 let bit_idx = bit % 8;
111 if byte_idx >= bitmap.len() {
112 return false;
113 }
114 (bitmap[byte_idx] >> bit_idx) & 1 != 0
115}
116
117fn bitmap_popcount_before(bitmap: &[u8], bit: usize) -> usize {
119 let full_bytes = bit / 8;
120 let remaining_bits = bit % 8;
121 let mut count = 0usize;
122 for &b in &bitmap[..full_bytes.min(bitmap.len())] {
123 count += b.count_ones() as usize;
124 }
125 if remaining_bits > 0 && full_bytes < bitmap.len() {
126 let mask = (1u8 << remaining_bits) - 1;
128 count += (bitmap[full_bytes] & mask).count_ones() as usize;
129 }
130 count
131}
132
133fn ranges_from_bitmap(bitmap: &[u8], max_pfn: u32, block_size: u32) -> Vec<PhysicalRange> {
135 let mut ranges = Vec::new();
136 let mut run_start: Option<u64> = None;
137 let bs = u64::from(block_size);
138
139 for pfn in 0..max_pfn as usize {
140 if bitmap_test(bitmap, pfn) {
141 if run_start.is_none() {
142 run_start = Some(pfn as u64 * bs);
143 }
144 } else if let Some(start) = run_start.take() {
145 ranges.push(PhysicalRange {
146 start,
147 end: pfn as u64 * bs,
148 });
149 }
150 }
151 if let Some(start) = run_start {
153 ranges.push(PhysicalRange {
154 start,
155 end: u64::from(max_pfn) * bs,
156 });
157 }
158 ranges
159}
160
161fn decompress_page(compressed: &[u8], flags: u32, block_size: u32) -> Result<Vec<u8>> {
163 let bs = block_size as usize;
164 match flags {
165 0 => {
166 if compressed.len() == bs {
168 Ok(compressed.to_vec())
169 } else {
170 Err(Error::Corrupt(format!(
171 "uncompressed page size {} != block_size {bs}",
172 compressed.len()
173 )))
174 }
175 }
176 COMPRESS_ZLIB => {
177 use std::io::Read as _;
178 let mut decoder = flate2::read::ZlibDecoder::new(compressed);
179 let mut out = vec![0u8; bs];
180 decoder
181 .read_exact(&mut out)
182 .map_err(|e| Error::Decompression(format!("zlib: {e}")))?;
183 Ok(out)
184 }
185 COMPRESS_LZO => Err(Error::Decompression("LZO not yet supported".into())),
186 COMPRESS_SNAPPY => {
187 let mut decoder = snap::raw::Decoder::new();
188 decoder
189 .decompress_vec(compressed)
190 .map_err(|e| Error::Decompression(format!("snappy: {e}")))
191 }
192 COMPRESS_ZSTD => {
193 use std::io::Read as _;
194 let cursor = std::io::Cursor::new(compressed);
195 let mut decoder = ruzstd::decoding::StreamingDecoder::new(cursor)
196 .map_err(|e| Error::Decompression(format!("zstd init: {e}")))?;
197 let mut out = vec![0u8; bs];
198 decoder
199 .read_exact(&mut out)
200 .map_err(|e| Error::Decompression(format!("zstd: {e}")))?;
201 Ok(out)
202 }
203 other => Err(Error::Decompression(format!(
204 "unknown compression flags: 0x{other:02X}"
205 ))),
206 }
207}
208
209impl KdumpProvider {
210 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
212 Self::parse(bytes.to_vec())
213 }
214
215 pub fn from_path(path: &Path) -> Result<Self> {
217 let data = std::fs::read(path)?;
218 Self::parse(data)
219 }
220
221 fn parse(data: Vec<u8>) -> Result<Self> {
223 if !is_kdump_signature(&data) {
224 return Err(Error::Corrupt("not a kdump/diskdump file".into()));
225 }
226
227 let fields_off = (0x0C + 390 + 3) & !3; let block_size_raw = read_i32(&data, fields_off)?;
233 let sub_hdr_size_raw = read_i32(&data, fields_off + 4)?;
234 let block_size = u32::try_from(block_size_raw)
235 .map_err(|_| Error::Corrupt(format!("negative block_size: {block_size_raw}")))?;
236 let sub_hdr_size = u32::try_from(sub_hdr_size_raw)
237 .map_err(|_| Error::Corrupt(format!("negative sub_hdr_size: {sub_hdr_size_raw}")))?;
238 let bitmap_blocks = read_u32(&data, fields_off + 8)?;
239 let max_mapnr = read_u32(&data, fields_off + 12)?;
240
241 let bs = block_size as usize;
242 if bs == 0 {
243 return Err(Error::Corrupt("block_size is 0".into()));
244 }
245
246 let bitmap_start_block = 1 + sub_hdr_size as usize;
248 let bm1_offset = bitmap_start_block * bs;
249 let bm_byte_len = bitmap_blocks as usize * bs;
250
251 let bm2_offset = bm1_offset + bm_byte_len;
253
254 if bm2_offset + bm_byte_len > data.len() {
256 return Err(Error::Corrupt("bitmaps extend beyond file".into()));
257 }
258
259 let bitmap2 = &data[bm2_offset..bm2_offset + bm_byte_len];
261 let mut num_descs = 0usize;
262 for pfn in 0..max_mapnr as usize {
263 if bitmap_test(bitmap2, pfn) {
264 num_descs += 1;
265 }
266 }
267
268 let desc_offset = bm2_offset + bm_byte_len;
270 let descs_raw_size = num_descs * PAGE_DESC_SIZE;
271 if desc_offset + descs_raw_size > data.len() {
272 return Err(Error::Corrupt("page descriptors extend beyond file".into()));
273 }
274
275 let ranges = ranges_from_bitmap(bitmap2, max_mapnr, block_size);
277
278 let cache = Mutex::new(lru::LruCache::new(
281 NonZeroUsize::new(CACHE_CAPACITY).unwrap_or(NonZeroUsize::MIN),
282 ));
283
284 Ok(Self {
285 data,
286 block_size,
287 max_mapnr,
288 bitmap2_offset: bm2_offset,
289 bitmap2_len: bm_byte_len,
290 desc_offset,
291 num_descs,
292 ranges,
293 cache,
294 })
295 }
296
297 fn bitmap2(&self) -> &[u8] {
299 &self.data[self.bitmap2_offset..self.bitmap2_offset + self.bitmap2_len]
300 }
301
302 fn load_page(&self, pfn: u64) -> Result<Vec<u8>> {
304 let bitmap2 = self.bitmap2();
305
306 if !bitmap_test(bitmap2, pfn as usize) {
308 return Ok(vec![]);
310 }
311
312 let desc_idx = bitmap_popcount_before(bitmap2, pfn as usize);
314 if desc_idx >= self.num_descs {
315 return Err(Error::Corrupt(format!(
316 "descriptor index {desc_idx} out of range (max {})",
317 self.num_descs
318 )));
319 }
320
321 let desc = parse_page_desc(&self.data, self.desc_offset + desc_idx * PAGE_DESC_SIZE)?;
322 let file_offset = usize::try_from(desc.offset)
323 .map_err(|_| Error::Corrupt(format!("negative page offset: {}", desc.offset)))?;
324 let size = desc.size as usize;
325
326 if file_offset + size > self.data.len() {
327 return Err(Error::Corrupt(format!(
328 "page data at offset {file_offset} + size {size} extends beyond file"
329 )));
330 }
331
332 let compressed = &self.data[file_offset..file_offset + size];
333 decompress_page(compressed, desc.flags, self.block_size)
334 }
335}
336
337impl PhysicalMemoryProvider for KdumpProvider {
338 fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
339 if buf.is_empty() {
340 return Ok(0);
341 }
342
343 let bs = u64::from(self.block_size);
344 let pfn = addr / bs;
345 let page_offset = (addr % bs) as usize;
346
347 {
349 let mut cache = self
350 .cache
351 .lock()
352 .map_err(|_| crate::Error::Corrupt("cache lock poisoned".into()))?;
353 if let Some(page) = cache.get(&pfn) {
354 let avail = page.len().saturating_sub(page_offset);
355 let to_read = buf.len().min(avail);
356 buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]);
357 return Ok(to_read);
358 }
359 }
360
361 if pfn >= u64::from(self.max_mapnr) || !bitmap_test(self.bitmap2(), pfn as usize) {
363 return Ok(0);
364 }
365
366 let page = self.load_page(pfn)?;
368 let avail = page.len().saturating_sub(page_offset);
369 let to_read = buf.len().min(avail);
370 buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]);
371
372 {
374 let mut cache = self
375 .cache
376 .lock()
377 .map_err(|_| crate::Error::Corrupt("cache lock poisoned".into()))?;
378 cache.put(pfn, page);
379 }
380
381 Ok(to_read)
382 }
383
384 fn ranges(&self) -> &[PhysicalRange] {
385 &self.ranges
386 }
387
388 fn format_name(&self) -> &str {
389 "kdump"
390 }
391
392 fn metadata(&self) -> Option<DumpMetadata> {
393 Some(DumpMetadata {
394 dump_type: Some("kdump".into()),
395 ..DumpMetadata::default()
396 })
397 }
398}
399
400pub struct KdumpPlugin;
402
403impl FormatPlugin for KdumpPlugin {
404 fn name(&self) -> &str {
405 "kdump"
406 }
407
408 fn probe(&self, header: &[u8]) -> u8 {
409 if is_kdump_signature(header) {
410 90
411 } else {
412 0
413 }
414 }
415
416 fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
417 Ok(Box::new(KdumpProvider::from_path(path)?))
418 }
419}
420
421inventory::submit!(&KdumpPlugin as &dyn FormatPlugin);
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use crate::test_builders::KdumpBuilder;
427
428 #[test]
429 fn probe_kdump_signature() {
430 let dump = KdumpBuilder::new().add_page(0, &[0xAAu8; 4096]).build();
431 let plugin = KdumpPlugin;
432 assert_eq!(plugin.probe(&dump), 90);
433 }
434
435 #[test]
436 fn probe_diskdump_signature() {
437 let mut dump = KdumpBuilder::new().add_page(0, &[0xAAu8; 4096]).build();
439 dump[0..8].copy_from_slice(b"DISKDUMP");
440 let plugin = KdumpPlugin;
441 assert_eq!(plugin.probe(&dump), 90);
442 }
443
444 #[test]
445 fn probe_non_kdump() {
446 let zeros = vec![0u8; 4096];
447 let plugin = KdumpPlugin;
448 assert_eq!(plugin.probe(&zeros), 0);
449 }
450
451 #[test]
452 fn probe_short_header_returns_zero() {
453 let plugin = KdumpPlugin;
454 assert_eq!(plugin.probe(&[0u8; 4]), 0);
456 assert_eq!(plugin.probe(&[]), 0);
458 }
459
460 #[test]
461 fn single_page_snappy_read() {
462 let mut page = vec![0u8; 4096];
463 page[0] = 0xDE;
464 page[1] = 0xAD;
465 page[2] = 0xBE;
466 page[3] = 0xEF;
467 let dump = KdumpBuilder::new()
468 .compression(0x04)
469 .add_page(1, &page)
470 .build();
471 let provider = KdumpProvider::from_bytes(&dump).unwrap();
472 let mut buf = [0u8; 4];
473 let n = provider.read_phys(4096, &mut buf).unwrap();
474 assert_eq!(n, 4);
475 assert_eq!(buf, [0xDE, 0xAD, 0xBE, 0xEF]);
476 }
477
478 #[test]
479 fn single_page_zlib_read() {
480 let mut page = vec![0u8; 4096];
481 page[100] = 0x42;
482 page[101] = 0x43;
483 let dump = KdumpBuilder::new()
484 .compression(0x01)
485 .add_page(2, &page)
486 .build();
487 let provider = KdumpProvider::from_bytes(&dump).unwrap();
488 let mut buf = [0u8; 2];
489 let n = provider.read_phys(2 * 4096 + 100, &mut buf).unwrap();
490 assert_eq!(n, 2);
491 assert_eq!(buf, [0x42, 0x43]);
492 }
493
494 #[test]
495 fn uncompressed_page_read() {
496 let mut page = vec![0u8; 4096];
497 page[0] = 0xFF;
498 page[4095] = 0x01;
499 let dump = KdumpBuilder::new()
500 .compression(0x00)
501 .add_page(0, &page)
502 .build();
503 let provider = KdumpProvider::from_bytes(&dump).unwrap();
504 let mut buf = [0u8; 1];
505 let n = provider.read_phys(0, &mut buf).unwrap();
506 assert_eq!(n, 1);
507 assert_eq!(buf, [0xFF]);
508 let n = provider.read_phys(4095, &mut buf).unwrap();
509 assert_eq!(n, 1);
510 assert_eq!(buf, [0x01]);
511 }
512
513 #[test]
514 fn multi_page_read() {
515 let mut page_a = vec![0xAAu8; 4096];
516 page_a[0] = 0x11;
517 let mut page_b = vec![0xBBu8; 4096];
518 page_b[0] = 0x22;
519 let dump = KdumpBuilder::new()
521 .add_page(2, &page_a)
522 .add_page(5, &page_b)
523 .build();
524 let provider = KdumpProvider::from_bytes(&dump).unwrap();
525
526 let mut buf = [0u8; 1];
527 let n = provider.read_phys(2 * 4096, &mut buf).unwrap();
528 assert_eq!(n, 1);
529 assert_eq!(buf, [0x11]);
530
531 let n = provider.read_phys(5 * 4096, &mut buf).unwrap();
532 assert_eq!(n, 1);
533 assert_eq!(buf, [0x22]);
534 }
535
536 #[test]
537 fn read_gap_returns_zero() {
538 let page = vec![0xAAu8; 4096];
539 let dump = KdumpBuilder::new().add_page(1, &page).build();
541 let provider = KdumpProvider::from_bytes(&dump).unwrap();
542
543 let mut buf = [0u8; 4];
545 let n = provider.read_phys(0, &mut buf).unwrap();
546 assert_eq!(n, 0);
547 }
548
549 #[test]
550 fn read_empty_buffer() {
551 let page = vec![0xAAu8; 4096];
552 let dump = KdumpBuilder::new().add_page(0, &page).build();
553 let provider = KdumpProvider::from_bytes(&dump).unwrap();
554
555 let mut buf = [0u8; 0];
556 let n = provider.read_phys(0, &mut buf).unwrap();
557 assert_eq!(n, 0);
558 }
559
560 #[test]
561 fn metadata_extraction() {
562 let page = vec![0u8; 4096];
563 let dump = KdumpBuilder::new().add_page(0, &page).build();
564 let provider = KdumpProvider::from_bytes(&dump).unwrap();
565
566 let meta = provider.metadata().expect("should return metadata");
567 assert_eq!(meta.dump_type.as_deref(), Some("kdump"));
568 }
569
570 #[test]
571 fn lru_cache_hit() {
572 let mut page = vec![0u8; 4096];
573 page[0] = 0xCA;
574 page[100] = 0xFE;
575 let dump = KdumpBuilder::new().add_page(0, &page).build();
576 let provider = KdumpProvider::from_bytes(&dump).unwrap();
577
578 let mut buf = [0u8; 1];
580 let n = provider.read_phys(0, &mut buf).unwrap();
581 assert_eq!(n, 1);
582 assert_eq!(buf, [0xCA]);
583
584 let n = provider.read_phys(100, &mut buf).unwrap();
586 assert_eq!(n, 1);
587 assert_eq!(buf, [0xFE]);
588 }
589
590 #[test]
591 fn lzo_returns_error() {
592 let page = vec![0xAAu8; 4096];
595 let mut dump = KdumpBuilder::new()
596 .compression(0x04)
597 .add_page(0, &page)
598 .build();
599
600 let desc_start = 4 * 4096;
605 let flags_off = desc_start + 12;
607 dump[flags_off..flags_off + 4].copy_from_slice(&0x02u32.to_le_bytes());
608
609 let provider = KdumpProvider::from_bytes(&dump).unwrap();
610 let mut buf = [0u8; 4];
611 let result = provider.read_phys(0, &mut buf);
612 assert!(result.is_err());
613 let err = result.unwrap_err();
614 assert!(
615 err.to_string().contains("LZO"),
616 "error should mention LZO: {err}"
617 );
618 }
619
620 #[test]
621 fn plugin_name() {
622 let plugin = KdumpPlugin;
623 assert_eq!(plugin.name(), "kdump");
624 }
625
626 #[test]
627 fn from_path_roundtrip() {
628 let mut page = vec![0u8; 4096];
629 page[0] = 0x99;
630 let dump = KdumpBuilder::new().add_page(0, &page).build();
631
632 let path = std::env::temp_dir().join("memf_test_kdump.bin");
633 std::fs::write(&path, &dump).unwrap();
634
635 let provider = KdumpProvider::from_path(&path).unwrap();
636 let mut buf = [0u8; 1];
637 let n = provider.read_phys(0, &mut buf).unwrap();
638 assert_eq!(n, 1);
639 assert_eq!(buf, [0x99]);
640
641 std::fs::remove_file(&path).ok();
642 }
643
644 #[test]
645 fn builder_produces_kdump_signature() {
646 let dump = KdumpBuilder::new().add_page(0, &[0u8; 4096]).build();
647 assert_eq!(&dump[0..8], b"KDUMP ");
648 }
649
650 #[test]
651 fn from_bytes_tiny_input_returns_error_not_panic() {
652 let result = KdumpProvider::from_bytes(&[0u8; 4]);
653 assert!(result.is_err(), "4 bytes is too short for kdump header");
654 }
655}