diff options
author | 2020-02-05 12:20:54 -0800 | |
---|---|---|
committer | 2020-02-05 12:20:54 -0800 | |
commit | 504a6b9af1bafe87ea093f235562fb8af55ea406 (patch) | |
tree | 4a8ccf9f7be27b110bea9558030e402aea43c9b0 | |
parent | Update CODEOWNERS (diff) | |
parent | Merge branch 'master' into proper-chroot (diff) |
Merge pull request #55 from python-discord/proper-chroot
Configure a proper chroot jail for NsJail
-rw-r--r-- | .dockerignore | 2 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | docker-compose.yml | 1 | ||||
-rw-r--r-- | docker/venv.Dockerfile | 2 | ||||
-rw-r--r-- | scripts/.profile | 20 | ||||
-rwxr-xr-x | scripts/dev.sh | 1 | ||||
-rw-r--r-- | snekbox.cfg | 97 | ||||
-rw-r--r-- | snekbox/nsjail.py | 26 | ||||
-rw-r--r-- | tests/test_nsjail.py | 68 |
9 files changed, 172 insertions, 49 deletions
diff --git a/.dockerignore b/.dockerignore index afc786a..4f43e08 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,8 @@ * # Make exceptions for what's needed -!docker/.profile !snekbox +!snekbox.cfg !tests !Pipfile !Pipfile.lock @@ -24,8 +24,8 @@ result <- | |<----------| | <----------+ The code is executed in a Python process that is launched through [NsJail](https://github.com/google/nsjail), which is responsible for sandboxing the Python process. NsJail is configured as follows: -* Root directory is mounted as read-only -* Time limit of 2 seconds +* All mounts are read-only +* Time limit of 5 seconds * Maximum of 1 PID * Maximum memory of 52428800 bytes * Loopback interface is down diff --git a/docker-compose.yml b/docker-compose.yml index d071a71..aeb2806 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,7 @@ services: image: pythondiscord/snekbox:latest network_mode: "host" init: true + ipc: none build: context: . dockerfile: docker/Dockerfile diff --git a/docker/venv.Dockerfile b/docker/venv.Dockerfile index be15f08..b415430 100644 --- a/docker/venv.Dockerfile +++ b/docker/venv.Dockerfile @@ -7,7 +7,7 @@ ENV PIP_NO_CACHE_DIR=false \ PIPENV_NOSPIN=1 \ PIPENV_VENV_IN_PROJECT=1 -COPY Pipfile Pipfile.lock /snekbox/ +COPY Pipfile Pipfile.lock snekbox.cfg /snekbox/ WORKDIR /snekbox RUN if [ -n "${DEV}" ]; then pipenv sync --dev; else pipenv sync; fi diff --git a/scripts/.profile b/scripts/.profile index daaf1dd..73fbb28 100644 --- a/scripts/.profile +++ b/scripts/.profile @@ -15,23 +15,7 @@ nsjpy() { echo "${MEM_MAX}" > /sys/fs/cgroup/memory/NSJAIL/memory.memsw.limit_in_bytes nsjail \ - -Mo \ - --rlimit_as 700 \ - --chroot / \ - -E LANG=en_US.UTF-8 \ - -E OMP_NUM_THREADS=1 \ - -E OPENBLAS_NUM_THREADS=1 \ - -E MKL_NUM_THREADS=1 \ - -E VECLIB_MAXIMUM_THREADS=1 \ - -E NUMEXPR_NUM_THREADS=1 \ - -R/usr -R/lib -R/lib64 \ - --user 65534 \ - --group 65534 \ - --time_limit 2 \ - --disable_proc \ - --iface_no_lo \ - --cgroup_pids_max=1 \ - --cgroup_mem_max="${MEM_MAX}" \ + --config "${NSJAIL_CFG:-/snekbox/snekbox.cfg}" \ $nsj_args -- \ - /snekbox/.venv/bin/python3 -Iq -c "$@" + /snekbox/.venv/bin/python3 -Iqu -c "$@" } diff --git a/scripts/dev.sh b/scripts/dev.sh index 6aeb1de..0275651 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -40,6 +40,7 @@ docker run \ --privileged \ --network host \ --hostname pdsnk-dev \ + --ipc="none" \ -e PYTHONDONTWRITEBYTECODE=1 \ -e PIPENV_PIPFILE="/snekbox/Pipfile" \ -e BASH_ENV="${PWD}/scripts/.profile" \ diff --git a/snekbox.cfg b/snekbox.cfg new file mode 100644 index 0000000..968271c --- /dev/null +++ b/snekbox.cfg @@ -0,0 +1,97 @@ +name: "snekbox" +description: "Execute Python" + +mode: ONCE +hostname: "snekbox" +cwd: "/snekbox" + +time_limit: 5 + +keep_env: false +envar: "LANG=en_US.UTF-8" +envar: "OMP_NUM_THREADS=1" +envar: "OPENBLAS_NUM_THREADS=1" +envar: "MKL_NUM_THREADS=1" +envar: "VECLIB_MAXIMUM_THREADS=1" +envar: "NUMEXPR_NUM_THREADS=1" + +keep_caps: false + +rlimit_as: 700 + +clone_newnet: true +clone_newuser: true +clone_newns: true +clone_newpid: true +clone_newipc: true +clone_newuts: true +clone_newcgroup: true + +uidmap { + inside_id: "65534" + outside_id: "65534" +} + +gidmap { + inside_id: "65534" + outside_id: "65534" +} + +mount_proc: false + +mount { + src: "/etc/ld.so.cache" + dst: "/etc/ld.so.cache" + is_bind: true + rw: false +} + +mount { + src: "/lib" + dst: "/lib" + is_bind: true + rw: false +} + +mount { + src: "/lib64" + dst: "/lib64" + is_bind: true + rw: false +} + +mount { + src: "/snekbox" + dst: "/snekbox" + is_bind: true + rw: false +} + +mount { + src: "/usr/lib" + dst: "/usr/lib" + is_bind: true + rw: false +} + +mount { + src: "/usr/local/lib" + dst: "/usr/local/lib" + is_bind: true + rw: false +} + +cgroup_mem_max: 52428800 +cgroup_mem_mount: "/sys/fs/cgroup/memory" +cgroup_mem_parent: "NSJAIL" + +cgroup_pids_max: 1 +cgroup_pids_mount: "/sys/fs/cgroup/pids" +cgroup_pids_parent: "NSJAIL" + +iface_no_lo: true + +exec_bin { + path: "/snekbox/.venv/bin/python3" + arg: "-Iqu" +} diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py index f160aa8..b5586bb 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") +NSJAIL_CFG = os.getenv("NSJAIL_CFG", "./snekbox.cfg") MEM_MAX = 52428800 @@ -31,10 +32,10 @@ class NsJail: """ Core Snekbox functionality, providing safe execution of Python code. - NsJail configuration: + Default NsJail configuration (snekbox.cfg): - - Root directory is mounted as read-only - - Time limit of 2 seconds + - All mounts are read-only + - Time limit of 5 seconds - Maximum of 1 PID - Maximum memory of 52428800 bytes - Loopback interface is down @@ -117,21 +118,8 @@ class NsJail: """Execute Python 3 code in an isolated environment and return the completed process.""" with NamedTemporaryFile() as nsj_log: args = ( - self.nsjail_binary, "-Mo", - "--rlimit_as", "700", - "--chroot", "/", - "-E", "LANG=en_US.UTF-8", - "-E", "OMP_NUM_THREADS=1", - "-E", "OPENBLAS_NUM_THREADS=1", - "-E", "MKL_NUM_THREADS=1", - "-E", "VECLIB_MAXIMUM_THREADS=1", - "-E", "NUMEXPR_NUM_THREADS=1", - "-R/usr", "-R/lib", "-R/lib64", - "--user", "65534", # nobody - "--group", "65534", # nobody/nogroup - "--time_limit", "2", - "--disable_proc", - "--iface_no_lo", + self.nsjail_binary, + "--config", NSJAIL_CFG, "--log", nsj_log.name, f"--cgroup_mem_max={MEM_MAX}", "--cgroup_mem_mount", str(CGROUP_MEMORY_PARENT.parent), @@ -140,7 +128,7 @@ class NsJail: "--cgroup_pids_mount", str(CGROUP_PIDS_PARENT.parent), "--cgroup_pids_parent", CGROUP_PIDS_PARENT.name, "--", - self.python_binary, "-Iq", "-c", code + self.python_binary, "-Iqu", "-c", code ) msg = "Executing code..." diff --git a/tests/test_nsjail.py b/tests/test_nsjail.py index bb176d9..0b755b2 100644 --- a/tests/test_nsjail.py +++ b/tests/test_nsjail.py @@ -56,14 +56,17 @@ class NsJailTests(unittest.TestCase): 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) + for path in ("/", "/etc", "/lib", "/lib64", "/snekbox", "/usr"): + with self.subTest(path=path): + code = dedent(f""" + with open('{path}/hello', 'w') as f: + f.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(""" @@ -122,3 +125,52 @@ class NsJailTests(unittest.TestCase): "INFO:snekbox.nsjail:pid=20 ([STANDALONE MODE]) exited with status: 2, (PIDs left: 0)", log.output ) + + def test_shm_and_tmp_not_mounted(self): + for path in ("/dev/shm", "/run/shm", "/tmp"): + with self.subTest(path=path): + code = dedent(f""" + with open('{path}/test', 'wb') as file: + file.write(bytes([255])) + """).strip() + + result = self.nsjail.python3(code) + self.assertEqual(result.returncode, 1) + self.assertIn("No such file or directory", result.stdout) + self.assertEqual(result.stderr, None) + + def test_multiprocessing_shared_memory_disabled(self): + code = dedent(""" + from multiprocessing.shared_memory import SharedMemory + try: + SharedMemory('test', create=True, size=16) + except FileExistsError: + pass + """).strip() + + result = self.nsjail.python3(code) + self.assertEqual(result.returncode, 1) + self.assertIn("Function not implemented", result.stdout) + self.assertEqual(result.stderr, None) + + def test_numpy_import(self): + result = self.nsjail.python3("import numpy") + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, "") + self.assertEqual(result.stderr, None) + + def test_output_order(self): + stdout_msg = "greetings from stdout!" + stderr_msg = "hello from stderr!" + code = dedent(f""" + print({stdout_msg!r}) + raise ValueError({stderr_msg!r}) + """).strip() + + result = self.nsjail.python3(code) + self.assertLess( + result.stdout.find(stdout_msg), + result.stdout.find(stderr_msg), + msg="stdout does not come before stderr" + ) + self.assertEqual(result.stderr, None) |