Skip to content

Commit e7650e5

Browse files
committed
Add unit tests for check_code_safety and update Dockerfile sandboxing details
- Introduces comprehensive unit tests in `security_test.py` to validate `check_code_safety`. - Refines `Dockerfile` comments for clarity on `nsjail` sandboxing, runtime requirements, and build dependencies.
1 parent 8034dc0 commit e7650e5

2 files changed

Lines changed: 70 additions & 22 deletions

File tree

Dockerfile

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,37 +32,43 @@ ENV FUNCTION_COMMAND="python"
3232
ENV FUNCTION_ARGS="-m,evaluation_function.main"
3333

3434
# The transport to use for the RPC server.
35-
# stdio (not ipc): the worker runs inside an nsjail mount namespace with a
36-
# private tmpfs /tmp (see the sandbox settings below), so a host unix-socket
37-
# rendezvous at /tmp/eval.sock would be unreachable. shimmy's sandbox is
38-
# designed around stdio; lf_toolkit routes its logs to stderr, keeping stdout
35+
# stdio (not ipc): the sandboxed worker runs inside an nsjail mount namespace,
36+
# so the /tmp/eval.sock IPC rendezvous shimmy would otherwise use is fragile.
37+
# stdio sidesteps it; lf_toolkit sends its logs to stderr, so stdout stays
3938
# clean for the RPC framing.
4039
ENV FUNCTION_RPC_TRANSPORT="stdio"
4140

4241
# --- Sandboxed execution of untrusted student code (shimmy + nsjail) ---
43-
# shimmy wraps the worker process -- and every `python` subprocess it spawns to
44-
# run a submission -- in an nsjail sandbox: unprivileged uid (nobody:nogroup),
45-
# a minimal bind-mounted view of the filesystem, and seccomp syscall filtering.
42+
# Always on for this function. shimmy wraps the worker process -- and every
43+
# `python` subprocess it spawns for a submission -- in an nsjail sandbox:
44+
# unprivileged uid (nobody:nogroup), a minimal bind-mounted filesystem, and a
45+
# seccomp syscall filter.
4646
#
47-
# Run-time requirements (cannot be expressed in the image):
48-
# * the container must run with --privileged (or --cap-add SYS_ADMIN) so
49-
# nsjail can create its namespaces -- see the shimmy README, "Sandboxed
50-
# Execution". Without it the worker will not boot.
51-
# * nsjail must exist at /usr/sbin/nsjail (provided by the base image's
52-
# shimmy stage).
53-
# If the worker fails to start, drop SANDBOX_SECCOMP first, then widen
54-
# SANDBOX_RO_BINDS (the list is linux/x86_64 + Debian-specific).
47+
# DEPENDS ON THE BASE IMAGE shipping nsjail. shimmy provides the `--sandbox`
48+
# feature but not the nsjail binary; `evaluation-function-base/python` currently
49+
# copies only the shimmy binary, not `/usr/sbin/nsjail` or its shared libs
50+
# (libprotobuf, libnl-route-3, libcap2). Until that is fixed upstream, a build
51+
# of this image has shimmy fail to start (missing /usr/sbin/nsjail).
52+
# Tracking: lambda-feedback/evaluation-function-base -- add nsjail to the image.
5553
#
56-
# Network is deliberately left enabled -- the function uploads matplotlib
57-
# plots to S3 via boto3. Untrusted network/filesystem use is blocked one layer
58-
# up, at the AST gate in evaluation_function/security.py (check_code_safety).
54+
# RUN-TIME: the container must run with --privileged (or --cap-add SYS_ADMIN)
55+
# so nsjail can create its namespaces (shimmy README, "Sandboxed Execution").
5956
#
60-
# No CPU/memory rlimits here: the RPC worker is long-lived and shared across
61-
# requests, so a cumulative RLIMIT_CPU / RLIMIT_AS would eventually kill it.
62-
# Per-execution wall-clock limits are enforced in evaluation.py (_TIMEOUT).
57+
# Network stays enabled -- matplotlib plots are uploaded to S3 via boto3.
58+
# Untrusted network/filesystem use is already rejected before execution by the
59+
# AST gate in evaluation_function/security.py (check_code_safety).
60+
#
61+
# /tmp is a read-write bind of the container's own /tmp (not SANDBOX_TMPFS):
62+
# nsjail's tmpfs defaults to 4 MiB, too small for matplotlib's font cache and
63+
# plot output. No CPU/memory rlimits -- the RPC worker is long-lived and shared
64+
# across requests, so a cumulative RLIMIT_CPU/AS would eventually kill it;
65+
# per-run wall-clock limits live in evaluation.py (_TIMEOUT).
66+
#
67+
# The bind list is linux/x86_64 + Debian-specific (matches the CI/prod build
68+
# platform). If the worker fails to start, drop SANDBOX_SECCOMP first.
6369
ENV SANDBOX_ENABLED="true" \
6470
SANDBOX_SECCOMP="true" \
6571
SANDBOX_RO_BINDS="/usr:/lib:/lib64:/bin:/sbin:/etc:/app" \
66-
SANDBOX_TMPFS="/tmp"
72+
SANDBOX_RW_BINDS="/tmp"
6773

6874
ENV LOG_LEVEL="debug"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import unittest
2+
3+
from .security import check_code_safety
4+
5+
6+
class TestCheckCodeSafety(unittest.TestCase):
7+
8+
def test_safe_code_returns_no_violations(self):
9+
self.assertEqual(check_code_safety("x = int(input())\nprint(x * x)"), [])
10+
11+
def test_safe_stdlib_import_allowed(self):
12+
self.assertEqual(check_code_safety("import math\nprint(math.pi)"), [])
13+
14+
def test_blocked_import(self):
15+
violations = check_code_safety("import os")
16+
self.assertEqual(violations, ["import of 'os' is not allowed"])
17+
18+
def test_blocked_from_import(self):
19+
violations = check_code_safety("from subprocess import call")
20+
self.assertEqual(violations, ["import of 'subprocess' is not allowed"])
21+
22+
def test_blocked_submodule_import(self):
23+
violations = check_code_safety("import urllib.request")
24+
self.assertEqual(violations, ["import of 'urllib' is not allowed"])
25+
26+
def test_blocked_builtin_call(self):
27+
violations = check_code_safety("exec('x = 1')")
28+
self.assertEqual(violations, ["use of 'exec()' is not allowed"])
29+
30+
def test_dunder_attribute_access(self):
31+
violations = check_code_safety("().__class__.__bases__")
32+
self.assertIn("access to '__class__' is not allowed", violations)
33+
self.assertIn("access to '__bases__' is not allowed", violations)
34+
35+
def test_syntax_error_is_not_a_violation(self):
36+
self.assertEqual(check_code_safety("def f(:\n"), [])
37+
38+
def test_multiple_violations_collected(self):
39+
violations = check_code_safety("import os\nimport socket\nopen('/etc/passwd')")
40+
self.assertIn("import of 'os' is not allowed", violations)
41+
self.assertIn("import of 'socket' is not allowed", violations)
42+
self.assertIn("use of 'open()' is not allowed", violations)

0 commit comments

Comments
 (0)