Skip to main content

memf_format/
kdump.rs

1//! Kdump (makedumpfile / diskdump) format provider.
2//!
3//! Parses kdump files with `KDUMP   ` or `DISKDUMP` header signatures.
4//! Uses lazy page decompression with an LRU cache for random-access reads.
5//! Supports zlib (flate2), snappy (snap), zstd (ruzstd), and uncompressed pages.
6//! LZO decompression is deferred with a clear error message.
7
8use std::num::NonZeroUsize;
9use std::path::Path;
10use std::sync::Mutex;
11
12use crate::{DumpMetadata, Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
13
14/// KDUMP signature: "KDUMP   " (8 bytes with 3 trailing spaces).
15const KDUMP_SIG: &[u8; 8] = b"KDUMP   ";
16/// DISKDUMP signature: "DISKDUMP" (8 bytes).
17const DISKDUMP_SIG: &[u8; 8] = b"DISKDUMP";
18
19/// Compression flag: zlib.
20const COMPRESS_ZLIB: u32 = 0x01;
21/// Compression flag: LZO.
22const COMPRESS_LZO: u32 = 0x02;
23/// Compression flag: snappy.
24const COMPRESS_SNAPPY: u32 = 0x04;
25/// Compression flag: zstd.
26const COMPRESS_ZSTD: u32 = 0x20;
27
28/// Size of a single `page_desc` entry in bytes.
29const PAGE_DESC_SIZE: usize = 24;
30
31/// LRU cache capacity (number of decompressed pages).
32const CACHE_CAPACITY: usize = 1024;
33
34/// A parsed page descriptor from the kdump file.
35#[derive(Debug, Clone)]
36struct PageDesc {
37    /// File offset of the compressed page data.
38    offset: i64,
39    /// Size of the compressed data in bytes.
40    size: u32,
41    /// Compression flags.
42    flags: u32,
43}
44
45/// Kdump format provider with lazy decompression and LRU cache.
46pub struct KdumpProvider {
47    /// Raw file data.
48    data: Vec<u8>,
49    /// Block size in bytes (typically 4096).
50    block_size: u32,
51    /// Maximum page frame number.
52    max_mapnr: u32,
53    /// 2nd bitmap (dumped PFNs): byte offset and length in `data`.
54    bitmap2_offset: usize,
55    bitmap2_len: usize,
56    /// File offset where page descriptors start.
57    desc_offset: usize,
58    /// Total number of page descriptors.
59    num_descs: usize,
60    /// Pre-computed physical ranges from the bitmap.
61    ranges: Vec<PhysicalRange>,
62    /// LRU cache: PFN -> decompressed page data.
63    cache: Mutex<lru::LruCache<u64, Vec<u8>>>,
64}
65
66/// Read a little-endian i32 from `data` at `offset`.
67fn 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
74/// Read a little-endian u32 from `data` at `offset`.
75fn 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
82/// Read a little-endian i64 from `data` at `offset`.
83fn 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
90/// Check whether the first 8 bytes match a known kdump/diskdump signature.
91fn 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
98/// Parse a page descriptor from the raw data at the given offset.
99fn 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
107/// Test whether a specific bit is set in a bitmap.
108fn 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
117/// Count the number of set bits in a bitmap before the given bit position.
118fn 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        // Count only the bits below the target bit position in the partial byte.
127        let mask = (1u8 << remaining_bits) - 1;
128        count += (bitmap[full_bytes] & mask).count_ones() as usize;
129    }
130    count
131}
132
133/// Build physical ranges from a bitmap: contiguous runs of set bits.
134fn 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    // Close any trailing run.
152    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
161/// Decompress page data based on the compression flags.
162fn 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            // Uncompressed: size must equal block_size.
167            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    /// Parse a kdump file from an in-memory byte slice.
211    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
212        Self::parse(bytes.to_vec())
213    }
214
215    /// Parse a kdump file from a file path.
216    pub fn from_path(path: &Path) -> Result<Self> {
217        let data = std::fs::read(path)?;
218        Self::parse(data)
219    }
220
221    /// Internal: parse the kdump file from owned data.
222    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        // Header field offsets:
228        // utsname starts at 0x0C, is 390 bytes (6 * 65).
229        // Align to 4: (0x0C + 390 + 3) & !3 = 0x19C
230        let fields_off = (0x0C + 390 + 3) & !3; // 0x19C
231
232        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        // Bitmaps start after disk_dump_header (block 0) + kdump_sub_header (sub_hdr_size blocks).
247        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        // 2nd bitmap follows immediately after the 1st.
252        let bm2_offset = bm1_offset + bm_byte_len;
253
254        // Validate bounds.
255        if bm2_offset + bm_byte_len > data.len() {
256            return Err(Error::Corrupt("bitmaps extend beyond file".into()));
257        }
258
259        // Count total dumped pages from bitmap2 to determine descriptor count.
260        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        // Page descriptors start after both bitmaps.
269        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        // Build physical ranges from the 2nd bitmap.
276        let ranges = ranges_from_bitmap(bitmap2, max_mapnr, block_size);
277
278        // CACHE_CAPACITY is a non-zero compile-time constant; the fallback to
279        // NonZeroUsize::MIN keeps construction infallible if it is ever set to 0.
280        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    /// Get the 2nd bitmap slice.
298    fn bitmap2(&self) -> &[u8] {
299        &self.data[self.bitmap2_offset..self.bitmap2_offset + self.bitmap2_len]
300    }
301
302    /// Read and decompress a page by its PFN.
303    fn load_page(&self, pfn: u64) -> Result<Vec<u8>> {
304        let bitmap2 = self.bitmap2();
305
306        // Check if PFN is in the dumped bitmap.
307        if !bitmap_test(bitmap2, pfn as usize) {
308            // Not dumped — return zeros.
309            return Ok(vec![]);
310        }
311
312        // Count set bits before this PFN to get the descriptor index.
313        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        // Check LRU cache first.
348        {
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        // Check bitmap: if PFN not dumped, return 0.
360        if pfn >= u64::from(self.max_mapnr) || !bitmap_test(self.bitmap2(), pfn as usize) {
361            return Ok(0);
362        }
363
364        // Load and decompress the page.
365        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        // Cache the decompressed page.
371        {
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
396/// Format plugin for kdump files.
397pub 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        // Build a kdump and overwrite signature to "DISKDUMP"
434        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        // Less than 8 bytes
451        assert_eq!(plugin.probe(&[0u8; 4]), 0);
452        // Empty
453        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        // PFN 2 and PFN 5: gap between them
516        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        // Only PFN 1 is mapped
536        let dump = KdumpBuilder::new().add_page(1, &page).build();
537        let provider = KdumpProvider::from_bytes(&dump).unwrap();
538
539        // Read PFN 0 (unmapped)
540        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        // First read: offset 0
575        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        // Second read: offset 100 (same page, should hit cache)
581        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        // Build a dump but manually set flags to 0x02 (LZO) in the page_desc.
589        // We can't use the builder for LZO, so build snappy then patch the flags.
590        let page = vec![0xAAu8; 4096];
591        let mut dump = KdumpBuilder::new()
592            .compression(0x04)
593            .add_page(0, &page)
594            .build();
595
596        // Find the page_desc and patch flags from 0x04 to 0x02.
597        // page_desc is at desc_start = (2 + 2*bitmap_blocks) * 4096
598        // For 1 PFN (max_pfn=1), bitmap needs ceil(1/8)=1 byte, ceil(1/4096)=1 block
599        // desc_start = (2 + 2*1) * 4096 = 4 * 4096 = 16384
600        let desc_start = 4 * 4096;
601        // flags field is at offset 12 within page_desc
602        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}