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
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        // Check bitmap: if PFN not dumped, return 0.
362        if pfn >= u64::from(self.max_mapnr) || !bitmap_test(self.bitmap2(), pfn as usize) {
363            return Ok(0);
364        }
365
366        // Load and decompress the page.
367        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        // Cache the decompressed page.
373        {
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
400/// Format plugin for kdump files.
401pub 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        // Build a kdump and overwrite signature to "DISKDUMP"
438        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        // Less than 8 bytes
455        assert_eq!(plugin.probe(&[0u8; 4]), 0);
456        // Empty
457        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        // PFN 2 and PFN 5: gap between them
520        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        // Only PFN 1 is mapped
540        let dump = KdumpBuilder::new().add_page(1, &page).build();
541        let provider = KdumpProvider::from_bytes(&dump).unwrap();
542
543        // Read PFN 0 (unmapped)
544        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        // First read: offset 0
579        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        // Second read: offset 100 (same page, should hit cache)
585        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        // Build a dump but manually set flags to 0x02 (LZO) in the page_desc.
593        // We can't use the builder for LZO, so build snappy then patch the flags.
594        let page = vec![0xAAu8; 4096];
595        let mut dump = KdumpBuilder::new()
596            .compression(0x04)
597            .add_page(0, &page)
598            .build();
599
600        // Find the page_desc and patch flags from 0x04 to 0x02.
601        // page_desc is at desc_start = (2 + 2*bitmap_blocks) * 4096
602        // For 1 PFN (max_pfn=1), bitmap needs ceil(1/8)=1 byte, ceil(1/4096)=1 block
603        // desc_start = (2 + 2*1) * 4096 = 4 * 4096 = 16384
604        let desc_start = 4 * 4096;
605        // flags field is at offset 12 within page_desc
606        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}