diff --git a/crates/pdf/src/ccitt.rs b/crates/pdf/src/ccitt.rs index ebd4cad7..fa013161 100644 --- a/crates/pdf/src/ccitt.rs +++ b/crates/pdf/src/ccitt.rs @@ -493,10 +493,16 @@ fn pack(changes: &[usize], columns: usize, black_is_1: bool, out: &mut Vec) } /// Decode CCITT fax data. Damaged data ends the image at the last good row (rows already -/// decoded are kept); `None` only when no row decodes. -pub fn decode(data: &[u8], p: &Params) -> Option> { +/// decoded are kept); `None` when no row decodes or the output would exceed `max`. +pub fn decode(data: &[u8], p: &Params, max: usize) -> Option> { let columns = p.columns.clamp(1, 1 << 16); let max_rows = if p.rows > 0 { p.rows.min(1 << 20) } else { 1 << 20 }; + let row_bytes = columns.checked_add(7)? / 8; + // With `/Rows` the image is padded to that many rows (below), so its size is known up front. + let padded_len = if p.rows > 0 { max_rows.checked_mul(row_bytes)? } else { 0 }; + if row_bytes > max || padded_len > max { + return None; + } let mut r = Reader { data, bit: 0 }; let mut out = vec![]; let mut refl: Vec = vec![]; @@ -530,14 +536,17 @@ pub fn decode(data: &[u8], p: &Params) -> Option> { if two_d { row_2d(&mut r, &refl, columns).ok() } else { row_1d(&mut r, columns) } }; let Some(line) = line else { break }; + // Unknown row counts are bounded as rows arrive, without returning a prefix. + if out.len().checked_add(row_bytes)? > max { + return None; + } pack(&line, columns, p.black_is_1, &mut out); refl = line; rows += 1; } // Rows missing from damaged or short data stay white. if p.rows > 0 && rows > 0 && rows < p.rows { - let row_bytes = columns.div_ceil(8); - out.resize(p.rows.min(1 << 20) * row_bytes, if p.black_is_1 { 0 } else { 0xFF }); + out.resize(padded_len, if p.black_is_1 { 0 } else { 0xFF }); } (rows > 0).then_some(out) } @@ -726,14 +735,14 @@ pub(crate) mod tests { for k in [-1, 0, 1, 3] { let data = encode(&img, k); let p = Params { k, columns: cols, rows, ..Params::default() }; - let out = decode(&data, &p).unwrap_or_else(|| panic!("{cols}×{rows} K {k}: no rows")); + let out = decode(&data, &p, 1 << 20).unwrap_or_else(|| panic!("{cols}×{rows} K {k}: no rows")); assert_eq!(unpack(&out, cols, rows), img, "{cols}×{rows} K {k}"); // Rows unknown: stop at the end of the data / EOFB. - let out = decode(&data, &Params { rows: 0, ..p.clone() }).unwrap(); + let out = decode(&data, &Params { rows: 0, ..p.clone() }, 1 << 20).unwrap(); assert_eq!(unpack(&out, cols, rows), img, "{cols}×{rows} K {k}, no /Rows"); // BlackIs1 inverts. - let inv = decode(&data, &Params { black_is_1: true, ..p }).unwrap(); - assert!(inv.iter().zip(&decode(&data, &Params { k, columns: cols, rows, ..Params::default() }).unwrap()).all(|(a, b)| a ^ b == 0xFF)); + let inv = decode(&data, &Params { black_is_1: true, ..p }, 1 << 20).unwrap(); + assert!(inv.iter().zip(&decode(&data, &Params { k, columns: cols, rows, ..Params::default() }, 1 << 20).unwrap()).all(|(a, b)| a ^ b == 0xFF)); } } } @@ -769,9 +778,9 @@ pub(crate) mod tests { ]; for (i, (k, data)) in cases.iter().enumerate() { let p = Params { k: *k, columns: 61, rows: 23, black_is_1: true, end_of_line: *k >= 0, ..Params::default() }; - let out = decode(&hex(data), &p).unwrap(); + let out = decode(&hex(data), &p, 1 << 20).unwrap(); assert_eq!(out, raw, "case {i} (K {k})"); - let out = decode(&hex(data), &Params { rows: 0, ..p }).unwrap(); + let out = decode(&hex(data), &Params { rows: 0, ..p }, 1 << 20).unwrap(); assert_eq!(out, raw, "case {i} (K {k}), no /Rows"); } } @@ -781,7 +790,7 @@ pub(crate) mod tests { let img = pattern(64, 20, 9); let data = encode(&img, -1); for cut in [0, 1, data.len() / 3, data.len() - 3] { - let out = decode(&data[..cut], &Params { k: -1, columns: 64, rows: 20, ..Params::default() }); + let out = decode(&data[..cut], &Params { k: -1, columns: 64, rows: 20, ..Params::default() }, 1 << 20); if let Some(out) = out { assert_eq!(out.len(), 20 * 8); } @@ -791,8 +800,8 @@ pub(crate) mod tests { *b ^= (i * 37) as u8; } for k in [-1, 0, 2] { - let _ = decode(&garbage, &Params { k, columns: 64, rows: 20, ..Params::default() }); - let _ = decode(&garbage, &Params { k, columns: 1, rows: 0, ..Params::default() }); + let _ = decode(&garbage, &Params { k, columns: 64, rows: 20, ..Params::default() }, 1 << 20); + let _ = decode(&garbage, &Params { k, columns: 1, rows: 0, ..Params::default() }, 1 << 20); } } } diff --git a/crates/pdf/src/object.rs b/crates/pdf/src/object.rs index fce77a75..b9016222 100644 --- a/crates/pdf/src/object.rs +++ b/crates/pdf/src/object.rs @@ -304,7 +304,7 @@ pub fn parse_num(w: &[u8]) -> Option { // ------------------------------------------------------------------ filters -/// The most bytes one FlateDecode filter may inflate to. Deflate expands up to about 1000:1, so +/// The most bytes one stream filter may decode to. Deflate expands up to about 1000:1, so /// unbounded, a few MB of PDF ask for gigabytes and a failed allocation aborts the process. The /// largest image `image::decode` accepts (64 Mi pixels) of four 16-bit components is 512 MiB; /// 1 GiB leaves room for more components and is PdfCraft's ceiling for one stream. 32-bit @@ -324,7 +324,8 @@ pub fn decode_stream(file: &File, d: &Dict, raw: &[u8]) -> Option> { decode_stream_within(file, d, raw, MAX_INFLATED) } -/// [`decode_stream`] with each FlateDecode filter's output bounded by `max` bytes. +/// [`decode_stream`] with every filter's output bounded by `max` bytes. Exceeding the +/// bound gives `None`, never a truncated prefix; predictors share the same bound. pub fn decode_stream_within(file: &File, d: &Dict, raw: &[u8], max: usize) -> Option> { let filters: Vec = match d.get("Filter").map(|f| file.resolve(f)) { None => vec![], @@ -341,19 +342,22 @@ pub fn decode_stream_within(file: &File, d: &Dict, raw: &[u8], max: usize) -> Op for (i, f) in filters.iter().enumerate() { data = match f.as_str() { "FlateDecode" | "Fl" => inflate(&data, max)?, - "ASCIIHexDecode" | "AHx" => ascii_hex(&data), - "ASCII85Decode" | "A85" => ascii85(&data), + "ASCIIHexDecode" | "AHx" => ascii_hex(&data, max)?, + "ASCII85Decode" | "A85" => ascii85_within(&data, max)?, "LZWDecode" | "LZW" => { - lzw(&data, parms.get(i).and_then(|p| p.as_ref()).and_then(|p| p.get("EarlyChange")).and_then(Obj::num).unwrap_or(1.0) != 0.0) + lzw(&data, parms.get(i).and_then(|p| p.as_ref()).and_then(|p| p.get("EarlyChange")).and_then(Obj::num).unwrap_or(1.0) != 0.0, max)? } - "RunLengthDecode" | "RL" => run_length(&data), - "CCITTFaxDecode" | "CCF" => crate::ccitt::decode(&data, &ccitt_params(file, parms.get(i).and_then(|p| p.as_ref())))?, + "RunLengthDecode" | "RL" => run_length(&data, max)?, + "CCITTFaxDecode" | "CCF" => crate::ccitt::decode(&data, &ccitt_params(file, parms.get(i).and_then(|p| p.as_ref())), max)?, _ => return None, }; + if data.len() > max { + return None; + } if let Some(Some(p)) = parms.get(i) && matches!(f.as_str(), "FlateDecode" | "Fl" | "LZWDecode" | "LZW") { - data = predict(&data, p); + data = predict(&data, p, max)?; } } Some(data) @@ -402,15 +406,35 @@ pub fn salvage_len(data_len: usize, max: usize) -> usize { data_len.saturating_mul(8).saturating_add(1024).min(max) } -fn ascii_hex(data: &[u8]) -> Vec { - let mut hex: Vec = data.iter().copied().take_while(|c| *c != b'>').filter(u8::is_ascii_hexdigit).collect(); - if hex.len() % 2 == 1 { - hex.push(b'0'); +fn ascii_hex(data: &[u8], max: usize) -> Option> { + let mut out = vec![]; + let mut high = None; + for c in data.iter().copied().take_while(|c| *c != b'>').filter(u8::is_ascii_hexdigit) { + if let Some(h) = high.take() { + if out.len() >= max { + return None; + } + out.push((h << 4) | hexv(c)); + } else { + high = Some(hexv(c)); + } } - hex.chunks(2).map(|p| (hexv(p[0]) << 4) | hexv(p[1])).collect() + if let Some(h) = high { + if out.len() >= max { + return None; + } + out.push(h << 4); + } + Some(out) } pub fn ascii85(data: &[u8]) -> Vec { + // The PostScript reader uses this unbounded entry point. No Vec can hold more + // than usize::MAX bytes, so its output cannot exceed this bound. + ascii85_within(data, usize::MAX).unwrap_or_default() +} + +fn ascii85_within(data: &[u8], max: usize) -> Option> { let mut out = vec![]; let mut group = [0u32; 5]; let mut n = 0; @@ -423,11 +447,19 @@ pub fn ascii85(data: &[u8]) -> Vec { i += 1; match c { b'~' => break, - b'z' if n == 0 => out.extend_from_slice(&[0; 4]), + b'z' if n == 0 => { + if out.len().checked_add(4)? > max { + return None; + } + out.extend_from_slice(&[0; 4]); + } b'!'..=b'u' => { group[n] = (c - b'!') as u32; n += 1; if n == 5 { + if out.len().checked_add(4)? > max { + return None; + } let v = group.iter().fold(0u32, |a, &d| a.wrapping_mul(85).wrapping_add(d)); out.extend_from_slice(&v.to_be_bytes()); n = 0; @@ -437,16 +469,19 @@ pub fn ascii85(data: &[u8]) -> Vec { } } if n > 1 { + if out.len().checked_add(n - 1)? > max { + return None; + } for g in group.iter_mut().skip(n) { *g = 84; } let v = group.iter().fold(0u32, |a, &d| a.wrapping_mul(85).wrapping_add(d)); out.extend_from_slice(&v.to_be_bytes()[..n - 1]); } - out + Some(out) } -fn lzw(data: &[u8], early: bool) -> Vec { +fn lzw(data: &[u8], early: bool, max: usize) -> Option> { let mut out = vec![]; let mut table: Vec> = (0..=255u16).map(|b| vec![b as u8]).collect(); table.push(vec![]); @@ -466,7 +501,7 @@ fn lzw(data: &[u8], early: bool) -> Vec { prev = None; continue; } - 257 => return out, + 257 => return Some(out), _ => {} } let entry = if code < table.len() { @@ -476,10 +511,16 @@ fn lzw(data: &[u8], early: bool) -> Vec { e.push(p[0]); e } else { - return out; + return Some(out); }; + if out.len().checked_add(entry.len())? > max { + return None; + } out.extend_from_slice(&entry); - if let Some(p) = prev { + // PDF codes have at most 12 bits. Freeze a full table until a clear code. + if let Some(p) = prev + && table.len() < 4096 + { let mut e = p; e.push(entry[0]); table.push(e); @@ -491,43 +532,61 @@ fn lzw(data: &[u8], early: bool) -> Vec { } } } - out + Some(out) } -fn run_length(data: &[u8]) -> Vec { +fn run_length(data: &[u8], max: usize) -> Option> { let mut out = vec![]; let mut i = 0; while i < data.len() { let l = data[i] as usize; i += 1; if l < 128 { - out.extend_from_slice(&data[i.min(data.len())..(i + l + 1).min(data.len())]); - i += l + 1; + let end = i.saturating_add(l + 1).min(data.len()); + let literal = data.get(i..end)?; + if out.len().checked_add(literal.len())? > max { + return None; + } + out.extend_from_slice(literal); + i = end; } else if l > 128 { if let Some(&b) = data.get(i) { + if out.len().checked_add(257 - l)? > max { + return None; + } out.extend(std::iter::repeat_n(b, 257 - l)); } - i += 1; + i = i.saturating_add(1); } else { break; } } - out + Some(out) } /// PNG / TIFF predictors (`/Predictor`, `/Colors`, `/BitsPerComponent`, `/Columns`). -fn predict(data: &[u8], p: &Dict) -> Vec { +fn predict(data: &[u8], p: &Dict, max: usize) -> Option> { + if data.len() > max { + return None; + } let g = |k: &str, d: f64| p.get(k).and_then(Obj::num).unwrap_or(d) as usize; let pred = g("Predictor", 1.0); if pred < 10 { - return data.to_vec(); + return Some(data.to_vec()); } - let bpp = (g("Colors", 1.0) * g("BitsPerComponent", 8.0)).div_ceil(8).max(1); - let row = (g("Columns", 1.0) * g("Colors", 1.0) * g("BitsPerComponent", 8.0)).div_ceil(8); + // A row that is zero bytes wide, overflows, or is not shorter than the data leaves no + // complete row: nothing comes out, as it always did for the first and last, and the row + // buffer (sized from `/Columns`) is not allocated. + let Some(pixel_bits) = g("Colors", 1.0).checked_mul(g("BitsPerComponent", 8.0)) else { return Some(vec![]) }; + let Some(bpp) = pixel_bits.checked_add(7).map(|n| (n / 8).max(1)) else { return Some(vec![]) }; + let Some(row) = g("Columns", 1.0).checked_mul(pixel_bits).and_then(|n| n.checked_add(7)).map(|n| n / 8).filter(|&n| n > 0 && n < data.len()) else { + return Some(vec![]); + }; + let stride = row.checked_add(1)?; let mut out = Vec::with_capacity(data.len()); let mut prev = vec![0u8; row]; - for chunk in data.chunks(row + 1) { - if chunk.len() < row + 1 { + for chunk in data.chunks(stride) { + if chunk.len() < stride { break; } let ft = chunk[0]; @@ -558,7 +617,7 @@ fn predict(data: &[u8], p: &Dict) -> Vec { out.extend_from_slice(&cur); prev = cur; } - out + Some(out) } // ------------------------------------------------------------------ the file diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index d5d2aa18..aa6f3c9e 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -204,6 +204,127 @@ fn nested_bomb_in_the_second_flate_filter_is_refused() { assert_eq!(object::decode_stream_within(&file, &chain, &raw, MAX).map(|v| v.len()), None); } +fn bounded_filter(dict: &str, raw: &[u8], max: usize) -> Option> { + let object::Obj::Dict(d) = object::Lexer::new(dict.as_bytes(), 0).next().unwrap() else { panic!("not a dictionary") }; + object::decode_stream_within(&object::File::parse(b""), &d, raw, max) +} + +#[test] +fn predictor_huge_columns_decode_to_nothing() { + // A row of a petabyte used to be allocated before looking at the data (an allocation + // failure aborts the process, so this test took the whole test binary down). + let raw = miniz_oxide::deflate::compress_to_vec_zlib(&[0, 42], 6); + let dict = "<< /Filter /FlateDecode /DecodeParms << /Predictor 12 /Columns 1000000000000000 >> >>"; + assert_eq!(bounded_filter(dict, &raw, 16), Some(vec![])); +} + +#[test] +fn predictor_overflow_and_empty_rows_decode_to_nothing() { + let raw = miniz_oxide::deflate::compress_to_vec_zlib(&[0, 42], 6); + for parms in [ + format!("/Columns {} /Colors 2 /BitsPerComponent 8", usize::MAX), + format!("/Columns 1 /Colors {} /BitsPerComponent 8", usize::MAX), + "/Columns 0".into(), + // A row is two bytes plus its tag byte: longer than the two bytes of data. + "/Columns 2".into(), + ] { + assert_eq!(bounded_filter(&format!("<< /Filter /FlateDecode /DecodeParms << /Predictor 12 {parms} >> >>"), &raw, 16), Some(vec![])); + } +} + +#[test] +fn run_length_chain_is_bounded() { + // Each stage expands repetitions of 0x81, which is itself an RL repeat code. + let raw = [0x81, 0x81, 128]; + let chain = "<< /Filter [/RunLengthDecode /RL /RunLengthDecode] >>"; + assert_eq!(bounded_filter(chain, &raw, 128).map(|v| v.len()), None); + assert_eq!(bounded_filter("<< /Filter /RL >>", &raw, 128), Some(vec![0x81; 128])); + // An over-limit intermediate stage must fail even if the next stage shrinks it. + assert_eq!(bounded_filter("<< /Filter [/RL /AHx] >>", &raw, 127), None); +} + +fn lzw_codes(codes: impl IntoIterator, early: bool) -> Vec { + let mut w = crate::ccitt::tests::W::default(); + let (mut bits, mut entries, mut prev) = (9, 258, false); + for code in codes { + w.put(code, bits); + match code { + 256 => (bits, entries, prev) = (9, 258, false), + 257 => break, + _ => { + if prev && entries < 4096 { + entries += 1; + } + prev = true; + if entries + usize::from(early) >= (1 << bits) && bits < 12 { + bits += 1; + } + } + } + } + w.bytes +} + +#[test] +fn lzw_long_run_is_bounded_and_full_table_decodes() { + // Grow strings of A until the 12-bit table is full, then send literal A codes. + // Both old and new implementations use well under 100 MB for this fixture. + for early in [false, true] { + let raw = lzw_codes([256, 65].into_iter().chain(258..=4095).chain(std::iter::repeat_n(65, 62_500)).chain([257]), early); + assert!((95_000..105_000).contains(&raw.len())); + let dict = format!("<< /Filter /LZWDecode /DecodeParms << /EarlyChange {} >> >>", usize::from(early)); + assert_eq!(bounded_filter(&dict, &raw, 1 << 20).map(|v| v.len()), None); + let expected = 3839 * 3840 / 2 + 62_500; + assert_eq!(bounded_filter(&dict, &raw, expected), Some(vec![b'A'; expected])); + // Clear restores the initial dictionary and code width after saturation. + let cleared = lzw_codes([256, 65].into_iter().chain(258..=4095).chain([256, 66, 257]), early); + let mut expected = vec![b'A'; 3839 * 3840 / 2]; + expected.push(b'B'); + assert_eq!(bounded_filter(&dict, &cleared, expected.len()), Some(expected)); + } +} + +#[test] +fn ccitt_declared_and_unknown_rows_are_bounded() { + // Eight white Group 4 rows. The old decoder pads declared missing rows to white. + assert_eq!(bounded_filter("<< /Filter /CCF /DecodeParms << /K -1 /Columns 64 /Rows 1024 >> >>", &[0xFF], 16), None); + assert_eq!(bounded_filter("<< /Filter /CCF /DecodeParms << /K -1 /Columns 64 >> >>", &[0xFF], 16), None); + assert_eq!(bounded_filter("<< /Filter /CCF /DecodeParms << /K -1 /Columns 8 /Rows 8 >> >>", &[0xFF], 8), Some(vec![0xFF; 8])); + // Padding below the limit remains unchanged. + assert_eq!(bounded_filter("<< /Filter /CCF /DecodeParms << /K -1 /Columns 8 /Rows 16 >> >>", &[0xFF], 16), Some(vec![0xFF; 16])); + // Without `/Rows` the size is never computed up front: 2^20 rows of 4096 bytes overflow a + // 32-bit `usize`, but eight rows of this width decode on every target. + assert_eq!(bounded_filter("<< /Filter /CCF /DecodeParms << /K -1 /Columns 32768 >> >>", &[0xFF], 32768), Some(vec![0xFF; 32768])); +} + +#[test] +fn ascii85_and_hex_outputs_are_bounded() { + assert_eq!(bounded_filter("<< /Filter /A85 >>", b"zz", 7), None); + assert_eq!(bounded_filter("<< /Filter /A85 >>", b"zz", 8), Some(vec![0; 8])); + assert_eq!(bounded_filter("<< /Filter /A85 >>", b"87cURD]i,\"Ebo80", 12), Some(b"Hello World!".to_vec())); + assert_eq!(bounded_filter("<< /Filter /A85 >>", b"87cURD]i,\"Ebo80", 11), None); + assert_eq!(bounded_filter("<< /Filter /AHx >>", b"4142434>", 3), None); + assert_eq!(bounded_filter("<< /Filter /AHx >>", b"4142434>", 4), Some(b"ABC@".to_vec())); +} + +#[test] +fn predictors_and_normal_lzw_decode_within_limit() { + for predictor in [1, 2, 12] { + let (data, expected) = if predictor == 12 { (vec![0, 10, 20, 2, 1, 2], vec![10, 20, 11, 22]) } else { (vec![10, 20], vec![10, 20]) }; + let raw = miniz_oxide::deflate::compress_to_vec_zlib(&data, 6); + let dict = format!("<< /Filter /FlateDecode /DecodeParms << /Predictor {predictor} /Columns 2 >> >>"); + assert_eq!(bounded_filter(&dict, &raw, data.len()), Some(expected)); + } + for early in [false, true] { + let raw = lzw_codes([256, 65, 66, 258, 257], early); + assert_eq!(bounded_filter("<< /Filter /LZW >>", &raw, 4), Some(b"ABAB".to_vec())); + assert_eq!(bounded_filter("<< /Filter /LZW >>", &raw, 3), None); + } + assert_eq!(bounded_filter("<< /Filter /RL >>", &[2, b'a', b'b', b'c', 254, b'd', 128], 6), Some(b"abcddd".to_vec())); + assert_eq!(bounded_filter("<< /Filter /RL >>", &[2, b'a', b'b', b'c', 128], 2), None); + assert_eq!(bounded_filter("<< /Filter /RL >>", &[128], 0), Some(vec![])); +} + // ------------------------------------------------------------------ M13.6: text, images, // patterns, soft masks, blend modes, pages