From 836ac1f7267d4aa68d08b1bfa1884e026e554d21 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 26 Jun 2026 20:24:05 +0300 Subject: [PATCH 1/2] gh-83274: Don't crash when a Tcl interpreter is deallocated in the wrong thread 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 --- Lib/test/test_tkinter/test_misc.py | 24 +++++++++++++++++++ ...6-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst | 3 +++ Modules/_tkinter.c | 22 +++++++++++++---- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 98af4d822dadc43..bf8652afda7b25c 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -2,12 +2,14 @@ import functools import platform import sys +import textwrap import unittest import tkinter 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, @@ -52,6 +54,28 @@ class Button2(tkinter.Button): 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') + 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 000000000000000..3b722d2176be920 --- /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 86b1b2d9a534d8e..c104b077a5e74f9 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3129,10 +3129,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); From ee52c8ee6c64ef24f476fa189dc5b8d46edc8e76 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 26 Jun 2026 21:47:33 +0300 Subject: [PATCH 2/2] gh-83274: Skip the wrong-thread warning check on the free-threaded build On the free-threaded build the interpreter may be deallocated in its own thread (deferred reference counting), so the RuntimeWarning is not emitted. The no-crash guarantee is still checked by assert_python_ok(). Co-Authored-By: Claude Opus 4.8 --- Lib/test/test_tkinter/test_misc.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index bf8652afda7b25c..53b8cf68ef6bf87 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -73,8 +73,13 @@ def test_dealloc_in_wrong_thread(self): """) rc, out, err = assert_python_ok('-c', script) self.assertEqual(out.strip(), b'ok') - self.assertIn(b'RuntimeWarning', err) - self.assertIn(b'gh-83274', err) + 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):