Skip to content

[Bug]: Umbrella issue: matplotlib C/C++ Extension Analysis Report #31424

@devdanzin

Description

@devdanzin

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

  1. std::string constructed from NULL ft_error_stringft_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.

    • ft2font.h:39,52
  2. Inverted PyErr_Occurred logic in enum type casterreturn !(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.

    • _enums.h:83
  3. Unchecked malloc + leaked Py_buffer in _copy_agg_buffermalloc(sizeof(Py_buffer)) not NULL-checked. If PyObject_GetBuffer fails, the buffer is leaked.

    • _macosx.m:1221-1225
  4. Unchecked PyOS_double_to_stringstrlen(NULL) crashPyOS_double_to_string() can return NULL on OOM. strlen(str) follows without a NULL check. Crash confirmed under OOM injection.

    • _path.h:1070-1073
  5. FigureCanvas_set_cursor returns NULL without exception — The default: case returns NULL without calling PyErr_Set*, causing SystemError.

    • _macosx.m:451
  6. FigureManager__set_window_mode returns NULL without exception — When self->window is NULL (after destroy()), returns NULL without setting an exception.

    • _macosx.m:653-654
  7. NULL char* streamed to std::stringstream in ft_glyph_warnface->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
  8. NSFileHandle leak in wake_on_fd_write[[NSFileHandle alloc] initWithFileDescriptor: fd] is never released.

    • _macosx.m:246-257

CONSIDER

  1. GIL not released during Agg renderingdraw_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
  2. 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
  3. Global FT_Library not thread-safe_ft2Library is shared across all threads. FreeType is not thread-safe for shared library instances.

    • ft2font.cpp:44
  4. 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
  5. 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
  6. Global p11x::enums map lacks synchronizationstd::unordered_map with py::object values, no synchronization for free-threading.

    • _enums.h:37
  7. NSTrackingArea leak in FigureCanvas_initalloc without matching release.

    • _macosx.m:378-382
  8. Window.close double-decref riskPy_DECREF(manager) if dealloc runs without prior destroy.

    • _macosx.m:1177-1188
  9. 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
  10. Timer NSTimer captures raw self pointer — Block captures Python object without Py_INCREF.

    • _macosx.m:1807-1816
  11. View stores raw canvas pointer — No Py_INCREF, potential dangling pointer.

    • _macosx.m:139
  12. Exception clobbering in ft2font catch-allcatch(std::exception&) replaces meaningful errors (including MemoryError, KeyboardInterrupt) with generic TypeError.

    • ft2font_wrapper.cpp:492-494
  13. Unchecked PyUnicode_EncodeFSDefault — NULL wrapped in py::bytes.

    • _tkagg.cpp:333-335
  14. PyErr_Fetch/PyErr_Restore deprecated since 3.12 — Should migrate to PyErr_GetRaisedException/PyErr_SetRaisedException.

    • ft2font_wrapper.cpp:394,402
  15. PY_SSIZE_T_CLEAN defines (×3) — No-op since 3.10, can be removed.

    • mplutils.h, py_adaptors.h, _macosx.m
  16. Solaris _XPG4/_XPG3 undefs — Likely dead code.

    • mplutils.h:22-28
  17. macOS SDK < 10.14 compat defines — Likely dead code.

    • _macosx.m:11-16

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions