parent: cope with broken /dev/pts on Linux; closes #462.

This commit is contained in:
David Wilson 2019-01-18 21:05:38 +00:00
parent ec056042e0
commit a4c7a98dd9
2 changed files with 70 additions and 0 deletions

View File

@ -41,6 +41,7 @@ import logging
import os import os
import signal import signal
import socket import socket
import struct
import subprocess import subprocess
import sys import sys
import termios import termios
@ -97,6 +98,10 @@ SYS_EXECUTABLE_MSG = (
) )
_sys_executable_warning_logged = False _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( SIGNAL_BY_NUM = dict(
(getattr(signal, name), name) (getattr(signal, name), name)
for name in sorted(vars(signal), reverse=True) for name in sorted(vars(signal), reverse=True)
@ -318,6 +323,48 @@ def _acquire_controlling_tty():
fcntl.ioctl(2, termios.TIOCSCTTY) 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(): def openpty():
""" """
Call :func:`os.openpty`, raising a descriptive error if the call fails. Call :func:`os.openpty`, raising a descriptive error if the call fails.
@ -331,6 +378,8 @@ def openpty():
return os.openpty() return os.openpty()
except OSError: except OSError:
e = sys.exc_info()[1] 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) raise mitogen.core.StreamError(OPENPTY_MSG, e)

View File

@ -1,4 +1,5 @@
import errno import errno
import fcntl
import os import os
import signal import signal
import subprocess import subprocess
@ -197,6 +198,26 @@ class OpenPtyTest(testlib.TestCase):
msg = mitogen.parent.OPENPTY_MSG % (openpty.side_effect,) msg = mitogen.parent.OPENPTY_MSG % (openpty.side_effect,)
self.assertEquals(e.args[0], msg) 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): class TtyCreateChildTest(testlib.TestCase):
func = staticmethod(mitogen.parent.tty_create_child) func = staticmethod(mitogen.parent.tty_create_child)