Skip to content

Commit

Permalink
Merge branch 'main' into gsoc-overlay-handling-with-fuse-overlayfs-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippWendler committed Aug 26, 2024
2 parents 1f6d696 + 45949a8 commit ba6bb91
Show file tree
Hide file tree
Showing 20 changed files with 337 additions and 61 deletions.
8 changes: 4 additions & 4 deletions .appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
# This file is part of BenchExec, a framework for reliable benchmarking:
# https://github.com/sosy-lab/benchexec
#
# SPDX-FileCopyrightText: 2007-2020 Dirk Beyer <https://www.sosy-lab.org>
# SPDX-FileCopyrightText: 2007-2024 Dirk Beyer <https://www.sosy-lab.org>
#
# SPDX-License-Identifier: Apache-2.0

image: "Visual Studio 2022"

environment:
matrix:
- PYTHON: "C:\\Python38"
- PYTHON: "C:\\Python39"
- PYTHON: "C:\\Python312"

build: off

install:
- set PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%
# The package installation via our setup.py file uses easy_install, which fails to correctly install lxml.
# As some Python environments don't have lxml preinstalled, install it here to avoid errors during the execution in those cases.
- python -m pip install --user ".[dev]"

test_script:
Expand Down
93 changes: 75 additions & 18 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,54 +33,105 @@ stages:
- images
- test

.unit-tests: &unit-tests
.tests:cgroupsv1: &tests-cgroupsv1
stage: test
before_script:
# Create user, we do not want to test as root
- adduser --disabled-login --gecos "" $PRIMARY_USER
# Activate coverage for subprocesses
- printf 'import coverage\ncoverage.process_startup()\n' > "/usr/local/lib/python${PYTHON_VERSION}/site-packages/sitecustomize.py"
# Give $PRIMARY_USER permission to create cgroups
- test/for_each_of_my_cgroups.sh chgrp $PRIMARY_USER
- test/for_each_of_my_cgroups.sh chmod g+w $PRIMARY_USER
# Install pytest-cov (does not work with --user)
- pip install pytest-cov
# Install BenchExec with `dev` dependencies
- sudo -u $PRIMARY_USER pip install --user ".[dev]"
# Start lxcfs
- lxcfs /var/lib/lxcfs &
script:
- sudo -u $PRIMARY_USER
COVERAGE_PROCESS_START=.coveragerc
coverage run -m pytest
- sudo -u $PRIMARY_USER python -m pytest -ra --cov
after_script:
- sudo -u $PRIMARY_USER coverage combine
- sudo -u $PRIMARY_USER coverage report
- sudo -u $PRIMARY_USER coverage xml
tags:
- privileged
coverage: '/TOTAL.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
paths:
- coverage.xml
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml

tests:cgroups1:python-3.8:
<<: *tests-cgroupsv1
variables:
PYTHON_VERSION: '3.8'

tests:cgroupsv1:python-3.9:
<<: *tests-cgroupsv1
variables:
PYTHON_VERSION: '3.9'

tests:cgroupsv1:python-3.10:
<<: *tests-cgroupsv1
variables:
PYTHON_VERSION: '3.10'

tests:cgroupsv1:python-3.11:
<<: *tests-cgroupsv1
variables:
PYTHON_VERSION: '3.11'

tests:cgroupsv1:python-3.12:
<<: *tests-cgroupsv1
variables:
PYTHON_VERSION: '3.12'

unit-tests:python-3.8:
<<: *unit-tests
.tests:cgroupsv2: &tests-cgroupsv2
stage: test
before_script:
# Prepare cgroups
- test/setup_cgroupsv2_in_container.sh
# Install pytest-cov (does not work with --user)
- pip install pytest-cov
# Install BenchExec with `dev` dependencies
- pip install --user ".[dev]"
script:
- python -m pytest -ra --cov
after_script:
- coverage xml
tags:
- cgroupsv2
coverage: '/TOTAL.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml

tests:cgroupsv2:python-3.8:
<<: *tests-cgroupsv2
variables:
PYTHON_VERSION: '3.8'

unit-tests:python-3.9:
<<: *unit-tests
tests:cgroupsv2:python-3.9:
<<: *tests-cgroupsv2
variables:
PYTHON_VERSION: '3.9'

unit-tests:python-3.10:
<<: *unit-tests
tests:cgroupsv2:python-3.10:
<<: *tests-cgroupsv2
variables:
PYTHON_VERSION: '3.10'

unit-tests:python-3.11:
<<: *unit-tests
tests:cgroupsv2:python-3.11:
<<: *tests-cgroupsv2
variables:
PYTHON_VERSION: '3.11'

tests:cgroupsv2:python-3.12:
<<: *tests-cgroupsv2
variables:
PYTHON_VERSION: '3.12'

# Static checks
check-format:
stage: test
Expand Down Expand Up @@ -228,3 +279,9 @@ build-docker:test:python-3.11:
variables:
DOCKERFILE: test/Dockerfile.python-3.11
IMAGE: test:python-3.11

build-docker:test:python-3.12:
extends: .build-docker
variables:
DOCKERFILE: test/Dockerfile.python-3.12
IMAGE: test:python-3.12
9 changes: 5 additions & 4 deletions benchexec/baseexecutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ def _start_execution(
@param child_setup_fn a function without parameters that is called in the child process
before the tool is started
@param parent_cleanup_fn a function that is called in the parent process
immediately after the tool terminated, with three parameters:
immediately after the tool terminated, with four parameters:
the result of parent_setup_fn, the result of the executed process as ProcessExitCode,
and the base path for looking up files as parameter values
the base path for looking up files as parameter values,
and the cgroup of the tool
@return: a tuple of PID of process and a blocking function, which waits for the process
and a triple of the exit code and the resource usage of the process
and the result of parent_cleanup_fn (do not use os.wait)
Expand Down Expand Up @@ -125,11 +126,11 @@ def wait_and_get_result():
exitcode, ru_child = self._wait_for_process(p.pid, args[0])

parent_cleanup = parent_cleanup_fn(
parent_setup, util.ProcessExitCode.from_raw(exitcode), ""
parent_setup, util.ProcessExitCode.from_raw(exitcode), "", cgroups
)
return exitcode, ru_child, parent_cleanup

return p.pid, wait_and_get_result
return p.pid, cgroups, wait_and_get_result

def _wait_for_process(self, pid, name):
"""Wait for the given process to terminate.
Expand Down
4 changes: 4 additions & 0 deletions benchexec/cgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ def handle_errors(self, critical_cgroups):
def create_fresh_child_cgroup(self, subsystems):
pass

@abstractmethod
def create_fresh_child_cgroup_for_delegation(self):
pass

@abstractmethod
def add_task(self, pid):
pass
Expand Down
16 changes: 14 additions & 2 deletions benchexec/cgroupsv1.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def get_group_name(gid):
else:
sys.exit(_ERROR_MSG_OTHER) # e.g., subsystem not mounted

def create_fresh_child_cgroup(self, subsystems):
def create_fresh_child_cgroup(self, subsystems, prefix=CGROUP_NAME_PREFIX):
"""
Create child cgroups of the current cgroup for at least the given subsystems.
@return: A Cgroup instance representing the new child cgroup(s).
Expand All @@ -374,7 +374,7 @@ def create_fresh_child_cgroup(self, subsystems):
]
continue

cgroup = tempfile.mkdtemp(prefix=CGROUP_NAME_PREFIX, dir=parentCgroup)
cgroup = tempfile.mkdtemp(prefix=prefix, dir=parentCgroup)
createdCgroupsPerSubsystem[subsystem] = cgroup
createdCgroupsPerParent[parentCgroup] = cgroup

Expand All @@ -395,6 +395,18 @@ def copy_parent_to_child(name):

return CgroupsV1(createdCgroupsPerSubsystem)

def create_fresh_child_cgroup_for_delegation(self, prefix="delegate_"):
"""
Create a child cgroup with all controllers.
On cgroupsv1 there is no difference to a regular child cgroup.
"""
child_cgroup = self.create_fresh_child_cgroup(self.subsystems.keys(), prefix)
assert (
self.subsystems.keys() == child_cgroup.subsystems.keys()
), "delegation failed for at least one controller"

return child_cgroup

def add_task(self, pid):
"""
Add a process to the cgroups represented by this instance.
Expand Down
13 changes: 12 additions & 1 deletion benchexec/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@
DIR_MODES = [DIR_HIDDEN, DIR_READ_ONLY, DIR_OVERLAY, DIR_FULL_ACCESS]
"""modes how a directory can be mounted in the container"""

LXCFS_PROC_DIR = b"/var/lib/lxcfs/proc"
LXCFS_BASE_DIR = b"/var/lib/lxcfs"
LXCFS_PROC_DIR = LXCFS_BASE_DIR + b"/proc"
SYS_CPU_DIR = b"/sys/devices/system/cpu"

_CLONE_NESTED_CALLBACK = ctypes.CFUNCTYPE(ctypes.c_int)
"""Type for callback of execute_in_namespace, nested in our primary callback."""
Expand Down Expand Up @@ -1184,6 +1186,15 @@ def setup_container_system_config(basedir, mountdir, dir_modes):
{"h": CONTAINER_HOME, "p": os.path.dirname(CONTAINER_HOME)},
)

# Virtualize CPU info with LXCFS if directory is not hidden nor full-access
if (
os.access(LXCFS_BASE_DIR + SYS_CPU_DIR, os.R_OK)
and determine_directory_mode(dir_modes, SYS_CPU_DIR) == DIR_READ_ONLY
):
make_bind_mount(
LXCFS_BASE_DIR + SYS_CPU_DIR, mountdir + SYS_CPU_DIR, private=True
)


def is_container_system_config_file(file):
"""Determine whether a given file is one of the files created by
Expand Down
43 changes: 28 additions & 15 deletions benchexec/containerexecutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,8 @@ def is_accessible(path):
# container, but we do not want a warning per run.
if not is_accessible(container.LXCFS_PROC_DIR):
logging.info(
"LXCFS is not available,"
" some host information like the uptime leaks into the container."
"LXCFS is not available, some host information like the uptime"
" and the total number of CPU cores leaks into the container."
)

if not NATIVE_CLONE_CALLBACK_SUPPORTED:
Expand Down Expand Up @@ -457,13 +457,14 @@ def execute_run(
cgroups = self._cgroups.create_fresh_child_cgroup(
self._cgroups.subsystems.keys()
)
pid = None
tool_pid = None
tool_cgroups = None
returnvalue = 0

logging.debug("Starting process.")

try:
pid, result_fn = self._start_execution(
tool_pid, tool_cgroups, result_fn = self._start_execution(
args=args,
stdin=None,
stdout=None,
Expand All @@ -481,7 +482,7 @@ def execute_run(
)

with self.SUB_PROCESS_PIDS_LOCK:
self.SUB_PROCESS_PIDS.add(pid)
self.SUB_PROCESS_PIDS.add(tool_pid)

# wait until process has terminated
returnvalue, unused_ru_child, unused = result_fn()
Expand All @@ -491,7 +492,7 @@ def execute_run(
logging.debug("Process terminated, exit code %s.", returnvalue)

with self.SUB_PROCESS_PIDS_LOCK:
self.SUB_PROCESS_PIDS.discard(pid)
self.SUB_PROCESS_PIDS.discard(tool_pid)

if temp_dir is not None:
logging.debug("Cleaning up temporary directory.")
Expand Down Expand Up @@ -969,15 +970,24 @@ def check_child_exit_code():
child_pid,
)

# cgroups is the cgroups where we configure limits.
# So for isolation, we need to create a child cgroup that becomes the root
# of the cgroup ns, such that the limit settings are not accessible in the
# container and cannot be changed.
if use_cgroup_ns:
cgroups = cgroups.create_fresh_child_cgroup_for_delegation()
# cgroups is the cgroup where we configure limits.
# We add another layer of cgroups below it, for two reasons:
# - We want to move our child process (the init process of the container)
# into a cgroups where the limits apply because LXCFS reports the limits
# of the init process of the container.
# - On cgroupsv2 we want to move the grandchild process (the started tool)
# into a cgroup that becomes the root of the cgroup ns,
# such that no other cgroup (in particular the one with the limits)
# is accessible in the container and the limits cannot be changed
# from within the container.
child_cgroup = cgroups.create_fresh_child_cgroup(
cgroups.subsystems.keys(), prefix="init_"
)
child_cgroup.add_task(child_pid)
grandchild_cgroups = cgroups.create_fresh_child_cgroup_for_delegation()

# start measurements
cgroups.add_task(grandchild_pid)
grandchild_cgroups.add_task(grandchild_pid)
parent_setup = parent_setup_fn()

# Signal grandchild that setup is finished
Expand Down Expand Up @@ -1015,7 +1025,10 @@ def wait_for_grandchild():

base_path = f"/proc/{child_pid}/root"
parent_cleanup = parent_cleanup_fn(
parent_setup, util.ProcessExitCode.from_raw(exitcode), base_path
parent_setup,
util.ProcessExitCode.from_raw(exitcode),
base_path,
grandchild_cgroups,
)

if result_files_patterns:
Expand All @@ -1032,7 +1045,7 @@ def wait_for_grandchild():

return exitcode, ru_child, parent_cleanup

return grandchild_pid, wait_for_grandchild
return grandchild_pid, grandchild_cgroups, wait_for_grandchild

def _setup_container_filesystem(self, temp_dir, output_dir, memlimit, memory_nodes):
"""Setup the filesystem layout in the container.
Expand Down
Loading

0 comments on commit ba6bb91

Please sign in to comment.