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 <noreply@anthropic.com>
This commit is contained in:
Serhiy Storchaka
2026-06-28 00:26:11 +03:00
committed by GitHub
parent 8107c53f0f
commit 46d1809ccd
3 changed files with 50 additions and 4 deletions
+29
View File
@@ -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
@@ -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.
+18 -4
View File
@@ -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);