Repository navigation
Fix segfault when a get_cdrawings() callback raises - #5139
julian-smith-artifex-com merged 1 commit into
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
|
Many thanks for figuring this out and creating this PR. I wonder whether we could simplify the new code though. Once we call So we'd end up with a smaller patch and simpler code: |
d95db3c to
f8d54d5
Compare
|
Thanks, that makes sense. I've updated the PR to your version (plus a two-line comment on why |
|
Thanks for updating. We've just pushed some quite extensive changes, so could you rebase on top of latest branch main? I don't think this will have any conflicts. Then we can run the test_quick workflow here and merge. |
jm_append_merge() reports a failed callback with messagef() before calling
PyErr_Clear(), so messagev() runs with the callback's exception still set.
Its function-local statics were initialised on the first call with
PyImport_ImportModule("pymupdf"), which returns NULL while an exception is
set, and the NULL was then passed to PyObject_GetAttrString(), so the first
exception raised by a callback in a process killed the interpreter with
SIGSEGV instead of being reported. Once the statics were set, later calls
still called pymupdf.message() with the exception set, which prints only
part of the message.
messagev() now puts any pending exception aside with PyErr_Fetch() before
its static variables are initialised, and restores it with PyErr_Restore()
after the message is output, discarding any error from the output itself in
that case; with no exception pending, an error from the output is left set
as before. The behaviour for a failing callback is otherwise unchanged: the
failure is reported and the exception is cleared.
tests/test_drawings.py:test_cdrawings_callback_exception() runs the case in
a child process, because the crash only shows when there has been no
earlier messagef() call in the process, and checks the message is output
whole.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
f8d54d5 to
4426213
Compare
|
[I've fixed Aptest so that test_quick only needs https access to pymupdf repositories, so tests have now run successfully.] I'll merge the PR now. Thanks again for your contribution! |
Problem
If a callback passed to
Page.get_cdrawings(callback=...)raises, the Python process dies with SIGSEGV instead of the failure being reported:Reproduced with the released wheels of 1.27.1, 1.28.0 and 1.28.2 on macOS arm64 (Python 3.14). The code involved is unchanged on
main.Root cause
jm_append_merge()reports a failed callback withmessagef("calling cdrawings callback function/method failed!")and only then callsPyErr_Clear(), somessagev()runs with the callback's exception still set.messagev()initialises two function-local statics on its first call:With an exception set,
PyImport_ImportModule()returns NULL (CPython raisesSystemError: ... returned a result with an exception set), and the NULL is passed toPyObject_GetAttrString(). The crash report showsPyObject_GetAttrString+24undermessagef()underjm_append_merge(), faulting at address 0x8.It only happens when there has been no earlier
messagef()call in the process. Once the statics have been initialised by a call with no exception pending, the same callback failure does not crash, butpymupdf.message()is still called with the exception set, and only part of the message is printed (the trailing newline is lost).Fix
messagev()now puts any pending exception aside withPyErr_Fetch()before its static variables are initialised, and restores it withPyErr_Restore()after the message is output, discarding any error from outputting the message in that case. With no exception pending, an error from the output is left set, as before. (Simplified as suggested in review.)Otherwise the behaviour of a failing callback is unchanged: the failure is reported through
pymupdf.message()and the exception is cleared, and extraction continues. Whether the callback's exception (includingKeyboardInterrupt) should instead propagate to the caller ofget_cdrawings()is a separate question that I have left alone.Testing
tests/test_drawings.py:test_cdrawings_callback_exception()runs the example above in a child process, because the crash depends on there having been no earliermessagef()call in the process, and asserts that the child exits 0, printed the whole message line, and printedreturned. Like the other child-process tests it is skipped on Pyodide.Built locally from the first version of this branch with the default MuPDF 1.28.2 on macOS arm64, Python 3.14.7,
PYMUPDF_SETUP_MUPDF_TESSERACT=0:test_cdrawings_callback_exceptionfails withsrc/extra.ireverted tomain(the child dies with SIGSEGV) and passes with the fix.tests/test_drawings.py,tests/test_insertpdf.pyandtests/test_cluster_drawings.py: 26 passed.For the current, simplified version I ran this repo's
test_quickworkflow on my fork (Linux, Python 3.12):Unrelated to this change, but in case it saves someone time: the build used SWIG 4.4.1, because SWIG 4.5.0 (what pip installs by default on macOS with Python 3.13+) fails to compile MuPDF 1.28.2's generated
mupdfcpp_swigcode withuse of undeclared identifier 'PyString_FromString'.🤖 Generated with Claude Code