diff options
| -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) | 
