Skip to content

Commit 0b4ebe5

Browse files
serhiy-storchakaE-Paineclaude
committed
[3.13] 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. (cherry picked from commit 46d1809) Co-authored-by: E. Paine <63801254+E-Paine@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f7eae63 commit 0b4ebe5

3 files changed

Lines changed: 51 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
@@ -1,13 +1,15 @@
11
import functools
22
import platform
33
import sys
4+
import textwrap
45
import unittest
56
import weakref
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,33 @@ 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+
if not support.Py_GIL_DISABLED:
77+
# On the free-threaded build the interpreter may instead be
78+
# deallocated in its own thread (deferred reference counting), so
79+
# the warning is not necessarily emitted. The crucial guarantee --
80+
# no crash -- is already checked by assert_python_ok() above.
81+
self.assertIn(b'RuntimeWarning', err)
82+
self.assertIn(b'gh-83274', err)
83+
5584
@requires_tk(8, 6, 6)
5685
def test_tk_busy(self):
5786
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: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3064,10 +3064,25 @@ static void
30643064
Tkapp_Dealloc(PyObject *self)
30653065
{
30663066
PyObject *tp = (PyObject *) Py_TYPE(self);
3067-
/*CHECK_TCL_APPARTMENT;*/
3068-
ENTER_TCL
3069-
Tcl_DeleteInterp(Tkapp_Interp(self));
3070-
LEAVE_TCL
3067+
if (((TkappObject *)self)->threaded &&
3068+
((TkappObject *)self)->thread_id != Tcl_GetCurrentThread()) {
3069+
/* Deleting the interpreter from another thread aborts the process
3070+
("Tcl_AsyncDelete: async handler deleted by the wrong thread").
3071+
Leak it instead (gh-83274). */
3072+
if (PyErr_WarnEx(PyExc_RuntimeWarning,
3073+
"the Tcl interpreter is leaked because it was "
3074+
"deallocated in a thread other than the one it was "
3075+
"created in (see gh-83274)", 1) < 0)
3076+
{
3077+
PyErr_FormatUnraisable("Exception ignored while finalizing "
3078+
"a Tcl interpreter");
3079+
}
3080+
}
3081+
else {
3082+
ENTER_TCL
3083+
Tcl_DeleteInterp(Tkapp_Interp(self));
3084+
LEAVE_TCL
3085+
}
30713086
Py_XDECREF(((TkappObject *)self)->trace);
30723087
PyObject_Free(self);
30733088
Py_DECREF(tp);

0 commit comments

Comments
 (0)