From db58a2c89c7fd8579119b938a9b2ef0becd9e903 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Tue, 3 Dec 2024 23:16:44 +0100 Subject: [PATCH 01/30] wrap the call to the tool to obtain a version inside of a podman container --- contrib/vcloud/benchmarkclient_executor.py | 110 +++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index 2e448aa12..015d69b2d 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -11,6 +11,7 @@ import shutil import subprocess import sys +from pathlib import Path import benchexec.tooladapter import benchexec.util @@ -35,11 +36,120 @@ def set_vcloud_jar_path(p): vcloud_jar = p +def find_tool_base_dir(tool_locator, executable): + dirs = [] + if tool_locator.tool_directory: + # join automatically handles the case where subdir is the empty string + dirs.append(tool_locator.tool_directory) + if tool_locator.use_path: + dirs.extend(benchexec.util.get_path()) + if tool_locator.use_current: + dirs.append(os.curdir) + + executable_path = Path(executable).resolve() + print(f"executable_path: {executable_path}") + for candidate_dir in dirs: + print(f"candidate_dir: {candidate_dir}") + abs_candidate_dir = Path(candidate_dir).resolve() + if executable_path.is_relative_to(abs_candidate_dir): + return abs_candidate_dir + + +class ContainerizedVersionGetter: + def __init__(self, tool_base_dir, image, fall_back): + self.tool_base_dir: Path = Path(tool_base_dir) + self.image = image + self.fall_back = fall_back + + def __call__( + self, + executable, + arg="--version", + use_stderr=False, + ignore_stderr=False, + line_prefix=None, + ): + """ + This function is a replacement for the get_version_from_tool. It wraps the call to the tool + in a container. + """ + if shutil.which("podman") is None: + logging.warning( + "Podman is not available on the system.\n Determining version will fall back to the default way." + ) + return self.fall_back( + executable, arg, use_stderr, ignore_stderr, line_prefix + ) + + command = [ + "podman", + "run", + "--rm", + "--entrypoint", + '[""]', + "--volume", + f"{self.tool_base_dir}:{self.tool_base_dir}", + "--workdir", + str(self.tool_base_dir), + self.image, + Path(executable).resolve().relative_to(self.tool_base_dir), + arg, + ] + logging.info("Using container with podman to determine version.") + logging.debug("Running command: %s", " ".join(map(str, command))) + try: + process = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.DEVNULL, + universal_newlines=True, + ) + except OSError as e: + logging.warning( + "Cannot run %s to determine version: %s", executable, e.strerror + ) + return "" + if process.stderr and not use_stderr and not ignore_stderr: + logging.warning( + "Cannot determine %s version, error output: %s", + executable, + process.stderr, + ) + return "" + if process.returncode: + logging.warning( + "Cannot determine %s version, exit code %s", + executable, + process.returncode, + ) + return "" + + output = (process.stderr if use_stderr else process.stdout).strip() + if line_prefix: + matches = ( + line[len(line_prefix) :].strip() + for line in output.splitlines() + if line.startswith(line_prefix) + ) + output = next(matches, "") + return output + + def init(config, benchmark): global _JustReprocessResults _JustReprocessResults = config.reprocessResults tool_locator = benchexec.tooladapter.create_tool_locator(config) benchmark.executable = benchmark.tool.executable(tool_locator) + if config.containerImage: + tool_base_dir = find_tool_base_dir(tool_locator, benchmark.executable) + print(f"tool_base_dir: {tool_base_dir}") + benchmark.tool._version_from_tool = ContainerizedVersionGetter( + tool_base_dir, + config.containerImage, + fall_back=benchmark.tool._version_from_tool, + ) + benchmark.tool_version = benchmark.tool.version(benchmark.executable) environment = benchmark.environment() if environment.get("keepEnv", None) or environment.get("additionalEnv", None): From d6e65d940b1322c6da4146c65178c6385823d01e Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Tue, 3 Dec 2024 23:38:15 +0100 Subject: [PATCH 02/30] explicitly check for Noneness of tool_base_dir --- contrib/vcloud/benchmarkclient_executor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index 015d69b2d..d2aef5dce 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -54,6 +54,8 @@ def find_tool_base_dir(tool_locator, executable): if executable_path.is_relative_to(abs_candidate_dir): return abs_candidate_dir + return None + class ContainerizedVersionGetter: def __init__(self, tool_base_dir, image, fall_back): @@ -143,12 +145,12 @@ def init(config, benchmark): benchmark.executable = benchmark.tool.executable(tool_locator) if config.containerImage: tool_base_dir = find_tool_base_dir(tool_locator, benchmark.executable) - print(f"tool_base_dir: {tool_base_dir}") - benchmark.tool._version_from_tool = ContainerizedVersionGetter( - tool_base_dir, - config.containerImage, - fall_back=benchmark.tool._version_from_tool, - ) + if tool_base_dir is not None: + benchmark.tool._version_from_tool = ContainerizedVersionGetter( + tool_base_dir, + config.containerImage, + fall_back=benchmark.tool._version_from_tool, + ) benchmark.tool_version = benchmark.tool.version(benchmark.executable) environment = benchmark.environment() From ca7aad9cea2f86b8edc7984b61891b035d53b4f3 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 16:45:36 +0100 Subject: [PATCH 03/30] add setns to libc --- benchexec/libc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/benchexec/libc.py b/benchexec/libc.py index f000c2d6f..72d49828a 100644 --- a/benchexec/libc.py +++ b/benchexec/libc.py @@ -70,6 +70,10 @@ def _check_errno(result, func, arguments): unshare.argtypes = [c_int] unshare.errcheck = _check_errno +setns = _libc.setns +"""Set the current process namespace(s).""" +setns.argtypes = [c_int, c_int] +setns.errcheck = _check_errno mmap = _libc.mmap """Map file into memory.""" From a2abe909fbe4951192afd999e5aa56193ec650d5 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 17:02:57 +0100 Subject: [PATCH 04/30] sort imports --- contrib/vcloud-benchmark.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contrib/vcloud-benchmark.py b/contrib/vcloud-benchmark.py index e3f1075a7..dfd352866 100755 --- a/contrib/vcloud-benchmark.py +++ b/contrib/vcloud-benchmark.py @@ -9,19 +9,20 @@ import logging import os +import subprocess import sys import tempfile import urllib.request -import subprocess sys.dont_write_bytecode = True # prevent creation of .pyc files sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from vcloud.vcloudbenchmarkbase import VcloudBenchmarkBase # noqa E402 from vcloud import vcloudutil # noqa E402 -from benchexec import __version__ # noqa E402 +from vcloud.vcloudbenchmarkbase import VcloudBenchmarkBase # noqa E402 + import benchexec.benchexec # noqa E402 import benchexec.tools # noqa E402 +from benchexec import __version__ # noqa E402 _ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "vcloud")) IVY_JAR_NAME = "ivy-2.5.0.jar" From 6ac1b2097948f064de2fb1568a01d26354f9e83a Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 17:27:59 +0100 Subject: [PATCH 05/30] add podman containerized tool --- contrib/vcloud/podman_containerized_tool.py | 297 ++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 contrib/vcloud/podman_containerized_tool.py diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py new file mode 100644 index 000000000..b225370fe --- /dev/null +++ b/contrib/vcloud/podman_containerized_tool.py @@ -0,0 +1,297 @@ +# This file is part of BenchExec, a framework for reliable benchmarking: +# https://github.com/sosy-lab/benchexec +# +# SPDX-FileCopyrightText: 2007-2020 Dirk Beyer +# +# SPDX-License-Identifier: Apache-2.0 + +import errno +import functools +import inspect +import logging +import multiprocessing +import os +import signal +import subprocess +import sys + +from benchexec import ( + BenchExecException, + container, + libc, + tooladapter, + util, +) + +tool: tooladapter.CURRENT_BASETOOL = None + +TOOL_DIRECTORY_MOUNT_POINT = "/mnt/__benchexec_tool_directory" + + +@tooladapter.CURRENT_BASETOOL.register # mark as instance of CURRENT_BASETOOL +class PodmanContainerizedTool(object): + """Wrapper for an instance of any subclass of one of the base-tool classes in + benchexec.tools.template. + The module and the subclass instance will be loaded in a subprocess that has been + put into a container. This means, for example, that the code of this module cannot + make network connections and that any changes made to files on disk have no effect. + + Because we use the multiprocessing module and thus communication is done + via serialization with pickle, this is not a secure solution: + Code from the tool-info module can use pickle to execute arbitrary code + in the main BenchExec process. + But the use of containers in BenchExec is for safety and robustness, not security. + + This class is heavily inspired by ContainerizedTool and it will create a podman + container and move the multiprocessing process into the namespace of the podman container. + """ + + def __init__(self, tool_module, config, image): + """Load tool-info module in subprocess. + @param tool_module: The name of the module to load. + Needs to define class named Tool. + @param config: A config object suitable for + benchexec.containerexecutor.handle_basic_container_args() + """ + if not config.tool_directory: + logging.warning( + "Podman continaerized toool currently only works if --tool-directory is set" + ) + raise ValueError( + "Podman continaerized toool currently only works if --tool-directory is set" + ) + + # We use multiprocessing.Pool as an easy way for RPC with another process. + self._pool = multiprocessing.Pool(1, _init_worker_process) + + self.container_id = None + # Call function that loads tool module and returns its doc + try: + self.__doc__, self.container_id = self._pool.apply( + _init_container_and_load_tool, + [tool_module], + { + "image": image, + "tool_directory": config.tool_directory, + }, + ) + except BaseException as e: + self._pool.terminate() + raise e + + def close(self): + self._forward_call("close", [], {}) + self._pool.close() + if self.container_id is None: + return + try: + # FIXME: Unexpected terminations could lead to the container not being stopped and removed + # SIGTERM sent by stop does not stop the container running tail -F /dev/null or sleep infinity + subprocess.run( + ["podman", "kill", "--signal", "SIGKILL", self.container_id], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as e: + logging.warning( + "Failed to stop container %s: %s", + self.container_id, + e.stderr.decode(), + ) + + def _forward_call(self, method_name, args, kwargs): + """Call given method indirectly on the tool instance in the container.""" + return self._pool.apply(_call_tool_func, [method_name, list(args), kwargs]) + + @classmethod + def _add_proxy_function(cls, method_name, method): + """Add function to given class that calls the specified method indirectly.""" + + @functools.wraps(method) # lets proxy_function look like method (name and doc) + def proxy_function(self, *args, **kwargs): + return self._forward_call(method_name, args, kwargs) + + if method_name == "working_directory": + # Add a cache. This method is called per run but would always return the + # same result. On some systems the calls are slow and this is worth it: + # https://github.com/python/cpython/issues/98493 + proxy_function = functools.lru_cache()(proxy_function) + + setattr(cls, member_name, proxy_function) + + +# The following will automatically add forwarding methods for all methods defined by the +# current tool-info API. This should work without any version-specific adjustments, +# so we declare compatibility with the latest version with @CURRENT_BASETOOL.register. +# We do not inherit from a BaseTool class to ensure that no default methods will be used +# accidentally. +for member_name, member in inspect.getmembers( + tooladapter.CURRENT_BASETOOL, inspect.isfunction +): + if member_name[0] == "_" or member_name == "close": + continue + PodmanContainerizedTool._add_proxy_function(member_name, member) + + +def _init_worker_process(): + """Initial setup of worker process from multiprocessing module.""" + + # Need to reset signal handling because multiprocessing relies on SIGTERM + # but benchexec adds a handler for it. + signal.signal(signal.SIGTERM, signal.SIG_DFL) + + # If Ctrl+C is pressed, each process receives SIGINT. We need to ignore it because + # concurrent worker threads of benchexec might still attempt to use the tool-info + # module until all of them are stopped, so this process must stay alive. + signal.signal(signal.SIGINT, signal.SIG_IGN) + + +def _init_container_and_load_tool(tool_module, *args, **kwargs): + """Initialize container for the current process and load given tool-info module.""" + try: + container_id = _init_container(*args, **kwargs) + except OSError as e: + if container.check_apparmor_userns_restriction(e): + raise BenchExecException(container._ERROR_MSG_USER_NS_RESTRICTION) + raise BenchExecException(f"Failed to configure container: {e}") + return _load_tool(tool_module), container_id + + +def _init_container( + image, + tool_directory, +): + """ + Move this process into a container. + """ + + volumes = [] + + tool_directory = os.path.abspath(tool_directory) + + # Mount the python loaded paths into the container + # The modules are mounted at the exact same path in the container + # because we do not yet know a solution to tell python to use + # different paths for the modules in the container. + python_paths = [path for path in sys.path if os.path.isdir(path)] + for path in python_paths: + abs_path = os.path.abspath(path) + volumes.extend(["--volume", f"{abs_path}:{abs_path}:ro"]) + + # Mount the tool directory into the container at a known location + volumes.extend( + [ + "--volume", + f"{tool_directory}:{TOOL_DIRECTORY_MOUNT_POINT}:O", + # :O creates an overlay mount. The tool can write files in the container + # but they are not visible outside the container. + "--workdir", + "/mnt", + ] + ) + + # Create a container that does nothing but keeps running + command = ( + ["podman", "run", "--entrypoint", "tail", "--rm", "-d"] + + volumes + + [image, "-F", "/dev/null"] + ) + + logging.debug( + "Command to start container: %s", + " ".join(map(str, command)), + ) + res = subprocess.run( + command, + stdout=subprocess.PIPE, + ) + + res.check_returncode() + container_id = res.stdout.decode().strip() + + container_pid = ( + subprocess.run( + ["podman", "inspect", "--format", "{{.State.Pid}}", container_id], + stdout=subprocess.PIPE, + ) + .stdout.decode() + .strip() + ) + + try: + logging.debug("Joining user namespace of container %s", container_id) + + # The user namespace must be joined first + user_ns = f"/proc/{container_pid}/ns/user" + with open(user_ns, "rb") as f: + libc.setns(f.fileno(), 0) + + for namespace in os.listdir(f"/proc/{container_pid}/ns"): + namespace = os.path.join(f"/proc/{container_pid}/ns", namespace) + + if namespace == user_ns: + continue + logging.debug("Joining namespace %s", namespace) + + try: + # We try to mount all listed namespaces, but some might not be available + with open(namespace, "rb") as f: + libc.setns(f.fileno(), 0) + + except OSError as e: + logging.debug( + "Failed to join namespace %s: %s", namespace, os.strerror(e.errno) + ) + + os.chdir("/mnt") + return container_id + + except OSError as e: + if ( + e.errno == errno.EPERM + and util.try_read_file("/proc/sys/kernel/unprivileged_userns_clone") == "0" + ): + raise BenchExecException( + "Unprivileged user namespaces forbidden on this system, please " + "enable them with 'sysctl -w kernel.unprivileged_userns_clone=1' " + "or disable container mode" + ) + elif ( + e.errno in {errno.ENOSPC, errno.EINVAL} + and util.try_read_file("/proc/sys/user/max_user_namespaces") == "0" + ): + # Ubuntu has ENOSPC, Centos seems to produce EINVAL in this case + raise BenchExecException( + "Unprivileged user namespaces forbidden on this system, please " + "enable by using 'sysctl -w user.max_user_namespaces=10000' " + "(or another value) or disable container mode" + ) + else: + raise BenchExecException( + "Creating namespace for container mode failed: " + os.strerror(e.errno) + ) + + +def _load_tool(tool_module): + logging.debug("Loading tool-info module %s in container", tool_module) + global tool + + tool = __import__(tool_module, fromlist=["Tool"]).Tool() + + tool = tooladapter.adapt_to_current_version(tool) + return tool.__doc__ + + +def _call_tool_func(name, args, kwargs): + """Call a method on the tool instance. + @param name: The method name to call. + @param args: List of arguments to be passed as positional arguments. + @param kwargs: Dict of arguments to be passed as keyword arguments. + """ + global tool + try: + return getattr(tool, name)(*args, **kwargs) + except SystemExit as e: + # SystemExit would terminate the worker process instead of being propagated. + raise BenchExecException(str(e.code)) From a28f1ad61594fc7a28e033fbe9fd280983733a33 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 17:28:51 +0100 Subject: [PATCH 06/30] hook into the load toolinfo process --- contrib/vcloud-benchmark.py | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/contrib/vcloud-benchmark.py b/contrib/vcloud-benchmark.py index dfd352866..fc5b3ac1a 100755 --- a/contrib/vcloud-benchmark.py +++ b/contrib/vcloud-benchmark.py @@ -21,6 +21,7 @@ from vcloud.vcloudbenchmarkbase import VcloudBenchmarkBase # noqa E402 import benchexec.benchexec # noqa E402 +import benchexec.model # noqa E402 import benchexec.tools # noqa E402 from benchexec import __version__ # noqa E402 @@ -29,6 +30,8 @@ IVY_PATH = os.path.join(_ROOT_DIR, "lib", IVY_JAR_NAME) IVY_DOWNLOAD_URL = "https://www.sosy-lab.org/ivy/org.apache.ivy/ivy/" + IVY_JAR_NAME +real_load_tool_info = benchexec.model.load_tool_info + def download_required_jars(config): # download ivy if needed @@ -72,6 +75,44 @@ def download_required_jars(config): temp_dir.cleanup() +def hook_load_tool_info(tool_name, config): + """ + Load the tool-info class. + @param tool_name: The name of the tool-info module. + Either a full Python package name or a name within the benchexec.tools package. + @return: A tuple of the full name of the used tool-info module and an instance of the tool-info class. + """ + tool_module = tool_name if "." in tool_name else f"benchexec.tools.{tool_name}" + try: + if config.containerImage: + import vcloud.podman_containerized_tool as pod + + tool = pod.PodmanContainerizedTool( + tool_module, config, config.containerImage + ) + else: + _, tool = real_load_tool_info(tool_module, config) + + except ImportError as ie: + logging.debug( + "Did not find module '%s'. " + "Python probably looked for it in one of the following paths:\n %s", + tool_module, + "\n ".join(path or "." for path in sys.path), + ) + sys.exit(f'Unsupported tool "{tool_name}" specified. ImportError: {ie}') + except AttributeError as ae: + sys.exit( + f'Unsupported tool "{tool_name}" specified, class "Tool" is missing: {ae}' + ) + except TypeError as te: + sys.exit(f'Unsupported tool "{tool_name}" specified. TypeError: {te}') + return tool_module, tool + + +benchexec.model.load_tool_info = hook_load_tool_info + + class VcloudBenchmark(VcloudBenchmarkBase): """ Benchmark class that defines the load_executor function. From 6252252cc1b849e13986c5a635e5e82cf8aad918 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 17:30:06 +0100 Subject: [PATCH 07/30] add CustomToolLocator that is used in conjunction with the PodmanContainerizedTool --- contrib/vcloud/benchmarkclient_executor.py | 153 +++++++-------------- 1 file changed, 49 insertions(+), 104 deletions(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index d2aef5dce..2965b741a 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -15,6 +15,7 @@ import benchexec.tooladapter import benchexec.util +from benchexec.tools.template import ToolNotFoundException from . import vcloudutil @@ -36,121 +37,65 @@ def set_vcloud_jar_path(p): vcloud_jar = p -def find_tool_base_dir(tool_locator, executable): - dirs = [] - if tool_locator.tool_directory: - # join automatically handles the case where subdir is the empty string - dirs.append(tool_locator.tool_directory) - if tool_locator.use_path: - dirs.extend(benchexec.util.get_path()) - if tool_locator.use_current: - dirs.append(os.curdir) - - executable_path = Path(executable).resolve() - print(f"executable_path: {executable_path}") - for candidate_dir in dirs: - print(f"candidate_dir: {candidate_dir}") - abs_candidate_dir = Path(candidate_dir).resolve() - if executable_path.is_relative_to(abs_candidate_dir): - return abs_candidate_dir +class CustomToolLocator: + def __init__(self, tool_directory=None, container_mount_point=None): + self.tool_directory = tool_directory + self.container_mount_point = container_mount_point - return None + def find_executable(self, executable_name, subdir=""): + logging.debug( + "Using custom tool locator to find executable %s", executable_name + ) + assert ( + os.path.basename(executable_name) == executable_name + ), "Executable needs to be a simple file name" + dirs = [] + + if not self.tool_directory: + raise ToolNotFoundException( + "Podman containerized tool info module execution is only possible with --tool-directory explicitly set." + ) + assert self.container_mount_point is not None, "Container mount point not set" -class ContainerizedVersionGetter: - def __init__(self, tool_base_dir, image, fall_back): - self.tool_base_dir: Path = Path(tool_base_dir) - self.image = image - self.fall_back = fall_back - - def __call__( - self, - executable, - arg="--version", - use_stderr=False, - ignore_stderr=False, - line_prefix=None, - ): - """ - This function is a replacement for the get_version_from_tool. It wraps the call to the tool - in a container. - """ - if shutil.which("podman") is None: - logging.warning( - "Podman is not available on the system.\n Determining version will fall back to the default way." - ) - return self.fall_back( - executable, arg, use_stderr, ignore_stderr, line_prefix - ) + dirs.append(os.path.join(self.container_mount_point, subdir)) + logging.debug("Searching for executable %s in %s", executable_name, dirs) - command = [ - "podman", - "run", - "--rm", - "--entrypoint", - '[""]', - "--volume", - f"{self.tool_base_dir}:{self.tool_base_dir}", - "--workdir", - str(self.tool_base_dir), - self.image, - Path(executable).resolve().relative_to(self.tool_base_dir), - arg, - ] - logging.info("Using container with podman to determine version.") - logging.debug("Running command: %s", " ".join(map(str, command))) - try: - process = subprocess.run( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.DEVNULL, - universal_newlines=True, - ) - except OSError as e: - logging.warning( - "Cannot run %s to determine version: %s", executable, e.strerror - ) - return "" - if process.stderr and not use_stderr and not ignore_stderr: - logging.warning( - "Cannot determine %s version, error output: %s", - executable, - process.stderr, - ) - return "" - if process.returncode: - logging.warning( - "Cannot determine %s version, exit code %s", - executable, - process.returncode, - ) - return "" - - output = (process.stderr if use_stderr else process.stdout).strip() - if line_prefix: - matches = ( - line[len(line_prefix) :].strip() - for line in output.splitlines() - if line.startswith(line_prefix) + executable = benchexec.util.find_executable2(executable_name, dirs) + if executable: + return os.path.relpath(executable, self.tool_directory) + + other_file = benchexec.util.find_executable2(executable_name, dirs, os.F_OK) + if other_file: + raise ToolNotFoundException( + f"Could not find executable '{executable_name}', " + f"but found file '{other_file}' that is not executable." ) - output = next(matches, "") - return output + + msg = ( + f"Could not find executable '{executable_name}'. " + f"The searched directories were: " + "".join("\n " + d for d in dirs) + ) + if not self.tool_directory: + msg += "\nYou can specify the tool's directory with --tool-directory." + + raise ToolNotFoundException(msg) def init(config, benchmark): global _JustReprocessResults _JustReprocessResults = config.reprocessResults - tool_locator = benchexec.tooladapter.create_tool_locator(config) - benchmark.executable = benchmark.tool.executable(tool_locator) + if config.containerImage: - tool_base_dir = find_tool_base_dir(tool_locator, benchmark.executable) - if tool_base_dir is not None: - benchmark.tool._version_from_tool = ContainerizedVersionGetter( - tool_base_dir, - config.containerImage, - fall_back=benchmark.tool._version_from_tool, - ) + from vcloud.podman_containerized_tool import TOOL_DIRECTORY_MOUNT_POINT + + tool_locator = CustomToolLocator( + config.tool_directory, TOOL_DIRECTORY_MOUNT_POINT + ) + else: + tool_locator = benchexec.tooladapter.create_tool_locator(config) + + benchmark.executable = benchmark.tool.executable(tool_locator) benchmark.tool_version = benchmark.tool.version(benchmark.executable) environment = benchmark.environment() From dab9a0dd3af4ecf83c2affc06bf9adce576cbf64 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 23:01:32 +0100 Subject: [PATCH 08/30] cd into the tool directory in case tools makes calls relative to itself --- contrib/vcloud/benchmarkclient_executor.py | 1 - contrib/vcloud/podman_containerized_tool.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index 2965b741a..b635fb7d9 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -96,7 +96,6 @@ def init(config, benchmark): tool_locator = benchexec.tooladapter.create_tool_locator(config) benchmark.executable = benchmark.tool.executable(tool_locator) - benchmark.tool_version = benchmark.tool.version(benchmark.executable) environment = benchmark.environment() if environment.get("keepEnv", None) or environment.get("additionalEnv", None): diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index b225370fe..14d6ffd60 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -244,7 +244,7 @@ def _init_container( "Failed to join namespace %s: %s", namespace, os.strerror(e.errno) ) - os.chdir("/mnt") + os.chdir(TOOL_DIRECTORY_MOUNT_POINT) return container_id except OSError as e: From ed499c4170af253ece5416d093e1e55690448a62 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 23:03:16 +0100 Subject: [PATCH 09/30] remove erroneous relpath call --- contrib/vcloud/benchmarkclient_executor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index b635fb7d9..176526143 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -58,12 +58,14 @@ def find_executable(self, executable_name, subdir=""): assert self.container_mount_point is not None, "Container mount point not set" + # At this point we know, that the tool is located at container_mount_point + # as the container as the tool_dir mounted to this location dirs.append(os.path.join(self.container_mount_point, subdir)) logging.debug("Searching for executable %s in %s", executable_name, dirs) executable = benchexec.util.find_executable2(executable_name, dirs) if executable: - return os.path.relpath(executable, self.tool_directory) + return executable other_file = benchexec.util.find_executable2(executable_name, dirs, os.F_OK) if other_file: From 6a4760b6d367b238e21ea6aebc455e09338b1789 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 23:21:39 +0100 Subject: [PATCH 10/30] find the files to upload to vcloud again --- contrib/vcloud/benchmarkclient_executor.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index 176526143..d0443e82f 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -94,11 +94,13 @@ def init(config, benchmark): tool_locator = CustomToolLocator( config.tool_directory, TOOL_DIRECTORY_MOUNT_POINT ) - else: - tool_locator = benchexec.tooladapter.create_tool_locator(config) + executable_for_version = benchmark.tool.executable(tool_locator) + benchmark.tool_version = benchmark.tool.version(executable_for_version) + # The vcloud uses the tool location later to determine which files need to be uploaded + tool_locator = benchexec.tooladapter.create_tool_locator(config) benchmark.executable = benchmark.tool.executable(tool_locator) - benchmark.tool_version = benchmark.tool.version(benchmark.executable) + environment = benchmark.environment() if environment.get("keepEnv", None) or environment.get("additionalEnv", None): sys.exit( From e673acd6ed032a7bb1da97acd6f11b331445108e Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 4 Dec 2024 23:48:38 +0100 Subject: [PATCH 11/30] the vcloud executor needs to know about the executable location on the host --- contrib/vcloud/benchmarkclient_executor.py | 31 +++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index d0443e82f..742171d75 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -97,9 +97,34 @@ def init(config, benchmark): executable_for_version = benchmark.tool.executable(tool_locator) benchmark.tool_version = benchmark.tool.version(executable_for_version) - # The vcloud uses the tool location later to determine which files need to be uploaded - tool_locator = benchexec.tooladapter.create_tool_locator(config) - benchmark.executable = benchmark.tool.executable(tool_locator) + # If the tool info does not call find_executable, we don't know if the + # executable path is containing the mount point. + # In this case we can check whether the path is relative + # and continue with the assumption that it is relative to the provided + # tool directory. + try: + executable_relative_to_mount_point = Path( + executable_for_version + ).relative_to(TOOL_DIRECTORY_MOUNT_POINT) + except ValueError: + if Path(executable_for_version).is_absolute(): + raise ValueError( + f"Executable path {executable_for_version} is not relative" + " and is not containing the expected container to the mount point" + " {TOOL_DIRECTORY_MOUNT_POINT}" + ) from None + executable_relative_to_mount_point = executable_for_version + + # The vcloud uses the tool location later to determine which files need to be uploaded + # So this needs to point to the actual path where the executable is on the host + benchmark.executable = ( + Path(config.tool_directory) / executable_relative_to_mount_point + ) + + else: + tool_locator = benchexec.tooladapter.create_tool_locator(config) + benchmark.executable = benchmark.tool.executable(tool_locator) + benchmark.tool_version = benchmark.tool.version(executable_for_version) environment = benchmark.environment() if environment.get("keepEnv", None) or environment.get("additionalEnv", None): From 517fbea021cb33e41a39e87ac3a2759e1bc79b64 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 10:35:09 +0100 Subject: [PATCH 12/30] custom tool locator need not now about original tool directory --- contrib/vcloud/benchmarkclient_executor.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index 742171d75..cf7a976e0 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -38,8 +38,7 @@ def set_vcloud_jar_path(p): class CustomToolLocator: - def __init__(self, tool_directory=None, container_mount_point=None): - self.tool_directory = tool_directory + def __init__(self, container_mount_point=None): self.container_mount_point = container_mount_point def find_executable(self, executable_name, subdir=""): @@ -51,11 +50,6 @@ def find_executable(self, executable_name, subdir=""): ), "Executable needs to be a simple file name" dirs = [] - if not self.tool_directory: - raise ToolNotFoundException( - "Podman containerized tool info module execution is only possible with --tool-directory explicitly set." - ) - assert self.container_mount_point is not None, "Container mount point not set" # At this point we know, that the tool is located at container_mount_point @@ -78,8 +72,6 @@ def find_executable(self, executable_name, subdir=""): f"Could not find executable '{executable_name}'. " f"The searched directories were: " + "".join("\n " + d for d in dirs) ) - if not self.tool_directory: - msg += "\nYou can specify the tool's directory with --tool-directory." raise ToolNotFoundException(msg) @@ -91,9 +83,7 @@ def init(config, benchmark): if config.containerImage: from vcloud.podman_containerized_tool import TOOL_DIRECTORY_MOUNT_POINT - tool_locator = CustomToolLocator( - config.tool_directory, TOOL_DIRECTORY_MOUNT_POINT - ) + tool_locator = CustomToolLocator(TOOL_DIRECTORY_MOUNT_POINT) executable_for_version = benchmark.tool.executable(tool_locator) benchmark.tool_version = benchmark.tool.version(executable_for_version) From ddecc421651352f4b5b41bef7445d172fd3ce69b Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 10:37:34 +0100 Subject: [PATCH 13/30] assert tool-directory is set --- contrib/vcloud/podman_containerized_tool.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index 14d6ffd60..cc40eec59 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -53,13 +53,9 @@ def __init__(self, tool_module, config, image): @param config: A config object suitable for benchexec.containerexecutor.handle_basic_container_args() """ - if not config.tool_directory: - logging.warning( - "Podman continaerized toool currently only works if --tool-directory is set" - ) - raise ValueError( - "Podman continaerized toool currently only works if --tool-directory is set" - ) + assert ( + config.tool_directory + ), "Tool directory must be set when using podman for tool info module." # We use multiprocessing.Pool as an easy way for RPC with another process. self._pool = multiprocessing.Pool(1, _init_worker_process) From 3808261794ba224a8f7f4c19f83ee591de0cc425 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 10:39:48 +0100 Subject: [PATCH 14/30] make it clearer whats the purpose of thee load tool info hook --- contrib/vcloud-benchmark.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/contrib/vcloud-benchmark.py b/contrib/vcloud-benchmark.py index fc5b3ac1a..c086c0314 100755 --- a/contrib/vcloud-benchmark.py +++ b/contrib/vcloud-benchmark.py @@ -82,16 +82,15 @@ def hook_load_tool_info(tool_name, config): Either a full Python package name or a name within the benchexec.tools package. @return: A tuple of the full name of the used tool-info module and an instance of the tool-info class. """ + if not config.containerImage: + return real_load_tool_info(tool_name, config) + tool_module = tool_name if "." in tool_name else f"benchexec.tools.{tool_name}" + try: - if config.containerImage: - import vcloud.podman_containerized_tool as pod + import vcloud.podman_containerized_tool as pod - tool = pod.PodmanContainerizedTool( - tool_module, config, config.containerImage - ) - else: - _, tool = real_load_tool_info(tool_module, config) + tool = pod.PodmanContainerizedTool(tool_module, config, config.containerImage) except ImportError as ie: logging.debug( From 50a7eaf09f55766d5ed72268fdb96d5a257f8c27 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 10:40:31 +0100 Subject: [PATCH 15/30] rename for clarity --- contrib/vcloud-benchmark.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/vcloud-benchmark.py b/contrib/vcloud-benchmark.py index c086c0314..0d446df89 100755 --- a/contrib/vcloud-benchmark.py +++ b/contrib/vcloud-benchmark.py @@ -30,7 +30,7 @@ IVY_PATH = os.path.join(_ROOT_DIR, "lib", IVY_JAR_NAME) IVY_DOWNLOAD_URL = "https://www.sosy-lab.org/ivy/org.apache.ivy/ivy/" + IVY_JAR_NAME -real_load_tool_info = benchexec.model.load_tool_info +original_load_tool_info = benchexec.model.load_tool_info def download_required_jars(config): @@ -83,7 +83,7 @@ def hook_load_tool_info(tool_name, config): @return: A tuple of the full name of the used tool-info module and an instance of the tool-info class. """ if not config.containerImage: - return real_load_tool_info(tool_name, config) + return original_load_tool_info(tool_name, config) tool_module = tool_name if "." in tool_name else f"benchexec.tools.{tool_name}" From 7a34dd40c9e7e460c21d0ef2d774eae645833dba Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 10:43:22 +0100 Subject: [PATCH 16/30] use shlex --- contrib/vcloud/podman_containerized_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index cc40eec59..dedb2db38 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -11,6 +11,7 @@ import logging import multiprocessing import os +import shlex import signal import subprocess import sys @@ -196,7 +197,7 @@ def _init_container( logging.debug( "Command to start container: %s", - " ".join(map(str, command)), + shlex.join(map(str, command)), ) res = subprocess.run( command, From e838dd7065072b6be6eaa7cb91bd056213f6d964 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 12:24:14 +0100 Subject: [PATCH 17/30] simplify container init --- contrib/vcloud/podman_containerized_tool.py | 79 ++++++--------------- 1 file changed, 22 insertions(+), 57 deletions(-) diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index dedb2db38..276cbd470 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -216,79 +216,44 @@ def _init_container( .strip() ) + def join_ns(namespace): + namespace = f"/proc/{container_pid}/ns/{namespace}" + logging.debug("Joining namespace %s .", namespace) + with open(namespace, "rb") as f: + libc.setns(f.fileno(), 0) + try: - logging.debug("Joining user namespace of container %s", container_id) + logging.debug("Joining namespaces of container %s.", container_id) + + necessary_namespaces = frozenset(("user", "mnt")) # The user namespace must be joined first - user_ns = f"/proc/{container_pid}/ns/user" - with open(user_ns, "rb") as f: - libc.setns(f.fileno(), 0) + # because the other namespaces depend on it + join_ns("user") for namespace in os.listdir(f"/proc/{container_pid}/ns"): - namespace = os.path.join(f"/proc/{container_pid}/ns", namespace) - - if namespace == user_ns: + if namespace in necessary_namespaces: continue - logging.debug("Joining namespace %s", namespace) - try: # We try to mount all listed namespaces, but some might not be available - with open(namespace, "rb") as f: - libc.setns(f.fileno(), 0) + join_ns(namespace) except OSError as e: logging.debug( "Failed to join namespace %s: %s", namespace, os.strerror(e.errno) ) + # The mount namespace must be joined so we want + # to fail if we cannot join the mount namespace. + # mnt must be joined last because after joining it, + # we can no longer access /proc//ns + join_ns("mnt") + os.chdir(TOOL_DIRECTORY_MOUNT_POINT) return container_id except OSError as e: - if ( - e.errno == errno.EPERM - and util.try_read_file("/proc/sys/kernel/unprivileged_userns_clone") == "0" - ): - raise BenchExecException( - "Unprivileged user namespaces forbidden on this system, please " - "enable them with 'sysctl -w kernel.unprivileged_userns_clone=1' " - "or disable container mode" - ) - elif ( - e.errno in {errno.ENOSPC, errno.EINVAL} - and util.try_read_file("/proc/sys/user/max_user_namespaces") == "0" - ): - # Ubuntu has ENOSPC, Centos seems to produce EINVAL in this case - raise BenchExecException( - "Unprivileged user namespaces forbidden on this system, please " - "enable by using 'sysctl -w user.max_user_namespaces=10000' " - "(or another value) or disable container mode" - ) - else: - raise BenchExecException( - "Creating namespace for container mode failed: " + os.strerror(e.errno) - ) - - -def _load_tool(tool_module): - logging.debug("Loading tool-info module %s in container", tool_module) - global tool - - tool = __import__(tool_module, fromlist=["Tool"]).Tool() - - tool = tooladapter.adapt_to_current_version(tool) - return tool.__doc__ - + raise BenchExecException( + "Joining the podman container failed: " + os.strerror(e.errno) + ) -def _call_tool_func(name, args, kwargs): - """Call a method on the tool instance. - @param name: The method name to call. - @param args: List of arguments to be passed as positional arguments. - @param kwargs: Dict of arguments to be passed as keyword arguments. - """ - global tool - try: - return getattr(tool, name)(*args, **kwargs) - except SystemExit as e: - # SystemExit would terminate the worker process instead of being propagated. - raise BenchExecException(str(e.code)) From a68c0ead6adba6903dbbe36a0c738407fc941abb Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 12:26:54 +0100 Subject: [PATCH 18/30] refactor containerized tool info modules for improved code sharing --- benchexec/containerized_tool.py | 38 +++-- contrib/vcloud/podman_containerized_tool.py | 176 +++++--------------- 2 files changed, 67 insertions(+), 147 deletions(-) diff --git a/benchexec/containerized_tool.py b/benchexec/containerized_tool.py index 3ecf46874..b314519f8 100644 --- a/benchexec/containerized_tool.py +++ b/benchexec/containerized_tool.py @@ -16,6 +16,7 @@ import signal import socket import tempfile +from abc import ABCMeta from benchexec import ( BenchExecException, @@ -26,12 +27,11 @@ util, ) - tool: tooladapter.CURRENT_BASETOOL = None @tooladapter.CURRENT_BASETOOL.register # mark as instance of CURRENT_BASETOOL -class ContainerizedTool(object): +class ContainerizedToolBase(object, metaclass=ABCMeta): """Wrapper for an instance of any subclass of one of the base-tool classes in benchexec.tools.template. The module and the subclass instance will be loaded in a subprocess that has been @@ -45,7 +45,7 @@ class ContainerizedTool(object): But the use of containers in BenchExec is for safety and robustness, not security. """ - def __init__(self, tool_module, config): + def __init__(self, tool_module, config, initializer): """Load tool-info module in subprocess. @param tool_module: The name of the module to load. Needs to define class named Tool. @@ -57,13 +57,13 @@ def __init__(self, tool_module, config): container_options = containerexecutor.handle_basic_container_args(config) temp_dir = tempfile.mkdtemp(prefix="Benchexec_tool_info_container_") - + self.container_id = None # Call function that loads tool module and returns its doc try: - self.__doc__ = self._pool.apply( + self.__doc__, self.container_id = self._pool.apply( _init_container_and_load_tool, - [tool_module, temp_dir], - container_options, + [initializer] + self.mk_args(tool_module, config, temp_dir), + self.mk_kwargs(container_options), ) except BaseException as e: self._pool.terminate() @@ -74,8 +74,15 @@ def __init__(self, tool_module, config): with contextlib.suppress(OSError): os.rmdir(temp_dir) + def mk_args(self, tool_module, config, tmp_dir): + return [tool_module, config, tmp_dir] + + def mk_kwargs(self, container_options): + return container_options + def close(self): self._forward_call("close", [], {}) + self._cleanup() self._pool.close() def _forward_call(self, method_name, args, kwargs): @@ -109,7 +116,7 @@ def proxy_function(self, *args, **kwargs): ): if member_name[0] == "_" or member_name == "close": continue - ContainerizedTool._add_proxy_function(member_name, member) + ContainerizedToolBase._add_proxy_function(member_name, member) def _init_worker_process(): @@ -125,15 +132,16 @@ def _init_worker_process(): signal.signal(signal.SIGINT, signal.SIG_IGN) -def _init_container_and_load_tool(tool_module, *args, **kwargs): +def _init_container_and_load_tool(initializer, tool_module, *args, **kwargs): """Initialize container for the current process and load given tool-info module.""" + container_id = None try: - _init_container(*args, **kwargs) + container_id = initializer(*args, **kwargs) except OSError as e: if container.check_apparmor_userns_restriction(e): raise BenchExecException(container._ERROR_MSG_USER_NS_RESTRICTION) raise BenchExecException(f"Failed to configure container: {e}") - return _load_tool(tool_module) + return _load_tool(tool_module), container_id def _init_container( @@ -288,3 +296,11 @@ def _call_tool_func(name, args, kwargs): except SystemExit as e: # SystemExit would terminate the worker process instead of being propagated. raise BenchExecException(str(e.code)) + + +class ContainerizedTool(ContainerizedToolBase): + def __init__(self, tool_module, config, initializer): + super().__init__(tool_module, config, initializer, initializer=_init_container) + + def _cleanup(self): + pass diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index 276cbd470..099f7adf9 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -5,156 +5,22 @@ # # SPDX-License-Identifier: Apache-2.0 -import errno -import functools -import inspect import logging -import multiprocessing import os import shlex -import signal import subprocess import sys from benchexec import ( BenchExecException, - container, libc, tooladapter, - util, ) - -tool: tooladapter.CURRENT_BASETOOL = None +from benchexec.containerized_tool import ContainerizedToolBase TOOL_DIRECTORY_MOUNT_POINT = "/mnt/__benchexec_tool_directory" -@tooladapter.CURRENT_BASETOOL.register # mark as instance of CURRENT_BASETOOL -class PodmanContainerizedTool(object): - """Wrapper for an instance of any subclass of one of the base-tool classes in - benchexec.tools.template. - The module and the subclass instance will be loaded in a subprocess that has been - put into a container. This means, for example, that the code of this module cannot - make network connections and that any changes made to files on disk have no effect. - - Because we use the multiprocessing module and thus communication is done - via serialization with pickle, this is not a secure solution: - Code from the tool-info module can use pickle to execute arbitrary code - in the main BenchExec process. - But the use of containers in BenchExec is for safety and robustness, not security. - - This class is heavily inspired by ContainerizedTool and it will create a podman - container and move the multiprocessing process into the namespace of the podman container. - """ - - def __init__(self, tool_module, config, image): - """Load tool-info module in subprocess. - @param tool_module: The name of the module to load. - Needs to define class named Tool. - @param config: A config object suitable for - benchexec.containerexecutor.handle_basic_container_args() - """ - assert ( - config.tool_directory - ), "Tool directory must be set when using podman for tool info module." - - # We use multiprocessing.Pool as an easy way for RPC with another process. - self._pool = multiprocessing.Pool(1, _init_worker_process) - - self.container_id = None - # Call function that loads tool module and returns its doc - try: - self.__doc__, self.container_id = self._pool.apply( - _init_container_and_load_tool, - [tool_module], - { - "image": image, - "tool_directory": config.tool_directory, - }, - ) - except BaseException as e: - self._pool.terminate() - raise e - - def close(self): - self._forward_call("close", [], {}) - self._pool.close() - if self.container_id is None: - return - try: - # FIXME: Unexpected terminations could lead to the container not being stopped and removed - # SIGTERM sent by stop does not stop the container running tail -F /dev/null or sleep infinity - subprocess.run( - ["podman", "kill", "--signal", "SIGKILL", self.container_id], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - ) - except subprocess.CalledProcessError as e: - logging.warning( - "Failed to stop container %s: %s", - self.container_id, - e.stderr.decode(), - ) - - def _forward_call(self, method_name, args, kwargs): - """Call given method indirectly on the tool instance in the container.""" - return self._pool.apply(_call_tool_func, [method_name, list(args), kwargs]) - - @classmethod - def _add_proxy_function(cls, method_name, method): - """Add function to given class that calls the specified method indirectly.""" - - @functools.wraps(method) # lets proxy_function look like method (name and doc) - def proxy_function(self, *args, **kwargs): - return self._forward_call(method_name, args, kwargs) - - if method_name == "working_directory": - # Add a cache. This method is called per run but would always return the - # same result. On some systems the calls are slow and this is worth it: - # https://github.com/python/cpython/issues/98493 - proxy_function = functools.lru_cache()(proxy_function) - - setattr(cls, member_name, proxy_function) - - -# The following will automatically add forwarding methods for all methods defined by the -# current tool-info API. This should work without any version-specific adjustments, -# so we declare compatibility with the latest version with @CURRENT_BASETOOL.register. -# We do not inherit from a BaseTool class to ensure that no default methods will be used -# accidentally. -for member_name, member in inspect.getmembers( - tooladapter.CURRENT_BASETOOL, inspect.isfunction -): - if member_name[0] == "_" or member_name == "close": - continue - PodmanContainerizedTool._add_proxy_function(member_name, member) - - -def _init_worker_process(): - """Initial setup of worker process from multiprocessing module.""" - - # Need to reset signal handling because multiprocessing relies on SIGTERM - # but benchexec adds a handler for it. - signal.signal(signal.SIGTERM, signal.SIG_DFL) - - # If Ctrl+C is pressed, each process receives SIGINT. We need to ignore it because - # concurrent worker threads of benchexec might still attempt to use the tool-info - # module until all of them are stopped, so this process must stay alive. - signal.signal(signal.SIGINT, signal.SIG_IGN) - - -def _init_container_and_load_tool(tool_module, *args, **kwargs): - """Initialize container for the current process and load given tool-info module.""" - try: - container_id = _init_container(*args, **kwargs) - except OSError as e: - if container.check_apparmor_userns_restriction(e): - raise BenchExecException(container._ERROR_MSG_USER_NS_RESTRICTION) - raise BenchExecException(f"Failed to configure container: {e}") - return _load_tool(tool_module), container_id - - def _init_container( image, tool_directory, @@ -162,7 +28,6 @@ def _init_container( """ Move this process into a container. """ - volumes = [] tool_directory = os.path.abspath(tool_directory) @@ -257,3 +122,42 @@ def join_ns(namespace): "Joining the podman container failed: " + os.strerror(e.errno) ) + +@tooladapter.CURRENT_BASETOOL.register # mark as instance of CURRENT_BASETOOL +class PodmanContainerizedTool(ContainerizedToolBase): + def __init__(self, tool_module, config, image): + assert ( + config.tool_directory + ), "Tool directory must be set when using podman for tool info module." + + self.tool_directory = config.tool_directory + self.image = image + + super().__init__(tool_module, config, _init_container) + + def mk_args(self, tool_module, config, tmp_dir): + return [tool_module] + + def mk_kwargs(self, container_options): + return { + "image": self.image, + "tool_directory": self.tool_directory, + } + + def _cleanup(self): + logging.debug("Stopping container with global id %s", self.container_id) + if self.container_id is None: + return + try: + subprocess.run( + ["podman", "kill", "--signal", "SIGKILL", self.container_id], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as e: + logging.warning( + "Failed to stop container %s: %s", + self.container_id, + e.stderr.decode(), + ) From bb2a48545599ca03c4edd21b774eb35198dafd73 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 12:36:20 +0100 Subject: [PATCH 19/30] remove wrong initializer --- benchexec/containerized_tool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchexec/containerized_tool.py b/benchexec/containerized_tool.py index b314519f8..bad093f3e 100644 --- a/benchexec/containerized_tool.py +++ b/benchexec/containerized_tool.py @@ -299,8 +299,8 @@ def _call_tool_func(name, args, kwargs): class ContainerizedTool(ContainerizedToolBase): - def __init__(self, tool_module, config, initializer): - super().__init__(tool_module, config, initializer, initializer=_init_container) + def __init__(self, tool_module, config): + super().__init__(tool_module, config, initializer=_init_container) def _cleanup(self): pass From 5bf07ad045c7f338b0b92978ea1f2ee2b1c24c77 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 13:51:18 +0100 Subject: [PATCH 20/30] remove config from mk_arg --- benchexec/containerized_tool.py | 6 +++--- contrib/vcloud/podman_containerized_tool.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/benchexec/containerized_tool.py b/benchexec/containerized_tool.py index bad093f3e..ca35e8632 100644 --- a/benchexec/containerized_tool.py +++ b/benchexec/containerized_tool.py @@ -62,7 +62,7 @@ def __init__(self, tool_module, config, initializer): try: self.__doc__, self.container_id = self._pool.apply( _init_container_and_load_tool, - [initializer] + self.mk_args(tool_module, config, temp_dir), + [initializer] + self.mk_args(tool_module, temp_dir), self.mk_kwargs(container_options), ) except BaseException as e: @@ -74,8 +74,8 @@ def __init__(self, tool_module, config, initializer): with contextlib.suppress(OSError): os.rmdir(temp_dir) - def mk_args(self, tool_module, config, tmp_dir): - return [tool_module, config, tmp_dir] + def mk_args(self, tool_module, tmp_dir): + return [tool_module, tmp_dir] def mk_kwargs(self, container_options): return container_options diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index 099f7adf9..aa10efcdc 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -135,7 +135,7 @@ def __init__(self, tool_module, config, image): super().__init__(tool_module, config, _init_container) - def mk_args(self, tool_module, config, tmp_dir): + def mk_args(self, tool_module, tmp_dir): return [tool_module] def mk_kwargs(self, container_options): From b4a3296e70bd92b4e40f7fc96de9b20c8b202b58 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 13:52:37 +0100 Subject: [PATCH 21/30] use the defined executable --- contrib/vcloud/benchmarkclient_executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index cf7a976e0..d0810e2ce 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -114,7 +114,7 @@ def init(config, benchmark): else: tool_locator = benchexec.tooladapter.create_tool_locator(config) benchmark.executable = benchmark.tool.executable(tool_locator) - benchmark.tool_version = benchmark.tool.version(executable_for_version) + benchmark.tool_version = benchmark.tool.version(benchmark.executable) environment = benchmark.environment() if environment.get("keepEnv", None) or environment.get("additionalEnv", None): From d06583591966211a65e28310a646be4c08f36474 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 16:18:34 +0100 Subject: [PATCH 22/30] improve refactoring: keep ContainerizedTool much more like it was before; skip third class --- benchexec/containerized_tool.py | 47 ++++++++------------- contrib/vcloud/podman_containerized_tool.py | 41 +++++++++++------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/benchexec/containerized_tool.py b/benchexec/containerized_tool.py index ca35e8632..4d60de713 100644 --- a/benchexec/containerized_tool.py +++ b/benchexec/containerized_tool.py @@ -16,7 +16,6 @@ import signal import socket import tempfile -from abc import ABCMeta from benchexec import ( BenchExecException, @@ -31,7 +30,7 @@ @tooladapter.CURRENT_BASETOOL.register # mark as instance of CURRENT_BASETOOL -class ContainerizedToolBase(object, metaclass=ABCMeta): +class ContainerizedTool(object): """Wrapper for an instance of any subclass of one of the base-tool classes in benchexec.tools.template. The module and the subclass instance will be loaded in a subprocess that has been @@ -45,7 +44,7 @@ class ContainerizedToolBase(object, metaclass=ABCMeta): But the use of containers in BenchExec is for safety and robustness, not security. """ - def __init__(self, tool_module, config, initializer): + def __init__(self, tool_module, config): """Load tool-info module in subprocess. @param tool_module: The name of the module to load. Needs to define class named Tool. @@ -55,16 +54,13 @@ def __init__(self, tool_module, config, initializer): # We use multiprocessing.Pool as an easy way for RPC with another process. self._pool = multiprocessing.Pool(1, _init_worker_process) - container_options = containerexecutor.handle_basic_container_args(config) - temp_dir = tempfile.mkdtemp(prefix="Benchexec_tool_info_container_") - self.container_id = None + self.container_options = containerexecutor.handle_basic_container_args(config) + self.temp_dir = tempfile.mkdtemp(prefix="Benchexec_tool_info_container_") + # Call function that loads tool module and returns its doc try: - self.__doc__, self.container_id = self._pool.apply( - _init_container_and_load_tool, - [initializer] + self.mk_args(tool_module, temp_dir), - self.mk_kwargs(container_options), - ) + self._setup_container(tool_module) + except BaseException as e: self._pool.terminate() raise e @@ -72,17 +68,17 @@ def __init__(self, tool_module, config, initializer): # Outside the container, the temp_dir is just an empty directory, because # the tmpfs mount is only visible inside. We can remove it immediately. with contextlib.suppress(OSError): - os.rmdir(temp_dir) - - def mk_args(self, tool_module, tmp_dir): - return [tool_module, tmp_dir] + os.rmdir(self.temp_dir) - def mk_kwargs(self, container_options): - return container_options + def _setup_container(self, tool_module): + self.__doc__, _ = self._pool.apply( + _init_container_and_load_tool, + [_init_container, tool_module, self.temp_dir], + self.container_options, + ) def close(self): self._forward_call("close", [], {}) - self._cleanup() self._pool.close() def _forward_call(self, method_name, args, kwargs): @@ -116,7 +112,7 @@ def proxy_function(self, *args, **kwargs): ): if member_name[0] == "_" or member_name == "close": continue - ContainerizedToolBase._add_proxy_function(member_name, member) + ContainerizedTool._add_proxy_function(member_name, member) def _init_worker_process(): @@ -134,14 +130,13 @@ def _init_worker_process(): def _init_container_and_load_tool(initializer, tool_module, *args, **kwargs): """Initialize container for the current process and load given tool-info module.""" - container_id = None try: - container_id = initializer(*args, **kwargs) + initializer_ret = initializer(*args, **kwargs) except OSError as e: if container.check_apparmor_userns_restriction(e): raise BenchExecException(container._ERROR_MSG_USER_NS_RESTRICTION) raise BenchExecException(f"Failed to configure container: {e}") - return _load_tool(tool_module), container_id + return _load_tool(tool_module), initializer_ret def _init_container( @@ -296,11 +291,3 @@ def _call_tool_func(name, args, kwargs): except SystemExit as e: # SystemExit would terminate the worker process instead of being propagated. raise BenchExecException(str(e.code)) - - -class ContainerizedTool(ContainerizedToolBase): - def __init__(self, tool_module, config): - super().__init__(tool_module, config, initializer=_init_container) - - def _cleanup(self): - pass diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index aa10efcdc..77969afc7 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -10,13 +10,17 @@ import shlex import subprocess import sys +from typing import Optional from benchexec import ( BenchExecException, libc, tooladapter, ) -from benchexec.containerized_tool import ContainerizedToolBase +from benchexec.containerized_tool import ( + ContainerizedTool, + _init_container_and_load_tool, +) TOOL_DIRECTORY_MOUNT_POINT = "/mnt/__benchexec_tool_directory" @@ -124,7 +128,11 @@ def join_ns(namespace): @tooladapter.CURRENT_BASETOOL.register # mark as instance of CURRENT_BASETOOL -class PodmanContainerizedTool(ContainerizedToolBase): +class PodmanContainerizedTool(ContainerizedTool): + tool_directory: str + image: str + container_id: Optional[str] + def __init__(self, tool_module, config, image): assert ( config.tool_directory @@ -132,20 +140,23 @@ def __init__(self, tool_module, config, image): self.tool_directory = config.tool_directory self.image = image + self.container_id = None + + super().__init__(tool_module, config) + + def _setup_container(self, tool_module): + self.__doc__, self.container_id = self._pool.apply( + _init_container_and_load_tool, + [_init_container, tool_module], + { + "image": self.image, + "tool_directory": self.tool_directory, + }, + ) - super().__init__(tool_module, config, _init_container) - - def mk_args(self, tool_module, tmp_dir): - return [tool_module] - - def mk_kwargs(self, container_options): - return { - "image": self.image, - "tool_directory": self.tool_directory, - } - - def _cleanup(self): - logging.debug("Stopping container with global id %s", self.container_id) + def close(self): + super().close() + logging.debug("Removing container with global id %s", self.container_id) if self.container_id is None: return try: From dde1397ff11f321ff1f0f220bc09eeaee70ae3ff Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 16:21:59 +0100 Subject: [PATCH 23/30] use podman create, init and rm instead of run and kill --- contrib/vcloud/podman_containerized_tool.py | 22 ++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index 77969afc7..7646afad9 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -59,9 +59,9 @@ def _init_container( # Create a container that does nothing but keeps running command = ( - ["podman", "run", "--entrypoint", "tail", "--rm", "-d"] + ["podman", "create", "--entrypoint", '[""]', "--rm"] + volumes - + [image, "-F", "/dev/null"] + + [image, "/bin/sh"] ) logging.debug( @@ -71,15 +71,26 @@ def _init_container( res = subprocess.run( command, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + check=True, ) - - res.check_returncode() container_id = res.stdout.decode().strip() + subprocess.run( + ["podman", "init", container_id], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + ) + container_pid = ( subprocess.run( ["podman", "inspect", "--format", "{{.State.Pid}}", container_id], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, ) .stdout.decode() .strip() @@ -161,9 +172,10 @@ def close(self): return try: subprocess.run( - ["podman", "kill", "--signal", "SIGKILL", self.container_id], + ["podman", "rm", self.container_id], check=True, stdout=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, stderr=subprocess.PIPE, ) except subprocess.CalledProcessError as e: From 374f90b36d9658e3be00c13ff10db1d4625bd53d Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Mon, 9 Dec 2024 16:35:28 +0100 Subject: [PATCH 24/30] use Paths, procedual style, and += for list --- contrib/vcloud/podman_containerized_tool.py | 29 +++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index 7646afad9..6304fe57c 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -10,6 +10,7 @@ import shlex import subprocess import sys +from pathlib import Path from typing import Optional from benchexec import ( @@ -40,22 +41,22 @@ def _init_container( # The modules are mounted at the exact same path in the container # because we do not yet know a solution to tell python to use # different paths for the modules in the container. - python_paths = [path for path in sys.path if os.path.isdir(path)] - for path in python_paths: - abs_path = os.path.abspath(path) - volumes.extend(["--volume", f"{abs_path}:{abs_path}:ro"]) + for path in map(Path, sys.path): + if not path.is_dir(): + continue + + abs_path = path.absolute() + volumes += ["--volume", f"{abs_path}:{abs_path}:ro"] # Mount the tool directory into the container at a known location - volumes.extend( - [ - "--volume", - f"{tool_directory}:{TOOL_DIRECTORY_MOUNT_POINT}:O", - # :O creates an overlay mount. The tool can write files in the container - # but they are not visible outside the container. - "--workdir", - "/mnt", - ] - ) + volumes += [ + "--volume", + f"{tool_directory}:{TOOL_DIRECTORY_MOUNT_POINT}:O", + # :O creates an overlay mount. The tool can write files in the container + # but they are not visible outside the container. + "--workdir", + "/mnt", + ] # Create a container that does nothing but keeps running command = ( From f9ff2076b6abab1f084350d202ad95fd6386dbf8 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 11 Dec 2024 10:45:53 +0100 Subject: [PATCH 25/30] move setup and cleanup specific to ContainerizedTool The setup function now takes the config as argument as well. --- benchexec/containerized_tool.py | 26 ++++++++++----------- contrib/vcloud/podman_containerized_tool.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/benchexec/containerized_tool.py b/benchexec/containerized_tool.py index 4d60de713..1ff61e7f7 100644 --- a/benchexec/containerized_tool.py +++ b/benchexec/containerized_tool.py @@ -54,28 +54,28 @@ def __init__(self, tool_module, config): # We use multiprocessing.Pool as an easy way for RPC with another process. self._pool = multiprocessing.Pool(1, _init_worker_process) - self.container_options = containerexecutor.handle_basic_container_args(config) - self.temp_dir = tempfile.mkdtemp(prefix="Benchexec_tool_info_container_") - # Call function that loads tool module and returns its doc try: - self._setup_container(tool_module) - + self._setup_container(tool_module, config) except BaseException as e: self._pool.terminate() raise e + + def _setup_container(self, tool_module, config): + container_options = containerexecutor.handle_basic_container_args(config) + temp_dir = tempfile.mkdtemp(prefix="Benchexec_tool_info_container_") + + try: + self.__doc__, _ = self._pool.apply( + _init_container_and_load_tool, + [_init_container, tool_module, temp_dir], + container_options, + ) finally: # Outside the container, the temp_dir is just an empty directory, because # the tmpfs mount is only visible inside. We can remove it immediately. with contextlib.suppress(OSError): - os.rmdir(self.temp_dir) - - def _setup_container(self, tool_module): - self.__doc__, _ = self._pool.apply( - _init_container_and_load_tool, - [_init_container, tool_module, self.temp_dir], - self.container_options, - ) + os.rmdir(temp_dir) def close(self): self._forward_call("close", [], {}) diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index 6304fe57c..0d1567b1e 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -156,7 +156,7 @@ def __init__(self, tool_module, config, image): super().__init__(tool_module, config) - def _setup_container(self, tool_module): + def _setup_container(self, tool_module, config): self.__doc__, self.container_id = self._pool.apply( _init_container_and_load_tool, [_init_container, tool_module], From 924e599f6f020199f019d185f1958cbfd24a8228 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 11 Dec 2024 15:01:31 +0100 Subject: [PATCH 26/30] report error of missing tool_directory earlier --- contrib/vcloud-benchmark.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contrib/vcloud-benchmark.py b/contrib/vcloud-benchmark.py index 0d446df89..932defcf2 100755 --- a/contrib/vcloud-benchmark.py +++ b/contrib/vcloud-benchmark.py @@ -23,7 +23,7 @@ import benchexec.benchexec # noqa E402 import benchexec.model # noqa E402 import benchexec.tools # noqa E402 -from benchexec import __version__ # noqa E402 +from benchexec import BenchExecException, __version__ # noqa E402 _ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "vcloud")) IVY_JAR_NAME = "ivy-2.5.0.jar" @@ -85,6 +85,13 @@ def hook_load_tool_info(tool_name, config): if not config.containerImage: return original_load_tool_info(tool_name, config) + if not config.tool_directory: + raise BenchExecException( + "Using a container image is currently only supported " + "if the tool directory is explicitly provided. Please set it " + "using the --tool-directory option." + ) + tool_module = tool_name if "." in tool_name else f"benchexec.tools.{tool_name}" try: From 430997fc9d82cffa3887e814cdf2ffd827d31862 Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 11 Dec 2024 15:04:07 +0100 Subject: [PATCH 27/30] do not leak a Path instance into benchexec --- contrib/vcloud/benchmarkclient_executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index d0810e2ce..1aeb56ef1 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -107,7 +107,7 @@ def init(config, benchmark): # The vcloud uses the tool location later to determine which files need to be uploaded # So this needs to point to the actual path where the executable is on the host - benchmark.executable = ( + benchmark.executable = str( Path(config.tool_directory) / executable_relative_to_mount_point ) From 33b0857d66ec8ef948b91ea6db27cdb71754ad6e Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 11 Dec 2024 15:06:46 +0100 Subject: [PATCH 28/30] add output check to podman inspect call --- contrib/vcloud/podman_containerized_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/vcloud/podman_containerized_tool.py b/contrib/vcloud/podman_containerized_tool.py index 0d1567b1e..3f782281a 100644 --- a/contrib/vcloud/podman_containerized_tool.py +++ b/contrib/vcloud/podman_containerized_tool.py @@ -80,10 +80,10 @@ def _init_container( subprocess.run( ["podman", "init", container_id], - check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, + check=True, ) container_pid = ( @@ -92,6 +92,7 @@ def _init_container( stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, + check=True, ) .stdout.decode() .strip() From c7c0e54541aa5356e99a6506b3a2dd158e25ec5f Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 11 Dec 2024 16:51:13 +0100 Subject: [PATCH 29/30] get rid of custom tool locator --- contrib/vcloud/benchmarkclient_executor.py | 48 +++------------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index 1aeb56ef1..0788be92b 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -13,9 +13,8 @@ import sys from pathlib import Path -import benchexec.tooladapter import benchexec.util -from benchexec.tools.template import ToolNotFoundException +from benchexec.tooladapter import CURRENT_BASETOOL, create_tool_locator from . import vcloudutil @@ -37,45 +36,6 @@ def set_vcloud_jar_path(p): vcloud_jar = p -class CustomToolLocator: - def __init__(self, container_mount_point=None): - self.container_mount_point = container_mount_point - - def find_executable(self, executable_name, subdir=""): - logging.debug( - "Using custom tool locator to find executable %s", executable_name - ) - assert ( - os.path.basename(executable_name) == executable_name - ), "Executable needs to be a simple file name" - dirs = [] - - assert self.container_mount_point is not None, "Container mount point not set" - - # At this point we know, that the tool is located at container_mount_point - # as the container as the tool_dir mounted to this location - dirs.append(os.path.join(self.container_mount_point, subdir)) - logging.debug("Searching for executable %s in %s", executable_name, dirs) - - executable = benchexec.util.find_executable2(executable_name, dirs) - if executable: - return executable - - other_file = benchexec.util.find_executable2(executable_name, dirs, os.F_OK) - if other_file: - raise ToolNotFoundException( - f"Could not find executable '{executable_name}', " - f"but found file '{other_file}' that is not executable." - ) - - msg = ( - f"Could not find executable '{executable_name}'. " - f"The searched directories were: " + "".join("\n " + d for d in dirs) - ) - - raise ToolNotFoundException(msg) - - def init(config, benchmark): global _JustReprocessResults _JustReprocessResults = config.reprocessResults @@ -83,7 +43,9 @@ def init(config, benchmark): if config.containerImage: from vcloud.podman_containerized_tool import TOOL_DIRECTORY_MOUNT_POINT - tool_locator = CustomToolLocator(TOOL_DIRECTORY_MOUNT_POINT) + tool_locator = CURRENT_BASETOOL.ToolLocator( + tool_directory=TOOL_DIRECTORY_MOUNT_POINT + ) executable_for_version = benchmark.tool.executable(tool_locator) benchmark.tool_version = benchmark.tool.version(executable_for_version) @@ -112,7 +74,7 @@ def init(config, benchmark): ) else: - tool_locator = benchexec.tooladapter.create_tool_locator(config) + tool_locator = create_tool_locator(config) benchmark.executable = benchmark.tool.executable(tool_locator) benchmark.tool_version = benchmark.tool.version(benchmark.executable) From 195dada91dabd10a02c49eba2e4f332a960a3a5a Mon Sep 17 00:00:00 2001 From: Henrik Wachowitz Date: Wed, 11 Dec 2024 16:52:20 +0100 Subject: [PATCH 30/30] improve checking of valid paths returned from the tool_info_module --- contrib/vcloud/benchmarkclient_executor.py | 49 +++++++++++++--------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/contrib/vcloud/benchmarkclient_executor.py b/contrib/vcloud/benchmarkclient_executor.py index 0788be92b..6819265a8 100644 --- a/contrib/vcloud/benchmarkclient_executor.py +++ b/contrib/vcloud/benchmarkclient_executor.py @@ -14,6 +14,7 @@ from pathlib import Path import benchexec.util +from benchexec import BenchExecException from benchexec.tooladapter import CURRENT_BASETOOL, create_tool_locator from . import vcloudutil @@ -49,29 +50,39 @@ def init(config, benchmark): executable_for_version = benchmark.tool.executable(tool_locator) benchmark.tool_version = benchmark.tool.version(executable_for_version) - # If the tool info does not call find_executable, we don't know if the - # executable path is containing the mount point. - # In this case we can check whether the path is relative - # and continue with the assumption that it is relative to the provided - # tool directory. - try: - executable_relative_to_mount_point = Path( - executable_for_version - ).relative_to(TOOL_DIRECTORY_MOUNT_POINT) - except ValueError: - if Path(executable_for_version).is_absolute(): - raise ValueError( + executable_for_version = Path(executable_for_version) + + # ensure executable_for_version is relative + if executable_for_version.is_absolute(): + try: + executable_for_version = executable_for_version.relative_to( + TOOL_DIRECTORY_MOUNT_POINT + ) + except ValueError as e: + raise BenchExecException( f"Executable path {executable_for_version} is not relative" - " and is not containing the expected container to the mount point" - " {TOOL_DIRECTORY_MOUNT_POINT}" - ) from None - executable_relative_to_mount_point = executable_for_version + " and is not containing the expected mount point in the container" + " {TOOL_DIRECTORY_MOUNT_POINT}." + ) from e + + # ensure that executable_for_version is not pointing to a directory + # outside of the tool directory + + executable_for_cloud = Path(config.tool_directory) / executable_for_version + + # Paths must be resolved to properly detect when the executable would + # escape the tool dir with '..' + if not executable_for_cloud.resolve().is_relative_to( + Path(config.tool_directory).resolve() + ): + raise BenchExecException( + f"Executable path {executable_for_cloud} is not within the tool directory" + f" {config.tool_directory}." + ) # The vcloud uses the tool location later to determine which files need to be uploaded # So this needs to point to the actual path where the executable is on the host - benchmark.executable = str( - Path(config.tool_directory) / executable_relative_to_mount_point - ) + benchmark.executable = str(executable_for_cloud) else: tool_locator = create_tool_locator(config)