From: Serhiy Storchaka Date: Sat, 27 Jun 2026 21:26:11 +0000 (+0300) Subject: gh-83274: Don't crash when a Tcl interpreter is deallocated in the wrong thread ... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=46d1809ccd4bc0e1439a4696b428d2a1c2edcfbf;p=thirdparty%2FPython%2Fcpython.git gh-83274: Don't crash when a Tcl interpreter is deallocated in the wrong thread (GH-152323) Deallocating the interpreter from a thread other than the one it was created in ran Tcl_DeleteInterp() there, which makes Tcl abort the process ("Tcl_AsyncDelete: async handler deleted by the wrong thread"). Tkapp_Dealloc() now leaks the interpreter in that case and reports a RuntimeWarning instead. Co-Authored-By: E. Paine <63801254+E-Paine@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 --- diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 4c003e697d23..a93f5dee349e 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -2,6 +2,7 @@ import collections.abc import functools import platform import sys +import textwrap import unittest import weakref import tkinter @@ -9,6 +10,7 @@ from tkinter import TclError import enum from test import support from test.support import os_helper +from test.support.script_helper import assert_python_ok from test.test_tkinter.support import setUpModule # noqa: F401 from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest, requires_tk, get_tk_patchlevel, @@ -53,6 +55,33 @@ class MiscTest(AbstractTkTest, unittest.TestCase): b4 = Button2(f2) self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4) + def test_dealloc_in_wrong_thread(self): + # gh-83274: deallocating the interpreter in the wrong thread must not + # crash. + script = textwrap.dedent(""" + import threading + import tkinter + root = tkinter.Tk() + root.destroy() + # Let another thread drop the last reference. + ready = threading.Event() + t = threading.Thread(target=lambda obj: ready.wait(), args=(root,)) + t.start() + del root + ready.set() + t.join() + print('ok') + """) + rc, out, err = assert_python_ok('-c', script) + self.assertEqual(out.strip(), b'ok') + if not support.Py_GIL_DISABLED: + # On the free-threaded build the interpreter may instead be + # deallocated in its own thread (deferred reference counting), so + # the warning is not necessarily emitted. The crucial guarantee -- + # no crash -- is already checked by assert_python_ok() above. + self.assertIn(b'RuntimeWarning', err) + self.assertIn(b'gh-83274', err) + @requires_tk(8, 6, 6) def test_tk_busy(self): root = self.root diff --git a/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst b/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst new file mode 100644 index 000000000000..3b722d2176be --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst @@ -0,0 +1,3 @@ +Deallocating a :mod:`tkinter` application from a thread other than the one it +was created in no longer crashes the interpreter. The underlying Tcl +interpreter is leaked instead, and a :exc:`RuntimeWarning` is reported. diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 8fa58d07096e..1f1fe4a91add 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3132,10 +3132,24 @@ Tkapp_Dealloc(PyObject *op) { TkappObject *self = TkappObject_CAST(op); PyTypeObject *tp = Py_TYPE(self); - /*CHECK_TCL_APPARTMENT;*/ - ENTER_TCL - Tcl_DeleteInterp(Tkapp_Interp(self)); - LEAVE_TCL + if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { + /* Deleting the interpreter from another thread aborts the process + ("Tcl_AsyncDelete: async handler deleted by the wrong thread"). + Leak it instead (gh-83274). */ + if (PyErr_WarnEx(PyExc_RuntimeWarning, + "the Tcl interpreter is leaked because it was " + "deallocated in a thread other than the one it was " + "created in (see gh-83274)", 1) < 0) + { + PyErr_FormatUnraisable("Exception ignored while finalizing " + "a Tcl interpreter"); + } + } + else { + ENTER_TCL + Tcl_DeleteInterp(Tkapp_Interp(self)); + LEAVE_TCL + } Py_XDECREF(self->trace); PyObject_Free(self); Py_DECREF(tp);