diff options
author | Tristan Maat <tristan.maat@codethink.co.uk> | 2018-01-31 14:04:43 +0000 |
---|---|---|
committer | Tristan Maat <tristan.maat@codethink.co.uk> | 2018-02-19 14:39:38 +0100 |
commit | 85b52097f981c665bfb80df7ddccadf43d3ec8d9 (patch) | |
tree | 3f20b20c7f8e599ea3b651864482f656f0cfaa03 | |
parent | 2643b095c59db6e8cbc2b8620c2ad820c7a5636d (diff) | |
download | buildstream-218-allow-specifying-the-chroot-binary-to-use-for-sandboxes-on-unix-platforms.tar.gz |
-rw-r--r-- | buildstream/_platform/unix.py | 10 | ||||
-rw-r--r-- | buildstream/sandbox/__init__.py | 1 | ||||
-rw-r--r-- | buildstream/sandbox/_sandboxchroot.py | 139 | ||||
-rw-r--r-- | buildstream/sandbox/_sandboxuserchroot.py | 158 |
4 files changed, 238 insertions, 70 deletions
diff --git a/buildstream/_platform/unix.py b/buildstream/_platform/unix.py index 6d7b46374..7c3a805fc 100644 --- a/buildstream/_platform/unix.py +++ b/buildstream/_platform/unix.py @@ -22,7 +22,7 @@ import os from .._artifactcache.tarcache import TarCache from .._exceptions import PlatformError -from ..sandbox import SandboxChroot +from ..sandbox import SandboxChroot, SandboxUserChroot from . import Platform @@ -35,12 +35,14 @@ class Unix(Platform): self._artifact_cache = TarCache(context) # Not necessarily 100% reliable, but we want to fail early. - if os.geteuid() != 0: - raise PlatformError("Root privileges are required to run without bubblewrap.") + # if os.geteuid() != 0: + # raise PlatformError("Root privileges are required to run without bubblewrap.") @property def artifactcache(self): return self._artifact_cache def create_sandbox(self, *args, **kwargs): - return SandboxChroot(*args, **kwargs) + # We can optionally create a SandboxUserChroot + return SandboxUserChroot(*args, **kwargs) + # return SandboxChroot(*args, **kwargs) diff --git a/buildstream/sandbox/__init__.py b/buildstream/sandbox/__init__.py index 7ee871cab..c55e53ed1 100644 --- a/buildstream/sandbox/__init__.py +++ b/buildstream/sandbox/__init__.py @@ -19,5 +19,6 @@ # Tristan Maat <tristan.maat@codethink.co.uk> from .sandbox import Sandbox, SandboxFlags +from ._sandboxuserchroot import SandboxUserChroot from ._sandboxchroot import SandboxChroot from ._sandboxbwrap import SandboxBwrap diff --git a/buildstream/sandbox/_sandboxchroot.py b/buildstream/sandbox/_sandboxchroot.py index 584f0e116..8ac0ba4f1 100644 --- a/buildstream/sandbox/_sandboxchroot.py +++ b/buildstream/sandbox/_sandboxchroot.py @@ -91,24 +91,12 @@ class SandboxChroot(Sandbox): return status - # chroot() + # popen() # - # A helper function to chroot into the rootfs. + # A helper function to create and manage a subprocess. We mimic + # subprocess.Popen's interface here. # - # Args: - # rootfs (str): The path of the sysroot to chroot into - # command (list): The command to execute in the chroot env - # stdin (file): The stdin - # stdout (file): The stdout - # stderr (file): The stderr - # cwd (str): The current working directory - # env (dict): The environment variables to use while executing the command - # flags (:class:`SandboxFlags`): The flags to enable on the sandbox - # - # Returns: - # (int): The exit code of the executed command - # - def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env, flags): + def popen(self, command, **kwargs): def kill_proc(): if process: # First attempt to gracefully terminate @@ -128,55 +116,76 @@ class SandboxChroot(Sandbox): group_id = os.getpgid(process.pid) os.killpg(group_id, signal.SIGCONT) + with _signals.suspendable(suspend_proc, resume_proc), \ + _signals.terminator(kill_proc): + process = subprocess.Popen(command, **kwargs) + + # Wait for the child process to finish, ensuring that + # a SIGINT has exactly the effect the user probably + # expects (i.e. let the child process handle it). + try: + while True: + try: + _, status = os.waitpid(process.pid, 0) + # If the process exits due to a signal, we + # brutally murder it to avoid zombies + if not os.WIFEXITED(status): + utils._kill_process_tree(process.pid) + + # Unlike in the bwrap case, here only the main + # process seems to receive the SIGINT. We pass + # on the signal to the child and then continue + # to wait. + except KeyboardInterrupt: + process.send_signal(signal.SIGINT) + continue + + break + # If we can't find the process, it has already died of + # its own accord, and therefore we don't need to check + # or kill anything. + except psutil.NoSuchProcess: + pass + + # Return the exit code - see the documentation for + # os.WEXITSTATUS to see why this is required. + if os.WIFEXITED(status): + code = os.WEXITSTATUS(status) + else: + code = -1 + + return code + + # chroot() + # + # A helper function to chroot into the rootfs. + # + # Args: + # rootfs (str): The path of the sysroot to chroot into + # command (list): The command to execute in the chroot env + # stdin (file): The stdin + # stdout (file): The stdout + # stderr (file): The stderr + # cwd (str): The current working directory + # env (dict): The environment variables to use while executing the command + # flags (:class:`SandboxFlags`): The flags to enable on the sandbox + # + # Returns: + # (int): The exit code of the executed command + # + def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env, flags): try: - with _signals.suspendable(suspend_proc, resume_proc), _signals.terminator(kill_proc): - process = subprocess.Popen( - command, - close_fds=True, - cwd=os.path.join(rootfs, cwd.lstrip(os.sep)), - env=env, - stdin=stdin, - stdout=stdout, - stderr=stderr, - # If you try to put gtk dialogs here Tristan (either) - # will personally scald you - preexec_fn=lambda: (os.chroot(rootfs), os.chdir(cwd)), - start_new_session=flags & SandboxFlags.INTERACTIVE - ) - - # Wait for the child process to finish, ensuring that - # a SIGINT has exactly the effect the user probably - # expects (i.e. let the child process handle it). - try: - while True: - try: - _, status = os.waitpid(process.pid, 0) - # If the process exits due to a signal, we - # brutally murder it to avoid zombies - if not os.WIFEXITED(status): - utils._kill_process_tree(process.pid) - - # Unlike in the bwrap case, here only the main - # process seems to receive the SIGINT. We pass - # on the signal to the child and then continue - # to wait. - except KeyboardInterrupt: - process.send_signal(signal.SIGINT) - continue - - break - # If we can't find the process, it has already died of - # its own accord, and therefore we don't need to check - # or kill anything. - except psutil.NoSuchProcess: - pass - - # Return the exit code - see the documentation for - # os.WEXITSTATUS to see why this is required. - if os.WIFEXITED(status): - code = os.WEXITSTATUS(status) - else: - code = -1 + return self.popen(command, + close_fds=True, + cwd=os.path.join(rootfs, cwd.lstrip(os.sep)), + env=env, + stdin=stdin, + stdout=stdout, + stderr=stderr, + # If you try to put gtk dialogs here Tristan (either) + # will personally scald you + preexec_fn=lambda: (os.chroot(rootfs), os.chdir(cwd)), + start_new_session=flags & SandboxFlags.INTERACTIVE) except subprocess.SubprocessError as e: # Exceptions in preexec_fn are simply reported as @@ -189,8 +198,6 @@ class SandboxChroot(Sandbox): else: raise SandboxError('Could not run command {}: {}'.format(command, e)) from e - return code - # create_devices() # # Create the nodes in /dev/ usually required for builds (null, diff --git a/buildstream/sandbox/_sandboxuserchroot.py b/buildstream/sandbox/_sandboxuserchroot.py new file mode 100644 index 000000000..6e98e8df1 --- /dev/null +++ b/buildstream/sandbox/_sandboxuserchroot.py @@ -0,0 +1,158 @@ +import os +import pwd +import sys +import stat +import subprocess +from pathlib import Path +from contextlib import ExitStack, contextmanager + +from .. import utils +from . import SandboxFlags +from ._mount import MountMap +from .._exceptions import SandboxError +from ._sandboxchroot import SandboxChroot +from .._message import Message, MessageType + + +# The sandbox directory needs to fulfill a few criteria: +# - Its parents must be owned by root +# - It and its children must be owned by the user defined in the +# configuration file +# - Neither it nor its parents must be more permissive than 755 +# - It cannot be in a directory mounted into the sandbox (duh) +# +# If we allow the user to specify this location (we probably should), +# those criteria would be nice to check for before sandbox execution. +# Although userchroot itself checks for some, the error messages are +# not particularly helpful. +# +SANDBOX_DIR = '/usr/local/sandboxes' + + +def assert_userchroot_configuration(): + configured = False + user = pwd.getpwuid(os.getuid())[0] + userchroot = utils.get_host_tool('userchroot') + config = Path(userchroot).parents[1].joinpath('etc/userchroot.conf') + + if config.exists(): + with open(config, 'r') as configf: + for line in configf: + if line.rstrip() == '{}:{}'.format(user, SANDBOX_DIR): + configured = True + break + + if not configured: + raise SandboxError("'userchroot' is not configured correctly. " + "Please add '{}:{}' to '{}'" + .format(user, SANDBOX_DIR, config)) + + +class SandboxUserChroot(SandboxChroot): + def run(self, command, flags, *, cwd=None, env=None): + # Ensure sandbox default configuration + if cwd is None: + cwd = self._get_work_directory() or '/' + + if env is None: + env = self._get_environment() + + if isinstance(command, str): + command = [command] + + stdout, stderr = self._get_output() + + # Create the mount map, this will tell us where + # each mount point needs to be mounted from and to + self.mount_map = MountMap(self, True) + + # Make sure userchroot is configured correctly + assert_userchroot_configuration() + + with ExitStack() as stack: + # Create sysroot + try: + os.makedirs(SANDBOX_DIR, exist_ok=True) + rootfs = stack.enter_context(utils._tempdir(dir=SANDBOX_DIR)) + except PermissionError as e: + raise SandboxError('Could not create sysroot in {}: {}' + .format(SANDBOX_DIR, e)) from e + + stack.enter_context(self.stage_sysroot(rootfs, flags, stdout, stderr)) + + # Chroot! + if flags & SandboxFlags.INTERACTIVE: + stdin = sys.stdin + else: + stdin = stack.enter_context(open(os.devnull, 'r')) + + status = self.chroot(rootfs, command, stdin, stdout, stderr, + cwd, env, flags) + return status + + def chroot(self, rootfs, command, stdin, stdout, stderr, cwd, env, + flags): + # Create a script in the root directory of the sysroot to + # execute the given commands. + script = "\n".join(["#!/bin/sh"] + command) + scriptpath = os.path.join(rootfs, 'buildstream-run.sh') + + with open(scriptpath, 'w') as scriptfile: + scriptfile.write(script) + perms = os.stat(scriptpath).st_mode + os.chmod(scriptpath, perms & stat.S_IXUSR) + + # Execute the script with userchroot + try: + command = [utils.get_host_tool('userchroot'), + rootfs, + '--install-devices', + '/buildstream-run.sh'] + return self.popen(command, + env=env, + stdin=stdin, + stdout=stdout, + stderr=stderr, + cwd=os.path.join(rootfs, cwd.lstrip(os.sep)), + start_new_session=flags & SandboxFlags.INTERACTIVE) + + except subprocess.SubprocessError as e: + raise SandboxError('Could not run command {}: {}'.format(command, e)) from e + + # mount_dirs() + # + # Since we aren't root we can't arbitrarily mount directories. Yet + # we *require* our FUSE filesystem for at least some operations. + # + # FUSE can be mounted by users, therefore this mount function + # attempts to safely mount our FUSE system on top of a copy of our + # sandbox files. + # + # This *does* mean that this platform is significantly slower than + # others, unfortunately... + # + @contextmanager + def stage_sysroot(self, rootfs, flags, stdout, stderr): + def mount(d): + overrides = self._get_mount_sources() + + if d in overrides: + src = overrides[d] + else: + src = self.mount_map.get_mount_source(d) + + dst = os.path.join(rootfs, d.lstrip(os.sep)) + + self.info('Mounting {} to {}'.format(src, dst)) + + with self.mount_map.mounted(rootfs): + yield + + mount('/') + + for mark in self._get_marked_directories(): + mount(mark['directory']) + + def info(self, message): + msg = Message('sandbox', MessageType.INFO, message) + self._get_context()._message(msg) |