Heap corruption in Document.tobytes() on 1.24.11/1.24.14 surfaces only under jemalloc (fixed in 1.25.5+) #5065
pborges-conduit
started this conversation in
Show and tell
Replies: 1 comment
|
Thank you for this information! |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
Document.tobytes()with the flag combination below appears to corrupt the process heap in 1.24.11 and 1.24.14 when jemalloc is the active allocator. It is already resolved in 1.25.5 and 1.26.4, so this is filed for documentation rather than as a fix request — we found nothing describing this failure mode when searching, and the allocator dependence makes it easy to misdiagnose.We hit it in production as a daily gunicorn worker SIGSEGV and initially chased entirely the wrong causes, so the details may save someone else the same detour.
Environment
python:3.11.10-slim, CPython 3.11.10, linux/amd64 (also reproduced on linux/arm64)LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2,MALLOC_ARENA_MAX=2Reproducer
Single-threaded. No threads involved anywhere.
The input is a synthetic scanned-style document (one grayscale JPEG per page, generated with
new_page()+insert_image()), and it validates cleanly —pypdfandfitzboth read all 67 pages before the loop starts.Observed behaviour
Fails within roughly 5–15 iterations, non-deterministically, in one of several ways:
SIGSEGVpymupdf.mupdf.FzErrorFormat: code=7: cycle in page treeraised frompdf_subset_fonts2Fatal Python error: Segmentation faultwithCurrent thread ... Garbage-collectingMuPDF error: format error: non-page object in page treeon stderrThe key detail: these occur on a freshly opened document, constructed from the same immutable
bytesthat succeeded on earlier iterations. Nothing carries over at the Python level, which points at process-global state corruption rather than a bad input document.Faulthandler traceback from one run:
and from another, after the loop had already printed
OK:Allocator dependence
This is the part that made it hard to find:
LD_PRELOADUnder glibc malloc the corruption appears to stay latent — freed regions are not reused aggressively enough to fault. jemalloc's faster reuse turns it into a hard crash. So on glibc this looks like a stable library, and only the allocator change exposes it.
Flag bisect on 1.24.11 with jemalloc (10 iterations each):
deflate+garbage=4+clean+deflate_images/deflate_fonts+use_objstms=1use_objstms=1is not required — it changes how fast the failure appears, not whether it happens. Longer runs fail without it too, so I would not treat any single flag as the trigger.Downstream impact, for context
In our service the corruption did not surface inside PyMuPDF at all. It detonated on the next allocation-heavy call — a
boto3S3 upload immediately after compression returned — producing a worker SIGSEGV with no Python traceback and a 502 to the caller. Because memory utilisation was flat (~2%) it did not look like a memory bug, and because only large files took this code path it looked file-specific. It took a while to land on the allocator.Two things that might be worth a documentation note, if you think it's warranted:
Document.tobytes()/save()withgarbage/clean/deflate_imageson image-heavy documents was not safe in 1.24.x.LD_PRELOADis common in containerised Python services for fragmentation reasons, so others may be carrying this latently on 1.24.x without ever seeing a crash.Workaround
Upgrading to 1.26.4 resolved it for us — no code changes, same flags. Validated with jemalloc enabled: 150 iterations of our full compression pipeline on a 34MB/67-page scan, plus a 56.5MB/300-page document through compression and splitting, all clean. Confirmed on both amd64 and arm64.
Not asking for anything here — just leaving a record in case it helps someone bisecting the same symptoms, or if you consider it worth a changelog/documentation mention for 1.24.x users.
All reactions