From 4b5148c3ea38d8df42a49ba67643807710381b81 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:24:21 +0200 Subject: [PATCH] [3.14] gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626) (GH-152658) TurtleScreenBase._update() redraws with cv.update(), which also reprocesses input events, so a handler that moves the turtle (such as screen.ondrag(turtle.goto)) reenters _update() for every queued event until the interpreter crashes. A reentrant _update() now only flushes drawing with update_idletasks(). (cherry picked from commit 6f103fab178c07cbb5f701b8ad97e275b6eb6c4c) Co-authored-by: Serhiy Storchaka Co-authored-by: Claude Opus 4.8 --- Lib/test/test_turtle.py | 22 +++++++++++++++++++ Lib/turtle.py | 12 +++++++++- ...6-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst | 3 +++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst diff --git a/Lib/test/test_turtle.py b/Lib/test/test_turtle.py index 12d2eed87414..c49ce9cdb6f6 100644 --- a/Lib/test/test_turtle.py +++ b/Lib/test/test_turtle.py @@ -577,6 +577,28 @@ class TestTurtleScreen(unittest.TestCase): s.update.assert_not_called() s.update.assert_called_once() + def test_update_is_not_reentrant(self): + # ondrag(goto) reenters _update() while cv.update() processes events; + # without a guard this recurses without bound (gh-50966). + s = turtle.TurtleScreen(cv=unittest.mock.MagicMock()) + depth = max_depth = 0 + + def reenter(): + nonlocal depth, max_depth + depth += 1 + max_depth = max(max_depth, depth) + if depth < 50: + s._update() # as an event handler would + depth -= 1 + + s.cv.update.reset_mock() # ignore calls made during construction + s.cv.update.side_effect = reenter + s._update() + # cv.update() runs once; reentrant calls only flush idle tasks. + self.assertEqual(s.cv.update.call_count, 1) + self.assertEqual(max_depth, 1) + self.assertTrue(s.cv.update_idletasks.called) + class TestTurtle(unittest.TestCase): def setUp(self): diff --git a/Lib/turtle.py b/Lib/turtle.py index d2a014f9e05d..db38a5a23581 100644 --- a/Lib/turtle.py +++ b/Lib/turtle.py @@ -486,6 +486,7 @@ class TurtleScreenBase(object): self.canvwidth = w self.canvheight = h self.xscale = self.yscale = 1.0 + self._updating = False def _createpoly(self): """Create an invisible polygon item on canvas self.cv) @@ -555,7 +556,16 @@ class TurtleScreenBase(object): def _update(self): """Redraw graphics items on canvas """ - self.cv.update() + if self._updating: + # Reentrant call (e.g. a drag handler moving the turtle, + # gh-50966): flush drawing without reprocessing input. + self.cv.update_idletasks() + return + self._updating = True + try: + self.cv.update() + finally: + self._updating = False def _delay(self, delay): """Delay subsequent canvas actions for delay ms.""" diff --git a/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst b/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst new file mode 100644 index 000000000000..96a0938c02d2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst @@ -0,0 +1,3 @@ +Fix unbounded recursion in :mod:`turtle` when a mouse event handler that moves +the turtle is reentered while the screen is being redrawn, for example with +``screen.ondrag(turtle.goto)``. This could previously crash the interpreter. -- 2.47.3