https://github.com/python/cpython/commit/46d1809ccd4bc0e1439a4696b428d2a1c2edcfbf
commit: 46d1809ccd4bc0e1439a4696b428d2a1c2edcfbf
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-28T00:26:11+03:00
summary:

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 <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst
M Lib/test/test_tkinter/test_misc.py
M Modules/_tkinter.c

diff --git a/Lib/test/test_tkinter/test_misc.py 
b/Lib/test/test_tkinter/test_misc.py
index 4c003e697d23b86..a93f5dee349e648 100644
--- a/Lib/test/test_tkinter/test_misc.py
+++ b/Lib/test/test_tkinter/test_misc.py
@@ -2,6 +2,7 @@
 import functools
 import platform
 import sys
+import textwrap
 import unittest
 import weakref
 import tkinter
@@ -9,6 +10,7 @@
 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 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')
+        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 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 8fa58d07096e30b..1f1fe4a91addf4c 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);

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to