Title: [Bug]: Umbrella issue: matplotlib Free-threaded Extension Analysis Report · Issue #32073 · matplotlib/matplotlib · GitHub
Open Graph Title: [Bug]: Umbrella issue: matplotlib Free-threaded Extension Analysis Report · Issue #32073 · matplotlib/matplotlib
X Title: [Bug]: Umbrella issue: matplotlib Free-threaded Extension Analysis Report · Issue #32073 · matplotlib/matplotlib
Description: Companion issue to #31424, report as of d7bc494: Findings by Priority Findings use global, non-restarting numbers (1–8). The same numbers are used in Recommendations. RACE Findings (fix immediately) — 1 # Finding File:Line Severity Agent...
Open Graph Description: Companion issue to #31424, report as of d7bc494: Findings by Priority Findings use global, non-restarting numbers (1–8). The same numbers are used in Recommendations. RACE Findings (fix immediately...
X Description: Companion issue to #31424, report as of d7bc494: Findings by Priority Findings use global, non-restarting numbers (1–8). The same numbers are used in Recommendations. RACE Findings (fix immediately...
Opengraph URL: https://github.com/matplotlib/matplotlib/issues/32073
X: @github
Domain: Github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"[Bug]: Umbrella issue: matplotlib Free-threaded Extension Analysis Report","articleBody":"Companion issue to #31424, report as of https://github.com/matplotlib/matplotlib/commit/d7bc4947e56f96fc2bc275b98b4270a21d9b7a4c:\n\n## Findings by Priority\n\nFindings use **global, non-restarting numbers** (1–8). The same numbers are used in Recommendations.\n\n### RACE Findings (fix immediately) — 1\n\n| # | Finding | File:Line | Severity | Agents |\n|---|---------|-----------|----------|--------|\n| 1 | **Global `FT_Library _ft2Library` shared across all threads.** Face creation `FT_Open_Face` and destruction `FT_Done_Face` mutate the library's internal face list and memory manager with no lock. FreeType is not thread-safe on a shared `FT_Library`. **Survives the Python thread-keyed font cache** — even a per-thread `FT2Font` is created/destroyed on the one shared library. | `ft2font.cpp:19` (decl), `:200` (`FT_Open_Face`), `:223` (`FT_Done_Face`); `ft2font.h:100` (extern) | **CRITICAL** | shared-state, unsafe-api, lock-discipline, STW (corroborated); orchestrator-verified |\n\n### UNSAFE Findings (fix before declaring free-threading support) — 2\n\n| # | Finding | File:Line | Severity |\n|---|---------|-----------|----------|\n| 2 | **Per-object `FT_Face` mutation via FreeType glyph/charmap/size calls.** `FT_Load_Glyph`, `FT_Set_Charmap`, `FT_Select_Charmap`, `FT_Set_Char_Size` (and `raqm_set_freetype_face`, which binds the same face) mutate the shared `face-\u003eglyph` slot. Unsafe if a single `FT2Font` is used from multiple threads. Latent in matplotlib's default path (thread-keyed cache) but **fully reachable via direct `ft2font.FT2Font(...)` use**, which the GIL-free opt-in explicitly permits. | `ft2font.cpp:249, 275, 280, 477, 556, 606, 642`; `ft2font.cpp:325, 402` (raqm binds face); `ft2font_wrapper.cpp:1770` | **HIGH** |\n| 3 | **`_tkagg` calls Tcl/Tk C API under `mod_gil_not_used()`.** `Tk_PhotoPutBlock` / `Tcl_SetVar` / `Tcl_SetVar2` operate on a `Tcl_Interp`, but Tcl interpreters are thread-apartment-bound and Tk is not free-threading-safe. Calling `blit` from a non-owning thread is undefined. | `_tkagg.cpp:102, 137, 176–184` | **MEDIUM** |\n\n### PROTECT Findings (add synchronization) — 3\n\n| # | Finding | File:Line | Severity |\n|---|---------|-----------|----------|\n| 4 | **`FT2Font` per-object mutable state cluster** — `face`, `glyphs`, `char_to_font`, `fallbacks`, `bbox`, `pen`, `image`, `kerning_factor`, `warn_if_used`. Mutated by nearly every method with no lock. The lock-discipline scanner flagged **17 `critical_section_candidate`s** here. Note the fallback design: an `FT2Font` holds `std::vector\u003cFT2Font*\u003e fallbacks` — if fonts were ever shared across threads, a fallback would be mutated concurrently by multiple parents. (Currently constrained per-thread by the font cache.) | `ft2font.h:183–193` (members); `ft2font_wrapper.cpp` ×17 (e.g. `:319`) | **HIGH** (latent) |\n| 5 | **`RendererAgg` / `BufferRegion` per-object state** — the lock-discipline scanner flagged **10 `critical_section_candidate`s** in the Agg wrapper (per-object buffer/attribute accessors). Safe only while a renderer/buffer is not shared across threads; needs a per-object guard otherwise. | `_backend_agg_wrapper.cpp` ×10 (e.g. `:17`) | **MEDIUM** |\n| 6 | **Write-once/lazy-init global caches.** `_tkagg` caches Tk/Tcl entry points in statics `TK_FIND_PHOTO`, `TK_PHOTO_PUT_BLOCK`, `TCL_SETVAR`, `TCL_SETVAR2` populated via `dlsym`; and `_macosx.m` holds many mutable `static` globals (`backend_inited`, `lastCommand`, …). Verify the Tk pointers are populated exactly once at import (not lazily under a race), and that the `_macosx` globals are confined to the AppKit main thread. | `_tkagg.cpp:90–95, 255–265`; `_macosx.m:61–73, 263` | **LOW** |\n\n### MIGRATE Findings (structural changes) — 2\n\n| # | Finding | File:Line | Severity |\n|---|---------|-----------|----------|\n| 7 | **No synchronization primitive exists despite a project-wide GIL-free opt-in.** Introduce a dedicated `std::mutex` guarding all `_ft2Library` access (Finding 1) and per-object guards for `FT2Font`/`RendererAgg` (Findings 4–5). Use `py::scoped_critical_section` (`\u003cpybind11/critical_section.h\u003e`) where Python objects are involved and a plain `std::mutex` for the pure-C FreeType resource. | project-wide (`src/`) | **MEDIUM** |\n| 8 | **`mod_gil_not_used()` asserted before it is enforced.** Until Findings 1–5 are addressed, the thread-safety assertion on all eight modules is unsound. Either gate the declarations behind the fixes or document the runtime contract (e.g. \"one thread per `FT2Font`/`RendererAgg` object\"). | all wrapper `PYBIND11_MODULE` lines | **MEDIUM** |\n\nhttps://gist.github.com/clin1234/6cdb5150ba3298576ca54fa07c017517#file-matplotlib-md","author":{"url":"https://github.com/clin1234","@type":"Person","name":"clin1234"},"datePublished":"2026-07-19T13:52:10.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/32073/matplotlib/issues/32073"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:ec51e150-8aa6-588a-bc27-4ecee6da00e9 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | BF5A:240318:FDB605:16BFC64:6A6335F4 |
| html-safe-nonce | 4758275d3d602fe08026d36c2828ced7c67a7b2d26f88c79950aff0a7d312f59 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJCRjVBOjI0MDMxODpGREI2MDU6MTZCRkM2NDo2QTYzMzVGNCIsInZpc2l0b3JfaWQiOiI2NTUwMDIwODc1NjE0MzY5MjY4IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | bba59fc1307f0cf5ef01b0c9cc17d11a55c2cd6a2b5fad159fd7e01ca4a20f64 |
| hovercard-subject-tag | issue:4922929913 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/matplotlib/matplotlib/32073/issue_layout |
| twitter:image | https://opengraph.githubassets.com/700960dff75a807b3ac7a671aa4ec05bb99853f93a504f0ced0b288f5a01b630/matplotlib/matplotlib/issues/32073 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/700960dff75a807b3ac7a671aa4ec05bb99853f93a504f0ced0b288f5a01b630/matplotlib/matplotlib/issues/32073 |
| og:image:alt | Companion issue to #31424, report as of d7bc494: Findings by Priority Findings use global, non-restarting numbers (1–8). The same numbers are used in Recommendations. RACE Findings (fix immediately... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | clin1234 |
| hostname | github.com |
| expected-hostname | github.com |
| None | 59e55daad7174ca59d63c6974d58276ccb5477442e550bebb3c035e1bef11c94 |
| turbo-cache-control | no-preview |
| go-import | github.com/matplotlib/matplotlib git https://github.com/matplotlib/matplotlib.git |
| octolytics-dimension-user_id | 215947 |
| octolytics-dimension-user_login | matplotlib |
| octolytics-dimension-repository_id | 1385122 |
| octolytics-dimension-repository_nwo | matplotlib/matplotlib |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 1385122 |
| octolytics-dimension-repository_network_root_nwo | matplotlib/matplotlib |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 990295d92a4cc7b63fbbd83a046217cd7d77d49c |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width