summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xpython/runner/interpreter.py (renamed from python/interpreter.py)0
-rw-r--r--python/runner/sandbox.c47
-rw-r--r--python/runner/terminator.c32
-rw-r--r--readme.md11
-rw-r--r--server/python_session.py28
5 files changed, 110 insertions, 8 deletions
diff --git a/python/interpreter.py b/python/runner/interpreter.py
index dae4d59..dae4d59 100755
--- a/python/interpreter.py
+++ b/python/runner/interpreter.py
diff --git a/python/runner/sandbox.c b/python/runner/sandbox.c
new file mode 100644
index 0000000..12e2720
--- /dev/null
+++ b/python/runner/sandbox.c
@@ -0,0 +1,47 @@
+#include <fcntl.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <sys/prctl.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+int main(int argc, char* argv[])
+{
+ if (argc < 3) {
+ fprintf(stderr, "usage: %s USERNAME FILE\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 = setuid(pw->pw_uid)) != 0)
+ fprintf(stderr, "setuid returned %d\n", ret);
+ if ((ret = setgid(pw->pw_gid)) != 0)
+ fprintf(stderr, "setgid returned %d\n", ret);
+
+ // limit CPU time to 1 second
+ struct rlimit const cpu_limit = { .rlim_cur = 1, .rlim_max = 1 };
+ if ((ret = setrlimit(RLIMIT_CPU, &cpu_limit)) != 0)
+ fprintf(stderr, "setrlimit(CPU) returned %d\n", ret);
+
+ // don't allow writing files of any size
+ struct rlimit const fsize_limit = { .rlim_cur = 0, .rlim_max = 0 };
+ if ((ret = setrlimit(RLIMIT_FSIZE, &fsize_limit)) != 0)
+ fprintf(stderr, "setrlimit(FSIZE) returned %d\n", ret);
+
+ // there will be no fork
+ struct rlimit const nproc_limit = { .rlim_cur = 0, .rlim_max = 0 };
+ if ((ret = setrlimit(RLIMIT_NPROC, &nproc_limit)) != 0)
+ fprintf(stderr, "setrlimit(NPROC) returned %d\n", ret);
+
+ char* const args[] = { argv[2], (char*)0 };
+ return execvp(argv[2], args);
+}
diff --git a/python/runner/terminator.c b/python/runner/terminator.c
new file mode 100644
index 0000000..a994bde
--- /dev/null
+++ b/python/runner/terminator.c
@@ -0,0 +1,32 @@
+#include <pwd.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.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 = setuid(pw->pw_uid)) != 0)
+ fprintf(stderr, "setuid returned %d\n", ret);
+ if ((ret = setgid(pw->pw_gid)) != 0)
+ fprintf(stderr, "setgid returned %d\n", ret);
+
+ pid_t pid = atol(argv[2]);
+ int signum = atoi(argv[3]);
+ kill(pid, signum);
+ return 0;
+}
diff --git a/readme.md b/readme.md
index a15b4f0..789fc95 100644
--- a/readme.md
+++ b/readme.md
@@ -49,6 +49,17 @@ nodejs
Run "npm install" inside the codeq-server/web directory to install all
dependencies (they will be installed inside the web directory)
+sandbox
+-------
+
+Go to directory codeq-server/python/runner and run the following commands to
+build the sandbox and set appropriate permissions:
+
+ make sandbox
+ mate terminator
+ sudo setcap cap_setuid,cap_setgid+ep sandbox
+ sudo setcap cap_setuid,cap_setgid+ep terminator
+
Settings
========
diff --git a/server/python_session.py b/server/python_session.py
index 24d33b0..f4c482c 100644
--- a/server/python_session.py
+++ b/server/python_session.py
@@ -116,15 +116,27 @@ class PythonSession(server.LanguageSession):
self._sent_hints.extend(hints)
def _interpreter(control, callback):
- directory = os.path.dirname(os.path.realpath(__file__))
- # TODO drop privileges using a wrapper
- script = os.path.join(directory, '..', 'python', 'interpreter.py')
+ 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)
proc = None
while True:
# Ensure the interpreter process is running.
if proc is None:
- proc = subprocess.Popen([script],
+ proc = subprocess.Popen(args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
@@ -143,7 +155,7 @@ def _interpreter(control, callback):
proc.stdin.write(data.encode('utf-8'))
proc.stdin.flush()
elif cmd == 'stop':
- proc.send_signal(signal.SIGINT)
+ kill(proc, signal.SIGINT)
elif cmd == 'done':
break
except:
@@ -155,7 +167,7 @@ def _interpreter(control, callback):
data = proc.stdout.read()
if data:
if len(data) > 20000:
- proc.kill()
+ kill(proc, signal.SIGKILL)
proc = None
callback('Child killed for talking too much.\n')
else:
@@ -169,7 +181,7 @@ def _interpreter(control, callback):
elif retcode == -31: # killed by seccomp
callback('Child killed due to sandbox misbehavior.\n')
else:
- callback('Child exited with status "{}".\n'.format(retcode))
+ callback('Child exited with status {}.\n'.format(retcode))
proc = None
# TODO we should select() on control and proc.stdout instead of polling
@@ -177,7 +189,7 @@ def _interpreter(control, callback):
# We are done, kill the child.
if proc is not None:
- proc.kill()
+ 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