M2: JBIG2 arithmetic near the end of an i32 doesn't overflow

A region's location, a refinement's offset, a symbol's corner, a Huffman
range line and a decoded integer of 2^31 could each overflow: a panic
wherever overflow is checked. The last four wrap, as a release build did.
Bitmap::combine saturates, which comes to the same: a side is at most
65,535, so a location that overflows is past the end of any page. What is
drawn doesn't change. Patch (12), after the numbers #5491 and #5569 take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
Grzegorz Martens
2026-11-01 18:32:33 +01:00
co-authored by Claude Opus 5.5
parent 47f42ba07b
commit 10107d943a
7 changed files with 118 additions and 29 deletions
+80
View File
@@ -3591,6 +3591,86 @@ fn jbig2_symbol_dictionaries_that_never_advance_terminate() {
}
}
/// A JBIG2 stream can put a region, a reference or a symbol at the very end of what an `i32` holds,
/// where adding to it overflowed: a panic wherever overflow is checked, a build's own tests included.
/// A region or a symbol out there is off the page, so nothing draws. Vendored hayro-jbig2 patch: the arithmetic wraps, as
/// a release build's did, or in `Bitmap::combine` saturates, which comes to the same. Three of the text
/// streams are arithmetic-coded: one symbol on a strip of -2^31 + 1, with its bottom left as the corner
/// placed, one on a strip of -2^31, and one refined by an offset of `i32::MIN`, which reads nothing of
/// the old symbol and still draws. Two are Huffman-coded, each with its first S at an offset of
/// 0x7FFFFFFF on a table's 32-bit range line, past what an `i32` holds, and are refused further on.
#[test]
fn jbig2_placements_at_the_end_of_an_i32_draw_nothing() {
const FAR_CORNER: &str = "0000000030000100000013000000100000000c0000000000000000420000000000010000010000001e000003fffdff02fefefe00000001000000014fdc3ce0205df55c36bfffac00000002062001010000001f000000100000000c000000000000000004000000000001969747cbe88bffac";
const STRIP: &str = "0000000030000100000013000000100000000c0000000000000000420000000000010000010000001e000003fffdff02fefefe00000001000000014fdda25e46956d3e0d3fffac00000002062001010000001f000000100000000c000000000000000004001000000001969747cbe5dbffac";
const UPPER_LINE: &str = "0000000030000100000013000000100000000c0000000000000000420000000000010000010000001d000003fffdff02fefefe00000001000000014fdaea38c99641e0c8ffac000000020620010100000038000000100000000c00000000000000000400110000000000010100000000000000000000000000000000003f7fffffff0000000000000000";
const LOWER_LINE: &str = "0000000030000100000013000000100000000c0000000000000000420000000000010000010000001d000003fffdff02fefefe00000001000000014fdaea38c99641e0c8ffac000000020620010100000038000000100000000c00000000000000000400110000000000010100000000000000000000000000000000003e7fffffff0000000000000000";
const OFFSET: &str = "0000000030000100000013000000100000000c0000000000000000420000000000010000010000001e000003fffdff02fefefe00000001000000014fdda9172f614a037ddfffac0000000206200101000000260000000800000006000000000000000004801200000001a9695690b3e5f44a2f105e30d3ffac";
let segment = |number: u32, kind: u8, data: &[u8]| {
let mut out = number.to_be_bytes().to_vec();
out.extend_from_slice(&[kind, 0, 1]);
out.extend_from_slice(&u32::try_from(data.len()).unwrap().to_be_bytes());
out.extend_from_slice(data);
out
};
let side = 8u32.to_be_bytes();
let page = |width: u32| {
let mut page = [width.to_be_bytes(), side].concat();
page.extend_from_slice(&[0; 8]); // resolution
page.extend_from_slice(&[0, 0, 0]); // flags, striping
segment(0, 48, &page)
};
let region = |width: u32, kind: u8, x: u32, y: u32, header: &[u8]| {
let mut region = [width.to_be_bytes(), side, x.to_be_bytes(), y.to_be_bytes()].concat();
region.push(0); // combination operator
region.extend_from_slice(header);
region.extend_from_slice(&[0x5a; 64]);
[page(width), segment(1, kind, &region)].concat()
};
let generic = |x, y| region(8, 38, x, y, &[0, 3, 0xff, 0xfd, 0xff, 2, 0xfe, 0xfe, 0xfe]); // template 0, AT pixels
// Immediate refinements of the page: template 0 with its usual AT pixels, with others, with typical
// prediction, and template 1.
let refined = |width, x, y, form: usize| {
region(width, 42, x, y, [&[0, 0xff, 0xff, 0xff, 0xff][..], &[0, 0xff, 0xff, 127, 127], &[2, 0xff, 0xff, 0xff, 0xff], &[1]][form])
};
let hex = |text: &str| (0..text.len() / 2).map(|i| u8::from_str_radix(&text[i * 2..i * 2 + 2], 16).unwrap()).collect::<Vec<u8>>();
let ink = |jbig2: Vec<u8>, width: u32, height: u32| {
let content = b"q 20 0 0 20 5 5 cm /Im1 Do Q";
let mut pdf = b"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 40 40] /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >> endobj\n".to_vec();
pdf.extend_from_slice(format!("4 0 obj << /Length {} >> stream\n", content.len()).as_bytes());
pdf.extend_from_slice(content);
pdf.extend_from_slice(b"\nendstream endobj\n");
pdf.extend_from_slice(
format!("5 0 obj << /Type /XObject /Subtype /Image /Width {width} /Height {height} /BitsPerComponent 1 /ColorSpace /DeviceGray /Filter /JBIG2Decode /Length {} >> stream\n", jbig2.len())
.as_bytes(),
);
pdf.extend_from_slice(&jbig2);
pdf.extend_from_slice(b"\nendstream endobj\ntrailer << /Root 1 0 R >>\n%%EOF\n");
let mut r = PageRenderer::new(Arc::new(pdf), RenderConfig::default());
let page = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, ..Default::default() });
assert!(page.error.is_none(), "{:?}", page.error);
page.rgba.as_chunks::<4>().0.iter().filter(|p| p[0] < 128).count()
};
assert!(ink(generic(2, 0), 8, 8) > 0, "a region on the page draws");
for form in 0..4 {
assert!(ink(refined(8, 0, 0, form), 8, 8) > 0, "a refinement on the page draws: {form}");
}
let end = 0x7FFF_FFFFu32;
for (x, y) in [(end, 0), (0, end), (end - 7, 0), (0, end - 7), (end - 129, 0), (0, end - 129)] {
assert_eq!(ink(generic(x, y), 8, 8), 0, "generic at {x:#x}, {y:#x}");
for form in 0..4 {
assert_eq!(ink(refined(8, x, y, form), 8, 8), 0, "refinement {form} at {x:#x}, {y:#x}");
}
}
// Past a 32-bit word of the row, where the reference is read again.
assert_eq!(ink(refined(64, 0, end, 0), 64, 8), 0, "a wide refinement");
assert_eq!(ink(hex(FAR_CORNER), 16, 12), 0, "a symbol's far side past the end");
assert_eq!(ink(hex(STRIP), 16, 12), 0, "a strip of -2^31");
assert!(ink(hex(OFFSET), 16, 12) > 0, "a symbol refined by an offset of i32::MIN");
assert_eq!(ink(hex(UPPER_LINE), 16, 12), 0, "an upper range line past an i32");
assert_eq!(ink(hex(LOWER_LINE), 16, 12), 0, "a lower range line past an i32");
}
/// Item 1 of storytold/pdfcraft#623: a page's content used to be cut at `MAX_DECODED_STREAM`,
/// so everything after the cut was lost (the text and the title block of the A1 sample). Content
/// that expands by a few times over is read to its end, however large it is.
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -149,7 +149,8 @@ impl Bitmap {
// TODO: Figure out how we can optimize this, especially since this is a
// bottleneck for symbols/patterns.
let dest_x_start = x.max(0);
let dest_x_end = (x + other.width as i32).min(self.width as i32);
// Saturating: a location can be anywhere an `i32` reaches, and past the end it is off the page.
let dest_x_end = x.saturating_add(other.width as i32).min(self.width as i32);
if dest_x_start >= dest_x_end {
return;
@@ -160,7 +161,7 @@ impl Bitmap {
let dest_x_end = dest_x_end as u32;
for src_y in 0..other.height {
let dest_y = y + src_y as i32;
let dest_y = y.saturating_add(src_y as i32);
if dest_y < 0 || dest_y >= self.height as i32 {
continue;
}
+20 -16
View File
@@ -179,10 +179,12 @@ impl<'a> RefinementContextGatherer<'a> {
pub(crate) fn start_row(&mut self, region: &Bitmap, reference: &Bitmap, y: u32) {
self.reg_y = y;
let ref_y = y as i32 - self.reference_dy;
// The offsets can be anywhere an `i32` reaches, so coordinates in the reference wrap here and
// below. Wrapped, they are off the reference, whose pixels there read as 0.
let ref_y = (y as i32).wrapping_sub(self.reference_dy);
// This will on purpose wrap to a very large `u32` for negative values, such that
// they resolve to 0 in `ContextGatherer::load_word`.
let ref_x0 = (-self.reference_dx) as u32;
let ref_x0 = self.reference_dx.wrapping_neg() as u32;
self.reg_cur_x = 0;
self.reg_m1 = if y >= 1 {
@@ -193,9 +195,9 @@ impl<'a> RefinementContextGatherer<'a> {
self.reg_cur = 0;
self.ref_cur_x = 0;
self.ref_m1 = ContextGatherer::load_word(reference, (ref_y - 1) as u32, 0);
self.ref_m1 = ContextGatherer::load_word(reference, ref_y.wrapping_sub(1) as u32, 0);
self.ref_cur = ContextGatherer::load_word(reference, ref_y as u32, 0);
self.ref_p1 = ContextGatherer::load_word(reference, (ref_y + 1) as u32, 0);
self.ref_p1 = ContextGatherer::load_word(reference, ref_y.wrapping_add(1) as u32, 0);
self.ctx = match self.template {
RefinementTemplate::Template1 => {
@@ -257,7 +259,7 @@ impl<'a> RefinementContextGatherer<'a> {
self.reg_cur = ContextGatherer::load_word(region, self.reg_y, new_start);
}
let ref_x_signed = x as i32 - self.reference_dx;
let ref_x_signed = (x as i32).wrapping_sub(self.reference_dx);
if ref_x_signed < 0 {
self.ref_cur_x = 0;
self.ref_m1 = 0;
@@ -268,18 +270,20 @@ impl<'a> RefinementContextGatherer<'a> {
if ref_x + 1 >= self.ref_cur_x + WORD_BITS || ref_x < self.ref_cur_x {
let new_start = ref_x.saturating_sub(1);
let ref_y = self.reg_y as i32 - self.reference_dy;
let ref_y = (self.reg_y as i32).wrapping_sub(self.reference_dy);
self.ref_cur_x = new_start;
self.ref_m1 = ContextGatherer::load_word(reference, (ref_y - 1) as u32, new_start);
self.ref_m1 =
ContextGatherer::load_word(reference, ref_y.wrapping_sub(1) as u32, new_start);
self.ref_cur = ContextGatherer::load_word(reference, ref_y as u32, new_start);
self.ref_p1 = ContextGatherer::load_word(reference, (ref_y + 1) as u32, new_start);
self.ref_p1 =
ContextGatherer::load_word(reference, ref_y.wrapping_add(1) as u32, new_start);
}
}
}
#[inline(always)]
pub(crate) fn tpgr_all_same(&self, x: u32) -> bool {
let ref_x = (x as i32 - self.reference_dx) as u32;
let ref_x = (x as i32).wrapping_sub(self.reference_dx) as u32;
let rbx = ref_x.wrapping_sub(self.ref_cur_x);
#[inline(always)]
@@ -310,7 +314,7 @@ impl<'a> RefinementContextGatherer<'a> {
#[inline(always)]
pub(crate) fn ref_center_pixel(&self, x: u32) -> u8 {
let ref_x = (x as i32 - self.reference_dx) as u32;
let ref_x = (x as i32).wrapping_sub(self.reference_dx) as u32;
let rbx = ref_x.wrapping_sub(self.ref_cur_x);
ContextGatherer::get_buf_pixel(self.ref_cur, rbx) as u8
@@ -329,7 +333,7 @@ impl<'a> RefinementContextGatherer<'a> {
#[inline(always)]
fn gather_template0_default(&mut self, _region: &Bitmap, _reference: &Bitmap, x: u32) -> u16 {
let bx = x - self.reg_cur_x;
let ref_x = (x as i32 - self.reference_dx) as u32;
let ref_x = (x as i32).wrapping_sub(self.reference_dx) as u32;
let rbx = ref_x.wrapping_sub(self.ref_cur_x);
let new_pixels = (ContextGatherer::get_buf_pixel(self.reg_m1, bx + 1) << 10)
@@ -345,12 +349,12 @@ impl<'a> RefinementContextGatherer<'a> {
#[inline(always)]
fn gather_template0_custom(&mut self, region: &Bitmap, reference: &Bitmap, x: u32) -> u16 {
let bx = x - self.reg_cur_x;
let ref_x_i = x as i32 - self.reference_dx;
let ref_x_i = (x as i32).wrapping_sub(self.reference_dx);
let rbx = ref_x_i as u32 - self.ref_cur_x;
let xi = x as i32;
let yi = self.reg_y as i32;
let ref_y = yi - self.reference_dy;
let ref_y = yi.wrapping_sub(self.reference_dy);
let at1 = self.at_pixels[0];
let at2 = self.at_pixels[1];
@@ -360,8 +364,8 @@ impl<'a> RefinementContextGatherer<'a> {
| (ContextGatherer::get_buf_pixel(self.reg_m1, bx + 1) << 10)
| (ContextGatherer::get_buf_pixel(self.reg_cur, bx.wrapping_sub(1)) << 9)
| ((reference.get_pixel(
(ref_x_i + at2.x as i32) as u32,
(ref_y + at2.y as i32) as u32,
ref_x_i.wrapping_add(at2.x as i32) as u32,
ref_y.wrapping_add(at2.y as i32) as u32,
) as u16)
<< 8)
| (ContextGatherer::get_buf_pixel(self.ref_m1, rbx.wrapping_add(1)) << 6)
@@ -375,7 +379,7 @@ impl<'a> RefinementContextGatherer<'a> {
#[inline(always)]
fn gather_template1(&mut self, _region: &Bitmap, _reference: &Bitmap, x: u32) -> u16 {
let bx = x - self.reg_cur_x;
let ref_x = (x as i32 - self.reference_dx) as u32;
let ref_x = (x as i32).wrapping_sub(self.reference_dx) as u32;
let rbx = ref_x.wrapping_sub(self.ref_cur_x);
let new_pixels = (ContextGatherer::get_buf_pixel(self.reg_m1, bx + 1) << 7)
+9 -6
View File
@@ -173,23 +173,26 @@ pub(crate) fn decode_with(
}
let symbol_s = current_s;
// From the corner a symbol is placed by to its far side. The strip and the offset can
// be anywhere an `i32` reaches, and wrapped, the symbol is off the region.
let back = |corner: i32, size: i32| corner.wrapping_sub(size).wrapping_add(1);
let (x, y) = if !header.flags.transposed {
match header.flags.reference_corner {
ReferenceCorner::TopLeft => (symbol_s, symbol_t),
ReferenceCorner::TopRight => (symbol_s - symbol_width + 1, symbol_t),
ReferenceCorner::BottomLeft => (symbol_s, symbol_t - symbol_height + 1),
ReferenceCorner::TopRight => (back(symbol_s, symbol_width), symbol_t),
ReferenceCorner::BottomLeft => (symbol_s, back(symbol_t, symbol_height)),
ReferenceCorner::BottomRight => {
(symbol_s - symbol_width + 1, symbol_t - symbol_height + 1)
(back(symbol_s, symbol_width), back(symbol_t, symbol_height))
}
}
} else {
match header.flags.reference_corner {
ReferenceCorner::TopLeft => (symbol_t, symbol_s),
ReferenceCorner::TopRight => (symbol_t - symbol_width + 1, symbol_s),
ReferenceCorner::BottomLeft => (symbol_t, symbol_s - symbol_height + 1),
ReferenceCorner::TopRight => (back(symbol_t, symbol_width), symbol_s),
ReferenceCorner::BottomLeft => (symbol_t, back(symbol_s, symbol_height)),
ReferenceCorner::BottomRight => {
(symbol_t - symbol_width + 1, symbol_s - symbol_height + 1)
(back(symbol_t, symbol_width), back(symbol_s, symbol_height))
}
}
};
+3 -3
View File
@@ -448,11 +448,11 @@ impl HuffmanNode {
// 4) "Otherwise, if table line I is the lower range table line for this
// table, then set: HTVAL = RANGELOW[I] − HTOFFSET"
// 5) "Otherwise, set: HTVAL = RANGELOW[I] + HTOFFSET"
// `HTVAL`
// `HTVAL`. Wrapping: a range line's offset can take it past what an `i32` holds.
let value = if leaf.is_lower {
leaf.range_low - range_offset
leaf.range_low.wrapping_sub(range_offset)
} else {
leaf.range_low + range_offset
leaf.range_low.wrapping_add(range_offset)
};
return Ok(Some(value));
+2 -1
View File
@@ -70,7 +70,8 @@ impl IntegerDecoder {
if s == 0 {
Some(v as i32)
} else if v > 0 {
Some(-(v as i32))
// Wrapping: `V` can be 2^31, whose negative is `i32::MIN`, as the cast already reads it.
Some((v as i32).wrapping_neg())
} else {
None
}