summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimotej Lazar <timotej.lazar@fri.uni-lj.si>2015-10-14 17:57:09 +0200
committerTimotej Lazar <timotej.lazar@fri.uni-lj.si>2015-10-14 17:57:09 +0200
commit520420556ad9f2871cd8c5910645193508cb4082 (patch)
tree08efd34b6c9d3cc071b6c5345b213ec9f7f59dd5
parent7ae2d8824ab59dfbda6eaf7f621b6d3bfdec56e7 (diff)
Use sandbox for testing Python programs
Use interpreter.py for running tests as well as interactive sessions. Signals are now sent with "sandbox <user> kill", so terminator is not needed anymore.
-rwxr-xr-xpython/runner/interpreter.py121
-rw-r--r--python/runner/terminator.c31
-rw-r--r--python/util.py2
-rwxr-xr-xscripts/deploy/codeq_refresh_and_deploy.sh6
-rw-r--r--server/python_session.py107
5 files changed, 127 insertions, 140 deletions
diff --git a/python/runner/interpreter.py b/python/runner/interpreter.py
index da60d72..722bc33 100755
--- a/python/runner/interpreter.py
+++ b/python/runner/interpreter.py
@@ -1,6 +1,8 @@
#!/usr/bin/python3 -u
import code
+import io
+import pickle
import sys
import seccomp
@@ -43,49 +45,88 @@ f.add_rule(seccomp.ALLOW, "access")
f.add_rule(seccomp.ALLOW, "select")
f.load()
-class MyConsole(code.InteractiveConsole):
- def interact(self, banner=None):
- if banner is not None:
- self.write('{}\n'.format(banner))
+if len(sys.argv) > 1 and sys.argv[1] == 'run':
+ # Receive a pickled tuple (code, expr, stdin) on standard input, run it,
+ # and print a pickled tuple (result, out, err, exc) on standard output.
+ data = sys.stdin.buffer.read()
+ code, expr, stdin = pickle.loads(data)
- buffer = []
- prompt = '>>> '
- while True:
- try:
- line = input(prompt)
- # Assume we are running the user's program; silence the prompt.
- if line == 'exec("""\\':
- self.write('<run>\n')
- prompt = ''
+ result, out, err, exc = None, None, None, None
+ try:
+ sys.stdin = io.StringIO(stdin)
+ sys.stdout = io.StringIO()
+ sys.stderr = io.StringIO()
+ 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()
+ outdata = pickle.dumps((result, out, err, exc))
+ sys.__stdout__.buffer.write(outdata)
+
+else:
+ # Run an interactive console.
+ class MyConsole(code.InteractiveConsole):
+ def interact(self, banner=None):
+ if banner is not None:
+ self.write('{}\n'.format(banner))
+
+ buffer = []
+ prompt = '>>> '
+ while True:
+ try:
+ line = input(prompt)
+ # Assume we are running the user's program; silence the prompt.
+ if line == 'exec("""\\':
+ self.write('<run>\n')
+ prompt = ''
- buffer.append(line)
- source = '\n'.join(buffer)
- more = self.runsource(source)
- if more:
- if prompt:
- prompt = '... '
- else:
+ buffer.append(line)
+ source = '\n'.join(buffer)
+ more = self.runsource(source)
+ if more:
+ if prompt:
+ prompt = '... '
+ else:
+ prompt = '>>> '
+ buffer = []
+ except KeyboardInterrupt:
prompt = '>>> '
buffer = []
- except KeyboardInterrupt:
- prompt = '>>> '
- buffer = []
- self.write('\n')
- except ValueError:
- break
- except EOFError:
- break
+ self.write('\n')
+ except ValueError:
+ break
+ except EOFError:
+ break
- def runcode(self, code):
- try:
- exec(code, self.locals)
- except KeyboardInterrupt:
- self.write('^C')
- raise
- except SystemExit as ex:
- raise
- except:
- # Show traceback for all other exceptions.
- self.showtraceback()
+ def runcode(self, code):
+ try:
+ exec(code, self.locals)
+ except KeyboardInterrupt:
+ self.write('^C')
+ raise
+ except SystemExit as ex:
+ raise
+ except:
+ # Show traceback for all other exceptions.
+ self.showtraceback()
-MyConsole().interact()
+ MyConsole().interact()
diff --git a/python/runner/terminator.c b/python/runner/terminator.c
deleted file mode 100644
index 9eaca83..0000000
--- a/python/runner/terminator.c
+++ /dev/null
@@ -1,31 +0,0 @@
-#include <pwd.h>
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-
-int main(int argc, char* argv[])
-{
- if (argc < 4) {
- fprintf(stderr, "usage: %s USERNAME PID SIGNAL\n", argv[0]);
- return 1;
- }
-
- // switch user (requires root or "setcap cap_setuid,cap_setgid+ep")
- char const* username = argv[1];
- struct passwd const* pw = getpwnam(username);
- if (!pw) {
- fprintf(stderr, "no such user: %s\n", username);
- return 1;
- }
- int ret = 0;
- if ((ret = setgid(pw->pw_gid)) != 0)
- fprintf(stderr, "setgid returned %d\n", ret);
- if ((ret = setuid(pw->pw_uid)) != 0)
- fprintf(stderr, "setuid returned %d\n", ret);
-
- pid_t pid = atol(argv[2]);
- int signum = atoi(argv[3]);
- kill(pid, signum);
- return 0;
-}
diff --git a/python/util.py b/python/util.py
index ce89558..8c1a0c7 100644
--- a/python/util.py
+++ b/python/util.py
@@ -59,6 +59,8 @@ def get_exception_desc(exc):
return [{'id':'eof_error'}]
if 'timed out' in exc:
return [{'id':'timed_out'}]
+ if 'sandbox violation' in exc:
+ return [{'id': 'sandbox_violation'}]
if 'NameError' in exc:
return [{'id':'name_error', 'args': {'message': exc}}]
elif 'TypeError' in exc:
diff --git a/scripts/deploy/codeq_refresh_and_deploy.sh b/scripts/deploy/codeq_refresh_and_deploy.sh
index f0a0aeb..e44739b 100755
--- a/scripts/deploy/codeq_refresh_and_deploy.sh
+++ b/scripts/deploy/codeq_refresh_and_deploy.sh
@@ -36,10 +36,10 @@ if git diff --name-status origin/$CODEQ_GIT_BRANCH | cut -c3- | grep -v \\.gitig
BUILD_WEB_RESOURCES=1
fi
- # rebuild sandbox binaries if out of date
- for prog in python/runner/sandbox python/runner/terminator; do
+ # rebuild sandbox if out of date
+ for prog in python/runner/sandbox; do
if ! make -q "${prog}"; then
- make -s "${prog}" && setcap cap_setuid,cap_setgid+ep "${prog}"
+ make "${prog}" && setcap cap_setuid,cap_setgid+ep "${prog}"
fi
done
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}))
-