]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-50966: Fix unbounded recursion in turtle drag handlers (GH-152626) (GH...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Tue, 30 Jun 2026 10:24:21 +0000 (12:24 +0200)
committerGitHub <noreply@github.com>
Tue, 30 Jun 2026 10:24:21 +0000 (13:24 +0300)
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 <storchaka@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Lib/test/test_turtle.py
Lib/turtle.py
Misc/NEWS.d/next/Library/2026-06-29-22-16-57.gh-issue-50966.Tq5mLp.rst [new file with mode: 0644]

index 12d2eed874148cebaa8a31a551f38a4d50d47027..c49ce9cdb6f6d955d0f8217d1d77203bc65a4dd7 100644 (file)
@@ -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):
index d2a014f9e05d4a390bc82d0edf1e2c1c19f60bed..db38a5a23581a7ff00783db03e2f5599fa8f470d 100644 (file)
@@ -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 (file)
index 0000000..96a0938
--- /dev/null
@@ -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.