Bug summary
This is an umbrella issue meant to present the findings of an analysis report so matplotlib developers and contributors can discuss them, figure out which are worth fixing, open individual issues and PRs, etc.
Findings by Priority
FIX
-
std::string constructed from NULL ft_error_string — ft_error_string() returns NULL for unknown FreeType error codes. THROW_FT_ERROR constructs std::string{nullptr} — undefined behavior (typically crashes in strlen). Triggered when FreeType returns an error code not in the compiled-in list.
-
Inverted PyErr_Occurred logic in enum type caster — return !(ival == -1 && !PyErr_Occurred()) returns true (success) when PyLong_AsLong fails with an active exception. One-character fix: the ! before PyErr_Occurred() should be removed.
-
Unchecked malloc + leaked Py_buffer in _copy_agg_buffer — malloc(sizeof(Py_buffer)) not NULL-checked. If PyObject_GetBuffer fails, the buffer is leaked.
-
Unchecked PyOS_double_to_string — strlen(NULL) crash — PyOS_double_to_string() can return NULL on OOM. strlen(str) follows without a NULL check. Crash confirmed under OOM injection.
-
FigureCanvas_set_cursor returns NULL without exception — The default: case returns NULL without calling PyErr_Set*, causing SystemError.
-
FigureManager__set_window_mode returns NULL without exception — When self->window is NULL (after destroy()), returns NULL without setting an exception.
-
NULL char* streamed to std::stringstream in ft_glyph_warn — face->family_name can be NULL. When NULL is inserted into std::set<FT_String*> and later streamed via ss << *it, it's undefined behavior.
ft2font.cpp:457, ft2font_wrapper.cpp:410
-
NSFileHandle leak in wake_on_fd_write — [[NSFileHandle alloc] initWithFileDescriptor: fd] is never released.
CONSIDER
-
GIL not released during Agg rendering — draw_path, draw_markers, draw_path_collection, etc. hold the GIL during software rasterization, blocking all threads. _image_wrapper.cpp already demonstrates the correct GIL-release pattern.
_backend_agg_wrapper.cpp:41-215
-
GIL not released during Qhull triangulation — 500K-point triangulation holds the GIL for 3+ seconds, reducing background thread throughput to 0.6% of baseline.
_qhull_wrapper.cpp:138-253
-
Global FT_Library not thread-safe — _ft2Library is shared across all threads. FreeType is not thread-safe for shared library instances.
-
FreeType stream read callback lacks GIL guard for free-threading — Calls Python I/O without GIL guard on free-threaded builds.
ft2font_wrapper.cpp:367-388
-
ft_glyph_warn calls Python APIs without GIL guard for free-threading — Module import/attr access without GIL on free-threaded builds.
ft2font_wrapper.cpp:405-418
-
Global p11x::enums map lacks synchronization — std::unordered_map with py::object values, no synchronization for free-threading.
-
NSTrackingArea leak in FigureCanvas_init — alloc without matching release.
-
Window.close double-decref risk — Py_DECREF(manager) if dealloc runs without prior destroy.
-
PyFT2Font_init leaks on exception — Raw new without unique_ptr. ~59 bytes leaked per failed font construction (corrupt font file), ~131 bytes per failed construction (failing file-like object).
ft2font_wrapper.cpp:457-505
-
Timer NSTimer captures raw self pointer — Block captures Python object without Py_INCREF.
-
View stores raw canvas pointer — No Py_INCREF, potential dangling pointer.
-
Exception clobbering in ft2font catch-all — catch(std::exception&) replaces meaningful errors (including MemoryError, KeyboardInterrupt) with generic TypeError.
ft2font_wrapper.cpp:492-494
-
Unchecked PyUnicode_EncodeFSDefault — NULL wrapped in py::bytes.
-
PyErr_Fetch/PyErr_Restore deprecated since 3.12 — Should migrate to PyErr_GetRaisedException/PyErr_SetRaisedException.
ft2font_wrapper.cpp:394,402
-
PY_SSIZE_T_CLEAN defines (×3) — No-op since 3.10, can be removed.
mplutils.h, py_adaptors.h, _macosx.m
-
Solaris _XPG4/_XPG3 undefs — Likely dead code.
-
macOS SDK < 10.14 compat defines — Likely dead code.
Code for reproduction
See https://gist.github.com/devdanzin/2a79188c7a41a03b1476544c7dd775c1#matplotlib-c-extension--reproducer-appendix for reproducers.
Actual outcome
Expected outcome
Additional information
The issue above is part of an analysis report created by cext-review-toolkit, a Claude Code plugin for reviewing CPython C extensions. It covers the C/C++ source in src/ (13 source files, 9 extension modules — 8 pybind11 + 1 raw Python/C API in _macosx.m).
Some findings may be false positives or low-priority issues not worth fixing. Feedback on any misclassified findings is welcome.
The full report (including a reproducers appendix) is available at https://gist.github.com/devdanzin/2a79188c7a41a03b1476544c7dd775c1.
Operating system
Linux
Matplotlib Version
main
Matplotlib Backend
No response
Python version
No response
Jupyter version
No response
Installation
git checkout
Bug summary
This is an umbrella issue meant to present the findings of an analysis report so matplotlib developers and contributors can discuss them, figure out which are worth fixing, open individual issues and PRs, etc.
Findings by Priority
FIX
std::stringconstructed from NULLft_error_string—ft_error_string()returns NULL for unknown FreeType error codes.THROW_FT_ERRORconstructsstd::string{nullptr}— undefined behavior (typically crashes instrlen). Triggered when FreeType returns an error code not in the compiled-in list.ft2font.h:39,52Inverted
PyErr_Occurredlogic in enum type caster —return !(ival == -1 && !PyErr_Occurred())returnstrue(success) whenPyLong_AsLongfails with an active exception. One-character fix: the!beforePyErr_Occurred()should be removed._enums.h:83Unchecked
malloc+ leakedPy_bufferin_copy_agg_buffer—malloc(sizeof(Py_buffer))not NULL-checked. IfPyObject_GetBufferfails, the buffer is leaked._macosx.m:1221-1225Unchecked
PyOS_double_to_string—strlen(NULL)crash —PyOS_double_to_string()can return NULL on OOM.strlen(str)follows without a NULL check. Crash confirmed under OOM injection._path.h:1070-1073FigureCanvas_set_cursorreturns NULL without exception — Thedefault:case returns NULL without callingPyErr_Set*, causingSystemError._macosx.m:451FigureManager__set_window_modereturns NULL without exception — Whenself->windowis NULL (afterdestroy()), returns NULL without setting an exception._macosx.m:653-654NULL
char*streamed tostd::stringstreaminft_glyph_warn—face->family_namecan be NULL. When NULL is inserted intostd::set<FT_String*>and later streamed viass << *it, it's undefined behavior.ft2font.cpp:457,ft2font_wrapper.cpp:410NSFileHandleleak inwake_on_fd_write—[[NSFileHandle alloc] initWithFileDescriptor: fd]is never released._macosx.m:246-257CONSIDER
GIL not released during Agg rendering —
draw_path,draw_markers,draw_path_collection, etc. hold the GIL during software rasterization, blocking all threads._image_wrapper.cppalready demonstrates the correct GIL-release pattern._backend_agg_wrapper.cpp:41-215GIL not released during Qhull triangulation — 500K-point triangulation holds the GIL for 3+ seconds, reducing background thread throughput to 0.6% of baseline.
_qhull_wrapper.cpp:138-253Global
FT_Librarynot thread-safe —_ft2Libraryis shared across all threads. FreeType is not thread-safe for shared library instances.ft2font.cpp:44FreeType stream read callback lacks GIL guard for free-threading — Calls Python I/O without GIL guard on free-threaded builds.
ft2font_wrapper.cpp:367-388ft_glyph_warncalls Python APIs without GIL guard for free-threading — Module import/attr access without GIL on free-threaded builds.ft2font_wrapper.cpp:405-418Global
p11x::enumsmap lacks synchronization —std::unordered_mapwithpy::objectvalues, no synchronization for free-threading._enums.h:37NSTrackingArealeak inFigureCanvas_init—allocwithout matchingrelease._macosx.m:378-382Window.closedouble-decref risk —Py_DECREF(manager)ifdeallocruns without priordestroy._macosx.m:1177-1188PyFT2Font_initleaks on exception — Rawnewwithoutunique_ptr. ~59 bytes leaked per failed font construction (corrupt font file), ~131 bytes per failed construction (failing file-like object).ft2font_wrapper.cpp:457-505Timer NSTimer captures raw
selfpointer — Block captures Python object withoutPy_INCREF._macosx.m:1807-1816View stores raw
canvaspointer — NoPy_INCREF, potential dangling pointer._macosx.m:139Exception clobbering in ft2font catch-all —
catch(std::exception&)replaces meaningful errors (includingMemoryError,KeyboardInterrupt) with genericTypeError.ft2font_wrapper.cpp:492-494Unchecked
PyUnicode_EncodeFSDefault— NULL wrapped inpy::bytes._tkagg.cpp:333-335PyErr_Fetch/PyErr_Restoredeprecated since 3.12 — Should migrate toPyErr_GetRaisedException/PyErr_SetRaisedException.ft2font_wrapper.cpp:394,402PY_SSIZE_T_CLEANdefines (×3) — No-op since 3.10, can be removed.mplutils.h,py_adaptors.h,_macosx.mSolaris
_XPG4/_XPG3undefs — Likely dead code.mplutils.h:22-28macOS SDK < 10.14 compat defines — Likely dead code.
_macosx.m:11-16Code for reproduction
See https://gist.github.com/devdanzin/2a79188c7a41a03b1476544c7dd775c1#matplotlib-c-extension--reproducer-appendix for reproducers.
Actual outcome
Expected outcome
Additional information
The issue above is part of an analysis report created by cext-review-toolkit, a Claude Code plugin for reviewing CPython C extensions. It covers the C/C++ source in
src/(13 source files, 9 extension modules — 8 pybind11 + 1 raw Python/C API in_macosx.m).Some findings may be false positives or low-priority issues not worth fixing. Feedback on any misclassified findings is welcome.
The full report (including a reproducers appendix) is available at https://gist.github.com/devdanzin/2a79188c7a41a03b1476544c7dd775c1.
Operating system
Linux
Matplotlib Version
main
Matplotlib Backend
No response
Python version
No response
Jupyter version
No response
Installation
git checkout