]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-135329: prevent infinite traceback loop on Ctrl-C for strace (GH-138133)
authoryihong <zouzou0208@gmail.com>
Tue, 16 Sep 2025 10:39:03 +0000 (18:39 +0800)
committerGitHub <noreply@github.com>
Tue, 16 Sep 2025 10:39:03 +0000 (12:39 +0200)
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
Co-authored-by: dura0ok <slpmcf@gmail.com>
Co-authored-by: graymon <greyschwinger@gmail.com>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Lib/_pyrepl/unix_console.py
Lib/test/test_pyrepl/eio_test_script.py [new file with mode: 0644]
Lib/test/test_pyrepl/test_unix_console.py
Misc/NEWS.d/next/Library/2025-08-25-18-06-04.gh-issue-138133.Zh9rGo.rst [new file with mode: 0644]

index 9953051bf7c4efbbdbcbb01eac2e5601704b074d..8b9122a48e775b33507f68fed19d567200ee2535 100644 (file)
@@ -340,7 +340,14 @@ class UnixConsole(Console):
         raw.lflag |= termios.ISIG
         raw.cc[termios.VMIN] = 1
         raw.cc[termios.VTIME] = 0
-        tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
+        try:
+            tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
+        except termios.error as e:
+            if e.args[0] != errno.EIO:
+                # gh-135329: when running under external programs (like strace),
+                # tcsetattr may fail with EIO. We can safely ignore this
+                # and continue with default terminal settings.
+                raise
 
         # In macOS terminal we need to deactivate line wrap via ANSI escape code
         if self.is_apple_terminal:
@@ -372,7 +379,11 @@ class UnixConsole(Console):
         self.__disable_bracketed_paste()
         self.__maybe_write_code(self._rmkx)
         self.flushoutput()
-        tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
+        try:
+            tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
+        except termios.error as e:
+            if e.args[0] != errno.EIO:
+                raise
 
         if self.is_apple_terminal:
             os.write(self.output_fd, b"\033[?7h")
@@ -411,6 +422,8 @@ class UnixConsole(Console):
                             return self.event_queue.get()
                         else:
                             continue
+                    elif err.errno == errno.EIO:
+                        raise SystemExit(errno.EIO)
                     else:
                         raise
                 else:
diff --git a/Lib/test/test_pyrepl/eio_test_script.py b/Lib/test/test_pyrepl/eio_test_script.py
new file mode 100644 (file)
index 0000000..e3ea6ca
--- /dev/null
@@ -0,0 +1,94 @@
+import errno
+import fcntl
+import os
+import pty
+import signal
+import sys
+import termios
+
+
+def handler(sig, f):
+    pass
+
+
+def create_eio_condition():
+    # SIGINT handler used to produce an EIO.
+    # See https://github.com/python/cpython/issues/135329.
+    try:
+        master_fd, slave_fd = pty.openpty()
+        child_pid = os.fork()
+        if child_pid == 0:
+            try:
+                os.setsid()
+                fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
+                child_process_group_id = os.getpgrp()
+                grandchild_pid = os.fork()
+                if grandchild_pid == 0:
+                    os.setpgid(0, 0)      # set process group for grandchild
+                    os.dup2(slave_fd, 0)  # redirect stdin
+                    if slave_fd > 2:
+                        os.close(slave_fd)
+                    # Fork grandchild for terminal control manipulation
+                    if os.fork() == 0:
+                        sys.exit(0)  # exit the child process that was just obtained
+                    else:
+                        try:
+                            os.tcsetpgrp(0, child_process_group_id)
+                        except OSError:
+                            pass
+                        sys.exit(0)
+                else:
+                    # Back to child
+                    try:
+                        os.setpgid(grandchild_pid, grandchild_pid)
+                    except ProcessLookupError:
+                        pass
+                    os.tcsetpgrp(slave_fd, grandchild_pid)
+                    if slave_fd > 2:
+                        os.close(slave_fd)
+                    os.waitpid(grandchild_pid, 0)
+                    # Manipulate terminal control to create EIO condition
+                    os.tcsetpgrp(master_fd, child_process_group_id)
+                    # Now try to read from master - this might cause EIO
+                    try:
+                        os.read(master_fd, 1)
+                    except OSError as e:
+                        if e.errno == errno.EIO:
+                            print(f"Setup created EIO condition: {e}", file=sys.stderr)
+                    sys.exit(0)
+            except Exception as setup_e:
+                print(f"Setup error: {setup_e}", file=sys.stderr)
+                sys.exit(1)
+        else:
+            # Parent process
+            os.close(slave_fd)
+            os.waitpid(child_pid, 0)
+            # Now replace stdin with master_fd and try to read
+            os.dup2(master_fd, 0)
+            os.close(master_fd)
+            # This should now trigger EIO
+            print(f"Unexpectedly got input: {input()!r}", file=sys.stderr)
+            sys.exit(0)
+    except OSError as e:
+        if e.errno == errno.EIO:
+            print(f"Got EIO: {e}", file=sys.stderr)
+            sys.exit(1)
+        elif e.errno == errno.ENXIO:
+            print(f"Got ENXIO (no such device): {e}", file=sys.stderr)
+            sys.exit(1)  # Treat ENXIO as success too
+        else:
+            print(f"Got other OSError: errno={e.errno} {e}", file=sys.stderr)
+            sys.exit(2)
+    except EOFError as e:
+        print(f"Got EOFError: {e}", file=sys.stderr)
+        sys.exit(3)
+    except Exception as e:
+        print(f"Got unexpected error: {type(e).__name__}: {e}", file=sys.stderr)
+        sys.exit(4)
+
+
+if __name__ == "__main__":
+    # Set up signal handler for coordination
+    signal.signal(signal.SIGUSR1, lambda *a: create_eio_condition())
+    print("READY", flush=True)
+    signal.pause()
index 6185c7e3c794e374a6827a8fb3c8346a14719ce0..3b0d2637dab9cb5e69da0a6dc091168594212e0f 100644 (file)
@@ -1,12 +1,16 @@
+import errno
 import itertools
 import os
+import signal
+import subprocess
 import sys
 import unittest
 from functools import partial
 from test.support import os_helper, force_not_colorized_test_class
+from test.support import script_helper
 
 from unittest import TestCase
-from unittest.mock import MagicMock, call, patch, ANY
+from unittest.mock import MagicMock, call, patch, ANY, Mock
 
 from .support import handle_all_events, code_to_events
 
@@ -312,3 +316,59 @@ class TestConsole(TestCase):
             os.environ = []
             console.prepare()  # needed to call restore()
             console.restore()  # this should succeed
+
+
+@unittest.skipIf(sys.platform == "win32", "No Unix console on Windows")
+class TestUnixConsoleEIOHandling(TestCase):
+
+    @patch('_pyrepl.unix_console.tcsetattr')
+    @patch('_pyrepl.unix_console.tcgetattr')
+    def test_eio_error_handling_in_restore(self, mock_tcgetattr, mock_tcsetattr):
+
+        import termios
+        mock_termios = Mock()
+        mock_termios.iflag = 0
+        mock_termios.oflag = 0
+        mock_termios.cflag = 0
+        mock_termios.lflag = 0
+        mock_termios.cc = [0] * 32
+        mock_termios.copy.return_value = mock_termios
+        mock_tcgetattr.return_value = mock_termios
+
+        console = UnixConsole(term="xterm")
+        console.prepare()
+
+        mock_tcsetattr.side_effect = termios.error(errno.EIO, "Input/output error")
+
+        # EIO error should be handled gracefully in restore()
+        console.restore()
+
+    @unittest.skipUnless(sys.platform == "linux", "Only valid on Linux")
+    def test_repl_eio(self):
+        # Use the pty-based approach to simulate EIO error
+        script_path = os.path.join(os.path.dirname(__file__), "eio_test_script.py")
+
+        proc = script_helper.spawn_python(
+            "-S", script_path,
+            stderr=subprocess.PIPE,
+            text=True
+        )
+
+        ready_line = proc.stdout.readline().strip()
+        if ready_line != "READY" or proc.poll() is not None:
+            self.fail("Child process failed to start properly")
+
+        os.kill(proc.pid, signal.SIGUSR1)
+        _, err = proc.communicate(timeout=5)  # sleep for pty to settle
+        self.assertEqual(
+            proc.returncode,
+            1,
+            f"Expected EIO/ENXIO error, got return code {proc.returncode}",
+        )
+        self.assertTrue(
+            (
+                "Got EIO:" in err
+                or "Got ENXIO:" in err
+            ),
+            f"Expected EIO/ENXIO error message in stderr: {err}",
+        )
diff --git a/Misc/NEWS.d/next/Library/2025-08-25-18-06-04.gh-issue-138133.Zh9rGo.rst b/Misc/NEWS.d/next/Library/2025-08-25-18-06-04.gh-issue-138133.Zh9rGo.rst
new file mode 100644 (file)
index 0000000..f9045ef
--- /dev/null
@@ -0,0 +1 @@
+Prevent infinite traceback loop when sending CTRL^C to Python through ``strace``.