# Copyright 2019, David Wilson # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its contributors # may be used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # !mitogen: minify_safe import ctypes import grp import logging import os import pwd import subprocess import sys import mitogen.core import mitogen.parent LOG = logging.getLogger(__name__) LIBC = ctypes.CDLL(None, use_errno=True) LIBC__strerror = LIBC.strerror LIBC__strerror.restype = ctypes.c_char_p class Error(mitogen.core.StreamError): pass def setns(kind, fd): if LIBC.setns(int(fd), 0) == -1: errno = ctypes.get_errno() msg = 'setns(%s, %s): %s' % (fd, kind, LIBC__strerror(errno)) raise OSError(errno, msg) def _run_command(args): argv = mitogen.parent.Argv(args) try: proc = subprocess.Popen( args=args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) except OSError: e = sys.exc_info()[1] raise Error('could not execute %s: %s', argv, e) output, _ = proc.communicate() if not proc.returncode: return output.decode('utf-8', 'replace') raise Error("%s exitted with status %d: %s", mitogen.parent.Argv(args), proc.returncode, output) def get_docker_pid(path, name): args = [path, 'inspect', '--format={{.State.Pid}}', name] output = _run_command(args) try: return int(output) except ValueError: raise Error("could not find PID from docker output.\n%s", output) def get_lxc_pid(path, name): output = _run_command([path, '-n', name]) for line in output.splitlines(): bits = line.split() if bits and bits[0] == 'PID:': return int(bits[1]) raise Error("could not find PID from lxc-info output.\n%s", output) def get_lxd_pid(path, name): output = _run_command([path, 'info', name]) for line in output.splitlines(): bits = line.split() if bits and bits[0] == 'Pid:': return int(bits[1]) raise Error("could not find PID from lxc output.\n%s", output) def get_machinectl_pid(path, name): output = _run_command([path, 'status', name]) for line in output.splitlines(): bits = line.split() if bits and bits[0] == 'Leader:': return int(bits[1]) raise Error("could not find PID from machinectl output.\n%s", output) GET_LEADER_BY_KIND = { 'docker': ('docker_path', get_docker_pid), 'lxc': ('lxc_info_path', get_lxc_pid), 'lxd': ('lxc_path', get_lxd_pid), 'machinectl': ('machinectl_path', get_machinectl_pid), } class Options(mitogen.parent.Options): container = None username = 'root' kind = None python_path = 'python' docker_path = 'docker' lxc_path = 'lxc' lxc_info_path = 'lxc-info' machinectl_path = 'machinectl' def __init__(self, container, kind, username=None, docker_path=None, lxc_path=None, lxc_info_path=None, machinectl_path=None, **kwargs): super(Options, self).__init__(**kwargs) if kind not in GET_LEADER_BY_KIND: raise Error('unsupported container kind: %r', kind) self.container = mitogen.core.to_text(container) self.kind = kind if username: self.username = mitogen.core.to_text(username) if docker_path: self.docker_path = docker_path if lxc_path: self.lxc_path = lxc_path if lxc_info_path: self.lxc_info_path = lxc_info_path if machinectl_path: self.machinectl_path = machinectl_path class Connection(mitogen.parent.Connection): options_class = Options child_is_immediate_subprocess = False # Order matters. https://github.com/karelzak/util-linux/commit/854d0fe/ NS_ORDER = ('ipc', 'uts', 'net', 'pid', 'mnt', 'user') def preexec_fn(self): nspath = '/proc/%d/ns/' % (self.leader_pid,) selfpath = '/proc/self/ns/' try: ns_fps = [ open(nspath + name) for name in self.NS_ORDER if os.path.exists(nspath + name) and ( os.readlink(nspath + name) != os.readlink(selfpath + name) ) ] except Exception: e = sys.exc_info()[1] raise Error(str(e)) os.chdir('/proc/%s/root' % (self.leader_pid,)) os.chroot('.') os.chdir('/') for fp in ns_fps: setns(fp.name, fp.fileno()) fp.close() for sym in 'endpwent', 'endgrent', 'endspent', 'endsgent': try: getattr(LIBC, sym)() except AttributeError: pass try: os.setgroups([grent.gr_gid for grent in grp.getgrall() if self.options.username in grent.gr_mem]) pwent = pwd.getpwnam(self.options.username) os.setreuid(pwent.pw_uid, pwent.pw_uid) # shadow-4.4/libmisc/setupenv.c. Not done: MAIL, PATH os.environ.update({ 'HOME': pwent.pw_dir, 'SHELL': pwent.pw_shell or '/bin/sh', 'LOGNAME': self.options.username, 'USER': self.options.username, }) if ((os.path.exists(pwent.pw_dir) and os.access(pwent.pw_dir, os.X_OK))): os.chdir(pwent.pw_dir) except Exception: e = sys.exc_info()[1] raise Error(self.username_msg, self.username, self.container, type(e).__name__, e) username_msg = 'while transitioning to user %r in container %r: %s: %s' def get_boot_command(self): # With setns(CLONE_NEWPID), new children of the caller receive a new # PID namespace, however the caller's namespace won't change. That # causes subsequent calls to clone() specifying CLONE_THREAD to fail # with EINVAL, as threads in the same process can't have varying PID # namespaces, meaning starting new threads in the exec'd program will # fail. The solution is forking, so inject a /bin/sh call to achieve # this. argv = super(Connection, self).get_boot_command() # bash will exec() if a single command was specified and the shell has # nothing left to do, so "; exit $?" gives bash a reason to live. return ['/bin/sh', '-c', '%s; exit $?' % (mitogen.parent.Argv(argv),)] def create_child(self, args): return mitogen.parent.create_child(args, preexec_fn=self.preexec_fn) def _get_name(self): return u'setns.' + self.options.container def connect(self, **kwargs): attr, func = GET_LEADER_BY_KIND[self.options.kind] tool_path = getattr(self.options, attr) self.leader_pid = func(tool_path, self.options.container) LOG.debug('Leader PID for %s container %r: %d', self.options.kind, self.options.container, self.leader_pid) return super(Connection, self).connect(**kwargs)