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
Record::payload()computes its slice bounds from the record header, butRegion::from_slice()only logs a warning when a record is truncated and thenstores the short data anyway. A truncated Crash Log blob therefore produces a
Recordwhosepayload()panics.Reproduced against
4692d62717e894e1828e81cfc8f4dd76068d6768.The code
lib/src/record.rs:lib/src/region.rs:Nothing relates
header.header_size()(8, 24, 28, 32, or28 + completion_status_size * 4,i.e. up to 1048) to
data.len().Reproduction
lib/tests/payload_bounds.rs, thencargo test --test payload_bounds:Output:
A
Record::default()also panics (range start index 8 out of range for slice of length 0), which is worth noting becauseRecordderivesDefaultand itsfields are public.
Impact
payload()is reachable for any Crash Log blob that a caller decodes, so atruncated 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:Alternatively, have
Region::from_slice()skip (or error on) a record whoseavailable bytes are fewer than
header_size() + checksum_len, instead of pushinga
Recordthat 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
cldicset anddata.len() < header_size() + 4produces an invalid slice, andthat requiring
header_size() + 4 <= data.len()restores the invariant.Environment
intel/crashlog@4692d62717e894e1828e81cfc8f4dd76068d6768