From 1a2b2295a9729104c34187abacfb1bc0f6090a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Billeter?= Date: Wed, 18 Dec 2019 11:27:31 +0100 Subject: sandbox: Drop chroot sandboxing backend The chroot sandboxing backend didn't work across platforms and has been replaced by the buildbox-run sandboxing backend. --- src/buildstream/sandbox/_sandboxchroot.py | 354 ------------------------------ 1 file changed, 354 deletions(-) delete mode 100644 src/buildstream/sandbox/_sandboxchroot.py diff --git a/src/buildstream/sandbox/_sandboxchroot.py b/src/buildstream/sandbox/_sandboxchroot.py deleted file mode 100644 index 1805131b1..000000000 --- a/src/buildstream/sandbox/_sandboxchroot.py +++ /dev/null @@ -1,354 +0,0 @@ -# -# Copyright (C) 2017 Codethink Limited -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see . -# -# Authors: -# Tristan Maat -# Tristan Van Berkom - -import os -import sys -import stat -import signal -import subprocess -from contextlib import contextmanager, ExitStack -import psutil - -from .._exceptions import SandboxError, PlatformError -from .. import utils -from .. import _signals -from ._mounter import Mounter -from ._mount import MountMap -from . import Sandbox, SandboxFlags, SandboxCommandError - - -class SandboxChroot(Sandbox): - _FUSE_MOUNT_OPTIONS = {"dev": True} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - uid = self._get_config().build_uid - gid = self._get_config().build_gid - if uid != 0 or gid != 0: - raise SandboxError( - "Chroot sandboxes cannot specify a non-root uid/gid " - "({},{} were supplied via config)".format(uid, gid) - ) - - self.mount_map = None - - @classmethod - def check_available(cls): - cls._uid = os.getuid() - cls._gid = os.getgid() - - available = cls._uid == 0 - if not available: - cls._dummy_reasons += ["uid is not 0"] - raise SandboxError("can not run chroot if uid is not 0") - - @classmethod - def check_sandbox_config(cls, local_platform, config): - # With the chroot sandbox, the UID/GID in the sandbox - # will match the host UID/GID (typically 0/0). - if config.build_uid != cls._uid or config.build_gid != cls._gid: - return False - - host_os = local_platform.get_host_os() - host_arch = local_platform.get_host_arch() - # Check host os and architecture match - if config.build_os != host_os: - raise PlatformError("Configured and host OS don't match.") - if config.build_arch != host_arch: - raise PlatformError("Configured and host architecture don't match.") - - return True - - def _run(self, command, flags, *, cwd, env): - - if not self._has_command(command[0], env): - raise SandboxCommandError( - "Staged artifacts do not provide command " "'{}'".format(command[0]), reason="missing-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, flags & SandboxFlags.ROOT_READ_ONLY, self._FUSE_MOUNT_OPTIONS) - - # Create a sysroot and run the command inside it - with ExitStack() as stack: - os.makedirs("/var/run/buildstream", exist_ok=True) - - # FIXME: While we do not currently do anything to prevent - # network access, we also don't copy /etc/resolv.conf to - # the new rootfs. - # - # This effectively disables network access, since DNs will - # never resolve, so anything a normal process wants to do - # will fail. Malicious processes could gain rights to - # anything anyway. - # - # Nonetheless a better solution could perhaps be found. - - rootfs = stack.enter_context(utils._tempdir(dir="/var/run/buildstream")) - stack.enter_context(self.create_devices(self._root, flags)) - stack.enter_context(self.mount_dirs(rootfs, flags, stdout, stderr)) - - if flags & SandboxFlags.INTERACTIVE: - stdin = sys.stdin - else: - stdin = stack.enter_context(open(os.devnull, "r")) - - # Ensure the cwd exists - if cwd is not None: - workdir = os.path.join(rootfs, cwd.lstrip(os.sep)) - os.makedirs(workdir, exist_ok=True) - status = self.chroot(rootfs, command, stdin, stdout, stderr, cwd, env, flags) - - self._vdir._mark_changed() - return status - - # 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): - def kill_proc(): - if process: - # First attempt to gracefully terminate - proc = psutil.Process(process.pid) - proc.terminate() - - try: - proc.wait(20) - except psutil.TimeoutExpired: - utils._kill_process_tree(process.pid) - - def suspend_proc(): - group_id = os.getpgid(process.pid) - os.killpg(group_id, signal.SIGSTOP) - - def resume_proc(): - group_id = os.getpgid(process.pid) - os.killpg(group_id, signal.SIGCONT) - - try: - with _signals.suspendable(suspend_proc, resume_proc), _signals.terminator(kill_proc): - process = subprocess.Popen( # pylint: disable=subprocess-popen-preexec-fn - 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 - - except subprocess.SubprocessError as e: - # Exceptions in preexec_fn are simply reported as - # 'Exception occurred in preexec_fn', turn these into - # a more readable message. - if str(e) == "Exception occurred in preexec_fn.": - raise SandboxError( - "Could not chroot into {} or chdir into {}. " - "Ensure you are root and that the relevant directory exists.".format(rootfs, cwd) - ) from e - - # Otherwise, raise a more general error - 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, - # none, etc.) - # - # Args: - # rootfs (str): The path of the sysroot to prepare - # flags (:class:`.SandboxFlags`): The sandbox flags - # - @contextmanager - def create_devices(self, rootfs, flags): - - devices = [] - # When we are interactive, we'd rather mount /dev due to the - # sheer number of devices - if not flags & SandboxFlags.INTERACTIVE: - - for device in Sandbox.DEVICES: - location = os.path.join(rootfs, device.lstrip(os.sep)) - os.makedirs(os.path.dirname(location), exist_ok=True) - try: - if os.path.exists(location): - os.remove(location) - - devices.append(self.mknod(device, location)) - except OSError as err: - if err.errno == 1: - raise SandboxError( - "Permission denied while creating device node: {}.".format(err) - + "BuildStream reqiures root permissions for these setttings." - ) - - raise - - yield - - for device in devices: - os.remove(device) - - # mount_dirs() - # - # Mount paths required for the command. - # - # Args: - # rootfs (str): The path of the sysroot to prepare - # flags (:class:`.SandboxFlags`): The sandbox flags - # stdout (file): The stdout - # stderr (file): The stderr - # - @contextmanager - def mount_dirs(self, rootfs, flags, stdout, stderr): - - # FIXME: This should probably keep track of potentially - # already existing files a la _sandboxwrap.py:239 - - @contextmanager - def mount_point(point, **kwargs): - mount_source_overrides = self._get_mount_sources() - if point in mount_source_overrides: # pylint: disable=consider-using-get - mount_source = mount_source_overrides[point] - else: - mount_source = self.mount_map.get_mount_source(point) - mount_point = os.path.join(rootfs, point.lstrip(os.sep)) - - with Mounter.bind_mount(mount_point, src=mount_source, stdout=stdout, stderr=stderr, **kwargs): - yield - - @contextmanager - def mount_src(src, **kwargs): - mount_point = os.path.join(rootfs, src.lstrip(os.sep)) - os.makedirs(mount_point, exist_ok=True) - - with Mounter.bind_mount(mount_point, src=src, stdout=stdout, stderr=stderr, **kwargs): - yield - - with ExitStack() as stack: - stack.enter_context(self.mount_map.mounted(self)) - - stack.enter_context(mount_point("/")) - - if flags & SandboxFlags.INTERACTIVE: - stack.enter_context(mount_src("/dev")) - - stack.enter_context(mount_src("/tmp")) - stack.enter_context(mount_src("/proc")) - - for mark in self._get_marked_directories(): - stack.enter_context(mount_point(mark["directory"])) - - # Remount root RO if necessary - if flags & flags & SandboxFlags.ROOT_READ_ONLY: - root_mount = Mounter.mount(rootfs, stdout=stdout, stderr=stderr, remount=True, ro=True, bind=True) - # Since the exit stack has already registered a mount - # for this path, we do not need to register another - # umount call. - root_mount.__enter__() - - yield - - # mknod() - # - # Create a device node equivalent to the given source node - # - # Args: - # source (str): Path of the device to mimic (e.g. '/dev/null') - # target (str): Location to create the new device in - # - # Returns: - # target (str): The location of the created node - # - def mknod(self, source, target): - try: - dev = os.stat(source) - major = os.major(dev.st_rdev) - minor = os.minor(dev.st_rdev) - - target_dev = os.makedev(major, minor) - - os.mknod(target, mode=stat.S_IFCHR | dev.st_mode, device=target_dev) - - except PermissionError as e: - raise SandboxError("Could not create device {}, ensure that you have root permissions: {}") - - except OSError as e: - raise SandboxError("Could not create device {}: {}".format(target, e)) from e - - return target -- cgit v1.2.1