From 251c5e2ba0e85103c55cf31026739b2e7e9d4b90 Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Wed, 16 Sep 2015 16:10:59 +0200 Subject: Implement async. comm. with Python interpreter Creating, destroying and communicationg with the interpreter subprocess is now handled by a thread attached to PythonSession. Interpreter is sandboxed using libseccomp. --- python/interpreter.py | 40 +++++++++++++++ python/runner/interpreter.py | 8 --- python/runner/main.py | 117 ------------------------------------------- 3 files changed, 40 insertions(+), 125 deletions(-) create mode 100755 python/interpreter.py delete mode 100755 python/runner/interpreter.py delete mode 100755 python/runner/main.py (limited to 'python') diff --git a/python/interpreter.py b/python/interpreter.py new file mode 100755 index 0000000..87de3aa --- /dev/null +++ b/python/interpreter.py @@ -0,0 +1,40 @@ +#!/usr/bin/python3 + +import code +import sys + +import seccomp + +f = seccomp.SyscallFilter(defaction=seccomp.KILL) +# Necessary for Python. +f.add_rule(seccomp.ALLOW, "exit_group") +f.add_rule(seccomp.ALLOW, "rt_sigaction") +f.add_rule(seccomp.ALLOW, "brk") + +# Mostly harmless. +f.add_rule(seccomp.ALLOW, "mprotect") + +# Allow reading from stdin and writing to stdout/stderr. +f.add_rule(seccomp.ALLOW, "read", seccomp.Arg(0, seccomp.EQ, sys.stdin.fileno())) +f.add_rule(seccomp.ALLOW, "write", seccomp.Arg(0, seccomp.EQ, sys.stdout.fileno())) +f.add_rule(seccomp.ALLOW, "write", seccomp.Arg(0, seccomp.EQ, sys.stderr.fileno())) + +f.add_rule(seccomp.ALLOW, "ioctl") +f.add_rule(seccomp.ALLOW, "mmap") +f.add_rule(seccomp.ALLOW, "munmap") + +# Needed for finding source code for exceptions. +f.add_rule(seccomp.ALLOW, "stat") +f.add_rule(seccomp.ALLOW, "open", seccomp.Arg(1, seccomp.MASKED_EQ, 0x3, 0)) +f.add_rule(seccomp.ALLOW, "fcntl") +f.add_rule(seccomp.ALLOW, "fstat") +f.add_rule(seccomp.ALLOW, "lseek") +f.add_rule(seccomp.ALLOW, "read") +f.add_rule(seccomp.ALLOW, "close") + +# Needed for code.InteractiveConsole. +f.add_rule(seccomp.ALLOW, "access") +f.add_rule(seccomp.ALLOW, "select") +f.load() + +code.interact(banner='') diff --git a/python/runner/interpreter.py b/python/runner/interpreter.py deleted file mode 100755 index 5fa320a..0000000 --- a/python/runner/interpreter.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/python3 - -# Apparently there is no (working) way to get a non-blocking stdout if we call -# the Python interpreter directly with subprocess.Popen. For some reason, this -# works. - -import code -code.interact(banner='') diff --git a/python/runner/main.py b/python/runner/main.py deleted file mode 100755 index 4e1af53..0000000 --- a/python/runner/main.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/python3 - -from fcntl import fcntl, F_GETFL, F_SETFL -import io -import multiprocessing -import multiprocessing.managers -import os -import subprocess -import sys -import threading -import time -import uuid - -interpreters = {} -module_access_lock = threading.Lock() - -# 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. -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. - import traceback - exc = traceback.format_exc() - finally: - out = sys.stdout.getvalue() - err = sys.stderr.getvalue() - sys.stdin.close() - sys.stdout.close() - sys.stderr.close() - conn.send((result, out, err, exc)) - -class Python(object): - # Call run_exec in a separate process for each input and return a list of - # results from those calls. If a call times out, 'timed out' is returned in - # place of exception traceback. - def run(self, code, inputs, timeout): - # 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)) - - # Wait for results. - results = [] - start = time.monotonic() - for p, conn in futures: - now = time.monotonic() - real_timeout = max(0, timeout - (now - start)) - if conn.poll(real_timeout): - results.append(conn.recv()) - else: - results.append('timed out') - p.terminate() - return results - - # Start and return a new Python interpreter process. - def create(self): - directory = os.path.dirname(os.path.realpath(__file__)) - script = os.path.join(directory, 'interpreter.py') - p = subprocess.Popen([script], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - # Set the non-blocking flag for stdout. - flags = fcntl(p.stdout, F_GETFL) - fcntl(p.stdout, F_SETFL, flags | os.O_NONBLOCK) - - interpreter_id = uuid.uuid4().hex - with module_access_lock: - interpreters[interpreter_id] = p - return interpreter_id - - # Read any available bytes from the interpreter's stdout. - def pull(self, interpreter_id): - with module_access_lock: - interpreter = interpreters[interpreter_id] - stdout = interpreter.stdout.read() - if stdout: - stdout = stdout.decode('utf-8') - return stdout - - # Push a string to the interpreter's stdin. - def push(self, interpreter_id, stdin): - with module_access_lock: - interpreter = interpreters[interpreter_id] - interpreter.stdin.write(stdin.encode('utf-8')) - interpreter.stdin.flush() - - # Kill an interpreter process. - def destroy(self, interpreter_id): - with module_access_lock: - interpreter = interpreters[interpreter_id] - del interpreters[interpreter_id] - interpreter.kill() - -class PythonManager(multiprocessing.managers.BaseManager): - pass - -PythonManager.register('Python', callable=Python) - -if __name__ == '__main__': - print('Python engine started.') - m = PythonManager(address=('localhost', 3031), authkey=b'c0d3q3y-python') - m.get_server().serve_forever() -- cgit v1.2.1