Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions cms/grading/Sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,24 +287,23 @@ def __init__(
# between sandboxes.
self.dirs.append((None, "/dev/shm", "tmp"))

# Set common environment variables.
# Set common configuration that is relevant for multiple
# languages.

self.set_env["PATH"] = "/usr/local/bin:/usr/bin:/bin"

# Specifically needed by Python, that searches the home for
# packages.
self.set_env["HOME"] = self._home_dest

# Needed on Ubuntu by PHP (and more), since /usr/bin only contains a
# symlink to one out of many alternatives.
# Needed on Ubuntu by PHP, Java, Pascal etc, since /usr/bin
# only contains a symlink to one out of many alternatives.
self.maybe_add_mapped_directory("/etc/alternatives")

# On Arch Linux, pypy3 is installed in `/opt` and `/usr/bin/pypy3` is
# just a symlink.
self.maybe_add_mapped_directory("/opt/pypy3")

# Likewise, needed by C# programs. The Mono runtime looks in
# /etc/mono/config to obtain the default DllMap, which includes, in
# particular, the System.Native assembly.
self.maybe_add_mapped_directory("/etc/mono", options="noexec")

# Tell isolate to get the sandbox ready. We do our best to cleanup
# after ourselves, but we might have missed something if a previous
# worker was interrupted in the middle of an execution, so we issue an
Expand Down Expand Up @@ -769,9 +768,6 @@ def archive(self, file_cacher: FileCacher) -> str | None:

# Put archive to FS
sandbox_archive.seek(0)
return file_cacher.put_file_from_fobj(
sandbox_archive, "Sandbox %s" % self.get_root_path()
)

def add_mapped_directory(
self,
Expand Down
15 changes: 15 additions & 0 deletions cms/grading/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import logging
import os
from abc import ABCMeta, abstractmethod
from cms.grading.Sandbox import Sandbox


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -135,6 +136,13 @@ def get_compilation_commands(
"""
pass

def configure_compilation_sandbox(self, sandbox: Sandbox):
"""
Set sandbox parameters necessary for running the compilation
commands.
"""
pass

@abstractmethod
def get_evaluation_commands(
self,
Expand All @@ -156,6 +164,13 @@ def get_evaluation_commands(
"""
pass

def configure_evaluation_sandbox(self, sandbox: Sandbox):
"""
Set sandbox parameters necessary for running the evaluation
commands.
"""
pass

# It's sometimes handy to use Language objects in sets or as dict
# keys. Since they have no state (they are just collections of
# constants and static methods) and are designed to be used as
Expand Down
9 changes: 9 additions & 0 deletions cms/grading/languages/csharp_mono.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,12 @@ def get_evaluation_commands(
self, executable_filename, main=None, args=None):
"""See Language.get_evaluation_commands."""
return [["/usr/bin/mono", executable_filename]]

def configure_compilation_sandbox(self, sandbox):
# The Mono runtime looks in /etc/mono/config to obtain the
# default DllMap, which includes, in particular, the
# System.Native assembly.
sandbox.maybe_add_mapped_directory("/etc/mono", options="noexec")

def configure_evaluation_sandbox(self, sandbox):
sandbox.maybe_add_mapped_directory("/etc/mono", options="noexec")
7 changes: 7 additions & 0 deletions cms/grading/languages/haskell_ghc.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ def get_compilation_commands(self,
executable_filename, source_filenames[0]])
return commands

def configure_compilation_sandbox(self, sandbox):
# Directory required to be visible during a compilation with GHC.
# GHC looks for the Haskell's package database in
# "/usr/lib/ghc/package.conf.d" (already visible by isolate's default,
# but it is a symlink to "/var/lib/ghc/package.conf.d")
sandbox.maybe_add_mapped_directory("/var/lib/ghc")

@staticmethod
def _capitalize(string: str):
dirname, basename = os.path.split(string)
Expand Down
8 changes: 8 additions & 0 deletions cms/grading/languages/java_jdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

"""

import os
from shlex import quote as shell_quote

from cms.grading import Language
Expand Down Expand Up @@ -91,3 +92,10 @@ def get_evaluation_commands(
command = ["/usr/bin/java", "-Deval=true", "-Xmx512M", "-Xss64M",
main] + args
return [unzip_command, command]

def configure_compilation_sandbox(self, sandbox):
# the jvm conf directory is often symlinked to /etc in
# distributions, but the location of it in /etc is inconsistent.
for path in os.listdir("/etc"):
if path == "java" or path.startswith("java-"):
sandbox.add_mapped_directory(f"/etc/{path}")
4 changes: 4 additions & 0 deletions cms/grading/languages/pascal_fpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ def get_compilation_commands(self,
command += ["-O2", "-XSs", "-o%s" % executable_filename]
command += [source_filenames[0]]
return [command]

def configure_compilation_sandbox(self, sandbox):
# Needed for /etc/fpc.cfg.
sandbox.maybe_add_mapped_directory("/etc")
8 changes: 8 additions & 0 deletions cms/grading/languages/python3_pypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,11 @@ def get_evaluation_commands(
"""See Language.get_evaluation_commands."""
args = args if args is not None else []
return [["/usr/bin/pypy3", executable_filename] + args]

def configure_compilation_sandbox(self, sandbox):
# Needed on Arch, where /usr/bin/pypy3 is a symlink into
# /opt/pypy3.
sandbox.maybe_add_mapped_directory("/opt/pypy3")

def configure_evaluation_sandbox(self, sandbox):
sandbox.maybe_add_mapped_directory("/opt/pypy3")
14 changes: 6 additions & 8 deletions cms/grading/steps/compilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from cms import config
from cms.grading.Sandbox import Sandbox
from cms.grading.language import Language
from cms.grading.steps.stats import StatsDict
from .messages import HumanMessage, MessageCollection
from .utils import generic_step
Expand Down Expand Up @@ -66,7 +67,7 @@ def N_(message: str):


def compilation_step(
sandbox: Sandbox, commands: list[list[str]]
sandbox: Sandbox, commands: list[list[str]], language: Language
) -> tuple[bool, bool | None, list[str] | None, StatsDict | None]:
"""Execute some compilation commands in the sandbox.

Expand All @@ -79,6 +80,7 @@ def compilation_step(

sandbox: the sandbox we consider, already created.
commands: compilation commands to execute.
language: language of the submission

return: a tuple with four items:
* success: True if the sandbox did not fail, in any command;
Expand All @@ -93,18 +95,14 @@ def compilation_step(

"""
# Set sandbox parameters suitable for compilation.
sandbox.add_mapped_directory("/etc")
# Directory required to be visible during a compilation with GHC.
# GHC looks for the Haskell's package database in
# "/usr/lib/ghc/package.conf.d" (already visible by isolate's default,
# but it is a symlink to "/var/lib/ghc/package.conf.d"
sandbox.maybe_add_mapped_directory("/var/lib/ghc")
sandbox.preserve_env = True
sandbox.max_processes = config.sandbox.compilation_sandbox_max_processes
sandbox.timeout = config.sandbox.compilation_sandbox_max_time_s
sandbox.wallclock_timeout = 2 * sandbox.timeout + 1
sandbox.address_space = config.sandbox.compilation_sandbox_max_memory_kib * 1024

# Set per-language sandbox parameters.
language.configure_compilation_sandbox(sandbox)

# Run the compilation commands, copying stdout and stderr to stats.
stats = generic_step(sandbox, commands, "compilation", collect_output=True)
if stats is None:
Expand Down
11 changes: 10 additions & 1 deletion cms/grading/steps/evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

from cms import config
from cms.grading.Sandbox import Sandbox
from cms.grading.language import Language
from .messages import HumanMessage, MessageCollection
from .stats import StatsDict, execution_stats

Expand Down Expand Up @@ -83,6 +84,7 @@ def N_(message: str):
def evaluation_step(
sandbox: Sandbox,
commands: list[list[str]],
language: Language | None,
time_limit: float | None = None,
memory_limit: int | None = None,
dirs_map: dict[str, tuple[str | None, str | None]] | None = None,
Expand All @@ -101,6 +103,8 @@ def evaluation_step(

sandbox: the sandbox we consider, already created.
commands: evaluation commands to execute.
language: language of the submission (or None if the commands to
execute are not from a Language's get_evaluation_commands).
time_limit: time limit in seconds (applied to each command);
if None, no time limit is enforced.
memory_limit: memory limit in bytes (applied to each command);
Expand Down Expand Up @@ -135,7 +139,7 @@ def evaluation_step(
"""
for command in commands:
success = evaluation_step_before_run(
sandbox, command, time_limit, memory_limit,
sandbox, command, language, time_limit, memory_limit,
None, dirs_map, writable_files, stdin_redirect,
stdout_redirect, multiprocess, wait=True)
if not success:
Expand All @@ -152,6 +156,7 @@ def evaluation_step(
def evaluation_step_before_run(
sandbox: Sandbox,
command: list[str],
language: Language | None,
time_limit: float | None = None,
memory_limit: int | None = None,
wall_limit: float | None = None,
Expand Down Expand Up @@ -222,6 +227,10 @@ def evaluation_step_before_run(
sandbox.set_multiprocess(multiprocess)
sandbox.close_fds = close_fds

# Configure per-language sandbox parameters.
if language:
language.configure_evaluation_sandbox(sandbox)

# Actually run the evaluation command.
logger.debug("Starting execution step.")
return sandbox.execute_without_std(command, wait=wait)
Expand Down
3 changes: 2 additions & 1 deletion cms/grading/tasktypes/Batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def _do_compile(self, job: CompilationJob, file_cacher: FileCacher):

# Run the compilation.
box_success, compilation_success, text, stats = \
compilation_step(sandbox, commands)
compilation_step(sandbox, commands, language)

# Retrieve the compiled executables.
job.success = box_success
Expand Down Expand Up @@ -311,6 +311,7 @@ def _execution_step(self, job: EvaluationJob, file_cacher: FileCacher):
box_success, evaluation_success, stats = evaluation_step(
sandbox,
commands,
language,
job.time_limit,
job.memory_limit,
writable_files=files_allowing_write,
Expand Down
5 changes: 4 additions & 1 deletion cms/grading/tasktypes/Communication.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def compile(self, job: CompilationJob, file_cacher: FileCacher):

# Run the compilation.
box_success, compilation_success, text, stats = \
compilation_step(sandbox, commands)
compilation_step(sandbox, commands, language)

# Retrieve the compiled executables.
job.success = box_success
Expand Down Expand Up @@ -324,6 +324,7 @@ def evaluate(self, job: EvaluationJob, file_cacher: FileCacher):
manager_ = evaluation_step_before_run(
sandbox_mgr,
manager_command,
None,
manager_time_limit,
config.sandbox.trusted_sandbox_max_memory_kib * 1024,
dirs_map=dict((fifo_dir[i], (sandbox_fifo_dir[i], "rw")) for i in indices),
Expand Down Expand Up @@ -359,11 +360,13 @@ def evaluate(self, job: EvaluationJob, file_cacher: FileCacher):
# Assumes that the actual execution of the user solution is the
# last command in commands, and that the previous are "setup"
# that don't need tight control.
# TODO: why can't this use normal evaluation step??
if len(commands) > 1:
trusted_step(sandbox_user[i], commands[:-1])
the_process = evaluation_step_before_run(
sandbox_user[i],
commands[-1],
language,
job.time_limit,
job.memory_limit,
dirs_map={fifo_dir[i]: (sandbox_fifo_dir[i], "rw")},
Expand Down
3 changes: 2 additions & 1 deletion cms/grading/tasktypes/Interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def compile(self, job, file_cacher):
sandbox.create_file_from_storage(filename, digest, file_cacher)

box_success, compilation_success, text, stats = compilation_step(
sandbox, commands
sandbox, commands, language
)

job.success = box_success
Expand Down Expand Up @@ -231,6 +231,7 @@ def evaluate(self, job, file_cacher):
"input.txt",
],
"solution_files": [executable_filename],
"solution_language": job.language,
"controller_wall_limit": self.controller_wall_limit,
"controller_time_limit": self.controller_time_limit,
"controller_memory_limit": self.controller_memory_limit,
Expand Down
4 changes: 3 additions & 1 deletion cms/grading/tasktypes/TwoSteps.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def compile(self, job: CompilationJob, file_cacher: FileCacher):

# Run the compilation.
box_success, compilation_success, text, stats = \
compilation_step(sandbox, commands)
compilation_step(sandbox, commands, language)

# Retrieve the compiled executables
job.success = box_success
Expand Down Expand Up @@ -253,6 +253,7 @@ def evaluate(self, job: EvaluationJob, file_cacher: FileCacher):
first = evaluation_step_before_run(
first_sandbox,
first_command,
None,
job.time_limit,
job.memory_limit,
dirs_map={fifo_dir: ("/fifo", "rw")},
Expand All @@ -277,6 +278,7 @@ def evaluate(self, job: EvaluationJob, file_cacher: FileCacher):
second = evaluation_step_before_run(
second_sandbox,
second_command,
None,
job.time_limit,
job.memory_limit,
dirs_map={fifo_dir: ("/fifo", "rw")},
Expand Down
6 changes: 6 additions & 0 deletions cms/grading/tasktypes/interactive_keeper.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
)
from cms.grading.steps.stats import merge_execution_stats
from cms.grading.steps import trusted_step
from cms.grading.languagemanager import get_language

# Note: we keep a separate interactive keeper (running in a separate
# process) to avoid opening many file descriptors in the main worker.
Expand Down Expand Up @@ -72,6 +73,7 @@ def main():
solution_commands = config["solution_commands"]
controller_files = config["controller_files"]
solution_files = config["solution_files"]
solution_language = config["solution_language"]
controller_wall_limit = config.get("controller_wall_limit")
controller_time_limit = config.get("controller_time_limit")
controller_memory_limit = config.get("controller_memory_limit")
Expand Down Expand Up @@ -99,9 +101,12 @@ def main():
with open(os.path.join(temp_dir, path), "rb") as g:
shutil.copyfileobj(g, f)

language = get_language(solution_language)

controller_proc = evaluation_step_before_run(
controller_sandbox,
controller_command,
None,
time_limit=controller_time_limit,
memory_limit=controller_memory_limit,
wall_limit=controller_wall_limit,
Expand Down Expand Up @@ -149,6 +154,7 @@ def main():
sol_proc = evaluation_step_before_run(
sandbox_sol,
solution_commands[-1],
language,
time_limit=solution_time_limit,
wall_limit=controller_wall_limit,
# the wall-clock limit mostly exists to eventually kill stuck
Expand Down
Loading
Loading