Skip to content

Commit 553b34c

Browse files
authored
Merge pull request #76 from usnavy13/dev
fix: make sandbox uid configurable across all languages
2 parents c612471 + 43ffc7c commit 553b34c

8 files changed

Lines changed: 76 additions & 27 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ MINIO_SECRET_KEY=minioadmin
2727
# ── Sandbox Pool (Python REPL) ─────────────────────────────────
2828
# SANDBOX_POOL_ENABLED=true
2929
# SANDBOX_POOL_PY=5 # Number of pre-warmed Python REPLs
30+
# SANDBOX_POOL_PARALLEL_BATCH=1 # Safer on busy multi-tenant hosts
3031
# REPL_ENABLED=true
32+
# SANDBOX_UID=1002 # Shared host UID for all sandbox languages
3133

3234
# ── Port ──────────────────────────────────────────────────────
3335
# PORT=8000 # External port the API is reachable on

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ For comprehensive testing details, see [TESTING.md](docs/TESTING.md).
136136
- Seccomp syscall filtering restricts available system calls
137137
- Cgroup-based resource limits prevent CPU, memory, and process exhaustion
138138
- rlimits restrict file sizes, open file descriptors, etc.
139-
- Code runs as non-root user (uid 1001)
139+
- Code runs as a shared non-root sandbox user (default uid `1001`, configurable with `SANDBOX_UID`)
140140
- Read-only bind mounts for language runtimes and libraries
141141
- API key authentication protects all endpoints
142142
- Input validation prevents injection attacks

docs/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ settings.max_memory_mb
363363
│ 4. Seccomp Filtering : Restricted syscalls │
364364
│ 5. Cgroup Limits : Memory, CPU, pids │
365365
│ 6. rlimits : File size, open files, stack size │
366-
│ 7. Non-root Execution : Code runs as uid 1001 (codeuser)
366+
│ 7. Non-root Execution : Code runs as shared uid 1001 by default
367367
│ │
368368
└─────────────────────────────────────────────────────────────────────────────┘
369369
```

docs/CONFIGURATION.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ nsjail is used for secure code execution in isolated sandboxes.
147147
**Security Notes:**
148148

149149
- nsjail provides PID, mount, and network namespace isolation
150-
- Code runs as non-root user (uid 1001) inside the sandbox
150+
- Code runs as a shared non-root UID inside the sandbox
151+
- All sandbox languages default to UID `1001`, and can be moved with `SANDBOX_UID`
151152
- The API container requires `SYS_ADMIN` capability for nsjail namespace creation
152153

153154
### Resource Limits
@@ -184,6 +185,8 @@ Pre-warmed Python REPL sandboxes reduce execution latency by eliminating interpr
184185
| `SANDBOX_POOL_ENABLED` | `true` | Enable Python REPL pool |
185186
| `SANDBOX_POOL_WARMUP_ON_STARTUP` | `true` | Pre-warm Python REPLs at startup |
186187
| `SANDBOX_POOL_PY` | `5` | Number of pre-warmed Python REPLs |
188+
| `SANDBOX_POOL_PARALLEL_BATCH` | `5` | Number of warmup sandboxes started concurrently |
189+
| `SANDBOX_UID` | `1001` | Shared host UID used by all sandbox languages |
187190

188191
**Note:** Sandboxes are destroyed immediately after execution. The pool is automatically replenished in the background. Non-Python languages do not use pooling.
189192

docs/SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ Code is analyzed for potentially dangerous patterns:
106106
- **Seccomp filtering**: Restricts available system calls
107107
- **Cgroup limits**: Memory, CPU, and PID limits enforced
108108
- **rlimits**: File size, open files, and stack size restricted
109-
- **Non-root execution**: Code runs as uid 1001 (codeuser)
109+
- **Non-root execution**: Code runs as a shared non-root sandbox UID (default `1001`, configurable with `SANDBOX_UID`)
110110

111111
**Note**: The API container requires `SYS_ADMIN` capability for nsjail to create namespaces and cgroups. No Docker socket is mounted.
112112

src/config/languages.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
execution settings (commands, resource multipliers, user IDs, etc.).
55
"""
66

7+
import os
78
from dataclasses import dataclass, field
89
from typing import Dict, Optional
910

@@ -26,12 +27,33 @@ class LanguageConfig:
2627
environment: Dict[str, str] = field(default_factory=dict)
2728

2829

30+
def _get_sandbox_user_id(default: int = 1001) -> int:
31+
"""Read the shared sandbox UID override from the environment."""
32+
raw_value = os.getenv("SANDBOX_UID")
33+
if raw_value in (None, ""):
34+
return default
35+
assert raw_value is not None
36+
37+
try:
38+
user_id = int(raw_value)
39+
except ValueError as exc:
40+
raise ValueError(f"SANDBOX_UID must be an integer, got: {raw_value}") from exc
41+
42+
if user_id < 0:
43+
raise ValueError(f"SANDBOX_UID must be >= 0, got: {user_id}")
44+
45+
return user_id
46+
47+
48+
SANDBOX_USER_ID = _get_sandbox_user_id()
49+
50+
2951
# All 13 supported languages with complete configuration
3052
LANGUAGES: Dict[str, LanguageConfig] = {
3153
"py": LanguageConfig(
3254
code="py",
3355
name="Python",
34-
user_id=999,
56+
user_id=SANDBOX_USER_ID,
3557
file_extension="py",
3658
execution_command="python3 -",
3759
uses_stdin=True,
@@ -41,7 +63,7 @@ class LanguageConfig:
4163
"js": LanguageConfig(
4264
code="js",
4365
name="JavaScript",
44-
user_id=1001,
66+
user_id=SANDBOX_USER_ID,
4567
file_extension="js",
4668
execution_command="node",
4769
uses_stdin=True,
@@ -51,7 +73,7 @@ class LanguageConfig:
5173
"ts": LanguageConfig(
5274
code="ts",
5375
name="TypeScript",
54-
user_id=1001,
76+
user_id=SANDBOX_USER_ID,
5577
file_extension="ts",
5678
execution_command="tsc code.ts --outDir . --module commonjs "
5779
"--target ES2019 && node code.js",
@@ -62,7 +84,7 @@ class LanguageConfig:
6284
"go": LanguageConfig(
6385
code="go",
6486
name="Go",
65-
user_id=1001,
87+
user_id=SANDBOX_USER_ID,
6688
file_extension="go",
6789
execution_command="go build -o code code.go && ./code",
6890
uses_stdin=False,
@@ -72,7 +94,7 @@ class LanguageConfig:
7294
"java": LanguageConfig(
7395
code="java",
7496
name="Java",
75-
user_id=999,
97+
user_id=SANDBOX_USER_ID,
7698
file_extension="java",
7799
execution_command="javac Code.java && java Code",
78100
uses_stdin=False,
@@ -82,7 +104,7 @@ class LanguageConfig:
82104
"c": LanguageConfig(
83105
code="c",
84106
name="C",
85-
user_id=1001,
107+
user_id=SANDBOX_USER_ID,
86108
file_extension="c",
87109
execution_command="gcc -o code code.c && ./code",
88110
uses_stdin=False,
@@ -92,7 +114,7 @@ class LanguageConfig:
92114
"cpp": LanguageConfig(
93115
code="cpp",
94116
name="C++",
95-
user_id=1001,
117+
user_id=SANDBOX_USER_ID,
96118
file_extension="cpp",
97119
execution_command="g++ -o code code.cpp && ./code",
98120
uses_stdin=False,
@@ -102,7 +124,7 @@ class LanguageConfig:
102124
"php": LanguageConfig(
103125
code="php",
104126
name="PHP",
105-
user_id=1001,
127+
user_id=SANDBOX_USER_ID,
106128
file_extension="php",
107129
execution_command="php",
108130
uses_stdin=True,
@@ -112,7 +134,7 @@ class LanguageConfig:
112134
"rs": LanguageConfig(
113135
code="rs",
114136
name="Rust",
115-
user_id=1001,
137+
user_id=SANDBOX_USER_ID,
116138
file_extension="rs",
117139
execution_command="rustc code.rs -o code && ./code",
118140
uses_stdin=False,
@@ -122,7 +144,7 @@ class LanguageConfig:
122144
"r": LanguageConfig(
123145
code="r",
124146
name="R",
125-
user_id=1001,
147+
user_id=SANDBOX_USER_ID,
126148
file_extension="r",
127149
execution_command="Rscript code.r",
128150
uses_stdin=False,
@@ -132,7 +154,7 @@ class LanguageConfig:
132154
"f90": LanguageConfig(
133155
code="f90",
134156
name="Fortran",
135-
user_id=1001,
157+
user_id=SANDBOX_USER_ID,
136158
file_extension="f90",
137159
execution_command="gfortran -o code code.f90 && ./code",
138160
uses_stdin=False,
@@ -142,7 +164,7 @@ class LanguageConfig:
142164
"d": LanguageConfig(
143165
code="d",
144166
name="D",
145-
user_id=0,
167+
user_id=SANDBOX_USER_ID,
146168
file_extension="d",
147169
execution_command="ldc2 code.d -of=code && ./code",
148170
uses_stdin=False,
@@ -152,7 +174,7 @@ class LanguageConfig:
152174
"bash": LanguageConfig(
153175
code="bash",
154176
name="Bash",
155-
user_id=1001,
177+
user_id=SANDBOX_USER_ID,
156178
file_extension="sh",
157179
execution_command="bash",
158180
uses_stdin=True,

tests/unit/test_language_config.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
functions, and correctness of all 13 supported languages.
55
"""
66

7+
import importlib
78
import pytest
9+
import src.config.languages as languages_module
810

911
from src.config.languages import (
1012
LANGUAGES,
11-
LanguageConfig,
1213
get_language,
1314
get_supported_languages,
1415
is_supported_language,
@@ -52,7 +53,7 @@ def test_language_code_present(self, code):
5253
def test_language_config_is_frozen_dataclass(self, code):
5354
"""Each language config must be a frozen LanguageConfig dataclass."""
5455
lang = LANGUAGES[code]
55-
assert isinstance(lang, LanguageConfig)
56+
assert isinstance(lang, languages_module.LanguageConfig)
5657
with pytest.raises(AttributeError):
5758
lang.code = "modified"
5859

@@ -104,14 +105,24 @@ class TestPythonLanguage:
104105

105106
def test_python_user_id(self):
106107
lang = get_language("py")
107-
assert lang.user_id == 999
108+
assert lang.user_id == 1001
108109

109110
def test_python_uses_stdin(self):
110111
assert uses_stdin("py") is True
111112

112113
def test_python_extension(self):
113114
assert get_file_extension("py") == "py"
114115

116+
def test_python_user_id_can_be_overridden(self, monkeypatch):
117+
monkeypatch.setenv("SANDBOX_UID", "1002")
118+
reloaded = importlib.reload(languages_module)
119+
try:
120+
assert reloaded.get_user_id_for_language("py") == 1002
121+
assert reloaded.get_user_id_for_language("js") == 1002
122+
finally:
123+
monkeypatch.delenv("SANDBOX_UID", raising=False)
124+
importlib.reload(languages_module)
125+
115126

116127
class TestStdinVsFileLanguages:
117128
"""Test that stdin and file-based language sets are correct."""
@@ -137,7 +148,7 @@ class TestGetLanguage:
137148
def test_returns_config_for_known_code(self, code):
138149
result = get_language(code)
139150
assert result is not None
140-
assert isinstance(result, LanguageConfig)
151+
assert isinstance(result, languages_module.LanguageConfig)
141152
assert result.code == code
142153

143154
def test_returns_none_for_unknown(self):
@@ -187,16 +198,28 @@ class TestGetUserIdForLanguage:
187198
"""Test get_user_id_for_language() function."""
188199

189200
def test_python_user_id(self):
190-
assert get_user_id_for_language("py") == 999
201+
assert get_user_id_for_language("py") == 1001
191202

192203
def test_java_user_id(self):
193-
assert get_user_id_for_language("java") == 999
204+
assert get_user_id_for_language("java") == 1001
205+
206+
def test_all_languages_share_overridden_user_id(self, monkeypatch):
207+
monkeypatch.setenv("SANDBOX_UID", "1003")
208+
reloaded = importlib.reload(languages_module)
209+
try:
210+
assert reloaded.get_user_id_for_language("py") == 1003
211+
assert reloaded.get_user_id_for_language("java") == 1003
212+
assert reloaded.get_user_id_for_language("bash") == 1003
213+
assert reloaded.get_user_id_for_language("d") == 1003
214+
finally:
215+
monkeypatch.delenv("SANDBOX_UID", raising=False)
216+
importlib.reload(languages_module)
194217

195218
def test_bash_user_id(self):
196219
assert get_user_id_for_language("bash") == 1001
197220

198221
def test_d_user_id(self):
199-
assert get_user_id_for_language("d") == 0
222+
assert get_user_id_for_language("d") == 1001
200223

201224
def test_raises_for_unknown(self):
202225
with pytest.raises(ValueError, match="Unsupported language"):

tests/unit/test_sandbox_manager.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,8 @@ def test_get_user_id_for_language(self):
291291
manager._base_dir = Path("/tmp/test")
292292
manager._initialization_error = None
293293

294-
# Python user ID is 999
295-
assert manager.get_user_id_for_language("py") == 999
296-
# JS user ID is 1001
294+
# All sandbox languages share the same non-root UID by default
295+
assert manager.get_user_id_for_language("py") == 1001
297296
assert manager.get_user_id_for_language("js") == 1001
298297

299298
def test_close_is_noop(self):

0 commit comments

Comments
 (0)