From bc80f6a1b6874d8b06b8d43f03a410f9ab682a28 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 5 Nov 2026 00:31:18 +0100 Subject: [PATCH 1/2] gh-161353: Fix handle leak in test_tcl Fix handle leaks in test_tcl when the test is run with -R 3:3. Call explicitly Tcl_FinalizeThread() in test_returning_deeply_nested() to clean up explicitly per-thread state. The test is run in a thread because of the @run_with_limited_c_stack() decorator. On Windows, Tcl_CreateInterp() creates an event in the Tcl notifier implementation. The event is stored in per-thread state which is only cleaned up at exit. Add _tkinter.finalize_thread() function. --- Lib/test/test_tcl.py | 5 +++++ Modules/_tkinter.c | 15 +++++++++++++++ Modules/clinic/_tkinter.c.h | 20 +++++++++++++++++++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_tcl.py b/Lib/test/test_tcl.py index 1d90ac10ad77ad..8211729fa03670 100644 --- a/Lib/test/test_tcl.py +++ b/Lib/test/test_tcl.py @@ -595,6 +595,11 @@ def test_returning_deeply_nested(self): interp.eval('set x a; for {set i 0} {$i < %d} {incr i} {set x [list $x]}' % support.exceeds_recursion_limit()) self.assertRaises(RecursionError, interp.getvar, 'x') + interp = None + + # gh-161353: Clean up explicitly per-thread state. Prevent false alarm + # of leaked handles on Windows when test_tcl is run with -R 3:3 option. + _tkinter.finalize_thread() def test_user_command(self): result = None diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 9a09687e55b981..3a9f6870b2fdcf 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3584,6 +3584,20 @@ _tkinter_getbusywaitinterval_impl(PyObject *module) return Tkinter_busywaitinterval; } +/*[clinic input] +_tkinter.finalize_thread + +Clean up per-thread state. +[clinic start generated code]*/ + +static PyObject * +_tkinter_finalize_thread_impl(PyObject *module) +/*[clinic end generated code: output=15314b213c7a31f6 input=f5ba0a72c0b4b173]*/ +{ + Tcl_FinalizeThread(); + Py_RETURN_NONE; +} + #include "clinic/_tkinter.c.h" static PyMethodDef Tktt_methods[] = @@ -3681,6 +3695,7 @@ static PyMethodDef moduleMethods[] = _TKINTER_CREATE_METHODDEF _TKINTER_SETBUSYWAITINTERVAL_METHODDEF _TKINTER_GETBUSYWAITINTERVAL_METHODDEF + _TKINTER_FINALIZE_THREAD_METHODDEF {NULL, NULL} }; diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index 1e197167409a03..0f0bdaa05bbdf4 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -956,6 +956,24 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) return return_value; } +PyDoc_STRVAR(_tkinter_finalize_thread__doc__, +"finalize_thread($module, /)\n" +"--\n" +"\n" +"Clean up per-thread state."); + +#define _TKINTER_FINALIZE_THREAD_METHODDEF \ + {"finalize_thread", (PyCFunction)_tkinter_finalize_thread, METH_NOARGS, _tkinter_finalize_thread__doc__}, + +static PyObject * +_tkinter_finalize_thread_impl(PyObject *module); + +static PyObject * +_tkinter_finalize_thread(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _tkinter_finalize_thread_impl(module); +} + #ifndef _TKINTER_TKAPP_CREATEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_CREATEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_CREATEFILEHANDLER_METHODDEF) */ @@ -963,4 +981,4 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */ -/*[clinic end generated code: output=229ae8ed00ee4e40 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=68c678de4905fd15 input=a9049054013a1b77]*/ From 3718c6ff9c2086c0ad0a8bee3a358e9839579bc9 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Nov 2026 17:33:27 +0100 Subject: [PATCH 2/2] Make finalize_thread() private: add underscore prefix --- Lib/test/test_tcl.py | 6 +++--- Modules/_tkinter.c | 8 ++++---- Modules/clinic/_tkinter.c.h | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_tcl.py b/Lib/test/test_tcl.py index 8211729fa03670..31ef1457ef3944 100644 --- a/Lib/test/test_tcl.py +++ b/Lib/test/test_tcl.py @@ -597,9 +597,9 @@ def test_returning_deeply_nested(self): self.assertRaises(RecursionError, interp.getvar, 'x') interp = None - # gh-161353: Clean up explicitly per-thread state. Prevent false alarm - # of leaked handles on Windows when test_tcl is run with -R 3:3 option. - _tkinter.finalize_thread() + # gh-161353: Clean up explicitly per-thread state to avoid leaking an + # event handle on Windows when test_tcl is run with -R 3:3 option. + _tkinter._finalize_thread() def test_user_command(self): result = None diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 3a9f6870b2fdcf..9540839b9682bc 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3585,14 +3585,14 @@ _tkinter_getbusywaitinterval_impl(PyObject *module) } /*[clinic input] -_tkinter.finalize_thread +_tkinter._finalize_thread Clean up per-thread state. [clinic start generated code]*/ static PyObject * -_tkinter_finalize_thread_impl(PyObject *module) -/*[clinic end generated code: output=15314b213c7a31f6 input=f5ba0a72c0b4b173]*/ +_tkinter__finalize_thread_impl(PyObject *module) +/*[clinic end generated code: output=4ed0d3be4d1f8de8 input=ea68096ee9b83d23]*/ { Tcl_FinalizeThread(); Py_RETURN_NONE; @@ -3695,7 +3695,7 @@ static PyMethodDef moduleMethods[] = _TKINTER_CREATE_METHODDEF _TKINTER_SETBUSYWAITINTERVAL_METHODDEF _TKINTER_GETBUSYWAITINTERVAL_METHODDEF - _TKINTER_FINALIZE_THREAD_METHODDEF + _TKINTER__FINALIZE_THREAD_METHODDEF {NULL, NULL} }; diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index 0f0bdaa05bbdf4..22529aaf7bee44 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -956,22 +956,22 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) return return_value; } -PyDoc_STRVAR(_tkinter_finalize_thread__doc__, -"finalize_thread($module, /)\n" +PyDoc_STRVAR(_tkinter__finalize_thread__doc__, +"_finalize_thread($module, /)\n" "--\n" "\n" "Clean up per-thread state."); -#define _TKINTER_FINALIZE_THREAD_METHODDEF \ - {"finalize_thread", (PyCFunction)_tkinter_finalize_thread, METH_NOARGS, _tkinter_finalize_thread__doc__}, +#define _TKINTER__FINALIZE_THREAD_METHODDEF \ + {"_finalize_thread", (PyCFunction)_tkinter__finalize_thread, METH_NOARGS, _tkinter__finalize_thread__doc__}, static PyObject * -_tkinter_finalize_thread_impl(PyObject *module); +_tkinter__finalize_thread_impl(PyObject *module); static PyObject * -_tkinter_finalize_thread(PyObject *module, PyObject *Py_UNUSED(ignored)) +_tkinter__finalize_thread(PyObject *module, PyObject *Py_UNUSED(ignored)) { - return _tkinter_finalize_thread_impl(module); + return _tkinter__finalize_thread_impl(module); } #ifndef _TKINTER_TKAPP_CREATEFILEHANDLER_METHODDEF @@ -981,4 +981,4 @@ _tkinter_finalize_thread(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */ -/*[clinic end generated code: output=68c678de4905fd15 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c94e363a6f9de67d input=a9049054013a1b77]*/