forked from johnny603/lux
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsandbox.py
More file actions
148 lines (135 loc) · 5.19 KB
/
Copy pathsandbox.py
File metadata and controls
148 lines (135 loc) · 5.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import os
import shutil
import subprocess
import tempfile
import time
import uuid
from pathlib import Path, PurePosixPath
from typing import Dict, Optional
import sandbox_audit
MAX_FILES = 16
MAX_FILENAME_BYTES = 255
MAX_FILE_BYTES = 256 * 1024
MAX_SCRIPT_BYTES = 64 * 1024
EXECUTION_TIMEOUT = 15
def _record_audit(event):
try:
sandbox_audit.record_execution(event)
except OSError:
pass
class DockerSandbox:
"""A small sandbox wrapper that hardens container execution for puzzle validation."""
def build_command(self, source_dir: str) -> list[str]:
command = [
"docker",
"run",
"--rm",
"--network",
"none",
"--read-only",
"--cap-drop",
"ALL",
"--security-opt",
"no-new-privileges",
"--pids-limit",
"64",
"--user",
"65534:65534",
"--tmpfs",
"/work:rw,nosuid,nodev,noexec,size=64m",
"-v",
f"{source_dir}:/src:ro",
"alpine:3.20",
"sh",
"-c",
"cp -R /src/. /work/ && cd /work && sh ./run.sh",
]
runtime = os.getenv("LUX_DOCKER_RUNTIME")
if runtime:
command[3:3] = ["--runtime", runtime]
return command
@staticmethod
def _safe_target(workdir: str, filename: str) -> str:
if not isinstance(filename, str) or not filename:
raise ValueError("filenames must be non-empty strings")
if len(filename.encode("utf-8")) > MAX_FILENAME_BYTES:
raise ValueError("filename is too long")
path = PurePosixPath(filename)
if path.is_absolute() or ".." in path.parts:
raise ValueError("filename must stay inside the sandbox")
root = Path(workdir).resolve()
target = (root / Path(*path.parts)).resolve()
if root not in target.parents:
raise ValueError("filename must stay inside the sandbox")
return str(target)
def run(self, files: Dict[str, str], script: str) -> Dict[str, Optional[object]]:
if not isinstance(files, dict) or len(files) > MAX_FILES:
raise ValueError(f"at most {MAX_FILES} files are allowed")
if not isinstance(script, str) or len(script.encode("utf-8")) > MAX_SCRIPT_BYTES:
raise ValueError("sandbox script is too large")
workdir = tempfile.mkdtemp(prefix="lux-sandbox-")
job_id = str(uuid.uuid4())
started = time.monotonic()
try:
for filename, content in files.items():
target = self._safe_target(workdir, filename)
if not isinstance(content, str) or len(content.encode("utf-8")) > MAX_FILE_BYTES:
raise ValueError("sandbox file is too large")
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, "w", encoding="utf-8") as handle:
handle.write(content)
with open(os.path.join(workdir, "run.sh"), "w", encoding="utf-8") as handle:
handle.write(script)
cmd = self.build_command(workdir)
try:
completed = subprocess.run(cmd, capture_output=True, timeout=EXECUTION_TIMEOUT)
timed_out = False
except subprocess.TimeoutExpired as error:
result = {
"passed": False,
"exit_code": None,
"stdout": (error.stdout or b"").decode("utf-8", errors="ignore"),
"stderr": "sandbox execution timed out",
"command": cmd,
"timed_out": True,
}
_record_audit(
{
"job_id": job_id,
"runtime": os.getenv("LUX_DOCKER_RUNTIME", "runc"),
"duration_ms": round((time.monotonic() - started) * 1000),
"exit_code": None,
"timed_out": True,
"passed": False,
}
)
return result
stdout = completed.stdout.decode("utf-8", errors="ignore")
stderr = completed.stderr.decode("utf-8", errors="ignore")
result = {
"passed": completed.returncode == 0,
"exit_code": completed.returncode,
"stdout": stdout,
"stderr": stderr,
"command": cmd,
"timed_out": timed_out,
}
_record_audit(
{
"job_id": job_id,
"runtime": os.getenv("LUX_DOCKER_RUNTIME", "runc"),
"duration_ms": round((time.monotonic() - started) * 1000),
"exit_code": completed.returncode,
"timed_out": timed_out,
"passed": result["passed"],
}
)
return result
finally:
shutil.rmtree(workdir, ignore_errors=True)
_RUNTIME: Optional[DockerSandbox] = None
def get_runtime() -> DockerSandbox:
global _RUNTIME
if _RUNTIME is None:
_RUNTIME = DockerSandbox()
return _RUNTIME