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 --- tests/test_nsjail.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_nsjail.py (limited to 'tests/test_nsjail.py') 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) -- 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/test_nsjail.py') 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/test_nsjail.py') 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/test_nsjail.py') 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