From 2657852ee3e97ee2dc233c932bd7c88bceec94b1 Mon Sep 17 00:00:00 2001
From: Scragly <29337040+scragly@users.noreply.github.com>
Date: Sun, 20 Jan 2019 20:42:34 +1000
Subject: Remove RMQ, Add API POST request method.
---
tests/test_snekbox.py | 20 ++++++++------------
1 file changed, 8 insertions(+), 12 deletions(-)
(limited to 'tests')
diff --git a/tests/test_snekbox.py b/tests/test_snekbox.py
index e2505d6..cc79a2a 100644
--- a/tests/test_snekbox.py
+++ b/tests/test_snekbox.py
@@ -1,12 +1,6 @@
import unittest
-import pytest
-import os
-import json
from snekbox import Snekbox
-from rmq import Rmq
-
-r = Rmq()
snek = Snekbox()
@@ -24,12 +18,14 @@ class SnekTests(unittest.TestCase):
# self.assertEquals(result.strip(), 'timed out or memory limit exceeded')
def test_timeout(self):
- code = ('x = "*"\n'
- 'while True:\n'
- ' try:\n'
- ' x = x * 99\n'
- ' except:\n'
- ' continue\n')
+ code = (
+ 'x = "*"\n'
+ 'while True:\n'
+ ' try:\n'
+ ' x = x * 99\n'
+ ' except:\n'
+ ' continue\n'
+ )
result = snek.python3(code)
self.assertEquals(result.strip(), 'timed out or memory limit exceeded')
--
cgit v1.2.3
From 0e09d10281798dd365364a12af4487fc150844c1 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Mon, 25 Mar 2019 12:36:33 -0700
Subject: Restructure project layout
* Move all code into a "snekbox" package
* Use logging code as __init__.py
* Rename Snekbox class to NsJail
* Create "site" sub-package
* Move templates into this sub-package
* Move Flask code into a new snekapp module
---
.flake8 | 2 +-
Pipfile | 2 +-
logs.py | 10 ---
snekbox.py | 133 -------------------------------------
snekbox/__init__.py | 10 +++
snekbox/nsjail.py | 95 ++++++++++++++++++++++++++
snekbox/site/snekapp.py | 38 +++++++++++
snekbox/site/templates/index.html | 14 ++++
snekbox/site/templates/result.html | 9 +++
templates/index.html | 14 ----
templates/result.html | 9 ---
tests/test_snekbox.py | 16 ++---
12 files changed, 176 insertions(+), 176 deletions(-)
delete mode 100644 logs.py
delete mode 100644 snekbox.py
create mode 100644 snekbox/__init__.py
create mode 100644 snekbox/nsjail.py
create mode 100644 snekbox/site/snekapp.py
create mode 100644 snekbox/site/templates/index.html
create mode 100644 snekbox/site/templates/result.html
delete mode 100644 templates/index.html
delete mode 100644 templates/result.html
(limited to 'tests')
diff --git a/.flake8 b/.flake8
index cc5f423..c897cb6 100644
--- a/.flake8
+++ b/.flake8
@@ -1,6 +1,6 @@
[flake8]
max-line-length=100
-application_import_names=snekbox,config,logs
+application_import_names=snekbox
ignore=
P102,B311,W503,E226,S311,
# Missing Docstrings
diff --git a/Pipfile b/Pipfile
index 221263d..3f67b54 100644
--- a/Pipfile
+++ b/Pipfile
@@ -29,7 +29,7 @@ lint = "flake8"
precommit = "pre-commit install"
test = "pytest tests --cov . --cov-report term-missing -v"
report = "pytest tests --cov . --cov-report=html"
-snekbox = "gunicorn -w 2 -b 0.0.0.0:8060 snekbox:app"
+snekbox = "gunicorn -w 2 -b 0.0.0.0:8060 snekbox.site.snekapp:app"
buildbox = "docker build -t pythondiscord/snekbox:latest -f docker/Dockerfile ."
pushbox = "docker push pythondiscord/snekbox:latest"
buildboxbase = "docker build -t pythondiscord/snekbox-base:latest -f docker/base.Dockerfile ."
diff --git a/logs.py b/logs.py
deleted file mode 100644
index fc6070e..0000000
--- a/logs.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import logging
-import sys
-
-logformat = logging.Formatter(fmt='[%(asctime)s] [%(process)s] [%(levelname)s] %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S %z')
-log = logging.getLogger(__name__)
-log.setLevel(logging.DEBUG)
-console = logging.StreamHandler(sys.stdout)
-console.setFormatter(logformat)
-log.addHandler(console)
diff --git a/snekbox.py b/snekbox.py
deleted file mode 100644
index 65fc4b3..0000000
--- a/snekbox.py
+++ /dev/null
@@ -1,133 +0,0 @@
-import os
-import subprocess
-import sys
-
-from flask import Flask, jsonify, render_template, request
-
-
-class Snekbox:
- """Core snekbox functionality, providing safe execution of Python code."""
-
- def __init__(self,
- nsjail_binary='nsjail',
- python_binary=os.path.dirname(sys.executable) + os.sep + 'python3.6'):
- self.nsjail_binary = nsjail_binary
- self.python_binary = python_binary
- self._nsjail_workaround()
-
- env = {
- 'PATH': (
- '/snekbox/.venv/bin:/usr/local/bin:/usr/local/'
- 'sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
- ),
- 'LANG': 'en_US.UTF-8',
- 'PYTHON_VERSION': '3.6.5',
- 'PYTHON_PIP_VERSION': '10.0.1',
- 'PYTHONDONTWRITEBYTECODE': '1',
- }
-
- def _nsjail_workaround(self):
- dirs = ['/sys/fs/cgroup/pids/NSJAIL', '/sys/fs/cgroup/memory/NSJAIL']
- for d in dirs:
- if not os.path.exists(d):
- os.makedirs(d)
-
- def python3(self, cmd):
- """
- Execute Python 3 code in a isolated environment.
-
- The value of ``cmd`` is passed using '-c' to a Python
- interpreter that is started in a ``nsjail``, isolating it
- from the rest of the system.
-
- Returns the output of executing the command (stdout) if
- successful, or a error message if the execution failed.
- """
-
- args = [self.nsjail_binary, '-Mo',
- '--rlimit_as', '700',
- '--chroot', '/',
- '-E', 'LANG=en_US.UTF-8',
- '-R/usr', '-R/lib', '-R/lib64',
- '--user', 'nobody',
- '--group', 'nogroup',
- '--time_limit', '2',
- '--disable_proc',
- '--iface_no_lo',
- '--cgroup_pids_max=1',
- '--cgroup_mem_max=52428800',
- '--quiet', '--',
- self.python_binary, '-ISq', '-c', cmd]
- try:
- proc = subprocess.Popen(args,
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- env=self.env,
- universal_newlines=True)
- except ValueError:
- return 'ValueError: embedded null byte'
-
- stdout, stderr = proc.communicate()
- if proc.returncode == 0:
- output = stdout
-
- elif proc.returncode == 1:
- try:
- filtered = []
- for line in stderr.split('\n'):
- if not line.startswith('['):
- filtered.append(line)
- output = '\n'.join(filtered)
- except IndexError:
- output = ''
-
- elif proc.returncode == 109:
- return 'timed out or memory limit exceeded'
-
- elif proc.returncode == 255:
- return 'permission denied (root required)'
-
- elif proc.returncode:
- return f'unknown error, code: {proc.returncode}'
-
- else:
- return 'unknown error, no error code'
-
- return output
-
-
-snekbox = Snekbox()
-
-# Load app
-app = Flask(__name__)
-app.use_reloader = False
-
-# Logging
-log = app.logger
-
-
-@app.route('/')
-def index():
- """Return a page with a form for inputting code to be executed."""
-
- return render_template('index.html')
-
-
-@app.route('/result', methods=["POST", "GET"])
-def result():
- """Execute code and return a page displaying the results."""
-
- if request.method == "POST":
- code = request.form["Code"]
- output = snekbox.python3(code)
- return render_template('result.html', code=code, result=output)
-
-
-@app.route('/input', methods=["POST"])
-def code_input():
- """Execute code and return the results."""
-
- body = request.get_json()
- output = snekbox.python3(body["code"])
- return jsonify(input=body["code"], output=output)
diff --git a/snekbox/__init__.py b/snekbox/__init__.py
new file mode 100644
index 0000000..fc6070e
--- /dev/null
+++ b/snekbox/__init__.py
@@ -0,0 +1,10 @@
+import logging
+import sys
+
+logformat = logging.Formatter(fmt='[%(asctime)s] [%(process)s] [%(levelname)s] %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S %z')
+log = logging.getLogger(__name__)
+log.setLevel(logging.DEBUG)
+console = logging.StreamHandler(sys.stdout)
+console.setFormatter(logformat)
+log.addHandler(console)
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
new file mode 100644
index 0000000..458a94e
--- /dev/null
+++ b/snekbox/nsjail.py
@@ -0,0 +1,95 @@
+import os
+import subprocess
+import sys
+
+
+class NsJail:
+ """Core Snekbox functionality, providing safe execution of Python code."""
+
+ def __init__(self,
+ nsjail_binary='nsjail',
+ python_binary=os.path.dirname(sys.executable) + os.sep + 'python3.6'):
+ self.nsjail_binary = nsjail_binary
+ self.python_binary = python_binary
+ self._nsjail_workaround()
+
+ env = {
+ 'PATH': (
+ '/snekbox/.venv/bin:/usr/local/bin:/usr/local/'
+ 'sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+ ),
+ 'LANG': 'en_US.UTF-8',
+ 'PYTHON_VERSION': '3.6.5',
+ 'PYTHON_PIP_VERSION': '10.0.1',
+ 'PYTHONDONTWRITEBYTECODE': '1',
+ }
+
+ def _nsjail_workaround(self):
+ dirs = ['/sys/fs/cgroup/pids/NSJAIL', '/sys/fs/cgroup/memory/NSJAIL']
+ for d in dirs:
+ if not os.path.exists(d):
+ os.makedirs(d)
+
+ def python3(self, cmd):
+ """
+ Execute Python 3 code in a isolated environment.
+
+ The value of ``cmd`` is passed using '-c' to a Python
+ interpreter that is started in a ``nsjail``, isolating it
+ from the rest of the system.
+
+ Returns the output of executing the command (stdout) if
+ successful, or a error message if the execution failed.
+ """
+
+ args = [self.nsjail_binary, '-Mo',
+ '--rlimit_as', '700',
+ '--chroot', '/',
+ '-E', 'LANG=en_US.UTF-8',
+ '-R/usr', '-R/lib', '-R/lib64',
+ '--user', 'nobody',
+ '--group', 'nogroup',
+ '--time_limit', '2',
+ '--disable_proc',
+ '--iface_no_lo',
+ '--cgroup_pids_max=1',
+ '--cgroup_mem_max=52428800',
+ '--quiet', '--',
+ self.python_binary, '-ISq', '-c', cmd]
+ try:
+ proc = subprocess.Popen(args,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=self.env,
+ universal_newlines=True)
+ except ValueError:
+ return 'ValueError: embedded null byte'
+
+ stdout, stderr = proc.communicate()
+ if proc.returncode == 0:
+ output = stdout
+
+ elif proc.returncode == 1:
+ try:
+ filtered = []
+ for line in stderr.split('\n'):
+ if not line.startswith('['):
+ filtered.append(line)
+ output = '\n'.join(filtered)
+ except IndexError:
+ output = ''
+
+ elif proc.returncode == 109:
+ return 'timed out or memory limit exceeded'
+
+ elif proc.returncode == 255:
+ return 'permission denied (root required)'
+
+ elif proc.returncode:
+ return f'unknown error, code: {proc.returncode}'
+
+ else:
+ return 'unknown error, no error code'
+
+ return output
diff --git a/snekbox/site/snekapp.py b/snekbox/site/snekapp.py
new file mode 100644
index 0000000..492d703
--- /dev/null
+++ b/snekbox/site/snekapp.py
@@ -0,0 +1,38 @@
+from flask import Flask, jsonify, render_template, request
+
+from snekbox.nsjail import NsJail
+
+nsjail = NsJail()
+
+# Load app
+app = Flask(__name__)
+app.use_reloader = False
+
+# Logging
+log = app.logger
+
+
+@app.route('/')
+def index():
+ """Return a page with a form for inputting code to be executed."""
+
+ return render_template('index.html')
+
+
+@app.route('/result', methods=["POST", "GET"])
+def result():
+ """Execute code and return a page displaying the results."""
+
+ if request.method == "POST":
+ code = request.form["Code"]
+ output = nsjail.python3(code)
+ return render_template('result.html', code=code, result=output)
+
+
+@app.route('/input', methods=["POST"])
+def code_input():
+ """Execute code and return the results."""
+
+ body = request.get_json()
+ output = nsjail.python3(body["code"])
+ return jsonify(input=body["code"], output=output)
diff --git a/snekbox/site/templates/index.html b/snekbox/site/templates/index.html
new file mode 100644
index 0000000..41980d1
--- /dev/null
+++ b/snekbox/site/templates/index.html
@@ -0,0 +1,14 @@
+
+
+
+ snekboxweb
+
+
+
+
diff --git a/snekbox/site/templates/result.html b/snekbox/site/templates/result.html
new file mode 100644
index 0000000..e339605
--- /dev/null
+++ b/snekbox/site/templates/result.html
@@ -0,0 +1,9 @@
+
+
+
+ snekboxweb
+
+ Code Evaluated:
{{ code }}
+ Results:
{{ result }}
+
+
diff --git a/templates/index.html b/templates/index.html
deleted file mode 100644
index 41980d1..0000000
--- a/templates/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- snekboxweb
-
-
-
-
diff --git a/templates/result.html b/templates/result.html
deleted file mode 100644
index e339605..0000000
--- a/templates/result.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
- snekboxweb
-
- Code Evaluated:
{{ code }}
- Results:
{{ result }}
-
-
diff --git a/tests/test_snekbox.py b/tests/test_snekbox.py
index cc79a2a..c08178f 100644
--- a/tests/test_snekbox.py
+++ b/tests/test_snekbox.py
@@ -1,20 +1,20 @@
import unittest
-from snekbox import Snekbox
+from snekbox.nsjail import NsJail
-snek = Snekbox()
+nsjail = NsJail()
class SnekTests(unittest.TestCase):
def test_nsjail(self):
- result = snek.python3('print("test")')
+ result = nsjail.python3('print("test")')
self.assertEquals(result.strip(), 'test')
# def test_memory_error(self):
# code = ('x = "*"\n'
# 'while True:\n'
# ' x = x * 99\n')
- # result = snek.python3(code)
+ # result = nsjail.python3(code)
# self.assertEquals(result.strip(), 'timed out or memory limit exceeded')
def test_timeout(self):
@@ -27,13 +27,13 @@ class SnekTests(unittest.TestCase):
' continue\n'
)
- result = snek.python3(code)
+ result = nsjail.python3(code)
self.assertEquals(result.strip(), 'timed out or memory limit exceeded')
def test_kill(self):
code = ('import subprocess\n'
'print(subprocess.check_output("kill -9 6", shell=True).decode())')
- result = snek.python3(code)
+ result = nsjail.python3(code)
if 'ModuleNotFoundError' in result.strip():
self.assertIn('ModuleNotFoundError', result.strip())
else:
@@ -43,7 +43,7 @@ class SnekTests(unittest.TestCase):
code = ('import os\n'
'while 1:\n'
' os.fork()')
- result = snek.python3(code)
+ result = nsjail.python3(code)
self.assertIn('Resource temporarily unavailable', result.strip())
def test_juan_golf(self): # in honour of Juan
@@ -52,5 +52,5 @@ class SnekTests(unittest.TestCase):
"bytecode = CodeType(0,1,0,0,0,b'',(),(),(),'','',1,b'')\n"
"exec(bytecode)")
- result = snek.python3(code)
+ result = nsjail.python3(code)
self.assertEquals('unknown error, code: 111', result.strip())
--
cgit v1.2.3
From 8d9a0029bd3c8a8629fbf8db3903b412bef538e1 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Wed, 29 May 2019 03:12:47 -0700
Subject: Add API tests for eval resource
---
tests/__init__.py | 1 -
tests/api/__init__.py | 16 ++++++++++++++++
tests/api/test_eval.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 65 insertions(+), 1 deletion(-)
create mode 100644 tests/api/__init__.py
create mode 100644 tests/api/test_eval.py
(limited to 'tests')
diff --git a/tests/__init__.py b/tests/__init__.py
index 792d600..e69de29 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/tests/api/__init__.py b/tests/api/__init__.py
new file mode 100644
index 0000000..745519d
--- /dev/null
+++ b/tests/api/__init__.py
@@ -0,0 +1,16 @@
+from unittest import mock
+
+from falcon import testing
+
+
+class SnekAPITestCase(testing.TestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.patcher = mock.patch("snekbox.api.resources.eval.NsJail", autospec=True)
+ self.mock_nsjail = self.patcher.start()
+ self.mock_nsjail.return_value.python3.return_value = "test output"
+ self.addCleanup(self.patcher.stop)
+
+ from snekbox.api import SnekAPI
+ self.app = SnekAPI()
diff --git a/tests/api/test_eval.py b/tests/api/test_eval.py
new file mode 100644
index 0000000..a5b83fd
--- /dev/null
+++ b/tests/api/test_eval.py
@@ -0,0 +1,49 @@
+from tests.api import SnekAPITestCase
+
+
+class TestEvalResource(SnekAPITestCase):
+ PATH = "/eval"
+
+ def test_post_valid_200(self):
+ body = {"input": "foo"}
+ result = self.simulate_post(self.PATH, json=body)
+
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(body["input"], result.json["input"])
+ self.assertEqual("test output", result.json["output"])
+
+ def test_post_invalid_schema_400(self):
+ body = {"stuff": "foo"}
+ result = self.simulate_post(self.PATH, json=body)
+
+ self.assertEqual(result.status_code, 400)
+
+ expected = {
+ "title": "Request data failed validation",
+ "description": "'input' is a required property"
+ }
+
+ self.assertEqual(expected, result.json)
+
+ def test_post_invalid_content_type_415(self):
+ body = "{\"input\": \"foo\"}"
+ headers = {"Content-Type": "application/xml"}
+ result = self.simulate_post(self.PATH, body=body, headers=headers)
+
+ self.assertEqual(result.status_code, 415)
+
+ expected = {
+ "title": "Unsupported media type",
+ "description": "application/xml is an unsupported media type."
+ }
+
+ self.assertEqual(expected, result.json)
+
+ def test_disallowed_method_405(self):
+ result = self.simulate_get(self.PATH)
+ self.assertEqual(result.status_code, 405)
+
+ def test_options_allow_post_only(self):
+ result = self.simulate_options(self.PATH)
+ self.assertEqual(result.status_code, 200)
+ self.assertEqual(result.headers.get("Allow"), "POST")
--
cgit v1.2.3
From feaae8cab07b3db7d0baf3de7b7bb5b515e9acf8 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Wed, 29 May 2019 16:22:30 -0700
Subject: Move SnekAPI import back to top of module
---
tests/api/__init__.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
(limited to 'tests')
diff --git a/tests/api/__init__.py b/tests/api/__init__.py
index 745519d..fd4679a 100644
--- a/tests/api/__init__.py
+++ b/tests/api/__init__.py
@@ -2,6 +2,8 @@ from unittest import mock
from falcon import testing
+from snekbox.api import SnekAPI
+
class SnekAPITestCase(testing.TestCase):
def setUp(self):
@@ -12,5 +14,4 @@ class SnekAPITestCase(testing.TestCase):
self.mock_nsjail.return_value.python3.return_value = "test output"
self.addCleanup(self.patcher.stop)
- from snekbox.api import SnekAPI
self.app = SnekAPI()
--
cgit v1.2.3
From 78757589a2cc6a76b83041dbb75b02896308da69 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Wed, 29 May 2019 22:47:31 -0700
Subject: Add flake8 plugin to only allow double quotes
---
.flake8 | 1 +
Pipfile | 1 +
Pipfile.lock | 94 ++++++++++++++++++++++++++++---------------------
snekbox/__init__.py | 4 +--
snekbox/nsjail.py | 66 +++++++++++++++++-----------------
snekbox/site/snekapp.py | 10 +++---
tests/test_snekbox.py | 46 ++++++++++++------------
7 files changed, 119 insertions(+), 103 deletions(-)
(limited to 'tests')
diff --git a/.flake8 b/.flake8
index f8eec98..347bdb0 100644
--- a/.flake8
+++ b/.flake8
@@ -16,3 +16,4 @@ exclude=
venv,.venv,
tests
import-order-style=pycharm
+inline-quotes = "
diff --git a/Pipfile b/Pipfile
index 788e900..69bb0df 100644
--- a/Pipfile
+++ b/Pipfile
@@ -20,6 +20,7 @@ flake8-tidy-imports = "*"
flake8-todo = "*"
flake8-string-format = "*"
flake8-formatter-junit-xml = "*"
+flake8-quotes = "*"
[requires]
python_version = "3.7"
diff --git a/Pipfile.lock b/Pipfile.lock
index 466a42b..b73b997 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "814185e2e1b964ab58af9a9df416ace7b5b416475d828ec9b31a9dfecb5693e1"
+ "sha256": "c1f4c3df791d8c4758f72cb8fb148a34d5c1ca02298b0d660844899f15f6ba85"
},
"pipfile-spec": 6,
"requires": {
@@ -25,11 +25,11 @@
},
"flask": {
"hashes": [
- "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
- "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
+ "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3",
+ "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"
],
"index": "pypi",
- "version": "==1.0.2"
+ "version": "==1.0.3"
},
"gunicorn": {
"hashes": [
@@ -48,10 +48,10 @@
},
"jinja2": {
"hashes": [
- "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
- "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
+ "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
+ "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
],
- "version": "==2.10"
+ "version": "==2.10.1"
},
"markupsafe": {
"hashes": [
@@ -88,19 +88,19 @@
},
"werkzeug": {
"hashes": [
- "sha256:96da23fa8ccecbc3ae832a83df5c722c11547d021637faacb0bec4dd2f4666c8",
- "sha256:ca5c2dcd367d6c0df87185b9082929d255358f5391923269335782b213d52655"
+ "sha256:865856ebb55c4dcd0630cdd8f3331a1847a819dda7e8c750d3db6f2aa6c0209c",
+ "sha256:a0b915f0815982fb2a09161cb8f31708052d0951c3ba433ccc5e1aa276507ca6"
],
- "version": "==0.15.1"
+ "version": "==0.15.4"
}
},
"develop": {
"aspy.yaml": {
"hashes": [
- "sha256:ae249074803e8b957c83fdd82a99160d0d6d26dff9ba81ba608b42eebd7d8cd3",
- "sha256:c7390d79f58eb9157406966201abf26da0d56c07e0ff0deadc39c8f4dbc13482"
+ "sha256:463372c043f70160a9ec950c3f1e4c3a82db5fca01d334b6bc89c7164d744bdc",
+ "sha256:e7c742382eff2caed61f87a39d13f99109088e5e93f04d76eb8d4b28aa143f45"
],
- "version": "==1.2.0"
+ "version": "==1.3.0"
},
"atomicwrites": {
"hashes": [
@@ -118,10 +118,10 @@
},
"cfgv": {
"hashes": [
- "sha256:39f8475d8eca48639f900daffa3f8bd2f60a31d989df41a9f81c5ad1779a66eb",
- "sha256:a6a4366d32799a6bfb6f577ebe113b27ba8d1bae43cb57133b1472c1c3dae227"
+ "sha256:32edbe09de6f4521224b87822103a8c16a614d31a894735f7a5b3bcf0eb3c37e",
+ "sha256:3bd31385cd2bebddbba8012200aaf15aa208539f1b33973759b4d02fc2148da5"
],
- "version": "==1.5.0"
+ "version": "==2.0.0"
},
"coverage": {
"hashes": [
@@ -213,6 +213,13 @@
],
"version": "==1.0.2"
},
+ "flake8-quotes": {
+ "hashes": [
+ "sha256:10c9af6b472d4302a8e721c5260856c3f985c5c082b04841aefd2f808ac02038"
+ ],
+ "index": "pypi",
+ "version": "==2.0.1"
+ },
"flake8-string-format": {
"hashes": [
"sha256:68ea72a1a5b75e7018cae44d14f32473c798cf73d75cbaed86c6a9a907b770b2",
@@ -238,17 +245,17 @@
},
"identify": {
"hashes": [
- "sha256:244e7864ef59f0c7c50c6db73f58564151d91345cd9b76ed793458953578cadd",
- "sha256:8ff062f90ad4b09cfe79b5dfb7a12e40f19d2e68a5c9598a49be45f16aba7171"
+ "sha256:432c548d6138cb57a3d8f62f079a025a29b8ae34a50dd3b496bbf661818f2bc0",
+ "sha256:d4401d60bf1938aa3074a352a5cc9044107edf11a6fedd3a1db172c141619b81"
],
- "version": "==1.4.1"
+ "version": "==1.4.3"
},
"importlib-metadata": {
"hashes": [
- "sha256:a17ce1a8c7bff1e8674cb12c992375d8d0800c9190177ecf0ad93e0097224095",
- "sha256:b50191ead8c70adfa12495fba19ce6d75f2e0275c14c5a7beb653d6799b512bd"
+ "sha256:a9f185022cfa69e9ca5f7eabfd5a58b689894cb78a11e3c8c89398a8ccbb8e7f",
+ "sha256:df1403cd3aebeb2b1dcd3515ca062eecb5bd3ea7611f18cba81130c68707e879"
],
- "version": "==0.8"
+ "version": "==0.17"
},
"junit-xml": {
"hashes": [
@@ -279,18 +286,18 @@
},
"pluggy": {
"hashes": [
- "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f",
- "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746"
+ "sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc",
+ "sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"
],
- "version": "==0.9.0"
+ "version": "==0.12.0"
},
"pre-commit": {
"hashes": [
- "sha256:d3d69c63ae7b7584c4b51446b0b583d454548f9df92575b2fe93a68ec800c4d3",
- "sha256:fc512f129b9526e35e80d656a16a31c198f584c4fce3a5c739045b5140584917"
+ "sha256:6ca409d1f22d444af427fb023a33ca8b69625d508a50e1b7eaabd59247c93043",
+ "sha256:94dd519597f5bff06a4b0df194a79c524b78f4b1534c1ce63241a9d4fb23b926"
],
"index": "pypi",
- "version": "==1.14.4"
+ "version": "==1.16.1"
},
"py": {
"hashes": [
@@ -323,19 +330,19 @@
},
"pytest": {
"hashes": [
- "sha256:592eaa2c33fae68c7d75aacf042efc9f77b27c08a6224a4f59beab8d9a420523",
- "sha256:ad3ad5c450284819ecde191a654c09b0ec72257a2c711b9633d677c71c9850c4"
+ "sha256:1a8aa4fa958f8f451ac5441f3ac130d9fc86ea38780dd2715e6d5c5882700b24",
+ "sha256:b8bf138592384bd4e87338cb0f256bf5f615398a649d4bd83915f0e4047a5ca6"
],
"index": "pypi",
- "version": "==4.3.1"
+ "version": "==4.5.0"
},
"pytest-cov": {
"hashes": [
- "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33",
- "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f"
+ "sha256:2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6",
+ "sha256:e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"
],
"index": "pypi",
- "version": "==2.6.1"
+ "version": "==2.7.1"
},
"pytest-dependency": {
"hashes": [
@@ -383,17 +390,24 @@
},
"virtualenv": {
"hashes": [
- "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417",
- "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39"
+ "sha256:99acaf1e35c7ccf9763db9ba2accbca2f4254d61d1912c5ee364f9cc4a8942a0",
+ "sha256:fe51cdbf04e5d8152af06c075404745a7419de27495a83f0d72518ad50be3ce8"
+ ],
+ "version": "==16.6.0"
+ },
+ "wcwidth": {
+ "hashes": [
+ "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
+ "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
],
- "version": "==16.4.3"
+ "version": "==0.1.7"
},
"zipp": {
"hashes": [
- "sha256:55ca87266c38af6658b84db8cfb7343cdb0bf275f93c7afaea0d8e7a209c7478",
- "sha256:682b3e1c62b7026afe24eadf6be579fb45fec54c07ea218bded8092af07a68c4"
+ "sha256:8c1019c6aad13642199fbe458275ad6a84907634cc9f0989877ccc4a2840139d",
+ "sha256:ca943a7e809cc12257001ccfb99e3563da9af99d52f261725e96dfe0f9275bc3"
],
- "version": "==0.3.3"
+ "version": "==0.5.1"
}
}
}
diff --git a/snekbox/__init__.py b/snekbox/__init__.py
index fc6070e..f14fc89 100644
--- a/snekbox/__init__.py
+++ b/snekbox/__init__.py
@@ -1,8 +1,8 @@
import logging
import sys
-logformat = logging.Formatter(fmt='[%(asctime)s] [%(process)s] [%(levelname)s] %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S %z')
+logformat = logging.Formatter(fmt="[%(asctime)s] [%(process)s] [%(levelname)s] %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S %z")
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
console = logging.StreamHandler(sys.stdout)
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
index 5c7d0f0..c807b9e 100644
--- a/snekbox/nsjail.py
+++ b/snekbox/nsjail.py
@@ -7,25 +7,25 @@ class NsJail:
"""Core Snekbox functionality, providing safe execution of Python code."""
def __init__(self,
- nsjail_binary='nsjail',
- python_binary=os.path.dirname(sys.executable) + os.sep + 'python3.7'):
+ nsjail_binary="nsjail",
+ python_binary=os.path.dirname(sys.executable) + os.sep + "python3.7"):
self.nsjail_binary = nsjail_binary
self.python_binary = python_binary
self._nsjail_workaround()
env = {
- 'PATH': (
- '/snekbox/.venv/bin:/usr/local/bin:/usr/local/'
- 'sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+ "PATH": (
+ "/snekbox/.venv/bin:/usr/local/bin:/usr/local/"
+ "sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
),
- 'LANG': 'en_US.UTF-8',
- 'PYTHON_VERSION': '3.7.3',
- 'PYTHON_PIP_VERSION': '19.0.3',
- 'PYTHONDONTWRITEBYTECODE': '1',
+ "LANG": "en_US.UTF-8",
+ "PYTHON_VERSION": "3.7.3",
+ "PYTHON_PIP_VERSION": "19.0.3",
+ "PYTHONDONTWRITEBYTECODE": "1",
}
def _nsjail_workaround(self):
- dirs = ['/sys/fs/cgroup/pids/NSJAIL', '/sys/fs/cgroup/memory/NSJAIL']
+ dirs = ["/sys/fs/cgroup/pids/NSJAIL", "/sys/fs/cgroup/memory/NSJAIL"]
for d in dirs:
if not os.path.exists(d):
os.makedirs(d)
@@ -41,20 +41,20 @@ class NsJail:
Returns the output of executing the command (stdout) if
successful, or a error message if the execution failed.
"""
- args = [self.nsjail_binary, '-Mo',
- '--rlimit_as', '700',
- '--chroot', '/',
- '-E', 'LANG=en_US.UTF-8',
- '-R/usr', '-R/lib', '-R/lib64',
- '--user', 'nobody',
- '--group', 'nogroup',
- '--time_limit', '2',
- '--disable_proc',
- '--iface_no_lo',
- '--cgroup_pids_max=1',
- '--cgroup_mem_max=52428800',
- '--quiet', '--',
- self.python_binary, '-ISq', '-c', cmd]
+ args = [self.nsjail_binary, "-Mo",
+ "--rlimit_as", "700",
+ "--chroot", "/",
+ "-E", "LANG=en_US.UTF-8",
+ "-R/usr", "-R/lib", "-R/lib64",
+ "--user", "nobody",
+ "--group", "nogroup",
+ "--time_limit", "2",
+ "--disable_proc",
+ "--iface_no_lo",
+ "--cgroup_pids_max=1",
+ "--cgroup_mem_max=52428800",
+ "--quiet", "--",
+ self.python_binary, "-ISq", "-c", cmd]
try:
proc = subprocess.Popen(args,
stdin=subprocess.PIPE,
@@ -63,7 +63,7 @@ class NsJail:
env=self.env,
universal_newlines=True)
except ValueError:
- return 'ValueError: embedded null byte'
+ return "ValueError: embedded null byte"
stdout, stderr = proc.communicate()
if proc.returncode == 0:
@@ -72,23 +72,23 @@ class NsJail:
elif proc.returncode == 1:
try:
filtered = []
- for line in stderr.split('\n'):
- if not line.startswith('['):
+ for line in stderr.split("\n"):
+ if not line.startswith("["):
filtered.append(line)
- output = '\n'.join(filtered)
+ output = "\n".join(filtered)
except IndexError:
- output = ''
+ output = ""
elif proc.returncode == 109:
- return 'timed out or memory limit exceeded'
+ return "timed out or memory limit exceeded"
elif proc.returncode == 255:
- return 'permission denied (root required)'
+ return "permission denied (root required)"
elif proc.returncode:
- return f'unknown error, code: {proc.returncode}'
+ return f"unknown error, code: {proc.returncode}"
else:
- return 'unknown error, no error code'
+ return "unknown error, no error code"
return output
diff --git a/snekbox/site/snekapp.py b/snekbox/site/snekapp.py
index ef96148..3954238 100644
--- a/snekbox/site/snekapp.py
+++ b/snekbox/site/snekapp.py
@@ -12,22 +12,22 @@ app.use_reloader = False
log = app.logger
-@app.route('/')
+@app.route("/")
def index():
"""Return a page with a form for inputting code to be executed."""
- return render_template('index.html')
+ return render_template("index.html")
-@app.route('/result', methods=["POST", "GET"])
+@app.route("/result", methods=["POST", "GET"])
def result():
"""Execute code and return a page displaying the results."""
if request.method == "POST":
code = request.form["Code"]
output = nsjail.python3(code)
- return render_template('result.html', code=code, result=output)
+ return render_template("result.html", code=code, result=output)
-@app.route('/input', methods=["POST"])
+@app.route("/input", methods=["POST"])
def code_input():
"""Execute code and return the results."""
body = request.get_json()
diff --git a/tests/test_snekbox.py b/tests/test_snekbox.py
index c08178f..46319d6 100644
--- a/tests/test_snekbox.py
+++ b/tests/test_snekbox.py
@@ -7,44 +7,44 @@ nsjail = NsJail()
class SnekTests(unittest.TestCase):
def test_nsjail(self):
- result = nsjail.python3('print("test")')
- self.assertEquals(result.strip(), 'test')
+ result = nsjail.python3("print('test')")
+ self.assertEquals(result.strip(), "test")
# def test_memory_error(self):
- # code = ('x = "*"\n'
- # 'while True:\n'
- # ' x = x * 99\n')
+ # code = ("x = "*"\n"
+ # "while True:\n"
+ # " x = x * 99\n")
# result = nsjail.python3(code)
- # self.assertEquals(result.strip(), 'timed out or memory limit exceeded')
+ # self.assertEquals(result.strip(), "timed out or memory limit exceeded")
def test_timeout(self):
code = (
- 'x = "*"\n'
- 'while True:\n'
- ' try:\n'
- ' x = x * 99\n'
- ' except:\n'
- ' continue\n'
+ "x = '*'\n"
+ "while True:\n"
+ " try:\n"
+ " x = x * 99\n"
+ " except:\n"
+ " continue\n"
)
result = nsjail.python3(code)
- self.assertEquals(result.strip(), 'timed out or memory limit exceeded')
+ self.assertEquals(result.strip(), "timed out or memory limit exceeded")
def test_kill(self):
- code = ('import subprocess\n'
- 'print(subprocess.check_output("kill -9 6", shell=True).decode())')
+ code = ("import subprocess\n"
+ "print(subprocess.check_output('kill -9 6', shell=True).decode())")
result = nsjail.python3(code)
- if 'ModuleNotFoundError' in result.strip():
- self.assertIn('ModuleNotFoundError', result.strip())
+ if "ModuleNotFoundError" in result.strip():
+ self.assertIn("ModuleNotFoundError", result.strip())
else:
- self.assertIn('(PIDs left: 0)', result.strip())
+ self.assertIn("(PIDs left: 0)", result.strip())
def test_forkbomb(self):
- code = ('import os\n'
- 'while 1:\n'
- ' os.fork()')
+ code = ("import os\n"
+ "while 1:\n"
+ " os.fork()")
result = nsjail.python3(code)
- self.assertIn('Resource temporarily unavailable', result.strip())
+ self.assertIn("Resource temporarily unavailable", result.strip())
def test_juan_golf(self): # in honour of Juan
code = ("func = lambda: None\n"
@@ -53,4 +53,4 @@ class SnekTests(unittest.TestCase):
"exec(bytecode)")
result = nsjail.python3(code)
- self.assertEquals('unknown error, code: 111', result.strip())
+ self.assertEquals("unknown error, code: 111", result.strip())
--
cgit v1.2.3
From e75c764f693c3688a59af0d679e0d3e94f003503 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Thu, 30 May 2019 03:19:34 -0700
Subject: Lint tests
Tests ignore all D1xx warnings because tests shouldn't require
docstrings.
---
.pre-commit-config.yaml | 16 +++++++++++++++-
tests/.flake8 | 15 +++++++++++++++
2 files changed, 30 insertions(+), 1 deletion(-)
create mode 100644 tests/.flake8
(limited to 'tests')
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5d2d40a..4f97db9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,6 +3,7 @@ repos:
rev: v2.0.0
hooks:
- id: flake8
+ name: Flake8 (snekbox)
args: [--config=.flake8]
exclude: ^tests/
additional_dependencies: [
@@ -15,4 +16,17 @@ repos:
flake8-formatter-junit-xml,
flake8-quotes
]
-
+ - id: flake8
+ name: Flake8 (tests)
+ args: [--config=tests/.flake8]
+ exclude: ^(?!tests/)
+ additional_dependencies: [
+ flake8-docstrings,
+ flake8-bugbear,
+ flake8-import-order,
+ flake8-tidy-imports,
+ flake8-todo,
+ flake8-string-format,
+ flake8-formatter-junit-xml,
+ flake8-quotes
+ ]
diff --git a/tests/.flake8 b/tests/.flake8
new file mode 100644
index 0000000..c1c5031
--- /dev/null
+++ b/tests/.flake8
@@ -0,0 +1,15 @@
+[flake8]
+max-line-length=100
+application_import_names=snekbox,tests
+ignore=
+ P102,B311,W503,E226,S311,
+ # Missing Docstrings
+ D1,
+ # Docstring Whitespace
+ D203,D212,D214,D215,
+ # Docstring Quotes
+ D301,D302,
+ # Docstring Content
+ D400,D401,D402,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414
+import-order-style=pycharm
+inline-quotes = "
--
cgit v1.2.3
From 6eb7afc321d7e6d2aeebcb90e4bb18cd5e43a6e2 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Thu, 30 May 2019 18:26:38 -0700
Subject: Revert "Lint tests"
This reverts commit e75c764f693c3688a59af0d679e0d3e94f003503.
---
.pre-commit-config.yaml | 16 +---------------
tests/.flake8 | 15 ---------------
2 files changed, 1 insertion(+), 30 deletions(-)
delete mode 100644 tests/.flake8
(limited to 'tests')
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4f97db9..5d2d40a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,7 +3,6 @@ repos:
rev: v2.0.0
hooks:
- id: flake8
- name: Flake8 (snekbox)
args: [--config=.flake8]
exclude: ^tests/
additional_dependencies: [
@@ -16,17 +15,4 @@ repos:
flake8-formatter-junit-xml,
flake8-quotes
]
- - id: flake8
- name: Flake8 (tests)
- args: [--config=tests/.flake8]
- exclude: ^(?!tests/)
- additional_dependencies: [
- flake8-docstrings,
- flake8-bugbear,
- flake8-import-order,
- flake8-tidy-imports,
- flake8-todo,
- flake8-string-format,
- flake8-formatter-junit-xml,
- flake8-quotes
- ]
+
diff --git a/tests/.flake8 b/tests/.flake8
deleted file mode 100644
index c1c5031..0000000
--- a/tests/.flake8
+++ /dev/null
@@ -1,15 +0,0 @@
-[flake8]
-max-line-length=100
-application_import_names=snekbox,tests
-ignore=
- P102,B311,W503,E226,S311,
- # Missing Docstrings
- D1,
- # Docstring Whitespace
- D203,D212,D214,D215,
- # Docstring Quotes
- D301,D302,
- # Docstring Content
- D400,D401,D402,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414
-import-order-style=pycharm
-inline-quotes = "
--
cgit v1.2.3
From 7916804de8176fa34e4dfc56c0543157e72985f1 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Tue, 4 Jun 2019 22:29:08 -0700
Subject: Add logging for NsJail
NsJail's is configured to log to a temporary file rather than stderr.
The contents of the file are parsed using regex after the process exits.
When not debugging, some blacklisted messages and most info-level
messages are skipped.
* Add a snekbox logger
* Log the Python code being executed if debugging
* Use nested single quotes in a test to fix a linter error
---
snekbox/__init__.py | 10 +++++
snekbox/nsjail.py | 106 ++++++++++++++++++++++++++++++++++++-------------
tests/api/test_eval.py | 2 +-
3 files changed, 90 insertions(+), 28 deletions(-)
(limited to 'tests')
diff --git a/snekbox/__init__.py b/snekbox/__init__.py
index af8429b..a48abd5 100644
--- a/snekbox/__init__.py
+++ b/snekbox/__init__.py
@@ -1,5 +1,6 @@
import logging
import os
+import sys
from gunicorn import glogging
@@ -22,3 +23,12 @@ class GunicornLogger(glogging.Logger):
self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO)
self.error_log.setLevel(self.loglevel)
+
+
+log = logging.getLogger("snekbox")
+log.setLevel(logging.DEBUG if DEBUG else logging.INFO)
+log.propagate = True
+formatter = logging.Formatter(GunicornLogger.error_fmt, GunicornLogger.datefmt)
+handler = logging.StreamHandler(sys.stdout)
+handler.setFormatter(formatter)
+log.addHandler(handler)
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
index 2484ba2..0ebfe0c 100644
--- a/snekbox/nsjail.py
+++ b/snekbox/nsjail.py
@@ -1,6 +1,20 @@
+import logging
+import re
import subprocess
import sys
+import textwrap
from pathlib import Path
+from tempfile import NamedTemporaryFile
+
+from snekbox import DEBUG
+
+log = logging.getLogger(__name__)
+
+# [level][timestamp][PID]? function_signature:line_no? message
+LOG_PATTERN = re.compile(
+ r"\[(?P(I)|[WEF])\]\[.+?\](?(2)|(?P\[\d+\] .+?:\d+ )) ?(?P.+)"
+)
+LOG_BLACKLIST = ("Process will be ",)
# Explicitly define constants for NsJail's default values.
CGROUP_PIDS_PARENT = Path("/sys/fs/cgroup/pids/NSJAIL")
@@ -56,39 +70,77 @@ class NsJail:
pids.mkdir(parents=True, exist_ok=True)
mem.mkdir(parents=True, exist_ok=True)
+ @staticmethod
+ def _parse_log(log_file):
+ """Parse and log NsJail's log messages."""
+ for line in log_file.read().decode("UTF-8").splitlines():
+ match = LOG_PATTERN.fullmatch(line)
+ if match is None:
+ log.warning(f"Failed to parse log line '{line}'")
+ continue
+
+ msg = match["msg"]
+ if not DEBUG and any(msg.startswith(s) for s in LOG_BLACKLIST):
+ # Skip blacklisted messages if not debugging.
+ continue
+
+ if DEBUG and match["func"]:
+ # Prepend PID, function signature, and line number if debugging.
+ msg = f"{match['func']}{msg}"
+
+ if match["level"] == "D":
+ log.debug(msg)
+ elif match["level"] == "I":
+ if DEBUG or msg.startswith("pid="):
+ # Skip messages unrelated to process exit if not debugging.
+ log.info(msg)
+ elif match["level"] == "W":
+ log.warning(msg)
+ else:
+ # Treat fatal as error.
+ log.error(msg)
+
def python3(self, code: str) -> str:
"""Execute Python 3 code in an isolated environment and return stdout or an error."""
- args = (
- self.nsjail_binary, "-Mo",
- "--rlimit_as", "700",
- "--chroot", "/",
- "-E", "LANG=en_US.UTF-8",
- "-R/usr", "-R/lib", "-R/lib64",
- "--user", "nobody",
- "--group", "nogroup",
- "--time_limit", "2",
- "--disable_proc",
- "--iface_no_lo",
- "--cgroup_mem_max=52428800",
- "--cgroup_mem_mount", str(CGROUP_MEMORY_PARENT.parent),
- "--cgroup_mem_parent", CGROUP_MEMORY_PARENT.name,
- "--cgroup_pids_max=1",
- "--cgroup_pids_mount", str(CGROUP_PIDS_PARENT.parent),
- "--cgroup_pids_parent", CGROUP_PIDS_PARENT.name,
- "--quiet", "--",
- self.python_binary, "-ISq", "-c", code
- )
-
- try:
- proc = subprocess.run(args, capture_output=True, env=ENV, text=True)
- except ValueError:
- return "ValueError: embedded null byte"
+ with NamedTemporaryFile() as nsj_log:
+ args = (
+ self.nsjail_binary, "-Mo",
+ "--rlimit_as", "700",
+ "--chroot", "/",
+ "-E", "LANG=en_US.UTF-8",
+ "-R/usr", "-R/lib", "-R/lib64",
+ "--user", "nobody",
+ "--group", "nogroup",
+ "--time_limit", "2",
+ "--disable_proc",
+ "--iface_no_lo",
+ "--log", nsj_log.name,
+ "--cgroup_mem_max=52428800",
+ "--cgroup_mem_mount", str(CGROUP_MEMORY_PARENT.parent),
+ "--cgroup_mem_parent", CGROUP_MEMORY_PARENT.name,
+ "--cgroup_pids_max=1",
+ "--cgroup_pids_mount", str(CGROUP_PIDS_PARENT.parent),
+ "--cgroup_pids_parent", CGROUP_PIDS_PARENT.name,
+ "--",
+ self.python_binary, "-ISq", "-c", code
+ )
+
+ try:
+ msg = "Executing code..."
+ if DEBUG:
+ msg = f"{msg[:-3]}:\n{textwrap.indent(code, ' ')}"
+ log.info(msg)
+
+ proc = subprocess.run(args, capture_output=True, env=ENV, text=True)
+ except ValueError:
+ return "ValueError: embedded null byte"
+
+ self._parse_log(nsj_log)
if proc.returncode == 0:
output = proc.stdout
elif proc.returncode == 1:
- filtered = (line for line in proc.stderr.split("\n") if not line.startswith("["))
- output = "\n".join(filtered)
+ output = proc.stderr
elif proc.returncode == 109:
return "timed out or memory limit exceeded"
elif proc.returncode == 255:
diff --git a/tests/api/test_eval.py b/tests/api/test_eval.py
index a5b83fd..bcd0ec4 100644
--- a/tests/api/test_eval.py
+++ b/tests/api/test_eval.py
@@ -26,7 +26,7 @@ class TestEvalResource(SnekAPITestCase):
self.assertEqual(expected, result.json)
def test_post_invalid_content_type_415(self):
- body = "{\"input\": \"foo\"}"
+ body = "{'input': 'foo'}"
headers = {"Content-Type": "application/xml"}
result = self.simulate_post(self.PATH, body=body, headers=headers)
--
cgit v1.2.3
From 66d3836dd27f3b0e9f1cb780c7c37c8c0f081c70 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Wed, 5 Jun 2019 00:46:04 -0700
Subject: Respond to eval with stdout, stderr, and the return code
The previous implementation limited the client's flexibility in
presenting the results of the process. A process can write to both
stdout and stderr and do so even when the return code is not 0 or 1.
* Return a CompletedProcess from NsJail
* Don't check the return code; this should be done client-side now
---
snekbox/api/resources/eval.py | 23 +++++++++++++++++------
snekbox/nsjail.py | 33 ++++++++++-----------------------
tests/api/__init__.py | 8 +++++++-
tests/api/test_eval.py | 5 +++--
4 files changed, 37 insertions(+), 32 deletions(-)
(limited to 'tests')
diff --git a/snekbox/api/resources/eval.py b/snekbox/api/resources/eval.py
index b2f4260..4779557 100644
--- a/snekbox/api/resources/eval.py
+++ b/snekbox/api/resources/eval.py
@@ -36,7 +36,16 @@ class EvalResource:
@validate(REQ_SCHEMA)
def on_post(self, req, resp):
"""
- Evaluate Python code and return the result.
+ Evaluate Python code and return stdout, stderr, and the return code.
+
+ The return codes mostly resemble those of a Unix shell. Some noteworthy cases:
+
+ - None
+ The NsJail process failed to launch
+ - 137 (SIGKILL)
+ Typically because NsJail killed the Python process due to time or memory constraints
+ - 255
+ NsJail encountered a fatal error
Request body:
@@ -47,8 +56,9 @@ class EvalResource:
Response format:
>>> {
- ... "input": "print(1 + 1)",
- ... "output": "2\\n"
+ ... "stdout": "2\\n",
+ ... "stderr": "",
+ ... "returncode": 0
... }
Status codes:
@@ -63,12 +73,13 @@ class EvalResource:
code = req.media["input"]
try:
- output = self.nsjail.python3(code)
+ result = self.nsjail.python3(code)
except Exception:
log.exception("An exception occurred while trying to process the request")
raise falcon.HTTPInternalServerError
resp.media = {
- "input": code,
- "output": output
+ "stdout": result.stdout,
+ "stderr": result.stderr,
+ "returncode": result.returncode
}
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
index 0ebfe0c..ff12ec4 100644
--- a/snekbox/nsjail.py
+++ b/snekbox/nsjail.py
@@ -100,8 +100,8 @@ class NsJail:
# Treat fatal as error.
log.error(msg)
- def python3(self, code: str) -> str:
- """Execute Python 3 code in an isolated environment and return stdout or an error."""
+ def python3(self, code: str) -> subprocess.CompletedProcess:
+ """Execute Python 3 code in an isolated environment and return the completed process."""
with NamedTemporaryFile() as nsj_log:
args = (
self.nsjail_binary, "-Mo",
@@ -125,29 +125,16 @@ class NsJail:
self.python_binary, "-ISq", "-c", code
)
- try:
- msg = "Executing code..."
- if DEBUG:
- msg = f"{msg[:-3]}:\n{textwrap.indent(code, ' ')}"
- log.info(msg)
+ msg = "Executing code..."
+ if DEBUG:
+ msg = f"{msg[:-3]}:\n{textwrap.indent(code, ' ')}"
+ log.info(msg)
- proc = subprocess.run(args, capture_output=True, env=ENV, text=True)
+ try:
+ result = subprocess.run(args, capture_output=True, env=ENV, text=True)
except ValueError:
- return "ValueError: embedded null byte"
+ return subprocess.CompletedProcess(args, None, "", "ValueError: embedded null byte")
self._parse_log(nsj_log)
- if proc.returncode == 0:
- output = proc.stdout
- elif proc.returncode == 1:
- output = proc.stderr
- elif proc.returncode == 109:
- return "timed out or memory limit exceeded"
- elif proc.returncode == 255:
- return "permission denied (root required)"
- elif proc.returncode:
- return f"unknown error, code: {proc.returncode}"
- else:
- return "unknown error, no error code"
-
- return output
+ return result
diff --git a/tests/api/__init__.py b/tests/api/__init__.py
index fd4679a..dcee5b5 100644
--- a/tests/api/__init__.py
+++ b/tests/api/__init__.py
@@ -1,3 +1,4 @@
+from subprocess import CompletedProcess
from unittest import mock
from falcon import testing
@@ -11,7 +12,12 @@ class SnekAPITestCase(testing.TestCase):
self.patcher = mock.patch("snekbox.api.resources.eval.NsJail", autospec=True)
self.mock_nsjail = self.patcher.start()
- self.mock_nsjail.return_value.python3.return_value = "test output"
+ self.mock_nsjail.return_value.python3.return_value = CompletedProcess(
+ args=[],
+ returncode=0,
+ stdout="output",
+ stderr="error"
+ )
self.addCleanup(self.patcher.stop)
self.app = SnekAPI()
diff --git a/tests/api/test_eval.py b/tests/api/test_eval.py
index bcd0ec4..03f0e39 100644
--- a/tests/api/test_eval.py
+++ b/tests/api/test_eval.py
@@ -9,8 +9,9 @@ class TestEvalResource(SnekAPITestCase):
result = self.simulate_post(self.PATH, json=body)
self.assertEqual(result.status_code, 200)
- self.assertEqual(body["input"], result.json["input"])
- self.assertEqual("test output", result.json["output"])
+ self.assertEqual("output", result.json["stdout"])
+ self.assertEqual("error", result.json["stderr"])
+ self.assertEqual(0, result.json["returncode"])
def test_post_invalid_schema_400(self):
body = {"stuff": "foo"}
--
cgit v1.2.3
From bc130a4d44f38824b6173c0babff4eefe18ac1db Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Sat, 15 Jun 2019 18:32:32 -0700
Subject: Merge stdout and stderr
Removes the need for redirecting stderr using contextlib in the input.
Furthermore, it captures errors which don't directly come from the
input, such as SyntaxErrors.
---
snekbox/api/resources/eval.py | 2 --
snekbox/nsjail.py | 10 ++++++++--
tests/api/test_eval.py | 1 -
3 files changed, 8 insertions(+), 5 deletions(-)
(limited to 'tests')
diff --git a/snekbox/api/resources/eval.py b/snekbox/api/resources/eval.py
index 4779557..c4bd666 100644
--- a/snekbox/api/resources/eval.py
+++ b/snekbox/api/resources/eval.py
@@ -57,7 +57,6 @@ class EvalResource:
>>> {
... "stdout": "2\\n",
- ... "stderr": "",
... "returncode": 0
... }
@@ -80,6 +79,5 @@ class EvalResource:
resp.media = {
"stdout": result.stdout,
- "stderr": result.stderr,
"returncode": result.returncode
}
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
index ff12ec4..1675b3e 100644
--- a/snekbox/nsjail.py
+++ b/snekbox/nsjail.py
@@ -131,9 +131,15 @@ class NsJail:
log.info(msg)
try:
- result = subprocess.run(args, capture_output=True, env=ENV, text=True)
+ result = subprocess.run(
+ args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ env=ENV,
+ text=True
+ )
except ValueError:
- return subprocess.CompletedProcess(args, None, "", "ValueError: embedded null byte")
+ return subprocess.CompletedProcess(args, None, "ValueError: embedded null byte", "")
self._parse_log(nsj_log)
diff --git a/tests/api/test_eval.py b/tests/api/test_eval.py
index 03f0e39..3350763 100644
--- a/tests/api/test_eval.py
+++ b/tests/api/test_eval.py
@@ -10,7 +10,6 @@ class TestEvalResource(SnekAPITestCase):
self.assertEqual(result.status_code, 200)
self.assertEqual("output", result.json["stdout"])
- self.assertEqual("error", result.json["stderr"])
self.assertEqual(0, result.json["returncode"])
def test_post_invalid_schema_400(self):
--
cgit v1.2.3
From 281df48622199c7a38a74da49782699184876492 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Sat, 22 Jun 2019 12:59:32 -0700
Subject: Rewrite NsJail tests
* Fix SIGSEGV test
* Add embedded null byte test
* Return None for stderr when there's a ValueError
---
snekbox/nsjail.py | 5 ++--
tests/test_nsjail.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++
tests/test_snekbox.py | 56 -------------------------------------
3 files changed, 80 insertions(+), 58 deletions(-)
create mode 100644 tests/test_nsjail.py
delete mode 100644 tests/test_snekbox.py
(limited to 'tests')
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
index 3bcc0a1..f82dcf0 100644
--- a/snekbox/nsjail.py
+++ b/snekbox/nsjail.py
@@ -5,6 +5,7 @@ import subprocess
import sys
import textwrap
from pathlib import Path
+from subprocess import CompletedProcess
from tempfile import NamedTemporaryFile
from typing import List
@@ -92,7 +93,7 @@ class NsJail:
# Treat fatal as error.
log.error(msg)
- def python3(self, code: str) -> subprocess.CompletedProcess:
+ def python3(self, code: str) -> CompletedProcess:
"""Execute Python 3 code in an isolated environment and return the completed process."""
with NamedTemporaryFile() as nsj_log:
args = (
@@ -130,7 +131,7 @@ class NsJail:
text=True
)
except ValueError:
- return subprocess.CompletedProcess(args, None, "ValueError: embedded null byte", "")
+ return CompletedProcess(args, None, "ValueError: embedded null byte", None)
log_lines = nsj_log.read().decode("UTF-8").splitlines()
if not log_lines and result.returncode == 255:
diff --git a/tests/test_nsjail.py b/tests/test_nsjail.py
new file mode 100644
index 0000000..1184b87
--- /dev/null
+++ b/tests/test_nsjail.py
@@ -0,0 +1,77 @@
+import logging
+import unittest
+from textwrap import dedent
+
+from snekbox.nsjail import NsJail
+
+
+class NsJailTests(unittest.TestCase):
+ def setUp(self):
+ super().setUp()
+
+ self.nsjail = NsJail()
+ self.logger = logging.getLogger("snekbox.nsjail")
+
+ def test_print_returns_0(self):
+ result = self.nsjail.python3("print('test')")
+ self.assertEqual(result.returncode, 0)
+ self.assertEqual(result.stdout, "test\n")
+ self.assertEqual(result.stderr, None)
+
+ def test_timeout_returns_137(self):
+ code = dedent("""
+ x = '*'
+ while True:
+ try:
+ x = x * 99
+ except:
+ continue
+ """).strip()
+
+ with self.assertLogs(self.logger) as log:
+ result = self.nsjail.python3(code)
+
+ self.assertEqual(result.returncode, 137)
+ self.assertEqual(result.stdout, "")
+ self.assertEqual(result.stderr, None)
+ self.assertIn("run time >= time limit", "\n".join(log.output))
+
+ def test_subprocess_resource_unavailable(self):
+ code = dedent("""
+ import subprocess
+ print(subprocess.check_output('kill -9 6', shell=True).decode())
+ """).strip()
+
+ result = self.nsjail.python3(code)
+ self.assertEqual(result.returncode, 1)
+ self.assertIn("Resource temporarily unavailable", result.stdout)
+ self.assertEqual(result.stderr, None)
+
+ def test_forkbomb_resource_unavailable(self):
+ code = dedent("""
+ import os
+ while 1:
+ os.fork()
+ """).strip()
+
+ result = self.nsjail.python3(code)
+ self.assertEqual(result.returncode, 1)
+ self.assertIn("Resource temporarily unavailable", result.stdout)
+ self.assertEqual(result.stderr, None)
+
+ def test_sigsegv_returns_139(self): # In honour of Juan.
+ code = dedent("""
+ import ctypes
+ ctypes.string_at(0)
+ """).strip()
+
+ result = self.nsjail.python3(code)
+ self.assertEqual(result.returncode, 139)
+ self.assertEqual(result.stdout, "")
+ self.assertEqual(result.stderr, None)
+
+ def test_null_byte_value_error(self):
+ result = self.nsjail.python3("\0")
+ self.assertEqual(result.returncode, None)
+ self.assertEqual(result.stdout, "ValueError: embedded null byte")
+ self.assertEqual(result.stderr, None)
diff --git a/tests/test_snekbox.py b/tests/test_snekbox.py
deleted file mode 100644
index 46319d6..0000000
--- a/tests/test_snekbox.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import unittest
-
-from snekbox.nsjail import NsJail
-
-nsjail = NsJail()
-
-
-class SnekTests(unittest.TestCase):
- def test_nsjail(self):
- result = nsjail.python3("print('test')")
- self.assertEquals(result.strip(), "test")
-
- # def test_memory_error(self):
- # code = ("x = "*"\n"
- # "while True:\n"
- # " x = x * 99\n")
- # result = nsjail.python3(code)
- # self.assertEquals(result.strip(), "timed out or memory limit exceeded")
-
- def test_timeout(self):
- code = (
- "x = '*'\n"
- "while True:\n"
- " try:\n"
- " x = x * 99\n"
- " except:\n"
- " continue\n"
- )
-
- result = nsjail.python3(code)
- self.assertEquals(result.strip(), "timed out or memory limit exceeded")
-
- def test_kill(self):
- code = ("import subprocess\n"
- "print(subprocess.check_output('kill -9 6', shell=True).decode())")
- result = nsjail.python3(code)
- if "ModuleNotFoundError" in result.strip():
- self.assertIn("ModuleNotFoundError", result.strip())
- else:
- self.assertIn("(PIDs left: 0)", result.strip())
-
- def test_forkbomb(self):
- code = ("import os\n"
- "while 1:\n"
- " os.fork()")
- result = nsjail.python3(code)
- self.assertIn("Resource temporarily unavailable", result.strip())
-
- def test_juan_golf(self): # in honour of Juan
- code = ("func = lambda: None\n"
- "CodeType = type(func.__code__)\n"
- "bytecode = CodeType(0,1,0,0,0,b'',(),(),(),'','',1,b'')\n"
- "exec(bytecode)")
-
- result = nsjail.python3(code)
- self.assertEquals("unknown error, code: 111", result.strip())
--
cgit v1.2.3
From 81e2a92019a184b2a558658777ceded99b360731 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Sat, 22 Jun 2019 13:27:47 -0700
Subject: Add a NsJail log parser test
* Add support for debug level to log regex
* Change type annotation of log_parse to Iterable
---
snekbox/nsjail.py | 6 +++---
tests/test_nsjail.py | 30 ++++++++++++++++++++++++++++++
2 files changed, 33 insertions(+), 3 deletions(-)
(limited to 'tests')
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
index f82dcf0..b68b0b9 100644
--- a/snekbox/nsjail.py
+++ b/snekbox/nsjail.py
@@ -7,7 +7,7 @@ import textwrap
from pathlib import Path
from subprocess import CompletedProcess
from tempfile import NamedTemporaryFile
-from typing import List
+from typing import Iterable
from snekbox import DEBUG
@@ -15,7 +15,7 @@ log = logging.getLogger(__name__)
# [level][timestamp][PID]? function_signature:line_no? message
LOG_PATTERN = re.compile(
- r"\[(?P(I)|[WEF])\]\[.+?\](?(2)|(?P\[\d+\] .+?:\d+ )) ?(?P.+)"
+ r"\[(?P(I)|[DWEF])\]\[.+?\](?(2)|(?P\[\d+\] .+?:\d+ )) ?(?P.+)"
)
LOG_BLACKLIST = ("Process will be ",)
@@ -64,7 +64,7 @@ class NsJail:
mem.mkdir(parents=True, exist_ok=True)
@staticmethod
- def _parse_log(log_lines: List[str]):
+ def _parse_log(log_lines: Iterable[str]):
"""Parse and log NsJail's log messages."""
for line in log_lines:
match = LOG_PATTERN.fullmatch(line)
diff --git a/tests/test_nsjail.py b/tests/test_nsjail.py
index 1184b87..e3b8eb3 100644
--- a/tests/test_nsjail.py
+++ b/tests/test_nsjail.py
@@ -10,6 +10,7 @@ class NsJailTests(unittest.TestCase):
super().setUp()
self.nsjail = NsJail()
+ self.nsjail.DEBUG = False
self.logger = logging.getLogger("snekbox.nsjail")
def test_print_returns_0(self):
@@ -75,3 +76,32 @@ class NsJailTests(unittest.TestCase):
self.assertEqual(result.returncode, None)
self.assertEqual(result.stdout, "ValueError: embedded null byte")
self.assertEqual(result.stderr, None)
+
+ def test_log_parser(self):
+ log_lines = (
+ "[D][2019-06-22T20:07:00+0000][16] void foo::bar()():100 This is a debug message.",
+ "[I][2019-06-22T20:07:48+0000] pid=20 ([STANDALONE MODE]) "
+ "exited with status: 2, (PIDs left: 0)",
+ "[W][2019-06-22T20:06:04+0000][14] void cmdline::logParams(nsjconf_t*)():250 "
+ "Process will be UID/EUID=0 in the global user namespace, and will have user "
+ "root-level access to files",
+ "[W][2019-06-22T20:07:00+0000][16] void foo::bar()():100 This is a warning!",
+ "[E][2019-06-22T20:07:00+0000][16] bool "
+ "cmdline::setupArgv(nsjconf_t*, int, char**, int)():316 No command-line provided",
+ "[F][2019-06-22T20:07:00+0000][16] int main(int, char**)():204 "
+ "Couldn't parse cmdline options",
+ "Invalid Line"
+ )
+
+ with self.assertLogs(self.logger, logging.DEBUG) as log:
+ self.nsjail._parse_log(log_lines)
+
+ self.assertIn("DEBUG:snekbox.nsjail:This is a debug message.", log.output)
+ self.assertIn("ERROR:snekbox.nsjail:Couldn't parse cmdline options", log.output)
+ self.assertIn("ERROR:snekbox.nsjail:No command-line provided", log.output)
+ self.assertIn("WARNING:snekbox.nsjail:Failed to parse log line 'Invalid Line'", log.output)
+ self.assertIn("WARNING:snekbox.nsjail:This is a warning!", log.output)
+ self.assertIn(
+ "INFO:snekbox.nsjail:pid=20 ([STANDALONE MODE]) exited with status: 2, (PIDs left: 0)",
+ log.output
+ )
--
cgit v1.2.3
From 158915a953879639722ab3bc1074fec7276117ba Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Wed, 26 Jun 2019 23:00:39 -0700
Subject: Disable memory swapping and add a memory limit test
If memory swapping was enabled locally, the memory test would fail.
Explicitly disabling swapping also removes reliance on the assumption
that it'll be disabled in production.
* Add a constant for the maximum memory
* Simplify the timeout test; it'd otherwise first run out of memory now
---
scripts/.profile | 9 ++++++++-
snekbox/nsjail.py | 14 +++++++++++++-
tests/test_nsjail.py | 19 +++++++++++++------
3 files changed, 34 insertions(+), 8 deletions(-)
(limited to 'tests')
diff --git a/scripts/.profile b/scripts/.profile
index 415e4f6..bff260d 100644
--- a/scripts/.profile
+++ b/scripts/.profile
@@ -1,12 +1,19 @@
nsjpy() {
+ local MEM_MAX=52428800
+
+ # All arguments except the last are considered to be for NsJail, not Python.
local nsj_args=""
while [ "$#" -gt 1 ]; do
nsj_args="${nsj_args:+${nsj_args} }$1"
shift
done
+ # Set up cgroups and disable memory swapping.
mkdir -p /sys/fs/cgroup/pids/NSJAIL
mkdir -p /sys/fs/cgroup/memory/NSJAIL
+ echo "${MEM_MAX}" > /sys/fs/cgroup/memory/NSJAIL/memory.limit_in_bytes
+ echo "${MEM_MAX}" > /sys/fs/cgroup/memory/NSJAIL/memory.memsw.limit_in_bytes
+
nsjail \
-Mo \
--rlimit_as 700 \
@@ -19,7 +26,7 @@ nsjpy() {
--disable_proc \
--iface_no_lo \
--cgroup_pids_max=1 \
- --cgroup_mem_max=52428800 \
+ --cgroup_mem_max="${MEM_MAX}" \
$nsj_args -- \
/snekbox/.venv/bin/python3 -Iq -c "$@"
}
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
index b68b0b9..b9c4fc7 100644
--- a/snekbox/nsjail.py
+++ b/snekbox/nsjail.py
@@ -24,6 +24,7 @@ CGROUP_PIDS_PARENT = Path("/sys/fs/cgroup/pids/NSJAIL")
CGROUP_MEMORY_PARENT = Path("/sys/fs/cgroup/memory/NSJAIL")
NSJAIL_PATH = os.getenv("NSJAIL_PATH", "/usr/sbin/nsjail")
+MEM_MAX = 52428800
class NsJail:
@@ -59,10 +60,21 @@ class NsJail:
NsJail doesn't do this automatically because it requires privileges NsJail usually doesn't
have.
+
+ Disables memory swapping.
"""
pids.mkdir(parents=True, exist_ok=True)
mem.mkdir(parents=True, exist_ok=True)
+ # Swap limit cannot be set to a value lower than memory.limit_in_bytes.
+ # Therefore, this must be set first.
+ with (mem / "memory.limit_in_bytes").open("w", encoding="utf=8") as f:
+ f.write(str(MEM_MAX))
+
+ # Swap limit is specified as the sum of the memory and swap limits.
+ with (mem / "memory.memsw.limit_in_bytes").open("w", encoding="utf=8") as f:
+ f.write(str(MEM_MAX))
+
@staticmethod
def _parse_log(log_lines: Iterable[str]):
"""Parse and log NsJail's log messages."""
@@ -108,7 +120,7 @@ class NsJail:
"--disable_proc",
"--iface_no_lo",
"--log", nsj_log.name,
- "--cgroup_mem_max=52428800",
+ f"--cgroup_mem_max={MEM_MAX}",
"--cgroup_mem_mount", str(CGROUP_MEMORY_PARENT.parent),
"--cgroup_mem_parent", CGROUP_MEMORY_PARENT.name,
"--cgroup_pids_max=1",
diff --git a/tests/test_nsjail.py b/tests/test_nsjail.py
index e3b8eb3..f1a60e6 100644
--- a/tests/test_nsjail.py
+++ b/tests/test_nsjail.py
@@ -2,7 +2,7 @@ import logging
import unittest
from textwrap import dedent
-from snekbox.nsjail import NsJail
+from snekbox.nsjail import MEM_MAX, NsJail
class NsJailTests(unittest.TestCase):
@@ -21,12 +21,8 @@ class NsJailTests(unittest.TestCase):
def test_timeout_returns_137(self):
code = dedent("""
- x = '*'
while True:
- try:
- x = x * 99
- except:
- continue
+ pass
""").strip()
with self.assertLogs(self.logger) as log:
@@ -37,6 +33,17 @@ class NsJailTests(unittest.TestCase):
self.assertEqual(result.stderr, None)
self.assertIn("run time >= time limit", "\n".join(log.output))
+ def test_memory_returns_137(self):
+ # Add a kilobyte just to be safe.
+ code = dedent(f"""
+ x = ' ' * {MEM_MAX + 1000}
+ """).strip()
+
+ result = self.nsjail.python3(code)
+ self.assertEqual(result.returncode, 137)
+ self.assertEqual(result.stdout, "")
+ self.assertEqual(result.stderr, None)
+
def test_subprocess_resource_unavailable(self):
code = dedent("""
import subprocess
--
cgit v1.2.3
From 905cdf11b3b76ebee672e54b4062e3904ed47072 Mon Sep 17 00:00:00 2001
From: MarkKoz
Date: Thu, 27 Jun 2019 17:53:09 -0700
Subject: Test that the file system is mounted as read only
---
tests/test_nsjail.py | 10 ++++++++++
1 file changed, 10 insertions(+)
(limited to 'tests')
diff --git a/tests/test_nsjail.py b/tests/test_nsjail.py
index f1a60e6..bb176d9 100644
--- a/tests/test_nsjail.py
+++ b/tests/test_nsjail.py
@@ -55,6 +55,16 @@ class NsJailTests(unittest.TestCase):
self.assertIn("Resource temporarily unavailable", result.stdout)
self.assertEqual(result.stderr, None)
+ def test_read_only_file_system(self):
+ code = dedent("""
+ open('hello', 'w').write('world')
+ """).strip()
+
+ result = self.nsjail.python3(code)
+ self.assertEqual(result.returncode, 1)
+ self.assertIn("Read-only file system", result.stdout)
+ self.assertEqual(result.stderr, None)
+
def test_forkbomb_resource_unavailable(self):
code = dedent("""
import os
--
cgit v1.2.3