Merge pull request #1482 from midasmatterhorn/fix/font-offset-overflow

pdf: checked_add for CFF Private DICT and Type 1 RD offsets from the file
This commit is contained in:
BFlat
2026-10-16 18:15:10 +07:00
committed by GitHub
2 changed files with 79 additions and 3 deletions
+65 -2
View File
@@ -394,11 +394,16 @@ impl Cff {
let private = |pd: &[(u16, Vec<f64>)]| -> (Vec<std::ops::Range<usize>>, (f64, f64)) {
let Some(pv) = get(pd, 18).filter(|v| v.len() == 2) else { return (vec![], (0.0, 0.0)) };
let (size, off) = (pv[0] as usize, pv[1] as usize);
let Some(bytes) = d.get(off..off + size) else { return (vec![], (0.0, 0.0)) };
let Some(bytes) = off.checked_add(size).and_then(|end| d.get(off..end)) else { return (vec![], (0.0, 0.0)) };
let pr = dict(bytes);
let nominal = get(&pr, 21).and_then(|v| v.first().copied()).unwrap_or(0.0);
let default = get(&pr, 20).and_then(|v| v.first().copied()).unwrap_or(0.0);
let subrs = get(&pr, 19).and_then(|v| v.first().copied()).and_then(|s| index(d, off + s as usize)).map(|x| x.0).unwrap_or_default();
let subrs = get(&pr, 19)
.and_then(|v| v.first().copied())
.and_then(|s| off.checked_add(s as usize))
.and_then(|p| index(d, p))
.map(|x| x.0)
.unwrap_or_default();
(subrs, (default, nominal))
};
let mut lsubrs = vec![];
@@ -1082,4 +1087,62 @@ mod tests {
let (p, _) = cff.outline(cff.gid_by_name("A").unwrap()).unwrap();
assert!(p.elements().is_empty());
}
/// A CFF whose top DICT points its Private DICT at offsets near `usize::MAX`: `size` and `off`
/// come from the file, and reading the Private DICT must not overflow `off + size`.
#[test]
fn huge_private_dict_offsets_do_not_overflow() {
fn idx(items: &[Vec<u8>]) -> Vec<u8> {
let mut out = (items.len() as u16).to_be_bytes().to_vec();
if items.is_empty() {
return out;
}
out.push(4);
let mut off = 1u32;
out.extend_from_slice(&off.to_be_bytes());
for it in items {
off += it.len() as u32;
out.extend_from_slice(&off.to_be_bytes());
}
for it in items {
out.extend_from_slice(it);
}
out
}
let int = |v: i32| {
let mut o = vec![29];
o.extend_from_slice(&v.to_be_bytes());
o
};
// The BCD real "1E300": a size and an offset that both saturate at `usize::MAX`.
let huge = vec![0x1E, 0x1B, 0x30, 0x0F];
let header = [1u8, 0, 4, 4];
let names = idx(&[b"Test".to_vec()]);
let strings = idx(&[]);
let gsubrs = idx(&[]);
let charset = vec![0u8];
let cs = idx(&[vec![14]]); // one `.notdef` charstring: endchar
let top_len = 5 + 1 + 5 + 1 + huge.len() * 2 + 1;
let top_index_len = 2 + 1 + 8 + top_len;
let base = header.len() + names.len() + top_index_len + strings.len() + gsubrs.len();
let cs_off = base + charset.len();
let mut top = vec![];
top.extend(int(0));
top.push(15); // charset: ISOAdobe
top.extend(int(cs_off as i32));
top.push(17); // CharStrings
top.extend(&huge);
top.extend(&huge);
top.push(18); // Private: [size, offset]
assert_eq!(top.len(), top_len);
let mut out = header.to_vec();
out.extend(names);
out.extend(idx(&[top]));
out.extend(strings);
out.extend(gsubrs);
out.extend(charset);
out.extend(cs);
let cff = Cff::parse(&out).expect("the damaged Private DICT is skipped, the font parses");
assert_eq!(cff.glyph_count(), 1);
}
}
+14 -1
View File
@@ -143,7 +143,7 @@ impl Type1 {
q += 1;
}
q += 1;
let bytes = priv_.get(q..q + n)?;
let bytes = priv_.get(q..q.checked_add(n)?)?;
Some((cs(bytes), q + n))
};
if let Some(p) = find(&priv_, b"/Subrs", 0) {
@@ -532,4 +532,17 @@ mod tests {
assert!((p.area().abs() - 250_000.0).abs() < 1e-6);
assert_eq!(p.bounding_box(), kurbo::Rect::new(50.0, 0.0, 550.0, 500.0));
}
/// A `/Subrs` entry whose `RD` length is `usize::MAX`: the bytes are not in the file, and
/// `q + n` must not overflow.
#[test]
fn huge_rd_lengths_do_not_overflow() {
let mut out = b"%!PS-AdobeFont-1.0: Test 001\n/FontMatrix [0.001 0 0 0.001 0 0] readonly def\ncurrentfile eexec\n".to_vec();
let mut priv_ = b"dup /Private 8 dict dup begin /lenIV -1 def\n/Subrs 1 array\ndup 0 18446744073709551615 RD\n".to_vec();
priv_.extend_from_slice(b"end\n/CharStrings 1 dict dup begin\n/.notdef 4 RD \x8b\x8b\r\x0e ND\nend\nend\nmark currentfile closefile\n");
out.extend(encrypt(&priv_, EEXEC_R, 4));
out.extend_from_slice(b"\n0000000000000000\ncleartomark\n");
let t = Type1::parse(&out).expect("the damaged Subrs entry is skipped");
assert!(t.has_glyph(".notdef"));
}
}