aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Mark <[email protected]>2020-02-05 12:20:54 -0800
committerGravatar GitHub <[email protected]>2020-02-05 12:20:54 -0800
commit504a6b9af1bafe87ea093f235562fb8af55ea406 (patch)
tree4a8ccf9f7be27b110bea9558030e402aea43c9b0
parentUpdate CODEOWNERS (diff)
parentMerge 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--.dockerignore2
-rw-r--r--README.md4
-rw-r--r--docker-compose.yml1
-rw-r--r--docker/venv.Dockerfile2
-rw-r--r--scripts/.profile20
-rwxr-xr-xscripts/dev.sh1
-rw-r--r--snekbox.cfg97
-rw-r--r--snekbox/nsjail.py26
-rw-r--r--tests/test_nsjail.py68
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
diff --git a/README.md b/README.md
index f1fcac5..bbb8d64 100644
--- a/README.md
+++ b/README.md
@@ -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)