Skip to content

SIGTRAP rendering a page with an MMR-coded JBIG2 image (CJBig2_GRDProc::StartDecodeMMR → FaxModule::FaxG4Decode) #440

Open

Description

@pbstriker38

Summary

Rendering a page that draws a JBIG2 image whose generic region is MMR-coded (T.88 generic region flags, bit 0 = 1) kills the process with SIGTRAP (exit 133). This happens on macOS arm64 and on Linux x86_64, in every version I tried (7.17.0 through 7.24.2). Arithmetic-coded JBIG2 renders fine. Parsing the same page without a render_config is fine. PDFium renders the file correctly.

We hit it through Docling: ThreadedDoclingParseDocumentBackend renders every page, so one such scan takes down the whole conversion process, along with anything else running in it.

Reproduce

Self-contained. It needs only docling-parse, plus Pillow (already a dependency) to write the Group 4 data. It builds a 1-page PDF holding a 64×16 DeviceGray image under /Filter /JBIG2Decode. The embedded stream is a page information segment followed by one immediate generic region with MMR = 1, whose data libtiff wrote as CCITT G4.

import io
import struct

from docling_parse.pdf_parser import DoclingThreadedPdfParser, RenderConfig, ThreadedPdfParserConfig
from PIL import Image, ImageDraw

WIDTH, HEIGHT = 64, 16

# MMR in a JBIG2 generic region is ITU-T T.6 (Group 4), so libtiff writes the region's data.
bitmap = Image.new("1", (WIDTH, HEIGHT), 0)
ImageDraw.Draw(bitmap).rectangle([16, 4, 31, 11], fill=1)
tiff = io.BytesIO()
bitmap.save(tiff, format="TIFF", compression="group4")
strip = Image.open(io.BytesIO(tiff.getvalue()))
offset, size = strip.tag_v2[273][0], strip.tag_v2[279][0]
mmr = tiff.getvalue()[offset:offset + size]


def segment(number: int, kind: int, data: bytes) -> bytes:
    """A JBIG2 segment header (no referred-to segments, page 1) and its data."""
    return struct.pack(">IBBBI", number, kind, 0, 1, len(data)) + data


# Embedded JBIG2 stream: page information (48), then an immediate generic region (38) covering
# the page, with MMR = 1 in its flags byte.
jbig2 = segment(0, 48, struct.pack(">IIIIBH", WIDTH, HEIGHT, 0, 0, 0, 0)) + segment(
    1, 38, struct.pack(">IIIIB", WIDTH, HEIGHT, 0, 0, 0) + b"\x01" + mmr
)

content = b"q 128 0 0 32 36 34 cm /Im0 Do Q"
objects = [
    b"<< /Type /Catalog /Pages 2 0 R >>",
    b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
    b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 100] "
    b"/Resources << /XObject << /Im0 4 0 R >> >> /Contents 5 0 R >>",
    b"<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceGray "
    b"/BitsPerComponent 1 /Filter /JBIG2Decode /Length %d >>\nstream\n" % (WIDTH, HEIGHT, len(jbig2))
    + jbig2 + b"\nendstream",
    b"<< /Length %d >>\nstream\n" % len(content) + content + b"\nendstream",
]
pdf = bytearray(b"%PDF-1.7\n")
offsets = []
for number, body in enumerate(objects, start=1):
    offsets.append(len(pdf))
    pdf += b"%d 0 obj\n" % number + body + b"\nendobj\n"
xref = len(pdf)
pdf += b"xref\n0 %d\n0000000000 65535 f \n" % (len(objects) + 1)
pdf += b"".join(b"%010d 00000 n \n" % offset for offset in offsets)
pdf += b"trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF\n" % (len(objects) + 1, xref)

parser = DoclingThreadedPdfParser(parser_config=ThreadedPdfParserConfig(render_config=RenderConfig()))
parser.load(io.BytesIO(bytes(pdf)))
for result in parser.iterate_results():
    print("rendered page", result.page_number, "has image:", result.has_image)
print("survived")
$ python repro.py; echo "exit $?"
Trace/breakpoint trap
exit 133

PDFium (pypdfium2 5.9.0) renders the same PDF: the box comes out as the expected 32×16 = 512 dark pixels at scale 1.

Where it stops

macOS crash report, crashing thread (docling-parse 7.24.0 wheel):

EXC_BREAKPOINT (SIGTRAP)
0  pdf_parsers.cpython-312-darwin.so  ???  (no symbol; called from frame 1)
1  fxcodec::FaxModule::FaxG4Decode(pdfium::span<const uint8_t>, int, int, int, int, uint8_t*) + 5084
2  CJBig2_GRDProc::StartDecodeMMR(std::unique_ptr<CJBig2_Image>*, CJBig2_BitStream*) + 208
3  CJBig2_Context::ParseGenericRegion(CJBig2_Segment*, PauseIndicatorIface*) + 80
4  CJBig2_Context::ProcessingParseSegmentData(CJBig2_Segment*, PauseIndicatorIface*) + 600
5  CJBig2_Context::DecodeSequential(PauseIndicatorIface*) + 604
6  CJBig2_Context::GetFirstPage(pdfium::span<uint8_t>, int, int, int, PauseIndicatorIface*) + 320
7  fxcodec::Jbig2Decoder::StartDecode(...) + 292
8  jbig2_decode(std::span<const uint8_t>, std::span<const uint8_t>, unsigned int, unsigned int) + 1660

An EXC_BREAKPOINT from a release build points to a PDFium CHECK / IMMEDIATE_CRASH (or a hardened span bounds check) inside the vendored src/third_party/pdfium_jbig2 copy of FaxG4Decode. The trap is in the MMR path itself, not in the image's content (see below).

What varies

  • Size: an 8×8 all-white MMR region renders. A 32×32 all-white region, 64×16 and 100×50 regions with a box, 640×200 text, and 33×7 all-black all trap.
  • How the image is used: a DeviceGray image, an /ImageMask stencil, /Filter [/JBIG2Decode] (a one-element array) and /Filter [/FlateDecode /JBIG2Decode] all trap.
  • Rendering: with no render_config (parse only, bitmaps as rectangles), the page parses fine.
  • The same bytes as CCITT: putting the region's coded rows under /Filter /CCITTFaxDecode /DecodeParms << /K -1 /Columns W /Rows H /EndOfBlock false >> renders correctly through docling-parse's own CCITT decoder, pixel-identical to PDFium's render of the JBIG2 original. That decoder also survived 32 truncated, bit-flipped and random G4 inputs without crashing.
  • Not tried: MMR in pattern dictionaries (HDMMR), halftone regions (HMMR) and the collective bitmaps of Huffman symbol dictionaries. PDFium decodes those through the same CJBig2_GRDProc MMR path, so they probably trap the same way.

Suggestion

MMR is T.6, which docling-parse already decodes for /CCITTFaxDecode /K -1. Routing JBIG2's MMR regions through that decoder would fix this. Short of that, failing the one image, rather than aborting the process, would turn a crash into a missing image.

Environment

  • docling-parse 7.24.0 (PyPI wheel), Python 3.12.13, macOS 26.5.2 arm64: traps.
  • python:3.12-slim, linux/amd64: docling-parse 7.17.0, 7.24.0, 7.24.1 and 7.24.2 all trap.
  • Docling 2.145.0's ThreadedDoclingParseDocumentBackend dies the same way when it renders the page (iter_pages(), get_page_image()).

Activity

  1. added a commit that references this issue on Nov 3, 2026
    7c4d638
  2. wittjeff commented on Nov 3, 2026

    @wittjeff
    Contributor

    Thanks for the precise report; the trap reproduces here on Linux with the 7.22.1 wheel as well (exit 133 on your script).

    The cause is in the vendored copy of pdfium's faxmodule.cpp, not in pdfium's decoder. Diffed against pdfium chromium/7000, the stub's FaxG4GetRow was rewritten: it has no end-of-row returns, no pass mode, and the T.6 mode codes are shifted by one bit. A row runs on into the next row's codes, a0 passes the row width, ref_buf[a0 / 8] leaves the stride and the span bounds check traps. That matches your size pattern: at 8x8 the stride is 4 bytes and a0 / 8 stays inside it, at 32x32 the width equals the stride in bits and the first lookup past the row traps. Separately, kFaxWhiteRunIns is missing the entry for white run 18, which misaligns every code after the 7-bit group and can walk a lookup off the table (white run 0 is an 8-bit code, so an all-black row hits this).

    Fix in #444: restore pdfium's row decoder, the missing table entry and the FindBit clamp, keeping the stub's signature. Pattern dictionaries, halftone regions and Huffman symbol dictionaries reach the same call, so they are covered too. I kept pdfium's decoder rather than routing MMR through the in-tree CCITT decoder: that would need a stride packer and a bit-position handoff for symbol dictionaries, and the upstream text is the smaller change. The new unit test builds MMR regions with libtiff through Pillow, as your script does, and checks every rendered sample against the source bitmap; your 64x16 box is one of its fixtures.

    Evidence per #428: the regression dataset has no MMR-coded JBIG2 (its one JBIG2 document is arithmetic-coded throughout), but the pdfium test corpus does. pdfium/bug_880920.pdf, a 3300x2550 scanned page with Huffman symbol dictionaries and MMR collective bitmaps, traps on main and renders on the branch within a mean absolute error of 0.159 of pypdfium2; third_party/jbig2/bitmap-mmr.pdf (generic region, MMR), bitmap-halftone-10bpp-mmr.pdf (pattern dictionary and halftone region) and bitmap-symbol-symhuff-texthuff.pdf (Huffman symbol dictionary) all trap on main and render pixel-identical to pypdfium2 on the branch. Three-panel comparisons and the per-file table are on the evidence branch. Of the 21 MMR files in that corpus, 6 now survive but render blank because a later Huffman refinement symbol dictionary fails in the vendored decoder; that is a separate limitation, not the trap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions