From 520420556ad9f2871cd8c5910645193508cb4082 Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Wed, 14 Oct 2015 17:57:09 +0200 Subject: Use sandbox for testing Python programs Use interpreter.py for running tests as well as interactive sessions. Signals are now sent with "sandbox kill", so terminator is not needed anymore. --- server/python_session.py | 107 ++++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 66 deletions(-) (limited to 'server') diff --git a/server/python_session.py b/server/python_session.py index e1be4ba..39bd8f4 100644 --- a/server/python_session.py +++ b/server/python_session.py @@ -4,6 +4,7 @@ from fcntl import fcntl, F_GETFL, F_SETFL import io import multiprocessing import os +import pickle import selectors import signal import subprocess @@ -36,22 +37,34 @@ class PythonSession(server.LanguageSession): # Launch processes. futures = [] for expr, stdin in inputs: - conn_parent, conn_child = multiprocessing.Pipe() - p = multiprocessing.Process(target=_run_exec, args=(conn_child, code, expr, stdin)) - p.start() - futures.append((p, conn_parent)) + p = subprocess.Popen(proc_args + ['run'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + data = pickle.dumps((code, expr, stdin)) + p.stdin.write(data) + futures.append(p) # Wait for results. results = [] start = time.monotonic() - for p, conn in futures: + for proc in futures: now = time.monotonic() real_timeout = max(0, timeout - (now - start)) - if conn.poll(real_timeout): - results.append(conn.recv()) - else: - results.append((None, None, None, 'timed out')) - p.terminate() + try: + stdout, _ = proc.communicate(timeout=real_timeout) + result = pickle.loads(stdout) + results.append(result) + except subprocess.TimeoutExpired: + results.append((None, '', '', 'timed out')) + except EOFError: + results.append((None, '', '', 'sandbox violation')) + if proc.poll() is None: + try: + proc_kill(proc, signal.SIGKILL) + except: + pass + return results def exec(self, program): @@ -114,23 +127,21 @@ class PythonSession(server.LanguageSession): hint_type.instantiate(hint, self._sent_hints) self._sent_hints.extend(hints) -def _interpreter(control, callback): - basedir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] - script = os.path.join(basedir, 'python', 'runner', 'interpreter.py') - - # If the sandbox wrapper exists, use it to switch to user "nobody" and - # enforce additional limits. Unless the daemon is running as root we are - # not able to signal nobody's PIDs, so switch user again for the killing. - sandbox = os.path.join(basedir, 'python', 'runner', 'sandbox') - terminator = os.path.join(basedir, 'python', 'runner', 'terminator') - if os.path.exists(sandbox) and os.path.exists(terminator): - newuser = 'nobody' # TODO make this configurable - args = [sandbox, newuser, script] - kill = lambda proc, sig: subprocess.call([terminator, newuser, str(proc.pid), str(sig)]) - else: - args = [script] - kill = lambda proc, sig: proc.send_signal(sig) +# If the sandbox wrapper exists, use it to switch to user "nobody" and enforce +# additional limits. Unless the daemon is running as root we are not able to +# signal nobody's PIDs, so switch user again for the killing. +_basedir = os.path.split(os.path.dirname(os.path.realpath(__file__)))[0] +_script = os.path.join(_basedir, 'python', 'runner', 'interpreter.py') +_sandbox = os.path.join(_basedir, 'python', 'runner', 'sandbox') +if os.path.exists(_sandbox): + newuser = 'nobody' # TODO make this configurable + proc_args = [_sandbox, newuser, _script] + proc_kill = lambda proc, sig: subprocess.call([_sandbox, newuser, 'kill', '-{}'.format(sig), str(proc.pid)]) +else: + proc_args = [_script] + proc_kill = lambda proc, sig: proc.send_signal(sig) +def _interpreter(control, callback): done = False proc = None @@ -156,7 +167,7 @@ def _interpreter(control, callback): proc.stdin.write(data.encode('utf-8')) proc.stdin.flush() elif cmd == 'stop': - kill(proc, signal.SIGINT) + proc_kill(proc, signal.SIGINT) elif cmd == 'done': done = True except: @@ -181,7 +192,7 @@ def _interpreter(control, callback): selector.unregister(conn) if proc.poll() is None: # Process has not terminated yet, make sure it does. - kill(proc, signal.SIGKILL) + proc_kill(proc, signal.SIGKILL) proc = None length = newlines = 0 callback('Interpreter restarted.\n') @@ -190,7 +201,7 @@ def _interpreter(control, callback): while not done: # Ensure the interpreter process is running. if proc is None: - proc = subprocess.Popen(args, + proc = subprocess.Popen(proc_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) @@ -210,42 +221,6 @@ def _interpreter(control, callback): # We are done, kill the child. selector.close() if proc is not None: - kill(proc, signal.SIGKILL) - -# Execute [code] and evaluate [expr]. Input is given by the string [stdin]. -# Return result of evaluation, the contents of stdout and stderr, and the -# exception traceback. -# TODO sandbox this -def _run_exec(conn, code, expr=None, stdin=''): - result, out, err, exc = None, None, None, None - sys.stdin = io.StringIO(stdin) - sys.stdout = io.StringIO() - sys.stderr = io.StringIO() - try: - env = {} - if code: - exec(code, env) - if expr: - result = eval(expr, env) - except Exception as ex: - # Exception is not JSON serializable, so return traceback as string - # (without the first entry, which is this function). - import traceback - e_type, e_value, e_tb = sys.exc_info() - stack = traceback.extract_tb(e_tb) - exc = ''.join( - ['Traceback (most recent call last):\n'] + - [' line {}, in {}\n'.format(lineno, name) + (line+'\n' if line else '') - for filename, lineno, name, line in stack[1:]] + - traceback.format_exception_only(e_type, e_value) - ).rstrip() - finally: - out = sys.stdout.getvalue() - err = sys.stderr.getvalue() - sys.stdin.close() - sys.stdout.close() - sys.stderr.close() - conn.send((result, out, err, exc)) + proc_kill(proc, signal.SIGKILL) server.language_session_handlers['python'] = lambda user_session, problem_id, language_identifier, group_identifier, problem_identifier: PythonSession(lambda text: user_session.send({'event': 'terminal_output', 'text': text})) - -- cgit v1.2.1