Skip to content

Commit 444618b

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 821fed5 commit 444618b

File tree

15 files changed

+139
-52
lines changed

15 files changed

+139
-52
lines changed

cms/grading/Sandbox.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -970,20 +970,19 @@ def __init__(self, file_cacher, name=None, temp_dir=None):
970970
# between sandboxes.
971971
self.dirs.append((None, "/dev/shm", "tmp"))
972972

973-
# Set common environment variables.
973+
# Set common configuration that is relevant for multiple
974+
# languages.
975+
976+
self.set_env["PATH"] = "/usr/local/bin:/usr/bin:/bin"
977+
974978
# Specifically needed by Python, that searches the home for
975979
# packages.
976980
self.set_env["HOME"] = self._home_dest
977981

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

982-
# Likewise, needed by C# programs. The Mono runtime looks in
983-
# /etc/mono/config to obtain the default DllMap, which includes, in
984-
# particular, the System.Native assembly.
985-
self.maybe_add_mapped_directory("/etc/mono", options="noexec")
986-
987986
# Tell isolate to get the sandbox ready. We do our best to cleanup
988987
# after ourselves, but we might have missed something if a previous
989988
# worker was interrupted in the middle of an execution, so we issue an

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
dirs_map, writable_files, stdin_redirect, stdout_redirect,
140144
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
dirs_map: dict[str, tuple[str | None, str | None]] | None = None,
@@ -216,6 +221,10 @@ def evaluation_step_before_run(
216221

217222
sandbox.set_multiprocess(multiprocess)
218223

224+
# Configure per-language sandbox parameters.
225+
if language:
226+
language.configure_evaluation_sandbox(sandbox)
227+
219228
# Actually run the evaluation command.
220229
logger.debug("Starting execution step.")
221230
return sandbox.execute_without_std(command, wait=wait)

cms/grading/tasktypes/Batch.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import os
2626

2727
from cms.db import Executable
28+
from cms.db.filecacher import FileCacher
29+
from cms.grading.Job import EvaluationJob
2830
from cms.grading.ParameterTypes import ParameterTypeCollection, \
2931
ParameterTypeChoice, ParameterTypeString
3032
from cms.grading.language import Language
@@ -242,7 +244,7 @@ def _do_compile(self, job, file_cacher):
242244

243245
# Run the compilation.
244246
box_success, compilation_success, text, stats = \
245-
compilation_step(sandbox, commands)
247+
compilation_step(sandbox, commands, language)
246248

247249
# Retrieve the compiled executables.
248250
job.success = box_success
@@ -266,7 +268,7 @@ def compile(self, job, file_cacher):
266268

267269
self._do_compile(job, file_cacher)
268270

269-
def _execution_step(self, job, file_cacher):
271+
def _execution_step(self, job: EvaluationJob, file_cacher: FileCacher):
270272
# Prepare the execution
271273
executable_filename = next(iter(job.executables.keys()))
272274
language = get_language(job.language)
@@ -308,6 +310,7 @@ def _execution_step(self, job, file_cacher):
308310
box_success, evaluation_success, stats = evaluation_step(
309311
sandbox,
310312
commands,
313+
language,
311314
job.time_limit,
312315
job.memory_limit,
313316
writable_files=files_allowing_write,

0 commit comments

Comments
 (0)