diff --git a/docs/changelog.rst b/docs/changelog.rst index 4cc89041..4004d8ae 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -67,6 +67,11 @@ Mitogen for Ansible matching *Permission denied* errors from some versions of ``su`` running on heavily loaded machines. +* `#410 `_: Use of ``AF_UNIX`` + sockets automatically replaced with plain UNIX pipes when SELinux is + detected, to work around a broken heuristic in popular SELinux policies that + prevents inheriting ``AF_UNIX`` sockets across privilege domains. + * `#549 `_: the open file descriptor limit for the Ansible process is increased to the available hard limit. It is common for distributions to ship with a much higher hard limit than their @@ -166,6 +171,7 @@ bug reports, testing, features and fixes in this release contributed by `Andreas Hubert `_. `Anton Markelov `_, `Dave Cottlehuber `_, +`El Mehdi CHAOUKI `_, `James Hogarth `_, `Nigel Metheringham `_, `Orion Poplawski `_, diff --git a/mitogen/parent.py b/mitogen/parent.py index bfd6fef5..c4642a58 100644 --- a/mitogen/parent.py +++ b/mitogen/parent.py @@ -71,6 +71,16 @@ from mitogen.core import IOLOG LOG = logging.getLogger(__name__) +# #410: we must avoid the use of socketpairs if SELinux is enabled. +try: + fp = open('/sys/fs/selinux/enforce', 'rb') + try: + SELINUX_ENABLED = bool(int(fp.read())) + finally: + fp.close() +except IOError: + SELINUX_ENABLED = False + try: next @@ -278,6 +288,38 @@ def create_socketpair(size=None): return parentfp, childfp +def create_best_pipe(escalates_privilege=False): + """ + By default we prefer to communicate with children over a UNIX socket, as a + single file descriptor can represent bidirectional communication, and a + cross-platform API exists to align buffer sizes with the needs of the + library. + + SELinux prevents us setting up a privileged process to inherit an AF_UNIX + socket, a facility explicitly designed as a better replacement for pipes, + because at some point in the mid 90s it might have been commonly possible + for AF_INET sockets to end up undesirably connected to a privileged + process, so let's make up arbitrary rules breaking all sockets instead. + + If SELinux is detected, fall back to using pipes. + + :returns: + `(parent_rfp, child_wfp, child_rfp, parent_wfp)` + """ + if (not escalates_privilege) or (not SELINUX_ENABLED): + parentfp, childfp = create_socketpair() + return parentfp, childfp, childfp, parentfp + + parent_rfp, child_wfp = mitogen.core.pipe() + try: + child_rfp, parent_wfp = mitogen.core.pipe() + return parent_rfp, child_wfp, child_rfp, parent_wfp + except: + parent_rfp.close() + child_wfp.close() + raise + + def popen(**kwargs): """ Wrap :class:`subprocess.Popen` to ensure any global :data:`_preexec_hook` @@ -292,7 +334,8 @@ def popen(**kwargs): return subprocess.Popen(preexec_fn=preexec_fn, **kwargs) -def create_child(args, merge_stdio=False, stderr_pipe=False, preexec_fn=None): +def create_child(args, merge_stdio=False, stderr_pipe=False, + escalates_privilege=False, preexec_fn=None): """ Create a child process whose stdin/stdout is connected to a socket. @@ -306,22 +349,27 @@ def create_child(args, merge_stdio=False, stderr_pipe=False, preexec_fn=None): :param bool stderr_pipe: If :data:`True` and `merge_stdio` is :data:`False`, arrange for `stderr` to be connected to a separate pipe, to allow any ongoing debug - logs generated by e.g. SSH to be outpu as the session progresses, + logs generated by e.g. SSH to be output as the session progresses, without interfering with `stdout`. + :param bool escalates_privilege: + If :data:`True`, the target program may escalate privileges, causing + SELinux to disconnect AF_UNIX sockets, so avoid those. + :param function preexec_fn: + If not :data:`None`, a function to run within the post-fork child + before executing the target program. :returns: :class:`Process` instance. """ + parent_rfp, child_wfp, child_rfp, parent_wfp = create_best_pipe( + escalates_privilege=escalates_privilege + ) + parentfp, childfp = create_socketpair() - # When running under a monkey patches-enabled gevent, the socket module - # yields descriptors who already have O_NONBLOCK, which is persisted across - # fork, totally breaking Python. Therefore, drop O_NONBLOCK from Python's - # future stdin fd. - mitogen.core.set_block(childfp.fileno()) stderr = None stderr_r = None if merge_stdio: - stderr = childfp + stderr = child_wfp elif stderr_pipe: stderr_r, stderr = mitogen.core.pipe() mitogen.core.set_cloexec(stderr_r.fileno()) @@ -329,27 +377,33 @@ def create_child(args, merge_stdio=False, stderr_pipe=False, preexec_fn=None): try: proc = popen( args=args, - stdin=childfp, - stdout=childfp, + stdin=child_rfp, + stdout=child_wfp, stderr=stderr, close_fds=True, preexec_fn=preexec_fn, ) except: - childfp.close() - parentfp.close() + child_rfp.close() + child_wfp.close() + parent_rfp.close() + parent_wfp.close() if stderr_pipe: stderr.close() stderr_r.close() raise - childfp.close() + child_rfp.close() + child_wfp.close() if stderr_pipe: stderr.close() - LOG.debug('create_child() child %d fd %d, parent %d, cmd: %s', - proc.pid, parentfp.fileno(), os.getpid(), Argv(args)) - return PopenProcess(proc, stdin=parentfp, stdout=parentfp, stderr=stderr_r) + return PopenProcess( + proc=proc, + stdin=parent_wfp, + stdout=parent_rfp, + stderr=stderr_r, + ) def _acquire_controlling_tty(): @@ -461,12 +515,14 @@ def tty_create_child(args): raise slave_fp.close() - LOG.debug('tty_create_child() child %d fd %d, parent %d, cmd: %s', - proc.pid, master_fp.fileno(), os.getpid(), Argv(args)) - return PopenProcess(proc, stdin=master_fp, stdout=master_fp) + return PopenProcess( + proc=proc, + stdin=master_fp, + stdout=master_fp, + ) -def hybrid_tty_create_child(args): +def hybrid_tty_create_child(args, escalates_privilege=False): """ Like :func:`tty_create_child`, except attach stdin/stdout to a socketpair like :func:`create_child`, but leave stderr and the controlling TTY @@ -479,20 +535,25 @@ def hybrid_tty_create_child(args): """ master_fp, slave_fp = openpty() try: - parentfp, childfp = create_socketpair() + parent_rfp, child_wfp, child_rfp, parent_wfp = create_best_pipe( + escalates_privilege=escalates_privilege, + ) try: - mitogen.core.set_block(childfp) + mitogen.core.set_block(child_rfp) + mitogen.core.set_block(child_wfp) proc = popen( args=args, - stdin=childfp, - stdout=childfp, + stdin=child_rfp, + stdout=child_wfp, stderr=slave_fp, preexec_fn=_acquire_controlling_tty, close_fds=True, ) except: - parentfp.close() - childfp.close() + parent_rfp.close() + child_wfp.close() + parent_wfp.close() + child_rfp.close() raise except: master_fp.close() @@ -500,10 +561,14 @@ def hybrid_tty_create_child(args): raise slave_fp.close() - childfp.close() - LOG.debug('hybrid_tty_create_child() pid=%d stdio=%d, tty=%d, cmd: %s', - proc.pid, parentfp.fileno(), master_fp.fileno(), Argv(args)) - return PopenProcess(proc, stdin=parentfp, stdout=parentfp, stderr=master_fp) + child_rfp.close() + child_wfp.close() + return PopenProcess( + proc=proc, + stdin=parent_wfp, + stdout=parent_rfp, + stderr=master_fp, + ) class Timer(object): @@ -1425,6 +1490,7 @@ class Connection(object): def start_child(self): args = self.get_boot_command() + LOG.debug('command line for %r: %s', self, Argv(args)) try: return self.create_child(args=args, **self.create_child_args) except OSError: diff --git a/mitogen/sudo.py b/mitogen/sudo.py index bcb2e7be..ea07d0c1 100644 --- a/mitogen/sudo.py +++ b/mitogen/sudo.py @@ -244,6 +244,9 @@ class Connection(mitogen.parent.Connection): diag_protocol_class = SetupProtocol options_class = Options create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) + create_child_args = { + 'escalates_privilege': True, + } child_is_immediate_subprocess = False def _get_name(self):