craft-doc: validate explicitly zero ICC frame sizes

This commit is contained in:
dotCooCoo committed 2026-11-03 11:09:25 -08:00
1 parent 21a7644df4
commit 37ec49baca
2 files changed
+31 -1

No files matched your search

+4 -1
View File
@@ -569,6 +569,9 @@ fn unzstd_bounded(frame: &[u8], size: DecodedSize, what: &str) -> Result<Vec<u8>
DecodedSize::Exact(n) | DecodedSize::AtMost(n) => n,
};
let declared = dec.decoder.content_size();
// After header validation, descriptor bits 7-6 (FCS flag) or 5 (single segment) mean
// the size is present, even when zero. See RFC 8878, section 3.1.1.1.1.1.
let has_content_size = frame.get(4).is_some_and(|descriptor| descriptor & 0xe0 != 0);
if matches!(size, DecodedSize::AtMost(_)) && declared > len as u64 {
return Err(Error::LimitExceeded(format!("{what}: declared payload exceeds {len} bytes")));
}
@@ -599,7 +602,7 @@ fn unzstd_bounded(frame: &[u8], size: DecodedSize, what: &str) -> Result<Vec<u8>
if matches!(size, DecodedSize::Exact(_)) && out.len() < len {
return Err(malformed(format!("{what} decodes to {} bytes, not {len}", out.len())));
}
if matches!(size, DecodedSize::AtMost(_)) && declared != 0 && declared != out.len() as u64 {
if matches!(size, DecodedSize::AtMost(_)) && has_content_size && declared != out.len() as u64 {
return Err(malformed(format!("{what}: declared payload size {declared} differs from {} decoded bytes", out.len())));
}
if !dec.decoder.is_finished() {
+27
View File
@@ -233,6 +233,33 @@ fn declared_nonzero_content_sizes_are_checked_before_and_after_decoding() {
assert_eq!(pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes()), store(&path, &z)]), &limits), Ok(Some(b"p".to_vec())));
}
#[test]
/// A present zero size permits an empty payload, while an omitted size permits streamed data.
fn explicit_zero_content_sizes_match_only_empty_payloads() {
// A two-byte FCS has an offset of 256, so zero uses one, four or eight bytes.
for (descriptor, width, single) in [(0x80, 4, false), (0xc0, 8, false), (0x20, 1, true), (0xa0, 4, true), (0xe0, 8, true)] {
for payload in [b"p".as_slice(), b"".as_slice()] {
let h = hash(payload);
let path = craft_pcraft_format::blob_path(&h).unwrap();
let m = named_manifest(&h);
let mut z = vec![0x28, 0xb5, 0x2f, 0xfd, descriptor];
if !single {
z.push(0); // 1 KiB window
}
z.extend(std::iter::repeat_n(0, width));
z.extend_from_slice(&[if payload.is_empty() { 1 } else { 9 }, 0, 0]);
z.extend_from_slice(payload);
let result = pcraft_icc_profile(&zip(&[store("manifest.json", m.as_bytes()), store(&path, &z)]), &Limits::default());
if payload.is_empty() {
assert_eq!(result, Ok(Some(Vec::new())), "descriptor {descriptor:#x}");
} else {
assert!(matches!(result, Err(Error::Malformed(_))), "descriptor {descriptor:#x}: {result:?}");
}
}
}
assert_eq!(pcraft_icc_profile(&bundle(b"p"), &Limits::default()), Ok(Some(b"p".to_vec())), "unknown content size still streams");
}
#[test]
/// Damaged envelopes and frames fail through the public accessor without escaping a panic.
fn truncations_and_mutations_do_not_panic() {