From 2c843101843b975ece546b8921d53b3dd4e6974d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 6 Jun 2019 16:54:33 -0700 Subject: Create shell script for building a dev image and running a shell * Put scripts in a new scripts folder --- scripts/.profile | 25 +++++++++++++++++++++++++ scripts/dev.sh | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 scripts/.profile create mode 100755 scripts/dev.sh (limited to 'scripts') diff --git a/scripts/.profile b/scripts/.profile new file mode 100644 index 0000000..415e4f6 --- /dev/null +++ b/scripts/.profile @@ -0,0 +1,25 @@ +nsjpy() { + local nsj_args="" + while [ "$#" -gt 1 ]; do + nsj_args="${nsj_args:+${nsj_args} }$1" + shift + done + + mkdir -p /sys/fs/cgroup/pids/NSJAIL + mkdir -p /sys/fs/cgroup/memory/NSJAIL + nsjail \ + -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 \ + $nsj_args -- \ + /snekbox/.venv/bin/python3 -Iq -c "$@" +} diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..490021f --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env sh + +# Sets up a development environment and runs a shell in a docker container. +# Usage: dev.sh [--build [--clean]] [ash_args ...] + +if [ "$1" = "--build" ]; then + shift + printf "Building pythondiscord/snekbox-venv:dev..." + + docker build \ + -t pythondiscord/snekbox-venv:dev \ + -f docker/venv.Dockerfile \ + --build-arg DEV=1 \ + -q \ + . \ + >/dev/null \ + && printf " done!\n" || exit "$?" + + if [ "$1" = "--clean" ]; then + shift + dangling_imgs=$(docker images -f "dangling=true" -q) + + if [ -n "${dangling_imgs}" ]; then + printf "Removing dangling images..." + + docker rmi $dangling_imgs >/dev/null \ + && printf " done!\n" || exit "$?" + fi + fi +fi + +docker run \ + -it \ + --rm \ + --privileged \ + --network host \ + -h pdsnk-dev \ + -e PYTHONDONTWRITEBYTECODE=1 \ + -e PIPENV_PIPFILE="/snekbox/Pipfile" \ + -e ENV="/snekbox-local/scripts/.profile" \ + -v "${PWD}":/snekbox-local \ + -w "/snekbox-local" \ + --entrypoint /bin/ash \ + pythondiscord/snekbox-venv:dev \ + "$@" -- cgit v1.2.3 From c1a6440899ced2f3f787352cd1d3ea1f49e520ee Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 20 Jun 2019 16:25:28 -0700 Subject: Fix ownership of coverage file When coverage runs in a container, it is ran under root so the resulting coverage file is owned by root. chown is used to change ownership to be the same as the folder it is in. --- scripts/dev.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/dev.sh b/scripts/dev.sh index 490021f..6ebae71 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -31,7 +31,7 @@ fi docker run \ -it \ - --rm \ + --name snekbox_test \ --privileged \ --network host \ -h pdsnk-dev \ @@ -43,3 +43,12 @@ docker run \ --entrypoint /bin/ash \ pythondiscord/snekbox-venv:dev \ "$@" + +# Fix ownership of coverage file +docker start snekbox_test >/dev/null +docker exec \ + -it \ + snekbox_test \ + /bin/ash \ + -c 'chown "$(stat -c "%u:%g" "/snekbox-local")" /snekbox-local/.coverage' +docker rm -f snekbox_test >/dev/null -- cgit v1.2.3 From 495baa3045c63040f460538e94eaaed6a6499fba Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 21 Jun 2019 19:35:57 -0700 Subject: Fix coverage not finding sources * Mount volume to the same path as the source directory on the host * Keep the container up in the background so it doesn't have to be restarted or the ownership fix --- scripts/dev.sh | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) (limited to 'scripts') diff --git a/scripts/dev.sh b/scripts/dev.sh index 6ebae71..097690b 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -29,26 +29,35 @@ if [ "$1" = "--build" ]; then fi fi +# Keep the container up in the background so it doesn't have to be restarted +# for the ownership fix. +# The volume is mounted to same the path in the container as the source +# directory on the host to ensure coverage can find the source files. docker run \ - -it \ + -td \ --name snekbox_test \ --privileged \ --network host \ -h pdsnk-dev \ -e PYTHONDONTWRITEBYTECODE=1 \ -e PIPENV_PIPFILE="/snekbox/Pipfile" \ - -e ENV="/snekbox-local/scripts/.profile" \ - -v "${PWD}":/snekbox-local \ - -w "/snekbox-local" \ + -e ENV="${PWD}/scripts/.profile" \ + -v "${PWD}":"${PWD}" \ + -w "${PWD}"\ --entrypoint /bin/ash \ pythondiscord/snekbox-venv:dev \ - "$@" + >/dev/null \ + +# Execute the given command(s) +docker exec -it snekbox_test /bin/ash "$@" # Fix ownership of coverage file -docker start snekbox_test >/dev/null +# BusyBox doesn't support --reference for chown docker exec \ -it \ + -e CWD="${PWD}" \ snekbox_test \ /bin/ash \ - -c 'chown "$(stat -c "%u:%g" "/snekbox-local")" /snekbox-local/.coverage' -docker rm -f snekbox_test >/dev/null + -c 'chown "$(stat -c "%u:%g" "${CWD}")" "${CWD}/.coverage"' + +docker rm -f snekbox_test >/dev/null # Stop and remove the container -- 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 'scripts') 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