issue #410: automatically work around SELinux braindamage.

pull/612/head
David Wilson 5 years ago
parent ce04fd39c9
commit e8b1bf5909

@ -67,6 +67,11 @@ Mitogen for Ansible
matching *Permission denied* errors from some versions of ``su`` running on
heavily loaded machines.
* `#410 <https://github.com/dw/mitogen/issues/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 <https://github.com/dw/mitogen/issues/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 <https://github.com/peshay>`_.
`Anton Markelov <https://github.com/strangeman>`_,
`Dave Cottlehuber <https://github.com/dch>`_,
`El Mehdi CHAOUKI <https://github.com/elmchaouki>`_,
`James Hogarth <https://github.com/hogarthj>`_,
`Nigel Metheringham <https://github.com/nigelm>`_,
`Orion Poplawski <https://github.com/opoplawski>`_,

@ -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:

@ -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):

Loading…
Cancel
Save