From 0d783b6d967007903746745e50f57d55fda670e8 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Mon, 20 Jul 2026 03:15:56 +0800 Subject: [PATCH] core/tools: claim PTY controlling TTY after setsid (sudo without -S) After setsid(), ioctl(TIOCSCTTY) (or reopen slave) so the session leader has a real controlling terminal. Live-tested: open_pty([sudo,echo,test]) shows a password prompt; ask_into_pty + read_pty yields test with exit 0. --- src/plyngent/tools/process/pty_backend.py | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/plyngent/tools/process/pty_backend.py b/src/plyngent/tools/process/pty_backend.py index f3941fe..b667843 100644 --- a/src/plyngent/tools/process/pty_backend.py +++ b/src/plyngent/tools/process/pty_backend.py @@ -258,6 +258,34 @@ else: raise OSError(msg) return _spawn_posix(command, cwd=cwd) + def _claim_controlling_tty(slave_fd: int) -> None: + """Make *slave_fd* the controlling terminal (session leader must call this). + + Best-effort: required for programs like ``sudo`` that open ``/dev/tty`` + and refuse a non-controlling PTY. Failures are ignored so plain tools + still run if the ioctl is unavailable. + """ + import fcntl + import termios + + # Linux: arg 0 = become controlling tty of this session. + tio_csctty = getattr(termios, "TIOCSCTTY", None) + if tio_csctty is not None: + with contextlib.suppress(OSError): + _ = fcntl.ioctl(slave_fd, tio_csctty, 0) + return + # Fallback: reopen slave device path (some BSDs / older kernels). + with contextlib.suppress(OSError): + name = os.ttyname(slave_fd) + reopened = os.open(name, os.O_RDWR) + try: + if reopened != slave_fd: + _ = os.dup2(reopened, slave_fd) + finally: + if reopened != slave_fd: + with contextlib.suppress(OSError): + os.close(reopened) + def _spawn_posix(command: list[str], *, cwd: Path) -> PosixPtyHandle: import pty @@ -269,6 +297,8 @@ else: try: os.close(master_fd) _ = os.setsid() + # Claim slave as controlling TTY before wiring stdio (sudo / PAM). + _claim_controlling_tty(slave_fd) _ = os.dup2(slave_fd, 0) _ = os.dup2(slave_fd, 1) _ = os.dup2(slave_fd, _STDERR_FD)