Skip to content

Page.get_texttrace() leaks None references — Fatal Python error (none_dealloc) in long-running processes (1.27.2.3 & 1.28.0) #5042

Description

@manya4ello

Description

Every call to Page.get_texttrace() decrements the reference count of None more times than it increments it. On Python 3.11 (where None is not yet immortal) the deficit accumulates; once it exceeds None's base refcount (~12k), the interpreter dies with:

Fatal Python error: none_dealloc: deallocating None:
bug likely caused by a refcount error in a C extension

Long-running processes (batch conversion, large test suites) are affected; short single-document scripts usually stay under the threshold.

Minimal reproduction

import sys, pymupdf

doc = pymupdf.open()
page = doc.new_page()
for i in range(10):
    page.insert_text((72, 72 + i * 20), f"Hello world span {i}")

before = sys.getrefcount(None)
for _ in range(200):
    page.get_texttrace()
after = sys.getrefcount(None)
print("None refcount drift:", after - before)   # expected 0

Observed output: None refcount drift: -2657 (≈ −13 per call for this page). Increase the loop count and the interpreter eventually crashes with none_dealloc. All other hot APIs we probed (get_text, get_pixmap, open/close, page_count) show a drift of exactly 0.

Details we measured

  • The deficit scales with span count (≈ −4/call on an empty page, dozens on dense pages).
  • Part of the deficit fires when the result is disposed, not at call time: at construction a legitimate INCREF(None) and an erroneous extra DECREF(None) cancel numerically, but the slot still holds a pointer to None — disposing the result later performs a real, uncompensated DECREF(None). So a simple before/after window around the bare call underestimates the leak.

Environment

  • PyMuPDF 1.27.2.3 (MuPDF 1.27.2) — also reproduced on 1.28.0
  • Python 3.11 (Windows 11 x64; also reproduced on ubuntu-latest CI)

Prior art

Looks like the same defect class as #2146 (fixed in 2023), now present in get_texttrace().

Happy to test a fix; we currently work around it with a measured compensation wrapper on our side.

Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions