Skip to content

Commit 6198479

Browse files
committed
Allow setting sandbox parameters per-language, make sandbox more restrictive
* Don't put /etc in the sandbox by default, only include the parts of /etc that are necessary for specific languages (except for pascal...) * Don't use preserve_env=True for compilation sandboxes, instead set PATH to a reasonable value manually.
1 parent c682a55 commit 6198479

17 files changed

Lines changed: 144 additions & 55 deletions

File tree

cms/grading/Sandbox.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -287,24 +287,23 @@ def __init__(
287287
# between sandboxes.
288288
self.dirs.append((None, "/dev/shm", "tmp"))
289289

290-
# Set common environment variables.
290+
# Set common configuration that is relevant for multiple
291+
# languages.
292+
293+
self.set_env["PATH"] = "/usr/local/bin:/usr/bin:/bin"
294+
291295
# Specifically needed by Python, that searches the home for
292296
# packages.
293297
self.set_env["HOME"] = self._home_dest
294298

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

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

303-
# Likewise, needed by C# programs. The Mono runtime looks in
304-
# /etc/mono/config to obtain the default DllMap, which includes, in
305-
# particular, the System.Native assembly.
306-
self.maybe_add_mapped_directory("/etc/mono", options="noexec")
307-
308307
# Tell isolate to get the sandbox ready. We do our best to cleanup
309308
# after ourselves, but we might have missed something if a previous
310309
# worker was interrupted in the middle of an execution, so we issue an
@@ -769,9 +768,6 @@ def archive(self, file_cacher: FileCacher) -> str | None:
769768

770769
# Put archive to FS
771770
sandbox_archive.seek(0)
772-
return file_cacher.put_file_from_fobj(
773-
sandbox_archive, "Sandbox %s" % self.get_root_path()
774-
)
775771

776772
def add_mapped_directory(
777773
self,

cms/grading/language.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import logging
2222
import os
2323
from abc import ABCMeta, abstractmethod
24+
from cms.grading.Sandbox import Sandbox
2425

2526

2627
logger = logging.getLogger(__name__)
@@ -135,6 +136,13 @@ def get_compilation_commands(
135136
"""
136137
pass
137138

139+
def configure_compilation_sandbox(self, sandbox: Sandbox):
140+
"""
141+
Set sandbox parameters necessary for running the compilation
142+
commands.
143+
"""
144+
pass
145+
138146
@abstractmethod
139147
def get_evaluation_commands(
140148
self,
@@ -156,6 +164,13 @@ def get_evaluation_commands(
156164
"""
157165
pass
158166

167+
def configure_evaluation_sandbox(self, sandbox: Sandbox):
168+
"""
169+
Set sandbox parameters necessary for running the evaluation
170+
commands.
171+
"""
172+
pass
173+
159174
# It's sometimes handy to use Language objects in sets or as dict
160175
# keys. Since they have no state (they are just collections of
161176
# constants and static methods) and are designed to be used as

cms/grading/languages/csharp_mono.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,12 @@ def get_evaluation_commands(
6767
self, executable_filename, main=None, args=None):
6868
"""See Language.get_evaluation_commands."""
6969
return [["/usr/bin/mono", executable_filename]]
70+
71+
def configure_compilation_sandbox(self, sandbox):
72+
# The Mono runtime looks in /etc/mono/config to obtain the
73+
# default DllMap, which includes, in particular, the
74+
# System.Native assembly.
75+
sandbox.maybe_add_mapped_directory("/etc/mono", options="noexec")
76+
77+
def configure_evaluation_sandbox(self, sandbox):
78+
sandbox.maybe_add_mapped_directory("/etc/mono", options="noexec")

cms/grading/languages/haskell_ghc.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ def get_compilation_commands(self,
6464
executable_filename, source_filenames[0]])
6565
return commands
6666

67+
def configure_compilation_sandbox(self, sandbox):
68+
# Directory required to be visible during a compilation with GHC.
69+
# GHC looks for the Haskell's package database in
70+
# "/usr/lib/ghc/package.conf.d" (already visible by isolate's default,
71+
# but it is a symlink to "/var/lib/ghc/package.conf.d")
72+
sandbox.maybe_add_mapped_directory("/var/lib/ghc")
73+
6774
@staticmethod
6875
def _capitalize(string: str):
6976
dirname, basename = os.path.split(string)

cms/grading/languages/java_jdk.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
2222
"""
2323

24+
import os
2425
from shlex import quote as shell_quote
2526

2627
from cms.grading import Language
@@ -91,3 +92,10 @@ def get_evaluation_commands(
9192
command = ["/usr/bin/java", "-Deval=true", "-Xmx512M", "-Xss64M",
9293
main] + args
9394
return [unzip_command, command]
95+
96+
def configure_compilation_sandbox(self, sandbox):
97+
# the jvm conf directory is often symlinked to /etc in
98+
# distributions, but the location of it in /etc is inconsistent.
99+
for path in os.listdir("/etc"):
100+
if path == "java" or path.startswith("java-"):
101+
sandbox.add_mapped_directory(f"/etc/{path}")

cms/grading/languages/pascal_fpc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ def get_compilation_commands(self,
6060
command += ["-O2", "-XSs", "-o%s" % executable_filename]
6161
command += [source_filenames[0]]
6262
return [command]
63+
64+
def configure_compilation_sandbox(self, sandbox):
65+
# Needed for /etc/fpc.cfg.
66+
sandbox.maybe_add_mapped_directory("/etc")

cms/grading/languages/python3_pypy.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,11 @@ def get_evaluation_commands(
7979
"""See Language.get_evaluation_commands."""
8080
args = args if args is not None else []
8181
return [["/usr/bin/pypy3", executable_filename] + args]
82+
83+
def configure_compilation_sandbox(self, sandbox):
84+
# Needed on Arch, where /usr/bin/pypy3 is a symlink into
85+
# /opt/pypy3.
86+
sandbox.maybe_add_mapped_directory("/opt/pypy3")
87+
88+
def configure_evaluation_sandbox(self, sandbox):
89+
sandbox.maybe_add_mapped_directory("/opt/pypy3")

cms/grading/steps/compilation.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
from cms import config
3131
from cms.grading.Sandbox import Sandbox
32+
from cms.grading.language import Language
3233
from cms.grading.steps.stats import StatsDict
3334
from .messages import HumanMessage, MessageCollection
3435
from .utils import generic_step
@@ -66,7 +67,7 @@ def N_(message: str):
6667

6768

6869
def compilation_step(
69-
sandbox: Sandbox, commands: list[list[str]]
70+
sandbox: Sandbox, commands: list[list[str]], language: Language
7071
) -> tuple[bool, bool | None, list[str] | None, StatsDict | None]:
7172
"""Execute some compilation commands in the sandbox.
7273
@@ -79,6 +80,7 @@ def compilation_step(
7980
8081
sandbox: the sandbox we consider, already created.
8182
commands: compilation commands to execute.
83+
language: language of the submission
8284
8385
return: a tuple with four items:
8486
* success: True if the sandbox did not fail, in any command;
@@ -93,18 +95,14 @@ def compilation_step(
9395
9496
"""
9597
# Set sandbox parameters suitable for compilation.
96-
sandbox.add_mapped_directory("/etc")
97-
# Directory required to be visible during a compilation with GHC.
98-
# GHC looks for the Haskell's package database in
99-
# "/usr/lib/ghc/package.conf.d" (already visible by isolate's default,
100-
# but it is a symlink to "/var/lib/ghc/package.conf.d"
101-
sandbox.maybe_add_mapped_directory("/var/lib/ghc")
102-
sandbox.preserve_env = True
10398
sandbox.max_processes = config.sandbox.compilation_sandbox_max_processes
10499
sandbox.timeout = config.sandbox.compilation_sandbox_max_time_s
105100
sandbox.wallclock_timeout = 2 * sandbox.timeout + 1
106101
sandbox.address_space = config.sandbox.compilation_sandbox_max_memory_kib * 1024
107102

103+
# Set per-language sandbox parameters.
104+
language.configure_compilation_sandbox(sandbox)
105+
108106
# Run the compilation commands, copying stdout and stderr to stats.
109107
stats = generic_step(sandbox, commands, "compilation", collect_output=True)
110108
if stats is None:

cms/grading/steps/evaluation.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
from cms import config
3232
from cms.grading.Sandbox import Sandbox
33+
from cms.grading.language import Language
3334
from .messages import HumanMessage, MessageCollection
3435
from .stats import StatsDict, execution_stats
3536

@@ -83,6 +84,7 @@ def N_(message: str):
8384
def evaluation_step(
8485
sandbox: Sandbox,
8586
commands: list[list[str]],
87+
language: Language | None,
8688
time_limit: float | None = None,
8789
memory_limit: int | None = None,
8890
dirs_map: dict[str, tuple[str | None, str | None]] | None = None,
@@ -101,6 +103,8 @@ def evaluation_step(
101103
102104
sandbox: the sandbox we consider, already created.
103105
commands: evaluation commands to execute.
106+
language: language of the submission (or None if the commands to
107+
execute are not from a Language's get_evaluation_commands).
104108
time_limit: time limit in seconds (applied to each command);
105109
if None, no time limit is enforced.
106110
memory_limit: memory limit in bytes (applied to each command);
@@ -135,7 +139,7 @@ def evaluation_step(
135139
"""
136140
for command in commands:
137141
success = evaluation_step_before_run(
138-
sandbox, command, time_limit, memory_limit,
142+
sandbox, command, language, time_limit, memory_limit,
139143
None, dirs_map, writable_files, stdin_redirect,
140144
stdout_redirect, multiprocess, wait=True)
141145
if not success:
@@ -152,6 +156,7 @@ def evaluation_step(
152156
def evaluation_step_before_run(
153157
sandbox: Sandbox,
154158
command: list[str],
159+
language: Language | None,
155160
time_limit: float | None = None,
156161
memory_limit: int | None = None,
157162
wall_limit: float | None = None,
@@ -222,6 +227,10 @@ def evaluation_step_before_run(
222227
sandbox.set_multiprocess(multiprocess)
223228
sandbox.close_fds = close_fds
224229

230+
# Configure per-language sandbox parameters.
231+
if language:
232+
language.configure_evaluation_sandbox(sandbox)
233+
225234
# Actually run the evaluation command.
226235
logger.debug("Starting execution step.")
227236
return sandbox.execute_without_std(command, wait=wait)

cms/grading/tasktypes/Batch.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ def _do_compile(self, job: CompilationJob, file_cacher: FileCacher):
244244

245245
# Run the compilation.
246246
box_success, compilation_success, text, stats = \
247-
compilation_step(sandbox, commands)
247+
compilation_step(sandbox, commands, language)
248248

249249
# Retrieve the compiled executables.
250250
job.success = box_success
@@ -311,6 +311,7 @@ def _execution_step(self, job: EvaluationJob, file_cacher: FileCacher):
311311
box_success, evaluation_success, stats = evaluation_step(
312312
sandbox,
313313
commands,
314+
language,
314315
job.time_limit,
315316
job.memory_limit,
316317
writable_files=files_allowing_write,

0 commit comments

Comments
 (0)