diff --git a/src/ocrmypdf/pdfinfo/__init__.py b/src/ocrmypdf/pdfinfo/__init__.py index 9832c0f5..4f0f4886 100644 --- a/src/ocrmypdf/pdfinfo/__init__.py +++ b/src/ocrmypdf/pdfinfo/__init__.py @@ -22,6 +22,7 @@ from enum import Enum from math import hypot, isclose from pathlib import Path from unittest.mock import Mock +from warnings import warn import re from pikepdf import PdfMatrix @@ -136,6 +137,13 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE): page. PDF units suit our needs so we initialize ctm to the identity matrix. + + According to the PDF specification, the maximum stack depth is 32. Other + viewers tolerate some amount beyond this. We issue a warning if the + stack depth exceeds the spec limit and set a hard limit beyond this to + bound our memory requirements. If the stack underflows behavior is + undefined in the spec, but we just pretend nothing happened and leave the + CTM unchanged. """ stack = [] @@ -150,18 +158,20 @@ def _interpret_contents(contentstream, initial_shorthand=UNIT_SQUARE): for n, graphobj in enumerate(_normalize_stack( pikepdf.parse_content_stream(contentstream, operator_whitelist))): operands, operator = graphobj - if operator == 'q': stack.append(ctm) - if len(stack) > 32: - raise RuntimeError( - "PDF graphics stack overflow, operator %i" % n) + if len(stack) > 32: # See docstring + if len(stack) > 128: + raise RuntimeError( + "PDF graphics stack overflowed hard limit, operator %i" % n) + warn("PDF graphics stack overflowed spec limit") elif operator == 'Q': try: ctm = stack.pop() except IndexError: - raise RuntimeError( - "PDF graphics stack underflow, operator %i" % n) + # Keeping the ctm the same seems to be the only sensible thing + # to do. Just pretend nothing happened, keep calm and carry on. + warn("PDF graphics stack underflowed - PDF may be malformed") elif operator == 'cm': ctm = PdfMatrix(operands) @ ctm elif operator == 'Do': diff --git a/tests/test_pdfinfo.py b/tests/test_pdfinfo.py index c27a847d..9726ed1a 100644 --- a/tests/test_pdfinfo.py +++ b/tests/test_pdfinfo.py @@ -27,7 +27,6 @@ import shutil import pytest import img2pdf import sys -import PyPDF2 as pypdf import pikepdf import pickle @@ -196,3 +195,22 @@ def test_corrupt_font_detection(resources, testfile): pdf = pdfinfo.PdfInfo(filename, detailed_page_analysis=True) assert pdf[0].has_corrupt_text + + +def test_stack_abuse(): + p = pikepdf.Pdf.new() + + stream = pikepdf.Stream(p, b'q ' * 35) + with pytest.warns(None) as record: + pdfinfo._interpret_contents(stream) + assert 'overflowed' in str(record[0].message) + + stream = pikepdf.Stream(p, b'q Q Q Q Q') + with pytest.warns(None) as record: + pdfinfo._interpret_contents(stream) + assert 'underflowed' in str(record[0].message) + + stream = pikepdf.Stream(p, b'q ' * 135) + with pytest.warns(None): + with pytest.raises(RuntimeError): + pdfinfo._interpret_contents(stream)