Skip to content

ROB: Guard ArrayObject.read_from_stream against uncaught RecursionError - #4430

Open
damasiyachiranj wants to merge 2 commits into
py-pdf:mainfrom
damasiyachiranj:fix-array-recursion-guard
Open

damasiyachiranj wants to merge 2 commits into
py-pdf:mainfrom
damasiyachiranj:fix-array-recursion-guard

Conversation

@damasiyachiranj

Copy link
Copy Markdown

Fixes #4427.

ArrayObject.read_from_stream recurses into read_object() for each element. A single array nested thousands of levels deep (e.g. [[[[...]]]]) drives recursion depth rather than element count, so array_maximum_element_count never engages, and Python's interpreter recursion limit is hit first — raising an uncaught RecursionError instead of a clean PdfReadError.

DictionaryObject.read_from_stream already guards against this (catches MemoryError/RecursionError around its recursive read_object() call and re-raises as PdfReadError, while still letting LimitReachedError propagate). This PR applies the same guard to ArrayObject.read_from_stream.

Added test_array_object__read_from_stream__deeply_nested_array_raises_pdfreaderror, which builds a 5000-deep nested array and asserts it now raises PdfReadError instead of an uncaught RecursionError.

A deeply nested array (e.g. "[[[[...]]]]") drives recursion depth in read_from_stream's
recursive descent, not element count, so array_maximum_element_count never engages and
an uncaught RecursionError escapes to the caller instead of a PdfReadError.

Mirrors the existing guard in DictionaryObject.read_from_stream: catches
(MemoryError, RecursionError) around the recursive read_object() call and re-raises
as PdfReadError, while still letting LimitReachedError (the element-count/token
budget) propagate unchanged.

Fixes py-pdf#4427.
Adds test_array_object__read_from_stream__deeply_nested_array_raises_pdfreaderror,
verifying a 5000-deep nested array now raises PdfReadError instead of an uncaught
RecursionError.
@codecov

codecov Bot commented Nov 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.87%. Comparing base (1e3e7e3) to head (8cfafc7).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4430   +/-   ##
=======================================
  Coverage   98.87%   98.87%           
=======================================
  Files          59       59           
  Lines       12299    12305    +6     
  Branches     2342     2342           
=======================================
+ Hits        12161    12167    +6     
  Misses         68       68           
  Partials       70       70           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@damasiyachiranj damasiyachiranj changed the title Fix: guard ArrayObject.read_from_stream against uncaught RecursionError ROB: Guard ArrayObject.read_from_stream against uncaught RecursionError Nov 14, 2026
# Always propagate security issues.
raise
except (MemoryError, RecursionError) as exc:
# A deeply nested array (e.g. "[[[[...]]]]") blows the interpreter's

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this large comment? The referenced case does not have one and nesting triggering recursion errors in such a scenario should be obvious and - if required - derivable from the corresponding PR once merged.



def test_array_object__read_from_stream__deeply_nested_array_raises_pdfreaderror() -> None:
# A single array nested thousands of levels deep drives recursion depth,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see how this comment provides benefits, especially as inline comment instead of docstring.

# not element count, so array_maximum_element_count never engages.
# Previously this raised an uncaught RecursionError instead of PdfReadError.
pdf = ReaderDummy()
depth = 5000

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a depth of 5000 to trigger an issue which occurs with exceeding sys.getrecursionlimit(), which usually is just 1000?

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing recursion guard in ArrayObject.read_from_stream() leads to uncaught RecursionError

2 participants