Skip to content

Commit 836ac1f

Browse files
serhiy-storchakaE-Paineclaude
committed
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 <noreply@anthropic.com>
1 parent 5a549e8 commit 836ac1f

3 files changed

Lines changed: 45 additions & 4 deletions

File tree

Lib/test/test_tkinter/test_misc.py

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

57+
def test_dealloc_in_wrong_thread(self):
58+
# gh-83274: deallocating the interpreter in the wrong thread must not
59+
# crash.
60+
script = textwrap.dedent("""
61+
import threading
62+
import tkinter
63+
root = tkinter.Tk()
64+
root.destroy()
65+
# Let another thread drop the last reference.
66+
ready = threading.Event()
67+
t = threading.Thread(target=lambda obj: ready.wait(), args=(root,))
68+
t.start()
69+
del root
70+
ready.set()
71+
t.join()
72+
print('ok')
73+
""")
74+
rc, out, err = assert_python_ok('-c', script)
75+
self.assertEqual(out.strip(), b'ok')
76+
self.assertIn(b'RuntimeWarning', err)
77+
self.assertIn(b'gh-83274', err)
78+
5579
@requires_tk(8, 6, 6)
5680
def test_tk_busy(self):
5781
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
@@ -3129,10 +3129,24 @@ Tkapp_Dealloc(PyObject *op)
31293129
{
31303130
TkappObject *self = TkappObject_CAST(op);
31313131
PyTypeObject *tp = Py_TYPE(self);
3132-
/*CHECK_TCL_APPARTMENT;*/
3133-
ENTER_TCL
3134-
Tcl_DeleteInterp(Tkapp_Interp(self));
3135-
LEAVE_TCL
3132+
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
3133+
/* Deleting the interpreter from another thread aborts the process
3134+
("Tcl_AsyncDelete: async handler deleted by the wrong thread").
3135+
Leak it instead (gh-83274). */
3136+
if (PyErr_WarnEx(PyExc_RuntimeWarning,
3137+
"the Tcl interpreter is leaked because it was "
3138+
"deallocated in a thread other than the one it was "
3139+
"created in (see gh-83274)", 1) < 0)
3140+
{
3141+
PyErr_FormatUnraisable("Exception ignored while finalizing "
3142+
"a Tcl interpreter");
3143+
}
3144+
}
3145+
else {
3146+
ENTER_TCL
3147+
Tcl_DeleteInterp(Tkapp_Interp(self));
3148+
LEAVE_TCL
3149+
}
31363150
Py_XDECREF(self->trace);
31373151
PyObject_Free(self);
31383152
Py_DECREF(tp);

0 commit comments

Comments
 (0)