summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJürg Billeter <j@bitron.ch>2019-12-18 11:27:31 +0100
committerChandan Singh <chandan@chandansingh.net>2019-12-31 11:54:03 +0000
commit1a2b2295a9729104c34187abacfb1bc0f6090a0c (patch)
treef09e73132c71080da24a6c62c4b4894da01dea98
parentc2449ad60f15badbd0efd4ac1c4148cf42e08378 (diff)
downloadbuildstream-1a2b2295a9729104c34187abacfb1bc0f6090a0c.tar.gz
sandbox: Drop chroot sandboxing backend
The chroot sandboxing backend didn't work across platforms and has been replaced by the buildbox-run sandboxing backend.
-rw-r--r--src/buildstream/sandbox/_sandboxchroot.py354
1 files changed, 0 insertions, 354 deletions
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 <http://www.gnu.org/licenses/>.
-#
-# Authors:
-# Tristan Maat <tristan.maat@codethink.co.uk>
-# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
-
-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