Skip to content

Fix leaked block tuples in extractBLOCKS() / _as_blocks() - #5261

Open
damient wants to merge 2 commits into
pymupdf:mainfrom
damient:fix-extractblocks-leak
Open

damient wants to merge 2 commits into
pymupdf:mainfrom
damient:fix-extractblocks-leak

Conversation

@damient

@damient damient commented Nov 6, 2026 •

Copy link
Copy Markdown

Follow-up to #5067 / #5074, which fixed the leaked text reference in the
same function: the block tuple itself still leaks.

_as_blocks() appended each block tuple to the result list with
LIST_APPEND(), which keeps the reference returned by PyTuple_New(). Every
tuple returned by Page.get_text("blocks") / TextPage.extractBLOCKS() —
with its floats, text and ints — therefore leaked on every call (from ~275 bytes
to ~1.9 KB per block, depending on the text length).

Changes:

  • src/extra.i: use LIST_APPEND_DROP() in _as_blocks(), like the other
    extractors (JM_append_word(), JM_make_textpage_dict()…), and remove
    LIST_APPEND(), which has no other caller.
  • tests/test_memory.py: test_extractblocks_refcount() checks that each
    block tuple has the same refcount as a tuple held by a list only, and that
    200 calls of get_text("blocks") do not grow tracemalloc's traced memory.

The output of get_text("blocks") is unchanged (see measures below).

Measures

Same script, same page (tests/resources/2201.00069.pdf, page 0, 15 blocks),
Linux aarch64 (Docker python:3.14), both wheels built from source with the
same MuPDF build — main @ 28b271b vs. this branch:

main this PR
refcount of a block tuple / control tuple 3 / 2 2 / 2
get_text("blocks"), 2000 calls, tracemalloc growth +57.889 MB +0.001 MB
get_text("words" / "dict" / "text"), 2000 calls +0.000 MB +0.000 MB
test_extractblocks_refcount fails (assert 3 == 2) passes (growth = 0)

get_text("blocks") output is byte-identical between both builds on all
189 PDFs of tests/resources/ (1485 pages, 26798 blocks). The new test
also fails against the released pymupdf==2.1.0 wheel (macOS arm64).

_as_blocks() appended each block tuple to the result list with
LIST_APPEND(), which does not drop the reference returned by
PyTuple_New(). Every tuple (and its float, str and int items) therefore
had a refcount one too high and was never freed: get_text('blocks')
leaked ~275 bytes per block on every call.

Use LIST_APPEND_DROP() like the other extractors, and remove
LIST_APPEND(), which has no other caller.
Checks that get_text('blocks') returns block tuples owned by the result
list only and that repeated calls do not grow traced memory.
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@damient

damient commented Nov 6, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Nov 6, 2026
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.

1 participant