From e9d1bb85b8b3be907f666efe73a3288ec4840b05 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Tue, 20 Oct 2026 13:49:07 +0800 Subject: [PATCH] fix: shim python create thread to audit threads --- src/slic3r/plugin/PythonInterpreter.cpp | 6 ++ src/slic3r/plugin/PythonPluginBridge.cpp | 87 ++++++++++++++++++++++++ src/slic3r/plugin/PythonPluginBridge.hpp | 9 +++ 3 files changed, 102 insertions(+) diff --git a/src/slic3r/plugin/PythonInterpreter.cpp b/src/slic3r/plugin/PythonInterpreter.cpp index 89dc91723c..ca0dfa16f4 100644 --- a/src/slic3r/plugin/PythonInterpreter.cpp +++ b/src/slic3r/plugin/PythonInterpreter.cpp @@ -2,6 +2,7 @@ #include "GeneratedConfig.hpp" #include "libslic3r/Utils.hpp" #include "PluginAuditManager.hpp" +#include "PythonPluginBridge.hpp" #include #include #include @@ -664,6 +665,11 @@ bool PythonInterpreter::initialize() // background-thread exceptions) to /log/python_*.log. install_python_stderr_redirect(); + // Attribute plugin-spawned Python threads to the plugin/capability that created them. + // Must run while the GIL is still held here, after the audit hook is in place and + // before the GIL is released below. + install_python_thread_audit_shim(); + // Release the GIL so other threads can acquire it via PyGILState_Ensure. // Without this, calls from background threads will block trying to acquire the GIL. m_main_thread_state = PyEval_SaveThread(); diff --git a/src/slic3r/plugin/PythonPluginBridge.cpp b/src/slic3r/plugin/PythonPluginBridge.cpp index 103de8e362..60ecf8bfe7 100644 --- a/src/slic3r/plugin/PythonPluginBridge.cpp +++ b/src/slic3r/plugin/PythonPluginBridge.cpp @@ -603,6 +603,93 @@ request aborts the plugin load.)pbdoc"); } +void install_python_thread_audit_shim() +{ + // Runs with the GIL held from PythonInterpreter::initialize(). CPython's own thread bootstrap + // calls the target with no knowledge of the audit context that created it: the calling + // thread is the only place the plugin key and capability are still available. Capture them + // here, then re-establish the same ScopedPluginAuditContext inside the spawned thread before + // the plugin body runs. The identity lives only in this C++ closure -- no Python-visible + // setter is added. + try { + py::object thread_module = py::module_::import("_thread"); + + const int installed = PyObject_HasAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed"); + if (installed == 1) { + BOOST_LOG_TRIVIAL(debug) << "[AUDIT] Python thread attribution shim already installed"; + return; + } + if (installed < 0) + PyErr_Clear(); + + py::object original = py::reinterpret_steal( + PyObject_GetAttrString(thread_module.ptr(), "start_new_thread")); + if (!original || !PyCallable_Check(original.ptr())) { + PyErr_Clear(); + BOOST_LOG_TRIVIAL(error) << "[AUDIT] _thread.start_new_thread is missing or not callable; " + "plugin threads will run without audit attribution"; + return; + } + + py::object shim = py::cpp_function( + [original](py::object function, py::object args, py::object kwargs) -> py::object { + const std::string plugin_key = PluginAuditManager::instance().current_plugin(); + if (plugin_key.empty()) { + // Not called from a plugin: preserve the stock implementation untouched. + return original(function, args, kwargs); + } + const std::string capability = PluginAuditManager::instance().current_capability(); + + py::object guarded = py::cpp_function( + [function, plugin_key, capability](py::args call_args, py::kwargs call_kwargs) -> py::object { + ScopedPluginAuditContext audit_scope(plugin_key, capability); + PyObject* result = PyObject_Call(function.ptr(), call_args.ptr(), call_kwargs.ptr()); + if (result == nullptr) + throw py::error_already_set(); + return py::reinterpret_steal(result); + }); + return original(guarded, args, kwargs); + }, + py::arg("function"), py::arg("args"), py::arg("kwargs") = py::none()); + + if (PyObject_SetAttrString(thread_module.ptr(), "start_new_thread", shim.ptr()) < 0) { + PyErr_Clear(); + BOOST_LOG_TRIVIAL(error) + << "[AUDIT] Failed to install Python thread attribution shim on _thread.start_new_thread"; + return; + } + if (PyObject_SetAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed", Py_True) < 0) + PyErr_Clear(); + + // threading.py caches the function at import time (as `_start_new_thread` in 3.12, and as + // `start_new_thread` in older versions). Patch the cached reference too when the module is + // already loaded; otherwise its Thread.start() would keep calling the unwrapped original. + py::object sys_modules = py::module_::import("sys").attr("modules"); + PyObject* threading_module = PyDict_GetItemString(sys_modules.ptr(), "threading"); + if (threading_module && threading_module != Py_None) { + const char* cached_names[] = {"_start_new_thread", "start_new_thread"}; + for (const char* cached_name : cached_names) { + const int has_cached_name = PyObject_HasAttrString(threading_module, cached_name); + if (has_cached_name < 0) { + PyErr_Clear(); + continue; + } + if (has_cached_name != 1) + continue; + if (PyObject_SetAttrString(threading_module, cached_name, shim.ptr()) < 0) { + PyErr_Clear(); + BOOST_LOG_TRIVIAL(warning) << "[AUDIT] Failed to update threading." << cached_name + << " with the Python thread attribution shim"; + } + } + } + + BOOST_LOG_TRIVIAL(info) << "[AUDIT] Python thread attribution shim installed"; + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install Python thread attribution shim: " << ex.what(); + } +} + } // namespace Slic3r #ifdef ORCA_PYTHON_STUBGEN_MODULE diff --git a/src/slic3r/plugin/PythonPluginBridge.hpp b/src/slic3r/plugin/PythonPluginBridge.hpp index 5178c6f5db..39d6191702 100644 --- a/src/slic3r/plugin/PythonPluginBridge.hpp +++ b/src/slic3r/plugin/PythonPluginBridge.hpp @@ -13,6 +13,15 @@ namespace Slic3r { class PluginCapabilityInterface; } namespace Slic3r { +// Install the thread-attribution shim: wraps `_thread.start_new_thread` (and the reference +// cached by an already-imported `threading` module) so a thread spawned from inside a plugin +// call runs with that call's plugin key/capability audit scope. The captured identity is held +// in a private C++ wrapper; nothing is exposed to Python that could set it. +// +// Call with the GIL held, after PluginAuditManager::install_hook() and before the interpreter +// releases the GIL. Safe to call more than once per interpreter. +void install_python_thread_audit_shim(); + // One materialized capability returned from finalize_plugin_capture: the C++ instance // and its resolved get_name() (cached while the GIL was held). struct CapturedCapability