Merge #2163: Fix u32 overflows in PDF CID width tables and CMap ranges (#2147) (@MarekSpodBiedry)

This commit is contained in:
aiongg
2026-10-24 01:21:39 +00:00
2 changed files with 55 additions and 7 deletions
+11 -7
View File
@@ -263,8 +263,8 @@ fn parse_cmap(data: &[u8]) -> CMap {
} }
Obj::Array(list) => { Obj::Array(list) => {
for (k, s) in list.iter().enumerate() { for (k, s) in list.iter().enumerate() {
if let Obj::Str(s) = s { if let (Obj::Str(s), Some(c)) = (s, lo.checked_add(k as u32)) {
m.unicode.insert(lo + k as u32, utf16(s)); m.unicode.insert(c, utf16(s));
} }
} }
} }
@@ -401,13 +401,15 @@ impl Font {
match w.get(i + 1).map(|x| file.resolve(x)) { match w.get(i + 1).map(|x| file.resolve(x)) {
Some(Obj::Array(list)) => { Some(Obj::Array(list)) => {
for (k, v) in list.iter().enumerate() { for (k, v) in list.iter().enumerate() {
f.cid_widths.insert(first + k as u32, file.resolve(v).num().unwrap_or(0.0) / 1000.0); if let Some(cid) = first.checked_add(k as u32) {
f.cid_widths.insert(cid, file.resolve(v).num().unwrap_or(0.0) / 1000.0);
}
} }
i += 2; i += 2;
} }
Some(Obj::Num(last)) => { Some(Obj::Num(last)) => {
let wv = w.get(i + 2).and_then(|x| file.resolve(x).num()).unwrap_or(0.0) / 1000.0; let wv = w.get(i + 2).and_then(|x| file.resolve(x).num()).unwrap_or(0.0) / 1000.0;
for c in first..=(*last as u32).min(first + 65535) { for c in first..=(*last as u32).min(first.saturating_add(65535)) {
f.cid_widths.insert(c, wv); f.cid_widths.insert(c, wv);
} }
i += 3; i += 3;
@@ -432,13 +434,15 @@ impl Font {
Some(Obj::Array(list)) => { Some(Obj::Array(list)) => {
let v: Vec<f64> = list.iter().filter_map(|x| file.resolve(x).num()).collect(); let v: Vec<f64> = list.iter().filter_map(|x| file.resolve(x).num()).collect();
for (k, t) in v.as_chunks::<3>().0.iter().enumerate() { for (k, t) in v.as_chunks::<3>().0.iter().enumerate() {
f.cid_vmetrics.insert(first + k as u32, (t[0] / 1000.0, t[1] / 1000.0, t[2] / 1000.0)); if let Some(cid) = first.checked_add(k as u32) {
f.cid_vmetrics.insert(cid, (t[0] / 1000.0, t[1] / 1000.0, t[2] / 1000.0));
}
} }
i += 2; i += 2;
} }
Some(Obj::Num(last)) => { Some(Obj::Num(last)) => {
let (Some(w1), Some(vx), Some(vy)) = (num(i + 2), num(i + 3), num(i + 4)) else { break }; let (Some(w1), Some(vx), Some(vy)) = (num(i + 2), num(i + 3), num(i + 4)) else { break };
for c in first..=(last.max(0.0) as u32).min(first + 65535) { for c in first..=(last.max(0.0) as u32).min(first.saturating_add(65535)) {
f.cid_vmetrics.insert(c, (w1 / 1000.0, vx / 1000.0, vy / 1000.0)); f.cid_vmetrics.insert(c, (w1 / 1000.0, vx / 1000.0, vy / 1000.0));
} }
i += 5; i += 5;
@@ -609,7 +613,7 @@ impl Font {
if self.cid_ranges.is_empty() { if self.cid_ranges.is_empty() {
return code; return code;
} }
self.cid_ranges.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).map(|(lo, _, c)| c + (code - lo)).unwrap_or(0) self.cid_ranges.iter().find(|(lo, hi, _)| code >= *lo && code <= *hi).and_then(|(lo, _, c)| c.checked_add(code - lo)).unwrap_or(0)
} }
/// Vertical metrics of a code in text space (before the font size): the vertical /// Vertical metrics of a code in text space (before the font size): the vertical
+44
View File
@@ -1090,3 +1090,47 @@ fn retained_layers_preserve_artwork_beyond_the_bounds_depth_limit() {
assert_eq!(cropped.pixel_size(), (120, 80), "unknown bounds keep the full page"); assert_eq!(cropped.pixel_size(), (120, 80), "unknown bounds keep the full page");
assert_eq!(offset, [0.0, 0.0]); assert_eq!(offset, [0.0, 0.0]);
} }
#[test]
fn cid_width_entries_at_the_top_of_the_cid_range_do_not_overflow() {
// A first CID of u32::MAX (larger values saturate to it) used to overflow `first + k` and `first + 65535`:
// a panic in debug builds; in release builds the list wrapped round to CID 0 and the range was dropped.
let load = |entry: &str| {
let type0 = "<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-H /DescendantFonts [13 0 R] >>";
let desc = format!("<< /Type /Font /Subtype /CIDFontType2 {entry} >>");
let file = object::File::parse(&page_pdf("", "", vec![(10, type0.into(), None), (13, desc, None)])).unwrap();
let d = file.objects.get(&(10, 0)).and_then(object::Obj::dict).unwrap();
font::Font::load(&file, d, &mut vec![])
};
let max = u32::MAX;
// c [w1 w2 ...]
let f = load("/W [4294967295 [500 600 700]]");
assert_eq!((f.width(max), f.width(0)), (0.5, 1.0));
// cfirst clast w
assert_eq!(load("/W [4294967295 4294967295 400]").width(max), 0.4);
// c [w1y vx vy ...]
let f = load("/W2 [4294967295 [1 2 3 4 5 6]]");
assert_eq!((f.vmetrics(max), f.vmetrics(0)), ((0.001, 0.002, 0.003), (-1.0, 0.5, 0.88)));
// cfirst clast w1y vx vy
assert_eq!(load("/W2 [4294967295 4294967295 1 2 3]").vmetrics(max), (0.001, 0.002, 0.003));
}
#[test]
fn cmap_entries_at_the_top_of_the_code_range_do_not_overflow() {
// A bfrange array starting at code 0xFFFFFFFF and a cidrange mapping onto CID u32::MAX used to overflow
// `lo + k` and `c + (code - lo)`: a panic in debug builds, a wrap round to code / CID 0 in release builds.
let to_unicode = "beginbfrange <FFFFFFFF> <FFFFFFFF> [<0041> <0042>] endbfrange";
let encoding = "begincodespacerange <0000> <FFFF> endcodespacerange begincidrange <0000> <FFFF> 4294967295 endcidrange";
let objs: Objs = vec![
(10, "<< /Type /Font /Subtype /Type0 /Encoding 15 0 R /ToUnicode 14 0 R /DescendantFonts [13 0 R] >>".into(), None),
(13, "<< /Type /Font /Subtype /CIDFontType2 /W [0 [700 800] 4294967295 [500]] >>".into(), None),
(14, "<< >>".into(), Some(to_unicode.as_bytes().to_vec())),
(15, "<< /Type /CMap >>".into(), Some(encoding.as_bytes().to_vec())),
];
let file = object::File::parse(&page_pdf("", "", objs)).unwrap();
let d = file.objects.get(&(10, 0)).and_then(object::Obj::dict).unwrap();
let f = font::Font::load(&file, d, &mut vec![]);
assert_eq!((f.unicode(0xFFFF_FFFF), f.unicode(0)), ("A".to_string(), String::new()));
// Code 0 is CID u32::MAX; code 2 would be past it, so it falls back to CID 0 like an unmapped code.
assert_eq!((f.width(0), f.width(2)), (0.5, 0.7));
}