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.cache.lock()
350 .map_err(|_| crate::Error::Corrupt("cache lock poisoned".into()))?;
351 if let Some(page) = cache.get(&pfn) {
352 let avail = page.len().saturating_sub(page_offset);
353 let to_read = buf.len().min(avail);
354 buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]);
355 return Ok(to_read);
356 }
357 }
358
359 if pfn >= u64::from(self.max_mapnr) || !bitmap_test(self.bitmap2(), pfn as usize) {
361 return Ok(0);
362 }
363
364 let page = self.load_page(pfn)?;
366 let avail = page.len().saturating_sub(page_offset);
367 let to_read = buf.len().min(avail);
368 buf[..to_read].copy_from_slice(&page[page_offset..page_offset + to_read]);
369
370 {
372 let mut cache = self.cache.lock()
373 .map_err(|_| crate::Error::Corrupt("cache lock poisoned".into()))?;
374 cache.put(pfn, page);
375 }
376
377 Ok(to_read)
378 }
379
380 fn ranges(&self) -> &[PhysicalRange] {
381 &self.ranges
382 }
383
384 fn format_name(&self) -> &str {
385 "kdump"
386 }
387
388 fn metadata(&self) -> Option<DumpMetadata> {
389 Some(DumpMetadata {
390 dump_type: Some("kdump".into()),
391 ..DumpMetadata::default()
392 })
393 }
394}
395
396pub struct KdumpPlugin;
398
399impl FormatPlugin for KdumpPlugin {
400 fn name(&self) -> &str {
401 "kdump"
402 }
403
404 fn probe(&self, header: &[u8]) -> u8 {
405 if is_kdump_signature(header) {
406 90
407 } else {
408 0
409 }
410 }
411
412 fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
413 Ok(Box::new(KdumpProvider::from_path(path)?))
414 }
415}
416
417inventory::submit!(&KdumpPlugin as &dyn FormatPlugin);
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::test_builders::KdumpBuilder;
423
424 #[test]
425 fn probe_kdump_signature() {
426 let dump = KdumpBuilder::new().add_page(0, &[0xAAu8; 4096]).build();
427 let plugin = KdumpPlugin;
428 assert_eq!(plugin.probe(&dump), 90);
429 }
430
431 #[test]
432 fn probe_diskdump_signature() {
433 let mut dump = KdumpBuilder::new().add_page(0, &[0xAAu8; 4096]).build();
435 dump[0..8].copy_from_slice(b"DISKDUMP");
436 let plugin = KdumpPlugin;
437 assert_eq!(plugin.probe(&dump), 90);
438 }
439
440 #[test]
441 fn probe_non_kdump() {
442 let zeros = vec![0u8; 4096];
443 let plugin = KdumpPlugin;
444 assert_eq!(plugin.probe(&zeros), 0);
445 }
446
447 #[test]
448 fn probe_short_header_returns_zero() {
449 let plugin = KdumpPlugin;
450 assert_eq!(plugin.probe(&[0u8; 4]), 0);
452 assert_eq!(plugin.probe(&[]), 0);
454 }
455
456 #[test]
457 fn single_page_snappy_read() {
458 let mut page = vec![0u8; 4096];
459 page[0] = 0xDE;
460 page[1] = 0xAD;
461 page[2] = 0xBE;
462 page[3] = 0xEF;
463 let dump = KdumpBuilder::new()
464 .compression(0x04)
465 .add_page(1, &page)
466 .build();
467 let provider = KdumpProvider::from_bytes(&dump).unwrap();
468 let mut buf = [0u8; 4];
469 let n = provider.read_phys(4096, &mut buf).unwrap();
470 assert_eq!(n, 4);
471 assert_eq!(buf, [0xDE, 0xAD, 0xBE, 0xEF]);
472 }
473
474 #[test]
475 fn single_page_zlib_read() {
476 let mut page = vec![0u8; 4096];
477 page[100] = 0x42;
478 page[101] = 0x43;
479 let dump = KdumpBuilder::new()
480 .compression(0x01)
481 .add_page(2, &page)
482 .build();
483 let provider = KdumpProvider::from_bytes(&dump).unwrap();
484 let mut buf = [0u8; 2];
485 let n = provider.read_phys(2 * 4096 + 100, &mut buf).unwrap();
486 assert_eq!(n, 2);
487 assert_eq!(buf, [0x42, 0x43]);
488 }
489
490 #[test]
491 fn uncompressed_page_read() {
492 let mut page = vec![0u8; 4096];
493 page[0] = 0xFF;
494 page[4095] = 0x01;
495 let dump = KdumpBuilder::new()
496 .compression(0x00)
497 .add_page(0, &page)
498 .build();
499 let provider = KdumpProvider::from_bytes(&dump).unwrap();
500 let mut buf = [0u8; 1];
501 let n = provider.read_phys(0, &mut buf).unwrap();
502 assert_eq!(n, 1);
503 assert_eq!(buf, [0xFF]);
504 let n = provider.read_phys(4095, &mut buf).unwrap();
505 assert_eq!(n, 1);
506 assert_eq!(buf, [0x01]);
507 }
508
509 #[test]
510 fn multi_page_read() {
511 let mut page_a = vec![0xAAu8; 4096];
512 page_a[0] = 0x11;
513 let mut page_b = vec![0xBBu8; 4096];
514 page_b[0] = 0x22;
515 let dump = KdumpBuilder::new()
517 .add_page(2, &page_a)
518 .add_page(5, &page_b)
519 .build();
520 let provider = KdumpProvider::from_bytes(&dump).unwrap();
521
522 let mut buf = [0u8; 1];
523 let n = provider.read_phys(2 * 4096, &mut buf).unwrap();
524 assert_eq!(n, 1);
525 assert_eq!(buf, [0x11]);
526
527 let n = provider.read_phys(5 * 4096, &mut buf).unwrap();
528 assert_eq!(n, 1);
529 assert_eq!(buf, [0x22]);
530 }
531
532 #[test]
533 fn read_gap_returns_zero() {
534 let page = vec![0xAAu8; 4096];
535 let dump = KdumpBuilder::new().add_page(1, &page).build();
537 let provider = KdumpProvider::from_bytes(&dump).unwrap();
538
539 let mut buf = [0u8; 4];
541 let n = provider.read_phys(0, &mut buf).unwrap();
542 assert_eq!(n, 0);
543 }
544
545 #[test]
546 fn read_empty_buffer() {
547 let page = vec![0xAAu8; 4096];
548 let dump = KdumpBuilder::new().add_page(0, &page).build();
549 let provider = KdumpProvider::from_bytes(&dump).unwrap();
550
551 let mut buf = [0u8; 0];
552 let n = provider.read_phys(0, &mut buf).unwrap();
553 assert_eq!(n, 0);
554 }
555
556 #[test]
557 fn metadata_extraction() {
558 let page = vec![0u8; 4096];
559 let dump = KdumpBuilder::new().add_page(0, &page).build();
560 let provider = KdumpProvider::from_bytes(&dump).unwrap();
561
562 let meta = provider.metadata().expect("should return metadata");
563 assert_eq!(meta.dump_type.as_deref(), Some("kdump"));
564 }
565
566 #[test]
567 fn lru_cache_hit() {
568 let mut page = vec![0u8; 4096];
569 page[0] = 0xCA;
570 page[100] = 0xFE;
571 let dump = KdumpBuilder::new().add_page(0, &page).build();
572 let provider = KdumpProvider::from_bytes(&dump).unwrap();
573
574 let mut buf = [0u8; 1];
576 let n = provider.read_phys(0, &mut buf).unwrap();
577 assert_eq!(n, 1);
578 assert_eq!(buf, [0xCA]);
579
580 let n = provider.read_phys(100, &mut buf).unwrap();
582 assert_eq!(n, 1);
583 assert_eq!(buf, [0xFE]);
584 }
585
586 #[test]
587 fn lzo_returns_error() {
588 let page = vec![0xAAu8; 4096];
591 let mut dump = KdumpBuilder::new()
592 .compression(0x04)
593 .add_page(0, &page)
594 .build();
595
596 let desc_start = 4 * 4096;
601 let flags_off = desc_start + 12;
603 dump[flags_off..flags_off + 4].copy_from_slice(&0x02u32.to_le_bytes());
604
605 let provider = KdumpProvider::from_bytes(&dump).unwrap();
606 let mut buf = [0u8; 4];
607 let result = provider.read_phys(0, &mut buf);
608 assert!(result.is_err());
609 let err = result.unwrap_err();
610 assert!(
611 err.to_string().contains("LZO"),
612 "error should mention LZO: {err}"
613 );
614 }
615
616 #[test]
617 fn plugin_name() {
618 let plugin = KdumpPlugin;
619 assert_eq!(plugin.name(), "kdump");
620 }
621
622 #[test]
623 fn from_path_roundtrip() {
624 let mut page = vec![0u8; 4096];
625 page[0] = 0x99;
626 let dump = KdumpBuilder::new().add_page(0, &page).build();
627
628 let path = std::env::temp_dir().join("memf_test_kdump.bin");
629 std::fs::write(&path, &dump).unwrap();
630
631 let provider = KdumpProvider::from_path(&path).unwrap();
632 let mut buf = [0u8; 1];
633 let n = provider.read_phys(0, &mut buf).unwrap();
634 assert_eq!(n, 1);
635 assert_eq!(buf, [0x99]);
636
637 std::fs::remove_file(&path).ok();
638 }
639
640 #[test]
641 fn builder_produces_kdump_signature() {
642 let dump = KdumpBuilder::new().add_page(0, &[0u8; 4096]).build();
643 assert_eq!(&dump[0..8], b"KDUMP ");
644 }
645
646 #[test]
647 fn from_bytes_tiny_input_returns_error_not_panic() {
648 let result = KdumpProvider::from_bytes(&[0u8; 4]);
649 assert!(result.is_err(), "4 bytes is too short for kdump header");
650 }
651}