diff --git a/mitogen/parent.py b/mitogen/parent.py index 394b449b..ec2ea1e7 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -41,6 +41,7 @@ import logging import os import signal import socket +import struct import subprocess import sys import termios @@ -97,6 +98,10 @@ SYS_EXECUTABLE_MSG = ( ) _sys_executable_warning_logged = False +LINUX_TIOCGPTN = 2147767344 # Get PTY number; asm-generic/ioctls.h +LINUX_TIOCSPTLCK = 1074025521 # Lock/unlock PTY; asm-generic/ioctls.h +IS_LINUX = os.uname()[0] == 'Linux' + SIGNAL_BY_NUM = dict( (getattr(signal, name), name) for name in sorted(vars(signal), reverse=True) @@ -318,6 +323,48 @@ def _acquire_controlling_tty(): fcntl.ioctl(2, termios.TIOCSCTTY) +def _linux_broken_devpts_openpty(): + """ + #462: On broken Linux hosts with mismatched configuration (e.g. old + /etc/fstab template installed), /dev/pts may be mounted without the gid= + mount option, causing new slave devices to be created with the group ID of + the calling process. This upsets glibc, whose openpty() is required by + specification to produce a slave owned by a special group ID (which is + always the 'tty' group). + + Glibc attempts to use "pt_chown" to fix ownership. If that fails, it + chown()s the PTY directly, which fails due to non-root, causing openpty() + to fail with EPERM ("Operation not permitted"). Since we don't need the + magical TTY group to run sudo and su, open the PTY ourselves in this case. + """ + master_fd = None + try: + # Opening /dev/ptmx causes a PTY pair to be allocated, and the + # corresponding slave /dev/pts/* device to be created, owned by UID/GID + # matching this process. + master_fd = os.open('/dev/ptmx', os.O_RDWR) + # Clear the lock bit from the PTY. This a prehistoric feature from a + # time when slave device files were persistent. + fcntl.ioctl(master_fd, LINUX_TIOCSPTLCK, struct.pack('i', 0)) + # Since v4.13 TIOCGPTPEER exists to open the slave in one step, but we + # must support older kernels. Ask for the PTY number. + pty_num_s = fcntl.ioctl(master_fd, LINUX_TIOCGPTN, + struct.pack('i', 0)) + pty_num, = struct.unpack('i', pty_num_s) + pty_name = '/dev/pts/%d' % (pty_num,) + # Now open it with O_NOCTTY to ensure it doesn't change our controlling + # TTY. Otherwise when we close the FD we get killed by the kernel, and + # the child we spawn that should really attach to it will get EPERM + # during _acquire_controlling_tty(). + slave_fd = os.open(pty_name, os.O_RDWR|os.O_NOCTTY) + return master_fd, slave_fd + except OSError: + if master_fd is not None: + os.close(master_fd) + e = sys.exc_info()[1] + raise mitogen.core.StreamError(OPENPTY_MSG, e) + + def openpty(): """ Call :func:`os.openpty`, raising a descriptive error if the call fails. @@ -331,6 +378,8 @@ def openpty(): return os.openpty() except OSError: e = sys.exc_info()[1] + if IS_LINUX and e.args[0] == errno.EPERM: + return _linux_broken_devpts_openpty() raise mitogen.core.StreamError(OPENPTY_MSG, e) diff --git a/tests/parent_test.py b/tests/parent_test.py index ec33415b..d0e198bb 100644 --- a/tests/parent_test.py +++ b/tests/parent_test.py @@ -1,4 +1,5 @@ import errno +import fcntl import os import signal import subprocess @@ -197,6 +198,26 @@ class OpenPtyTest(testlib.TestCase): msg = mitogen.parent.OPENPTY_MSG % (openpty.side_effect,) self.assertEquals(e.args[0], msg) + @unittest2.skipIf(condition=(os.uname()[0] != 'Linux'), + reason='Fallback only supported on Linux') + @mock.patch('os.openpty') + def test_broken_linux_fallback(self, openpty): + openpty.side_effect = OSError(errno.EPERM) + master_fd, slave_fd = self.func() + try: + st = os.fstat(master_fd) + self.assertEquals(5, os.major(st.st_rdev)) + flags = fcntl.fcntl(master_fd, fcntl.F_GETFL) + self.assertTrue(flags & os.O_RDWR) + + st = os.fstat(slave_fd) + self.assertEquals(136, os.major(st.st_rdev)) + flags = fcntl.fcntl(slave_fd, fcntl.F_GETFL) + self.assertTrue(flags & os.O_RDWR) + finally: + os.close(master_fd) + os.close(slave_fd) + class TtyCreateChildTest(testlib.TestCase): func = staticmethod(mitogen.parent.tty_create_child)