fix(render): a JBIG2 symbol with no rows costs a step, and a refined instance is charged first

From CodeRabbit on the work count.

A symbol with no rows cost no work, though it is decoded all the same, so a stream of thousands of
dictionaries of such symbols, exporting none, went uncounted. Each symbol now costs a step more than
its pixels.

A refined text instance was charged after it was decoded, so one could still be 2^28 pixels past what
was left before the decoder refused. It's charged once its size is read, before anything is allocated.

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-10-31 11:51:15 +01:00
co-authored by Claude Opus 5.5
parent 8243952569
commit 72e763ad12
4 changed files with 51 additions and 23 deletions
+38 -11
View File
@@ -3673,6 +3673,31 @@ fn zero_width_jbig2_dictionary(symbols: u32, exported: bool) -> Vec<u8> {
dictionary
}
/// A Huffman-coded symbol dictionary of `symbols` symbols of width 1 and no rows, none exported.
/// Only table B.5 has a negative height class delta, so it gets to no rows from one.
fn no_row_jbig2_dictionary(symbols: u32) -> Vec<u8> {
let mut bits = Bits::default();
bits.put(0, 1); // height class delta 1 (B.5), a class of no symbols
bits.put(0b111111, 6); // end of the class (B.2)
bits.put(0, 5); // BMSIZE 0, uncompressed (B.1)
bits.align();
bits.put(0b1111110, 7); // height class delta -1 (B.5): -255 and 254
bits.put(254, 8);
bits.put(0b10, 2); // width delta 1 (B.2)
for _ in 1..symbols {
bits.put(0, 1); // width delta 0 (B.2)
}
bits.put(0b111111, 6); // end of the class (B.2)
bits.put(0, 5); // BMSIZE 0, uncompressed (B.1)
bits.align();
bits.table_b1(symbols); // the first export run, not exported, is all of them
let mut dictionary = vec![0x00, 0x05]; // Huffman, tables B.5, B.2, B.1, B.1
dictionary.extend_from_slice(&0u32.to_be_bytes()); // exported
dictionary.extend_from_slice(&symbols.to_be_bytes()); // new
dictionary.extend_from_slice(&bits.bytes);
dictionary
}
/// A Huffman-coded symbol dictionary of `symbols` white symbols of width 1 and `height` rows, none
/// exported, for `height` from 76 up.
fn narrow_jbig2_dictionary(symbols: u32, height: u32) -> Vec<u8> {
@@ -4083,31 +4108,33 @@ fn jbig2_decoding_work_is_counted_item_by_item() {
data: jbig2_pattern_dictionary(32, 2),
cost: two_patterns(1, 32),
},
// Each symbol costs one more than its pixels, so that one of no rows isn't free.
Item { what: "symbols of no rows", before: vec![], spent: 0, kind: 0, refs: &[], data: no_row_jbig2_dictionary(5), cost: 5 },
// Rows cost in a symbol of no pixels too.
Item { what: "symbols without pixels", before: vec![], spent: 0, kind: 0, refs: &[], data: zero_width_jbig2_dictionary(3, false), cost: 12 },
Item { what: "a tall symbol of no width", before: vec![], spent: 0, kind: 0, refs: &[], data: zero(1000), cost: 4000 },
Item { what: "new symbols", before: vec![], spent: 0, kind: 0, refs: &[], data: square_jbig2_dictionary(80, false), cost: 6720 },
Item { what: "referred symbols", before: symbol(), spent: 4, kind: 0, refs: &[1], data: exporting_none, cost: 1 },
Item { what: "symbols without pixels", before: vec![], spent: 0, kind: 0, refs: &[], data: zero_width_jbig2_dictionary(3, false), cost: 15 },
Item { what: "a tall symbol of no width", before: vec![], spent: 0, kind: 0, refs: &[], data: zero(1000), cost: 4001 },
Item { what: "new symbols", before: vec![], spent: 0, kind: 0, refs: &[], data: square_jbig2_dictionary(80, false), cost: 6721 },
Item { what: "referred symbols", before: symbol(), spent: 5, kind: 0, refs: &[1], data: exporting_none, cost: 1 },
// Its symbol, and its one row and word; Huffman-coded, a table line for the symbol.
Item { what: "immediate text", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
Item { what: "intermediate text", before: symbol(), spent: 4, kind: 4, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
Item { what: "a text region's pixels", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(32, 32, 0), cost: 75 },
Item { what: "immediate text", before: symbol(), spent: 5, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
Item { what: "intermediate text", before: symbol(), spent: 5, kind: 4, refs: &[1], data: jbig2_text_region(1, 1, 0), cost: 13 },
Item { what: "a text region's pixels", before: symbol(), spent: 5, kind: 6, refs: &[1], data: jbig2_text_region(32, 32, 0), cost: 75 },
Item {
what: "an intermediate text region's pixels",
before: symbol(),
spent: 4,
spent: 5,
kind: 4,
refs: &[1],
data: jbig2_text_region(32, 32, 0),
cost: 75,
},
// The region's 13, a copy of the symbol's row and the round of the loop.
Item { what: "an instance", before: symbol(), spent: 4, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 1), cost: 15 },
Item { what: "an instance", before: symbol(), spent: 5, kind: 6, refs: &[1], data: jbig2_text_region(1, 1, 1), cost: 15 },
// The region's 13, 80 rows of 4 words copied, the round of the loop.
Item {
what: "an instance of a large symbol",
before: vec![(0, square_jbig2_dictionary(80, true))],
spent: 6720,
spent: 6721,
kind: 6,
refs: &[1],
data: jbig2_text_region(1, 1, 1),
@@ -4117,7 +4144,7 @@ fn jbig2_decoding_work_is_counted_item_by_item() {
Item {
what: "a refined instance",
before: vec![(0, square_jbig2_dictionary(80, true))],
spent: 6720,
spent: 6721,
kind: 6,
refs: &[1],
data: jbig2_refined_text_region(),
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -135,8 +135,9 @@ pub(crate) fn decode(
if ctx.pixels > MAX_DICTIONARY_PIXELS {
bail!(OverflowError::BitmapDimension);
}
// PdfCraft patch: see `bitmap::MAX_WORK`.
let work = decode_work(symbol_width, ctx.height_class_height);
// PdfCraft patch: see `bitmap::MAX_WORK`. One with no rows costs no pixels, but it is
// decoded all the same, and a stream can hold thousands of dictionaries of them.
let work = decode_work(symbol_width, ctx.height_class_height).saturating_add(1);
*ctx.work_left = ctx.work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
// PdfCraft patch: see `room`. Rows take a word at least, so symbols of one column
// held 270 MiB within the pixel bound, however few the dictionary exported.
+9 -9
View File
@@ -151,7 +151,7 @@ pub(crate) fn decode_with(
let symbol_id = ctx.read_symbol_id()?;
let symbol_bitmap =
decode_symbol_instance_bitmap(&mut ctx, symbols, header, symbol_id)?;
decode_symbol_instance_bitmap(&mut ctx, symbols, header, symbol_id, work_left)?;
let symbol_bitmap_ref: &Bitmap = match &symbol_bitmap {
SymbolBitmap::Reference(idx) => symbols.get(*idx).ok_or(SymbolError::OutOfRange)?,
@@ -161,14 +161,8 @@ pub(crate) fn decode_with(
let symbol_height = symbol_bitmap_ref.height as i32;
// PdfCraft patch: see `bitmap::MAX_WORK`. An instance of a large symbol costs its copy,
// and 10,000 of one took 64 s. A refined one was decoded as well. Even one with no
// pixels goes round this loop.
let (width, height) = (symbol_bitmap_ref.width, symbol_bitmap_ref.height);
let copied = copy_work(width, height).saturating_add(1);
let work = match &symbol_bitmap {
SymbolBitmap::Reference(_) => copied,
SymbolBitmap::Owned(_) => copied.saturating_add(decode_work(width, height)),
};
// and 10,000 of one took 64 s. Even one with no pixels goes round this loop.
let work = copy_work(symbol_bitmap_ref.width, symbol_bitmap_ref.height).saturating_add(1);
*work_left = work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
if !header.flags.transposed
@@ -546,6 +540,7 @@ fn decode_symbol_instance_bitmap(
symbols: &[&Bitmap],
header: &TextRegionHeader<'_>,
symbol_id: usize,
work_left: &mut u64,
) -> Result<SymbolBitmap> {
if !header.flags.use_refinement || ctx.read_refinement_flag()? == 0 {
return Ok(SymbolBitmap::Reference(symbol_id));
@@ -577,6 +572,11 @@ fn decode_symbol_instance_bitmap(
.checked_add(rdy)
.ok_or(OverflowError::ReferenceOffset)?;
// PdfCraft patch: see `bitmap::MAX_WORK`. Before it is decoded: charged after, one instance
// could still be 2^28 pixels past what was left.
let work = decode_work(refined_width, refined_height);
*work_left = work_left.checked_sub(work).ok_or(OverflowError::DecodingWork)?;
let mut refined_bitmap = Bitmap::new(refined_width, refined_height)?;
ctx.decode_refinement_bitmap(