Skip to content

Record::payload() panics on truncated records: header_size() is never bounded against data.len() #92

Description

@renz011tzar

Record::payload() computes its slice bounds from the record header, but
Region::from_slice() only logs a warning when a record is truncated and then
stores the short data anyway. A truncated Crash Log blob therefore produces a
Record whose payload() panics.

Reproduced against 4692d62717e894e1828e81cfc8f4dd76068d6768.

The code

lib/src/record.rs:

pub fn payload(&self) -> &[u8] {
    let begin = self.header.header_size();
    let end = if self.header.version.cldic {
        // Checksum is present at the end of the record
        self.data.len() - 4        // (1) underflows when data.len() < 4
    } else {
        self.data.len()
    };
    &self.data[begin..end]         // (2) panics when begin > end
}

lib/src/region.rs:

let limit = cursor + record_size;
if limit > bytes.len() {
    log::warn!(
        "Truncated record detected: record is expected to be {}B but is {}B",
        record_size, bytes.len() - cursor
    )                                          // <-- warning only, no rejection
}

region.records.push(Record {
    header,
    data: bytes[cursor..limit.min(bytes.len())].into(),   // <-- stored truncated
    ..Default::default()
});

Nothing relates header.header_size() (8, 24, 28, 32, or 28 + completion_status_size * 4,
i.e. up to 1048) to data.len().

Reproduction

lib/tests/payload_bounds.rs, then cargo test --test payload_bounds:

use intel_crashlog::prelude::*;

#[test]
fn truncated_record_makes_payload_panic() {
    // version dword: bit30 = cldic, bits 8..12 = header_type (0 -> header_size 8),
    // revision = 1 so the dword is neither 0 nor 0xdeadbeef.
    let version: u32 = (1 << 30) | 0x01;
    let record_size: u16 = 0x0100;  // declares a large record; the blob is only 8 bytes
    let extended: u16 = 0;

    let mut blob = Vec::new();
    blob.extend_from_slice(&version.to_le_bytes());
    blob.extend_from_slice(&record_size.to_le_bytes());
    blob.extend_from_slice(&extended.to_le_bytes());

    let region = Region::from_slice(&blob).expect("region should decode");
    let record = region.records.first().expect("one record");
    let _ = record.payload();
}

Output:

header_size=8 data.len()=8 cldic=true
panicked at src/record.rs:40:19:
slice index starts at 8 but ends at 4

A Record::default() also panics (range start index 8 out of range for slice of length 0), which is worth noting because Record derives Default and its
fields are public.

Impact

payload() is reachable for any Crash Log blob that a caller decodes, so a
truncated or malformed input panics the decoder rather than producing an error.
For the CLI that is an abort on a bad file; for the EFI application in this
repository, or for any embedder that treats Crash Log data as untrusted, it is a
denial of service.

This is a panic, not memory unsafety — Rust's slice bounds check fires, so
nothing is read out of bounds.

I have filed this publicly because it is a robustness/correctness defect with no
memory-safety impact and a trivially public reproduction. If Intel would rather
handle it through the process in SECURITY.md, I am happy to resubmit there.

Suggested fix

Make payload() total, and/or reject truncated records at parse time:

pub fn payload(&self) -> &[u8] {
    let begin = self.header.header_size();
    let checksum_len = if self.header.version.cldic { 4 } else { 0 };
    let Some(end) = self.data.len().checked_sub(checksum_len) else {
        return &[];
    };
    if begin > end {
        return &[];
    }
    &self.data[begin..end]
}

Alternatively, have Region::from_slice() skip (or error on) a record whose
available bytes are fewer than header_size() + checksum_len, instead of pushing
a Record that cannot satisfy its own accessor.

Machine-checked note

I modelled the bounds in Verus
(5 verified, 0 errors), including a general lemma that every record with
cldic set and data.len() < header_size() + 4 produces an invalid slice, and
that requiring header_size() + 4 <= data.len() restores the invariant.

Environment

  • intel/crashlog @ 4692d62717e894e1828e81cfc8f4dd76068d6768
  • rustc 1.96.0, macOS arm64

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions