Skip to content

Commit 46d1809

Browse files
serhiy-storchakaE-Paineclaude
authored
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>
1 parent 8107c53 commit 46d1809

3 files changed

Lines changed: 50 additions & 4 deletions

File tree

Lib/test/test_tkinter/test_misc.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
import functools
33
import platform
44
import sys
5+
import textwrap
56
import unittest
67
import weakref
78
import tkinter
89
from tkinter import TclError
910
import enum
1011
from test import support
1112
from test.support import os_helper
13+
from test.support.script_helper import assert_python_ok
1214
from test.test_tkinter.support import setUpModule # noqa: F401
1315
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
1416
requires_tk, get_tk_patchlevel,
@@ -53,6 +55,33 @@ class Button2(tkinter.Button):
5355
b4 = Button2(f2)
5456
self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4)
5557

58+
def test_dealloc_in_wrong_thread(self):
59+
# gh-83274: deallocating the interpreter in the wrong thread must not
60+
# crash.
61+
script = textwrap.dedent("""
62+
import threading
63+
import tkinter
64+
root = tkinter.Tk()
65+
root.destroy()
66+
# Let another thread drop the last reference.
67+
ready = threading.Event()
68+
t = threading.Thread(target=lambda obj: ready.wait(), args=(root,))
69+
t.start()
70+
del root
71+
ready.set()
72+
t.join()
73+
print('ok')
74+
""")
75+
rc, out, err = assert_python_ok('-c', script)
76+
self.assertEqual(out.strip(), b'ok')
77+
if not support.Py_GIL_DISABLED:
78+
# On the free-threaded build the interpreter may instead be
79+
# deallocated in its own thread (deferred reference counting), so
80+
# the warning is not necessarily emitted. The crucial guarantee --
81+
# no crash -- is already checked by assert_python_ok() above.
82+
self.assertIn(b'RuntimeWarning', err)
83+
self.assertIn(b'gh-83274', err)
84+
5685
@requires_tk(8, 6, 6)
5786
def test_tk_busy(self):
5887
root = self.root
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Deallocating a :mod:`tkinter` application from a thread other than the one it
2+
was created in no longer crashes the interpreter. The underlying Tcl
3+
interpreter is leaked instead, and a :exc:`RuntimeWarning` is reported.

Modules/_tkinter.c

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3132,10 +3132,24 @@ Tkapp_Dealloc(PyObject *op)
31323132
{
31333133
TkappObject *self = TkappObject_CAST(op);
31343134
PyTypeObject *tp = Py_TYPE(self);
3135-
/*CHECK_TCL_APPARTMENT;*/
3136-
ENTER_TCL
3137-
Tcl_DeleteInterp(Tkapp_Interp(self));
3138-
LEAVE_TCL
3135+
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
3136+
/* Deleting the interpreter from another thread aborts the process
3137+
("Tcl_AsyncDelete: async handler deleted by the wrong thread").
3138+
Leak it instead (gh-83274). */
3139+
if (PyErr_WarnEx(PyExc_RuntimeWarning,
3140+
"the Tcl interpreter is leaked because it was "
3141+
"deallocated in a thread other than the one it was "
3142+
"created in (see gh-83274)", 1) < 0)
3143+
{
3144+
PyErr_FormatUnraisable("Exception ignored while finalizing "
3145+
"a Tcl interpreter");
3146+
}
3147+
}
3148+
else {
3149+
ENTER_TCL
3150+
Tcl_DeleteInterp(Tkapp_Interp(self));
3151+
LEAVE_TCL
3152+
}
31393153
Py_XDECREF(self->trace);
31403154
PyObject_Free(self);
31413155
Py_DECREF(tp);

0 commit comments

Comments
 (0)