diff --git a/Lib/test/test_tcl.py b/Lib/test/test_tcl.py index b0458e46c6bece..c79af770269a69 100644 --- a/Lib/test/test_tcl.py +++ b/Lib/test/test_tcl.py @@ -565,6 +565,36 @@ class List(list): self.assertEqual(self.passValue(values := [Value(), "pad"]), expect) self.assertEqual(self.passValue(values := List([Value(), "pad"])), expect) + def test_passing_deeply_nested(self): + call = self.interp.tk.call + setvar = self.interp.tk.setvar + for wrap in (lambda x: [x]), (lambda x: (x,)): + value = 'a' + for i in range(1001): + value = wrap(value) + self.assertEqual(int(call('string', 'length', value)), 1) + value = wrap(value) + self.assertRaises(ValueError, call, 'set', '_', value) + self.assertRaises(ValueError, setvar, '_', value) + for i in range(support.exceeds_recursion_limit()): + value = wrap(value) + self.assertRaises(ValueError, call, 'set', '_', value) + self.assertRaises(ValueError, setvar, '_', value) + value = ['a', 1] + value.append(value) + self.assertRaises(ValueError, call, 'set', '_', value) + self.assertRaises(ValueError, setvar, '_', value) + + @support.run_with_limited_c_stack() + def test_returning_deeply_nested(self): + if not self.wantobjects: + self.skipTest('requires wantobjects') + # The interpreter can only be used in the thread that created it. + interp = Tcl() + interp.eval('set x a; for {set i 0} {$i < %d} {incr i} {set x [list $x]}' + % support.exceeds_recursion_limit()) + self.assertRaises(RecursionError, interp.getvar, 'x') + def test_user_command(self): result = None def testfunc(arg): diff --git a/Misc/NEWS.d/next/Library/2026-10-22-00-29-50.gh-issue-160176.tkNst1.rst b/Misc/NEWS.d/next/Library/2026-10-22-00-29-50.gh-issue-160176.tkNst1.rst new file mode 100644 index 00000000000000..5f11a8237ba81a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-00-29-50.gh-issue-160176.tkNst1.rst @@ -0,0 +1,4 @@ +Fix a crash in :mod:`tkinter` when a deeply nested or recursive list or +tuple is passed to Tcl, or a deeply nested Tcl list is converted to Python. +These now raise :exc:`ValueError` (beyond 1000 levels) and +:exc:`RecursionError` respectively. diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 07798dfaa42a0c..e665a2d3470971 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -1047,13 +1047,21 @@ asBignumObj(PyObject *value) return result; } -static Tcl_Obj* AsObj(PyObject *value); +static Tcl_Obj* AsObjNested(PyObject *value, int depth); static Tcl_Obj* -TupleAsObj(PyObject *value, int wrapped) +TupleAsObj(PyObject *value, int wrapped, int depth) { Tcl_Obj *result = NULL; Py_ssize_t size = PyTuple_GET_SIZE(value); + /* The same limit as in _flatten1(). It bounds not only the recursion + here, but also the recursion in Tcl when it generates the string + representation of the nested list, so a C stack check is not enough. */ + if (depth > 1000) { + PyErr_SetString(PyExc_ValueError, + "nesting too deep in conversion to Tcl object"); + return NULL; + } if (size == 0) { return Tcl_NewListObj(0, NULL); } @@ -1068,8 +1076,13 @@ TupleAsObj(PyObject *value, int wrapped) return NULL; } for (Py_ssize_t i = 0; i < size; i++) { - Tcl_Obj *item = AsObj(PyTuple_GET_ITEM(value, i)); + Tcl_Obj *item = AsObjNested(PyTuple_GET_ITEM(value, i), depth + 1); if (item == NULL) { + /* Release the already converted items. */ + while (--i >= 0) { + Tcl_IncrRefCount(argv[i]); + Tcl_DecrRefCount(argv[i]); + } goto exit; } argv[i] = item; @@ -1082,7 +1095,7 @@ TupleAsObj(PyObject *value, int wrapped) } static Tcl_Obj* -AsObj(PyObject *value) +AsObjNested(PyObject *value, int depth) { Tcl_Obj *result; @@ -1128,7 +1141,7 @@ AsObj(PyObject *value) return Tcl_NewDoubleObj(PyFloat_AS_DOUBLE(value)); if (PyTuple_Check(value)) { - return TupleAsObj(value, false); + return TupleAsObj(value, false, depth); } if (PyList_Check(value)) { @@ -1136,7 +1149,7 @@ AsObj(PyObject *value) if (value_as_tuple == NULL) { return NULL; } - result = TupleAsObj(value_as_tuple, true); + result = TupleAsObj(value_as_tuple, true, depth); Py_DECREF(value_as_tuple); return result; } @@ -1221,12 +1234,18 @@ AsObj(PyObject *value) PyObject *v = PyObject_Str(value); if (!v) return 0; - result = AsObj(v); + result = AsObjNested(v, depth); Py_DECREF(v); return result; } } +static Tcl_Obj* +AsObj(PyObject *value) +{ + return AsObjNested(value, 0); +} + static PyObject * fromBoolean(TkappObject *tkapp, Tcl_Obj *value) { @@ -1390,7 +1409,12 @@ FromObj(TkappObject *tkapp, Tcl_Obj *value) Py_DECREF(result); return Tkinter_Error(tkapp); } + if (Py_EnterRecursiveCall(" while converting a Tcl list")) { + Py_DECREF(result); + return NULL; + } elem = FromObj(tkapp, tcl_elem); + Py_LeaveRecursiveCall(); if (!elem) { Py_DECREF(result); return NULL;