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
Reactions are currently unavailable
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.
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.
GIL not released during Qhull triangulation — 500K-point triangulation holds the GIL for 3+ seconds, reducing background thread throughput to 0.6% of baseline.
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.
ft_glyph_warn calls Python APIs without GIL guard for free-threading — Module import/attr access without GIL on free-threaded builds.
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).
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.
Unchecked PyUnicode_EncodeFSDefault — NULL wrapped in py::bytes.
PyErr_Fetch/PyErr_Restore deprecated since 3.12 — Should migrate to PyErr_GetRaisedException/PyErr_SetRaisedException.
PY_SSIZE_T_CLEAN defines (×3) — No-op since 3.10, can be removed.
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