rbx.cp 0.13.4__py3-none-any.whl → 0.13.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rbx/box/checkers.py +2 -9
- rbx/box/cli.py +0 -1
- rbx/box/code.py +27 -80
- rbx/box/environment.py +16 -6
- rbx/box/generators.py +26 -3
- rbx/box/global_package.py +1 -1
- rbx/box/header.py +26 -8
- rbx/box/package.py +0 -14
- rbx/box/setter_config.py +11 -0
- rbx/box/solutions.py +12 -4
- rbx/box/tasks.py +9 -4
- rbx/box/testing/testing_package.py +69 -2
- rbx/box/ui/screens/run_explorer.py +0 -8
- rbx/box/ui/utils/run_ui.py +7 -3
- rbx/box/ui/widgets/test_output_box.py +1 -1
- rbx/box/unit.py +4 -4
- rbx/box/validators.py +3 -1
- rbx/grading/caching.py +65 -15
- rbx/grading/judge/cacher.py +5 -3
- rbx/grading/judge/program.py +300 -0
- rbx/grading/judge/sandbox.py +30 -200
- rbx/grading/judge/sandboxes/stupid_sandbox.py +234 -240
- rbx/grading/judge/sandboxes/tee.py +31 -0
- rbx/grading/judge/storage.py +7 -1
- rbx/grading/steps.py +89 -201
- rbx/grading/steps_with_caching.py +15 -6
- rbx/resources/presets/default/problem/problem.rbx.yml +0 -2
- rbx/resources/templates/rbx.h +43 -2
- rbx/testing_utils.py +7 -0
- rbx/utils.py +104 -6
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/METADATA +1 -1
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/RECORD +35 -40
- rbx/grading/judge/sandboxes/isolate.py +0 -695
- rbx/grading/judge/sandboxes/timeit.py +0 -358
- rbx/grading/judge/test.py +0 -38
- rbx/grading/judge/testiso.py +0 -54
- rbx/grading/processing_context.py +0 -71
- rbx/resources/envs/isolate.rbx.yml +0 -36
- rbx/resources/presets/default/problem/sols/slow.cpp +0 -15
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/LICENSE +0 -0
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/WHEEL +0 -0
- {rbx_cp-0.13.4.dist-info → rbx_cp-0.13.6.dist-info}/entry_points.txt +0 -0
@@ -1,695 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import logging
|
4
|
-
import os
|
5
|
-
import pathlib
|
6
|
-
import shutil
|
7
|
-
import stat
|
8
|
-
import subprocess
|
9
|
-
import tempfile
|
10
|
-
from typing import IO, Any, Dict, List, Optional
|
11
|
-
|
12
|
-
from rbx import utils
|
13
|
-
from rbx.config import get_app_path
|
14
|
-
from rbx.grading.judge.cacher import FileCacher
|
15
|
-
from rbx.grading.judge.sandbox import (
|
16
|
-
SandboxBase,
|
17
|
-
SandboxParams,
|
18
|
-
wait_without_std,
|
19
|
-
)
|
20
|
-
|
21
|
-
logger = logging.getLogger(__name__)
|
22
|
-
|
23
|
-
|
24
|
-
class IsolateSandbox(SandboxBase):
|
25
|
-
"""This class creates, deletes and manages the interaction with a
|
26
|
-
sandbox. The sandbox doesn't support concurrent operation, not
|
27
|
-
even for reading.
|
28
|
-
|
29
|
-
The Sandbox offers API for retrieving and storing file, as well as
|
30
|
-
executing programs in a controlled environment. There are anyway a
|
31
|
-
few files reserved for use by the Sandbox itself:
|
32
|
-
|
33
|
-
* commands.log: a text file with the commands ran into this
|
34
|
-
Sandbox, one for each line;
|
35
|
-
|
36
|
-
* run.log.N: for each N, the log produced by the sandbox when running
|
37
|
-
command number N.
|
38
|
-
|
39
|
-
"""
|
40
|
-
|
41
|
-
next_id = 0
|
42
|
-
|
43
|
-
# If the command line starts with this command name, we are just
|
44
|
-
# going to execute it without sandboxing, and with all permissions
|
45
|
-
# on the current directory.
|
46
|
-
SECURE_COMMANDS = ['/bin/cp', '/bin/mv', '/usr/bin/zip', '/usr/bin/unzip']
|
47
|
-
|
48
|
-
log: Optional[Dict[str, Any]]
|
49
|
-
|
50
|
-
def __init__(
|
51
|
-
self,
|
52
|
-
file_cacher: Optional[FileCacher] = None,
|
53
|
-
name: Optional[str] = None,
|
54
|
-
temp_dir: Optional[pathlib.Path] = None,
|
55
|
-
params: Optional[SandboxParams] = None,
|
56
|
-
debug: bool = False,
|
57
|
-
):
|
58
|
-
"""Initialization.
|
59
|
-
|
60
|
-
For arguments documentation, see SandboxBase.__init__.
|
61
|
-
|
62
|
-
"""
|
63
|
-
if not temp_dir:
|
64
|
-
temp_dir = pathlib.Path(tempfile.gettempdir())
|
65
|
-
SandboxBase.__init__(self, file_cacher, name, temp_dir, params)
|
66
|
-
|
67
|
-
self.box_id = IsolateSandbox.next_id % 10
|
68
|
-
IsolateSandbox.next_id += 1
|
69
|
-
|
70
|
-
# We create a directory "home" inside the outer temporary directory,
|
71
|
-
# that will be bind-mounted to "/tmp" inside the sandbox (some
|
72
|
-
# compilers need "/tmp" to exist, and this is a cheap shortcut to
|
73
|
-
# create it). The sandbox also runs code as a different user, and so
|
74
|
-
# we need to ensure that they can read and write to the directory.
|
75
|
-
# But we don't want everybody on the system to, which is why the
|
76
|
-
# outer directory exists with no read permissions.
|
77
|
-
self._outer_dir = pathlib.Path(
|
78
|
-
tempfile.mkdtemp(dir=str(self.temp_dir), prefix='cms-%s-' % (self.name))
|
79
|
-
)
|
80
|
-
self._home = self._outer_dir / 'home'
|
81
|
-
self._home_dest = pathlib.PosixPath('/tmp')
|
82
|
-
self._home.mkdir(parents=True, exist_ok=True)
|
83
|
-
self.allow_writing_all()
|
84
|
-
|
85
|
-
self.exec_name = 'isolate'
|
86
|
-
self.box_exec = self.detect_box_executable()
|
87
|
-
# Used for -M - the meta file ends up in the outer directory. The
|
88
|
-
# actual filename will be <info_basename>.<execution_number>.
|
89
|
-
self.info_basename = self._outer_dir / 'run.log'
|
90
|
-
self.log = None
|
91
|
-
self.exec_num = -1
|
92
|
-
self.cmd_file = self._outer_dir / 'commands.log'
|
93
|
-
self.chdir = self._home_dest
|
94
|
-
self.debug = debug
|
95
|
-
logger.debug(
|
96
|
-
"Sandbox in `%s' created, using box `%s'.", self._home, self.box_exec
|
97
|
-
)
|
98
|
-
|
99
|
-
# Ensure we add a few extra things to params.
|
100
|
-
self.set_params(params or SandboxParams())
|
101
|
-
|
102
|
-
# Tell isolate to get the sandbox ready. We do our best to cleanup
|
103
|
-
# after ourselves, but we might have missed something if a previous
|
104
|
-
# worker was interrupted in the middle of an execution, so we issue an
|
105
|
-
# idempotent cleanup.
|
106
|
-
self.cleanup()
|
107
|
-
self.initialize()
|
108
|
-
|
109
|
-
def set_params(self, params: SandboxParams):
|
110
|
-
"""Set the parameters of the sandbox.
|
111
|
-
|
112
|
-
params (SandboxParams): the parameters to set.
|
113
|
-
|
114
|
-
"""
|
115
|
-
super().set_params(params)
|
116
|
-
self.add_mapped_directory(self._home, dest=self._home_dest, options='rw')
|
117
|
-
|
118
|
-
# Set common environment variables.
|
119
|
-
# Specifically needed by Python, that searches the home for
|
120
|
-
# packages.
|
121
|
-
self.params.set_env['HOME'] = str(self._home_dest)
|
122
|
-
|
123
|
-
def add_mapped_directory(
|
124
|
-
self,
|
125
|
-
src: pathlib.Path,
|
126
|
-
dest: Optional[pathlib.Path] = None,
|
127
|
-
options: Optional[str] = None,
|
128
|
-
ignore_if_not_existing: bool = False,
|
129
|
-
):
|
130
|
-
"""Add src to the directory to be mapped inside the sandbox.
|
131
|
-
|
132
|
-
src (Path): directory to make visible.
|
133
|
-
dest (Path|None): if not None, the path where to bind src.
|
134
|
-
options (str|None): if not None, isolate's directory rule options.
|
135
|
-
ignore_if_not_existing (bool): if True, ignore the mapping when src
|
136
|
-
does not exist (instead of having isolate terminate with an
|
137
|
-
error).
|
138
|
-
|
139
|
-
"""
|
140
|
-
self.params.add_mapped_directory(
|
141
|
-
src, dest, options, ignore_if_not_existing=ignore_if_not_existing
|
142
|
-
)
|
143
|
-
|
144
|
-
def maybe_add_mapped_directory(
|
145
|
-
self,
|
146
|
-
src: pathlib.Path,
|
147
|
-
dest: Optional[pathlib.Path] = None,
|
148
|
-
options: Optional[str] = None,
|
149
|
-
):
|
150
|
-
"""Same as add_mapped_directory, with ignore_if_not_existing."""
|
151
|
-
return self.add_mapped_directory(
|
152
|
-
src, dest, options, ignore_if_not_existing=True
|
153
|
-
)
|
154
|
-
|
155
|
-
def allow_writing_all(self):
|
156
|
-
"""Set permissions in such a way that any operation is allowed."""
|
157
|
-
self._home.chmod(0o777)
|
158
|
-
for child in self._home.iterdir():
|
159
|
-
child.chmod(0o777)
|
160
|
-
|
161
|
-
def allow_writing_none(self):
|
162
|
-
"""Set permissions in such a way that the user cannot write anything."""
|
163
|
-
self._home.chmod(0o755)
|
164
|
-
for child in self._home.iterdir():
|
165
|
-
child.chmod(0o755)
|
166
|
-
|
167
|
-
def allow_writing_only(self, inner_paths: List[pathlib.Path]):
|
168
|
-
"""Set permissions in so that the user can write only some paths.
|
169
|
-
|
170
|
-
By default the user can only write to the home directory. This
|
171
|
-
method further restricts permissions so that it can only write
|
172
|
-
to some files inside the home directory.
|
173
|
-
|
174
|
-
inner_paths ([Path]): the only paths that the user is allowed to
|
175
|
-
write to; they should be "inner" paths (from the perspective
|
176
|
-
of the sandboxed process, not of the host system); they can
|
177
|
-
be absolute or relative (in which case they are interpreted
|
178
|
-
relative to the home directory); paths that point to a file
|
179
|
-
outside the home directory are ignored.
|
180
|
-
|
181
|
-
"""
|
182
|
-
outer_paths: List[pathlib.Path] = []
|
183
|
-
for inner_path in inner_paths:
|
184
|
-
abs_inner_path = utils.abspath(self._home_dest / inner_path)
|
185
|
-
# If an inner path is absolute (e.g., /fifo0/u0_to_m) then
|
186
|
-
# it may be outside home and we should ignore it.
|
187
|
-
if not abs_inner_path.is_relative_to(utils.abspath(self._home_dest)):
|
188
|
-
continue
|
189
|
-
rel_inner_path = abs_inner_path.relative_to(self._home_dest)
|
190
|
-
outer_path = self._home / rel_inner_path
|
191
|
-
outer_paths.append(outer_path)
|
192
|
-
|
193
|
-
# If one of the specified file do not exists, we touch it to
|
194
|
-
# assign the correct permissions.
|
195
|
-
for path in outer_paths:
|
196
|
-
if not path.exists():
|
197
|
-
path.touch()
|
198
|
-
|
199
|
-
# Close everything, then open only the specified.
|
200
|
-
self.allow_writing_none()
|
201
|
-
for path in outer_paths:
|
202
|
-
path.chmod(0o722)
|
203
|
-
|
204
|
-
def get_root_path(self) -> pathlib.Path:
|
205
|
-
"""Return the toplevel path of the sandbox.
|
206
|
-
|
207
|
-
return (Path): the root path.
|
208
|
-
|
209
|
-
"""
|
210
|
-
return self._outer_dir
|
211
|
-
|
212
|
-
def relative_path(self, path: pathlib.Path) -> pathlib.Path:
|
213
|
-
"""Translate from a relative path inside the sandbox to a system path.
|
214
|
-
|
215
|
-
path (Path): relative path of the file inside the sandbox.
|
216
|
-
|
217
|
-
return (Path): the absolute path.
|
218
|
-
|
219
|
-
"""
|
220
|
-
return self._home / path
|
221
|
-
|
222
|
-
def detect_box_executable(self) -> pathlib.Path:
|
223
|
-
"""Try to find an isolate executable. It first looks in
|
224
|
-
./isolate/, then the local directory, then in a relative path
|
225
|
-
from the file that contains the Sandbox module, then in the
|
226
|
-
system paths.
|
227
|
-
|
228
|
-
return (Path): the path to a valid (hopefully) isolate.
|
229
|
-
|
230
|
-
"""
|
231
|
-
paths: List[pathlib.Path] = [
|
232
|
-
pathlib.PosixPath('./isolate') / self.exec_name,
|
233
|
-
pathlib.PosixPath('.') / self.exec_name,
|
234
|
-
get_app_path() / self.exec_name,
|
235
|
-
pathlib.PosixPath('/usr/local/bin') / self.exec_name,
|
236
|
-
pathlib.PosixPath(self.exec_name),
|
237
|
-
]
|
238
|
-
for path in paths:
|
239
|
-
# Consider only non-directory, executable files with SUID flag on.
|
240
|
-
if path.exists() and not path.is_dir() and os.access(str(path), os.X_OK):
|
241
|
-
st = path.stat()
|
242
|
-
if st.st_mode & stat.S_ISUID != 0:
|
243
|
-
return path
|
244
|
-
|
245
|
-
# As default, return self.exec_name alone, that means that
|
246
|
-
# system path is used.
|
247
|
-
return paths[-1]
|
248
|
-
|
249
|
-
def build_box_options(self) -> List[str]:
|
250
|
-
"""Translate the options defined in the instance to a string
|
251
|
-
that can be postponed to isolate as an arguments list.
|
252
|
-
|
253
|
-
return ([string]): the arguments list as strings.
|
254
|
-
|
255
|
-
"""
|
256
|
-
res = list()
|
257
|
-
if self.box_id is not None:
|
258
|
-
res += [f'--box-id={self.box_id}']
|
259
|
-
if self.params.cgroup:
|
260
|
-
res += ['--cg']
|
261
|
-
if self.chdir is not None:
|
262
|
-
res += [f'--chdir={str(self.chdir)}']
|
263
|
-
for dirmount in self.params.dirs:
|
264
|
-
s = str(dirmount.dst) + '=' + str(dirmount.src)
|
265
|
-
if dirmount.options is not None:
|
266
|
-
s += ':' + dirmount.options
|
267
|
-
res += [f'--dir={s}']
|
268
|
-
if self.params.preserve_env:
|
269
|
-
res += ['--full-env']
|
270
|
-
for var in self.params.inherit_env:
|
271
|
-
res += [f'--env={var}']
|
272
|
-
for var, value in self.params.set_env.items():
|
273
|
-
res += [f'--env={var}={value}']
|
274
|
-
if self.params.fsize is not None:
|
275
|
-
# Isolate wants file size as KiB.
|
276
|
-
fsize = self.params.fsize
|
277
|
-
res += [f'--fsize={fsize}']
|
278
|
-
if self.params.stdin_file is not None:
|
279
|
-
inner_stdin = self.inner_absolute_path(self.params.stdin_file)
|
280
|
-
res += ['--stdin=%s' % str(inner_stdin)]
|
281
|
-
if self.params.stack_space is not None:
|
282
|
-
# Isolate wants stack size as KiB.
|
283
|
-
stack_space = self.params.stack_space * 1024
|
284
|
-
res += [f'--stack={stack_space}']
|
285
|
-
if self.params.address_space is not None:
|
286
|
-
# Isolate wants memory size as KiB.
|
287
|
-
address_space = self.params.address_space * 1024
|
288
|
-
if self.params.cgroup:
|
289
|
-
res += [f'--cg-mem={address_space}']
|
290
|
-
else:
|
291
|
-
res += [f'--mem={address_space}']
|
292
|
-
if self.params.stdout_file is not None:
|
293
|
-
inner_stdout = self.inner_absolute_path(self.params.stdout_file)
|
294
|
-
res += ['--stdout=%s' % str(inner_stdout)]
|
295
|
-
if self.params.max_processes is not None:
|
296
|
-
res += [f'--processes={self.params.max_processes}']
|
297
|
-
else:
|
298
|
-
res += ['--processes']
|
299
|
-
if self.params.stderr_file is not None:
|
300
|
-
inner_stderr = self.inner_absolute_path(self.params.stderr_file)
|
301
|
-
res += ['--stderr=%s' % str(inner_stderr)]
|
302
|
-
if self.params.timeout is not None:
|
303
|
-
# Isolate wants time in seconds.
|
304
|
-
timeout = float(self.params.timeout) / 1000
|
305
|
-
res += ['--time=%g' % timeout]
|
306
|
-
res += ['--verbose'] * self.params.verbosity
|
307
|
-
if self.params.wallclock_timeout is not None:
|
308
|
-
wallclock_timeout = float(self.params.wallclock_timeout) / 1000
|
309
|
-
res += ['--wall-time=%g' % wallclock_timeout]
|
310
|
-
if self.params.extra_timeout is not None:
|
311
|
-
extra_timeout = float(self.params.extra_timeout) / 1000
|
312
|
-
res += ['--extra-time=%g' % extra_timeout]
|
313
|
-
res += ['--meta=%s' % ('%s.%d' % (self.info_basename, self.exec_num))]
|
314
|
-
res += ['--run']
|
315
|
-
return res
|
316
|
-
|
317
|
-
def hydrate_logs(self):
|
318
|
-
"""Read the content of the log file of the sandbox (usually
|
319
|
-
run.log.N for some integer N), and set self.log as a dict
|
320
|
-
containing the info in the log file (time, memory, status,
|
321
|
-
...).
|
322
|
-
|
323
|
-
"""
|
324
|
-
# self.log is a dictionary of lists (usually lists of length
|
325
|
-
# one).
|
326
|
-
self.log = {}
|
327
|
-
info_file = pathlib.Path('%s.%d' % (self.info_basename, self.exec_num))
|
328
|
-
try:
|
329
|
-
with self.get_file_text(info_file) as log_file:
|
330
|
-
for line in log_file:
|
331
|
-
key, value = line.strip().split(':', 1)
|
332
|
-
if key in self.log:
|
333
|
-
self.log[key].append(value)
|
334
|
-
else:
|
335
|
-
self.log[key] = [value]
|
336
|
-
except OSError as error:
|
337
|
-
raise OSError(
|
338
|
-
'Error while reading execution log file %s. %r' % (info_file, error)
|
339
|
-
) from error
|
340
|
-
|
341
|
-
def get_execution_time(self) -> Optional[float]:
|
342
|
-
"""Return the time spent in the sandbox, reading the logs if
|
343
|
-
necessary.
|
344
|
-
|
345
|
-
return (float): time spent in the sandbox.
|
346
|
-
|
347
|
-
"""
|
348
|
-
assert self.log is not None
|
349
|
-
if 'time' in self.log:
|
350
|
-
return float(self.log['time'][0])
|
351
|
-
return None
|
352
|
-
|
353
|
-
def get_execution_wall_clock_time(self) -> Optional[float]:
|
354
|
-
"""Return the total time from the start of the sandbox to the
|
355
|
-
conclusion of the task, reading the logs if necessary.
|
356
|
-
|
357
|
-
return (float): total time the sandbox was alive.
|
358
|
-
|
359
|
-
"""
|
360
|
-
assert self.log is not None
|
361
|
-
if 'time-wall' in self.log:
|
362
|
-
return float(self.log['time-wall'][0])
|
363
|
-
return None
|
364
|
-
|
365
|
-
def use_soft_timeout(self) -> bool:
|
366
|
-
return True
|
367
|
-
|
368
|
-
def get_memory_used(self) -> Optional[int]:
|
369
|
-
"""Return the memory used by the sandbox, reading the logs if
|
370
|
-
necessary.
|
371
|
-
|
372
|
-
return (int): memory used by the sandbox (in kbytes).
|
373
|
-
|
374
|
-
"""
|
375
|
-
assert self.log is not None
|
376
|
-
if 'cg-mem' in self.log:
|
377
|
-
# Isolate returns memory measurements in KiB.
|
378
|
-
return int(self.log['cg-mem'][0])
|
379
|
-
return None
|
380
|
-
|
381
|
-
def get_killing_signal(self) -> int:
|
382
|
-
"""Return the signal that killed the sandboxed process,
|
383
|
-
reading the logs if necessary.
|
384
|
-
|
385
|
-
return (int): offending signal, or 0.
|
386
|
-
|
387
|
-
"""
|
388
|
-
assert self.log is not None
|
389
|
-
if 'exitsig' in self.log:
|
390
|
-
return int(self.log['exitsig'][0])
|
391
|
-
return 0
|
392
|
-
|
393
|
-
def get_exit_code(self) -> int:
|
394
|
-
"""Return the exit code of the sandboxed process, reading the
|
395
|
-
logs if necessary.
|
396
|
-
|
397
|
-
return (int): exitcode, or 0.
|
398
|
-
|
399
|
-
"""
|
400
|
-
assert self.log is not None
|
401
|
-
if 'exitcode' in self.log:
|
402
|
-
return int(self.log['exitcode'][0])
|
403
|
-
return 0
|
404
|
-
|
405
|
-
def get_status_list(self) -> List[str]:
|
406
|
-
"""Reads the sandbox log file, and set and return the status
|
407
|
-
of the sandbox.
|
408
|
-
|
409
|
-
return (list): list of statuses of the sandbox.
|
410
|
-
|
411
|
-
"""
|
412
|
-
assert self.log is not None
|
413
|
-
if 'status' in self.log:
|
414
|
-
return self.log['status']
|
415
|
-
return []
|
416
|
-
|
417
|
-
def get_exit_status(self) -> str:
|
418
|
-
"""Get the list of statuses of the sandbox and return the most
|
419
|
-
important one.
|
420
|
-
|
421
|
-
return (string): the main reason why the sandbox terminated.
|
422
|
-
|
423
|
-
"""
|
424
|
-
# TODO: figure out EXIT_TERMINATED
|
425
|
-
assert self.log is not None
|
426
|
-
status_list = self.get_status_list()
|
427
|
-
if 'XX' in status_list:
|
428
|
-
return self.EXIT_SANDBOX_ERROR
|
429
|
-
elif 'TO' in status_list:
|
430
|
-
if 'message' in self.log and 'wall' in self.log['message'][0]:
|
431
|
-
return self.EXIT_TIMEOUT_WALL
|
432
|
-
else:
|
433
|
-
return self.EXIT_TIMEOUT
|
434
|
-
elif 'SG' in status_list:
|
435
|
-
return self.EXIT_SIGNAL
|
436
|
-
elif 'RE' in status_list:
|
437
|
-
return self.EXIT_NONZERO_RETURN
|
438
|
-
# OK status is not reported in the log file, it's implicit.
|
439
|
-
return self.EXIT_OK
|
440
|
-
|
441
|
-
def get_human_exit_description(self) -> str:
|
442
|
-
"""Get the status of the sandbox and return a human-readable
|
443
|
-
string describing it.
|
444
|
-
|
445
|
-
return (string): human-readable explaination of why the
|
446
|
-
sandbox terminated.
|
447
|
-
|
448
|
-
"""
|
449
|
-
status = self.get_exit_status()
|
450
|
-
if status == self.EXIT_OK:
|
451
|
-
return (
|
452
|
-
'Execution successfully finished (with exit code %d)'
|
453
|
-
% self.get_exit_code()
|
454
|
-
)
|
455
|
-
elif status == self.EXIT_SANDBOX_ERROR:
|
456
|
-
return 'Execution failed because of sandbox error'
|
457
|
-
elif status == self.EXIT_TIMEOUT:
|
458
|
-
return 'Execution timed out'
|
459
|
-
elif status == self.EXIT_TIMEOUT_WALL:
|
460
|
-
return 'Execution timed out (wall clock limit exceeded)'
|
461
|
-
elif status == self.EXIT_SIGNAL:
|
462
|
-
return 'Execution killed with signal %s' % self.get_killing_signal()
|
463
|
-
elif status == self.EXIT_NONZERO_RETURN:
|
464
|
-
return 'Execution failed because the return code was nonzero'
|
465
|
-
return ''
|
466
|
-
|
467
|
-
def get_detailed_logs(self) -> str:
|
468
|
-
"""Return the detailed logs of the sandbox.
|
469
|
-
|
470
|
-
return (string): the detailed logs of the sandbox.
|
471
|
-
|
472
|
-
"""
|
473
|
-
return str(self.log)
|
474
|
-
|
475
|
-
def inner_absolute_path(self, path: pathlib.Path) -> pathlib.Path:
|
476
|
-
"""Translate from a relative path inside the sandbox to an
|
477
|
-
absolute path inside the sandbox.
|
478
|
-
|
479
|
-
path (string): relative path of the file inside the sandbox.
|
480
|
-
|
481
|
-
return (string): the absolute path of the file inside the sandbox.
|
482
|
-
|
483
|
-
"""
|
484
|
-
return self._home_dest / path
|
485
|
-
|
486
|
-
def _popen(
|
487
|
-
self,
|
488
|
-
command: List[str],
|
489
|
-
stdin: Optional[IO[bytes] | int] = None,
|
490
|
-
stdout: Optional[IO[bytes] | int] = None,
|
491
|
-
stderr: Optional[IO[bytes] | int] = None,
|
492
|
-
close_fds: bool = True,
|
493
|
-
) -> subprocess.Popen:
|
494
|
-
"""Execute the given command in the sandbox using
|
495
|
-
subprocess.Popen, assigning the corresponding standard file
|
496
|
-
descriptors.
|
497
|
-
|
498
|
-
command ([string]): executable filename and arguments of the
|
499
|
-
command.
|
500
|
-
stdin (file|None): a file descriptor.
|
501
|
-
stdout (file|None): a file descriptor.
|
502
|
-
stderr (file|None): a file descriptor.
|
503
|
-
close_fds (bool): close all file descriptor before executing.
|
504
|
-
|
505
|
-
return (Popen): popen object.
|
506
|
-
|
507
|
-
"""
|
508
|
-
self.log = None
|
509
|
-
self.exec_num += 1
|
510
|
-
|
511
|
-
# We run a selection of commands without isolate, as they need
|
512
|
-
# to create new files. This is safe because these commands do
|
513
|
-
# not depend on the user input.
|
514
|
-
if command[0] in IsolateSandbox.SECURE_COMMANDS:
|
515
|
-
logger.debug(
|
516
|
-
'Executing non-securely: %s at %s',
|
517
|
-
str(command),
|
518
|
-
self._home,
|
519
|
-
)
|
520
|
-
try:
|
521
|
-
prev_permissions = stat.S_IMODE(self._home.stat().st_mode)
|
522
|
-
self._home.chmod(0o700)
|
523
|
-
with open(self.cmd_file, 'at', encoding='utf-8') as cmds:
|
524
|
-
cmds.write('%s\n' % str(command))
|
525
|
-
p = subprocess.Popen(
|
526
|
-
command,
|
527
|
-
cwd=str(self._home),
|
528
|
-
stdin=stdin,
|
529
|
-
stdout=stdout,
|
530
|
-
stderr=stderr,
|
531
|
-
close_fds=close_fds,
|
532
|
-
)
|
533
|
-
self._home.chmod(prev_permissions)
|
534
|
-
# For secure commands, we clear the output so that it
|
535
|
-
# is not forwarded to the contestants. Secure commands
|
536
|
-
# are "setup" commands, which should not fail or
|
537
|
-
# provide information for the contestants.
|
538
|
-
if self.params.stdout_file:
|
539
|
-
(self._home / self.params.stdout_file).open('wb').close()
|
540
|
-
if self.params.stderr_file:
|
541
|
-
(self._home / self.params.stderr_file).open('wb').close()
|
542
|
-
self._write_empty_run_log(self.exec_num)
|
543
|
-
except OSError:
|
544
|
-
logger.critical(
|
545
|
-
'Failed to execute program in sandbox with command: %s',
|
546
|
-
str(command),
|
547
|
-
exc_info=True,
|
548
|
-
)
|
549
|
-
raise
|
550
|
-
return p
|
551
|
-
|
552
|
-
args = [self.box_exec] + self.build_box_options() + ['--'] + command
|
553
|
-
logger.debug(
|
554
|
-
"Executing program in sandbox with command: `%s'.",
|
555
|
-
str(args),
|
556
|
-
)
|
557
|
-
# Temporarily allow writing new files.
|
558
|
-
prev_permissions = stat.S_IMODE(self._home.stat().st_mode)
|
559
|
-
self._home.chmod(0o700)
|
560
|
-
with open(self.cmd_file, 'at', encoding='utf-8') as commands:
|
561
|
-
commands.write('%s\n' % (str(args)))
|
562
|
-
self._home.chmod(prev_permissions)
|
563
|
-
try:
|
564
|
-
p = subprocess.Popen(
|
565
|
-
args, stdin=stdin, stdout=stdout, stderr=stderr, close_fds=close_fds
|
566
|
-
)
|
567
|
-
except OSError:
|
568
|
-
logger.critical(
|
569
|
-
'Failed to execute program in sandbox with command: %s',
|
570
|
-
str(args),
|
571
|
-
exc_info=True,
|
572
|
-
)
|
573
|
-
raise
|
574
|
-
|
575
|
-
return p
|
576
|
-
|
577
|
-
def _write_empty_run_log(self, index: int):
|
578
|
-
"""Write a fake run.log file with no information."""
|
579
|
-
info_file = pathlib.PosixPath('%s.%d' % (self.info_basename, index))
|
580
|
-
with info_file.open('wt', encoding='utf-8') as f:
|
581
|
-
f.write('time:0.000\n')
|
582
|
-
f.write('time-wall:0.000\n')
|
583
|
-
f.write('max-rss:0\n')
|
584
|
-
f.write('cg-mem:0\n')
|
585
|
-
|
586
|
-
def execute_without_std(
|
587
|
-
self,
|
588
|
-
command: List[str],
|
589
|
-
) -> bool:
|
590
|
-
"""Execute the given command in the sandbox using
|
591
|
-
subprocess.Popen and discarding standard input, output and
|
592
|
-
error. More specifically, the standard input gets closed just
|
593
|
-
after the execution has started; standard output and error are
|
594
|
-
read until the end, in a way that prevents the execution from
|
595
|
-
being blocked because of insufficient buffering.
|
596
|
-
|
597
|
-
command ([string]): executable filename and arguments of the
|
598
|
-
command.
|
599
|
-
|
600
|
-
return (bool|Popen): return True if the sandbox didn't report
|
601
|
-
errors (caused by the sandbox itself), False otherwise.
|
602
|
-
"""
|
603
|
-
self.clear_pid()
|
604
|
-
popen = self._popen(
|
605
|
-
command,
|
606
|
-
stdin=subprocess.PIPE,
|
607
|
-
stdout=subprocess.PIPE,
|
608
|
-
stderr=subprocess.PIPE,
|
609
|
-
close_fds=True,
|
610
|
-
)
|
611
|
-
|
612
|
-
# If the caller wants us to wait for completion, we also avoid
|
613
|
-
# std*** to interfere with command. Otherwise we let the
|
614
|
-
# caller handle these issues.
|
615
|
-
with popen as p:
|
616
|
-
self.set_pid(p.pid)
|
617
|
-
exitcode = self.translate_box_exitcode(
|
618
|
-
wait_without_std([p], actually_pipe_to_stdout=self.debug)[0]
|
619
|
-
)
|
620
|
-
self.hydrate_logs()
|
621
|
-
return exitcode
|
622
|
-
|
623
|
-
def translate_box_exitcode(self, exitcode: int) -> bool:
|
624
|
-
"""Translate the sandbox exit code to a boolean sandbox success.
|
625
|
-
|
626
|
-
Isolate emits the following exit codes:
|
627
|
-
* 0 -> both sandbox and internal process finished successfully (meta
|
628
|
-
file will contain "status:OK" -> return True;
|
629
|
-
* 1 -> sandbox finished successfully, but internal process was
|
630
|
-
terminated, e.g., due to timeout (meta file will contain
|
631
|
-
status:x" with x in (TO, SG, RE)) -> return True;
|
632
|
-
* 2 -> sandbox terminated with an error (meta file will contain
|
633
|
-
"status:XX") -> return False.
|
634
|
-
|
635
|
-
"""
|
636
|
-
if exitcode == 0 or exitcode == 1:
|
637
|
-
return True
|
638
|
-
elif exitcode == 2:
|
639
|
-
return False
|
640
|
-
else:
|
641
|
-
raise Exception('Sandbox exit status (%d) unknown' % exitcode)
|
642
|
-
|
643
|
-
def initialize(self):
|
644
|
-
"""Initialize isolate's box."""
|
645
|
-
init_cmd = (
|
646
|
-
[self.box_exec]
|
647
|
-
+ (['--cg'] if self.params.cgroup else [])
|
648
|
-
+ ['--box-id=%d' % self.box_id, '--init']
|
649
|
-
)
|
650
|
-
try:
|
651
|
-
subprocess.check_call(init_cmd)
|
652
|
-
except subprocess.CalledProcessError as e:
|
653
|
-
raise Exception('Failed to initialize sandbox') from e
|
654
|
-
|
655
|
-
def cleanup(self, delete: bool = False):
|
656
|
-
"""See Sandbox.cleanup()."""
|
657
|
-
# The user isolate assigns within the sandbox might have created
|
658
|
-
# subdirectories and files therein, making the user outside the sandbox
|
659
|
-
# unable to delete the whole tree. If the caller asked us to delete the
|
660
|
-
# sandbox, we first issue a chmod within isolate to make sure that we
|
661
|
-
# will be able to delete everything. If not, we leave the files as they
|
662
|
-
# are to avoid masking possible problems the admin wanted to debug.
|
663
|
-
|
664
|
-
exe = (
|
665
|
-
[self.box_exec]
|
666
|
-
+ (['--cg'] if self.params.cgroup else [])
|
667
|
-
+ ['--box-id=%d' % self.box_id]
|
668
|
-
)
|
669
|
-
|
670
|
-
if delete:
|
671
|
-
# Ignore exit status as some files may be owned by our user
|
672
|
-
subprocess.call(
|
673
|
-
exe
|
674
|
-
+ [
|
675
|
-
'--dir=%s=%s:rw' % (str(self._home_dest), str(self._home)),
|
676
|
-
'--run',
|
677
|
-
'--',
|
678
|
-
'/bin/chmod',
|
679
|
-
'777',
|
680
|
-
'-R',
|
681
|
-
str(self._home_dest),
|
682
|
-
],
|
683
|
-
stdout=subprocess.DEVNULL,
|
684
|
-
stderr=subprocess.STDOUT,
|
685
|
-
)
|
686
|
-
|
687
|
-
# Tell isolate to cleanup the sandbox.
|
688
|
-
subprocess.check_call(
|
689
|
-
exe + ['--cleanup'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT
|
690
|
-
)
|
691
|
-
|
692
|
-
if delete:
|
693
|
-
logger.debug('Deleting sandbox in %s.', self._outer_dir)
|
694
|
-
# Delete the working directory.
|
695
|
-
shutil.rmtree(str(self._outer_dir), ignore_errors=True)
|