Soumik-404 commited on
Commit
273d53f
·
1 Parent(s): d6a3dd3

feat: add code sandbox

Browse files
Dockerfile CHANGED
@@ -8,6 +8,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
8
  curl \
9
  ffmpeg \
10
  libmagic1 \
 
 
 
11
  && rm -rf /var/lib/apt/lists/*
12
 
13
  RUN groupadd --gid 1000 appuser && \
 
8
  curl \
9
  ffmpeg \
10
  libmagic1 \
11
+ nodejs \
12
+ npm \
13
+ openjdk-17-jdk-headless \
14
  && rm -rf /var/lib/apt/lists/*
15
 
16
  RUN groupadd --gid 1000 appuser && \
app/api/v1/code_executor.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Optional
5
+
6
+ from fastapi import APIRouter, Depends
7
+
8
+ from app.api.deps import require_auth
9
+ from app.core.logger import get_logger
10
+ from app.models.schemas import (
11
+ CodeExecutionItemResult,
12
+ CodeExecutionRequest,
13
+ CodeExecutionResponse,
14
+ )
15
+ from app.services.code_executor_service import CodeExecutorService
16
+
17
+ router = APIRouter()
18
+ _logger = get_logger(__name__)
19
+
20
+ _executor: Optional[CodeExecutorService] = None
21
+
22
+
23
+ def get_executor() -> CodeExecutorService:
24
+ global _executor
25
+ if _executor is None:
26
+ _executor = CodeExecutorService()
27
+ return _executor
28
+
29
+
30
+ @router.post(
31
+ "/code/execute",
32
+ response_model=CodeExecutionResponse,
33
+ summary="Execute code snippets (Python, JavaScript, Java)",
34
+ )
35
+ async def execute_code(
36
+ body: CodeExecutionRequest,
37
+ token: str = Depends(require_auth),
38
+ executor: CodeExecutorService = Depends(get_executor),
39
+ ) -> CodeExecutionResponse:
40
+ _logger.info("Code execution request: items=%s", len(body.items))
41
+
42
+ start = time.perf_counter()
43
+ results: list[CodeExecutionItemResult] = []
44
+
45
+ for item in body.items:
46
+ t0 = time.perf_counter()
47
+ try:
48
+ result = await executor.execute(item.code, item.language)
49
+ elapsed = (time.perf_counter() - t0) * 1000
50
+ results.append(CodeExecutionItemResult(
51
+ success=result["success"],
52
+ output=result.get("output", ""),
53
+ error=result.get("error"),
54
+ exit_code=result.get("exit_code"),
55
+ execution_time_ms=round(elapsed, 2),
56
+ language=result.get("language", item.language),
57
+ timed_out=result.get("timed_out", False),
58
+ ))
59
+ except Exception as exc:
60
+ elapsed = (time.perf_counter() - t0) * 1000
61
+ _logger.exception("Item execution error")
62
+ results.append(CodeExecutionItemResult(
63
+ success=False,
64
+ error=str(exc),
65
+ execution_time_ms=round(elapsed, 2),
66
+ language=item.language,
67
+ ))
68
+
69
+ total_ms = (time.perf_counter() - start) * 1000
70
+ success_count = sum(1 for r in results if r.success)
71
+ failed_count = len(results) - success_count
72
+ all_ok = failed_count == 0
73
+
74
+ _logger.info(
75
+ "Code execution done: items=%s, success=%s, failed=%s, total_ms=%s",
76
+ len(results), success_count, failed_count, round(total_ms, 3),
77
+ )
78
+ return CodeExecutionResponse(
79
+ success=all_ok,
80
+ time_ms=round(total_ms, 3),
81
+ success_count=success_count,
82
+ failed_count=failed_count,
83
+ results=results,
84
+ )
app/api/v1/router.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
 
3
  from fastapi import APIRouter
4
 
5
- from app.api.v1 import batch, convert, database, embeddings, system
6
  from app.api.verify import router as verify_router
7
 
8
  api_v1_router = APIRouter()
@@ -11,4 +11,5 @@ api_v1_router.include_router(batch.router, tags=["Batch"])
11
  api_v1_router.include_router(system.router, tags=["System"])
12
  api_v1_router.include_router(database.router, tags=["Database"])
13
  api_v1_router.include_router(embeddings.router, tags=["Embeddings"])
 
14
  api_v1_router.include_router(verify_router, prefix="/verify", tags=["Verify"])
 
2
 
3
  from fastapi import APIRouter
4
 
5
+ from app.api.v1 import batch, code_executor, convert, database, embeddings, system
6
  from app.api.verify import router as verify_router
7
 
8
  api_v1_router = APIRouter()
 
11
  api_v1_router.include_router(system.router, tags=["System"])
12
  api_v1_router.include_router(database.router, tags=["Database"])
13
  api_v1_router.include_router(embeddings.router, tags=["Embeddings"])
14
+ api_v1_router.include_router(code_executor.router, tags=["Code Executor"])
15
  api_v1_router.include_router(verify_router, prefix="/verify", tags=["Verify"])
app/config.py CHANGED
@@ -35,6 +35,13 @@ class Settings(BaseSettings):
35
 
36
  self_ping_url: str = "https://aetherbase-llm-ready-data.hf.space/ping"
37
  spacy_model: str = "en_core_web_sm"
 
 
 
 
 
 
 
38
  @property
39
  def max_upload_mb(self) -> int:
40
  return self.max_upload_bytes // (1024 * 1024)
 
35
 
36
  self_ping_url: str = "https://aetherbase-llm-ready-data.hf.space/ping"
37
  spacy_model: str = "en_core_web_sm"
38
+
39
+ code_exec_max_time: int = 10
40
+ code_exec_max_memory: int = 128
41
+ code_exec_max_output: int = 65536
42
+ code_exec_max_concurrent: int = 16
43
+ code_exec_workdir: str = "/tmp/code-runner"
44
+
45
  @property
46
  def max_upload_mb(self) -> int:
47
  return self.max_upload_bytes // (1024 * 1024)
app/models/schemas.py CHANGED
@@ -266,3 +266,31 @@ class VisionUrlRequest(BaseModel):
266
  if not url.startswith(("http://", "https://")):
267
  raise ValueError(f"Invalid URL scheme: {url}")
268
  return v
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  if not url.startswith(("http://", "https://")):
267
  raise ValueError(f"Invalid URL scheme: {url}")
268
  return v
269
+
270
+
271
+ class CodeItem(BaseModel):
272
+ language: Literal["python", "javascript", "java"]
273
+ code: str = Field(..., min_length=1, max_length=65536, description="Source code to execute")
274
+
275
+
276
+ class CodeExecutionRequest(BaseModel):
277
+ items: List[CodeItem] = Field(..., min_length=1, max_length=5, description="Code execution items (max 5)")
278
+
279
+
280
+ class CodeExecutionItemResult(BaseModel):
281
+ success: bool
282
+ output: str = ""
283
+ error: Optional[str] = None
284
+ exit_code: Optional[int] = None
285
+ execution_time_ms: Optional[float] = None
286
+ language: str
287
+ timed_out: bool = False
288
+
289
+
290
+ class CodeExecutionResponse(BaseModel):
291
+ success: bool
292
+ time_ms: float
293
+ success_count: int
294
+ failed_count: int
295
+ error_message: Optional[str] = None
296
+ results: List[CodeExecutionItemResult]
app/services/code_executor_service.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import os
6
+ import re
7
+ import shutil
8
+ import signal
9
+ import subprocess
10
+ import tempfile
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from app.config import get_settings
16
+
17
+ _logger = logging.getLogger(__name__)
18
+
19
+ _APP_DIR = Path(__file__).resolve().parent
20
+ _settings = get_settings()
21
+
22
+
23
+ class CodeSanitizer:
24
+ _BLOCKED_PATTERNS: dict[str, list[tuple[str, str]]] = {
25
+ "python": [
26
+ (r"import\s+subprocess\b", "subprocess not allowed"),
27
+ (r"from\s+subprocess\b", "subprocess not allowed"),
28
+ (r"import\s+ctypes\b", "ctypes not allowed"),
29
+ (r"from\s+ctypes\b", "ctypes not allowed"),
30
+ (r"import\s+os\b", "os not allowed"),
31
+ (r"from\s+os\b", "os not allowed"),
32
+ (r"import\s+sys\b", "sys not allowed"),
33
+ (r"from\s+sys\b", "sys not allowed"),
34
+ (r"import\s+socket\b", "socket not allowed"),
35
+ (r"from\s+socket\b", "socket not allowed"),
36
+ (r"import\s+builtins\b", "builtins not allowed"),
37
+ (r"from\s+builtins\b", "builtins not allowed"),
38
+ (r"import\s+signal\b", "signal not allowed"),
39
+ (r"from\s+signal\b", "signal not allowed"),
40
+ (r"import\s+shutil\b", "shutil not allowed"),
41
+ (r"from\s+shutil\b", "shutil not allowed"),
42
+ (r"__import__\s*\(", "__import__() not allowed"),
43
+ (r"exec\s*\(", "exec() not allowed"),
44
+ (r"eval\s*\(", "eval() not allowed"),
45
+ (r"compile\s*\(", "compile() not allowed"),
46
+ (r"\.__subclasses__\s*\(\)", "subclass escape not allowed"),
47
+ (r"\.__bases__", "__bases__ not allowed"),
48
+ (r"\.__mro__", "__mro__ not allowed"),
49
+ (r"\.__globals__", "__globals__ not allowed"),
50
+ (r"\.__code__", "__code__ not allowed"),
51
+ (r"\.__closure__", "__closure__ not allowed"),
52
+ (r"\.__dict__", "__dict__ not allowed"),
53
+ (r"\.__builtins__", "__builtins__ not allowed"),
54
+ (r"\.__class__", "__class__ not allowed"),
55
+ ],
56
+ "javascript": [
57
+ (r"require\s*\(", "require() not allowed"),
58
+ (r"process\s*\.", "process not allowed"),
59
+ (r"__dirname", "__dirname not allowed"),
60
+ (r"__filename", "__filename not allowed"),
61
+ (r"global\s*\.", "global not allowed"),
62
+ (r"globalThis\s*\.", "globalThis not allowed"),
63
+ (r"eval\s*\(", "eval() not allowed"),
64
+ (r"Function\s*\(", "Function() constructor not allowed"),
65
+ (r"child_process", "child_process not allowed"),
66
+ (r"fs\s*\.", "fs not allowed"),
67
+ (r"net\s*\.", "net not allowed"),
68
+ (r"http\s*\.", "http not allowed"),
69
+ (r"https\s*\.", "https not allowed"),
70
+ (r"worker_threads", "worker_threads not allowed"),
71
+ (r"Buffer\s*\.", "Buffer not allowed"),
72
+ ],
73
+ "java": [
74
+ (r"ProcessBuilder", "ProcessBuilder not allowed"),
75
+ (r"Runtime\.exec", "Runtime.exec() not allowed"),
76
+ (r"Runtime\.getRuntime\s*\(\s*\)", "Runtime.getRuntime() not allowed"),
77
+ (r"System\.exit\s*\(", "System.exit() not allowed"),
78
+ (r"System\.gc\s*\(", "System.gc() not allowed"),
79
+ (r"File\s*\(", "File operations not allowed"),
80
+ (r"FileInputStream", "FileInputStream not allowed"),
81
+ (r"FileOutputStream", "FileOutputStream not allowed"),
82
+ (r"FileReader", "FileReader not allowed"),
83
+ (r"FileWriter", "FileWriter not allowed"),
84
+ (r"Socket\s*\(", "Socket not allowed"),
85
+ (r"ServerSocket\s*\(", "ServerSocket not allowed"),
86
+ (r"URL\s*\(", "URL not allowed"),
87
+ (r"Class\.forName", "Class.forName() not allowed"),
88
+ (r"Thread\s*\(", "Thread not allowed"),
89
+ (r"ThreadPoolExecutor", "ThreadPoolExecutor not allowed"),
90
+ (r"Runtime\.", "Runtime not allowed"),
91
+ (r"System\.getProperty", "System.getProperty not allowed"),
92
+ (r"System\.getenv", "System.getenv not allowed"),
93
+ ],
94
+ }
95
+
96
+ @classmethod
97
+ def sanitize(cls, code: str, language: str) -> tuple[bool, Optional[str]]:
98
+ patterns = cls._BLOCKED_PATTERNS.get(language, [])
99
+ for pattern, message in patterns:
100
+ if re.search(pattern, code):
101
+ return False, f"Forbidden: {message}"
102
+ return True, None
103
+
104
+ @classmethod
105
+ def validate_java_class(cls, code: str) -> tuple[bool, Optional[str]]:
106
+ if not re.search(r"public\s+class\s+Main\s*\{", code):
107
+ return False, "Java code must have 'public class Main' with 'public static void main(String[] args)'"
108
+ return True, None
109
+
110
+
111
+ class CodeExecutorService:
112
+ def __init__(self) -> None:
113
+ self._max_execution_time = _settings.code_exec_max_time
114
+ self._max_output_bytes = _settings.code_exec_max_output
115
+ self._max_memory_mb = _settings.code_exec_max_memory
116
+ self._semaphore = asyncio.Semaphore(_settings.code_exec_max_concurrent)
117
+ self._workdir = Path(_settings.code_exec_workdir)
118
+ self._workdir.mkdir(parents=True, exist_ok=True)
119
+
120
+ async def execute(
121
+ self,
122
+ code: str,
123
+ language: str,
124
+ timeout: Optional[int] = None,
125
+ ) -> dict:
126
+ sanitized, err = CodeSanitizer.sanitize(code, language)
127
+ if not sanitized:
128
+ return {
129
+ "success": False, "output": "", "error": err,
130
+ "exit_code": None, "execution_time_ms": None,
131
+ "language": language, "timed_out": False,
132
+ }
133
+
134
+ if language == "java":
135
+ valid, err = CodeSanitizer.validate_java_class(code)
136
+ if not valid:
137
+ return {
138
+ "success": False, "output": "", "error": err,
139
+ "exit_code": None, "execution_time_ms": None,
140
+ "language": language, "timed_out": False,
141
+ }
142
+
143
+ exec_timeout = min(timeout or self._max_execution_time, self._max_execution_time)
144
+
145
+ async with self._semaphore:
146
+ run_dir = None
147
+ start_time = time.monotonic()
148
+ try:
149
+ run_dir = Path(tempfile.mkdtemp(dir=self._workdir))
150
+ filename = self._filename_for(language)
151
+ src_path = run_dir / filename
152
+ src_path.write_text(code, encoding="utf-8")
153
+
154
+ cmd = self._build_command(language, run_dir, filename)
155
+ result = await asyncio.to_thread(
156
+ self._run_subprocess, cmd, exec_timeout,
157
+ )
158
+
159
+ elapsed_ms = (time.monotonic() - start_time) * 1000
160
+
161
+ if result["timed_out"]:
162
+ return {
163
+ "success": False,
164
+ "output": result["stdout"],
165
+ "error": f"Execution timed out after {exec_timeout}s",
166
+ "exit_code": result["exit_code"],
167
+ "execution_time_ms": round(elapsed_ms, 2),
168
+ "language": language,
169
+ "timed_out": True,
170
+ }
171
+
172
+ return {
173
+ "success": result["exit_code"] == 0 if result["exit_code"] is not None else False,
174
+ "output": result["stdout"],
175
+ "error": result["stderr"] or None,
176
+ "exit_code": result["exit_code"],
177
+ "execution_time_ms": round(elapsed_ms, 2),
178
+ "language": language,
179
+ "timed_out": False,
180
+ }
181
+
182
+ except FileNotFoundError as exc:
183
+ _logger.error("Runtime not found: %s", exc)
184
+ return {
185
+ "success": False, "output": "", "error": f"Runtime not found: {exc.filename}",
186
+ "execution_time_ms": None, "language": language, "timed_out": False,
187
+ }
188
+ except Exception as exc:
189
+ _logger.exception("Executor error")
190
+ return {
191
+ "success": False, "output": "", "error": f"Internal error: {exc}",
192
+ "execution_time_ms": None, "language": language, "timed_out": False,
193
+ }
194
+ finally:
195
+ if run_dir and run_dir.exists():
196
+ try:
197
+ shutil.rmtree(run_dir, ignore_errors=True)
198
+ except Exception:
199
+ pass
200
+
201
+ def _run_subprocess(
202
+ self,
203
+ cmd: list[str],
204
+ timeout: float,
205
+ ) -> dict:
206
+ proc = subprocess.Popen(
207
+ cmd,
208
+ stdin=subprocess.DEVNULL,
209
+ stdout=subprocess.PIPE,
210
+ stderr=subprocess.PIPE,
211
+ )
212
+ try:
213
+ stdout_bytes, stderr_bytes = proc.communicate(timeout=timeout)
214
+ timed_out = False
215
+ except subprocess.TimeoutExpired:
216
+ try:
217
+ if os.name == "nt":
218
+ proc.kill()
219
+ else:
220
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
221
+ except Exception:
222
+ proc.kill()
223
+ stdout_bytes, stderr_bytes = proc.communicate()
224
+ timed_out = True
225
+
226
+ max_out = self._max_output_bytes
227
+ return {
228
+ "stdout": (stdout_bytes.decode("utf-8", errors="replace")[:max_out] if stdout_bytes else ""),
229
+ "stderr": (stderr_bytes.decode("utf-8", errors="replace")[:max_out] if stderr_bytes else ""),
230
+ "exit_code": proc.returncode,
231
+ "timed_out": timed_out,
232
+ }
233
+
234
+ async def check_runtimes(self) -> dict[str, str]:
235
+ status = {}
236
+ for lang, runtime in [("python", "python3"), ("javascript", "node"), ("java", "javac")]:
237
+ path = shutil.which(runtime)
238
+ status[lang] = f"found at {path}" if path else "missing"
239
+ return status
240
+
241
+ @staticmethod
242
+ def _filename_for(language: str) -> str:
243
+ return {"python": "code.py", "javascript": "code.js", "java": "Main.java"}[language]
244
+
245
+ @staticmethod
246
+ def _build_command(language: str, run_dir: Path, filename: str) -> list[str]:
247
+ sandbox_py = _APP_DIR / "py_sandbox.py"
248
+ sandbox_js = _APP_DIR / "js_sandbox.js"
249
+ if language == "python":
250
+ return ["python3", str(sandbox_py), str(run_dir / filename)]
251
+ elif language == "javascript":
252
+ return ["node", str(sandbox_js), str(run_dir / filename)]
253
+ elif language == "java":
254
+ return ["sh", "-c",
255
+ f"cd {run_dir} && javac Main.java 2>&1 && java -XX:CompressedClassSpaceSize=64m -Xmx96m Main 2>&1"]
256
+ return []
app/services/js_sandbox.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ const vm = require('vm');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const codePath = process.argv[2];
7
+ if (!codePath) {
8
+ console.error('Usage: node js_sandbox.js <code_path>');
9
+ process.exit(1);
10
+ }
11
+
12
+ const safeConsole = {
13
+ log: console.log,
14
+ warn: console.warn,
15
+ error: console.error,
16
+ info: console.info,
17
+ };
18
+
19
+ const sandbox = {
20
+ console: safeConsole,
21
+ Math, Date, JSON,
22
+ parseInt, parseFloat, isNaN, isFinite,
23
+ NaN, Infinity, undefined: undefined, null: null,
24
+ Number, String, Boolean, Array, Object, RegExp,
25
+ Map, Set, WeakMap, WeakSet, Promise, Symbol,
26
+ Error, TypeError, RangeError, SyntaxError, ReferenceError, EvalError, URIError,
27
+ ArrayBuffer, DataView,
28
+ Int8Array, Int16Array, Int32Array,
29
+ Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array,
30
+ Float32Array, Float64Array,
31
+ BigInt, BigInt64Array, BigUint64Array,
32
+ decodeURI, decodeURIComponent, encodeURI, encodeURIComponent,
33
+ atob, btoa,
34
+ performance: { now: () => Date.now() },
35
+ setTimeout: setTimeout,
36
+ clearTimeout: clearTimeout,
37
+ setInterval: setInterval,
38
+ clearInterval: clearInterval,
39
+ };
40
+
41
+ const context = vm.createContext(sandbox);
42
+ const code = fs.readFileSync(codePath, 'utf8');
43
+
44
+ try {
45
+ vm.runInContext(code, context, {
46
+ filename: 'code.js',
47
+ timeout: 10000,
48
+ displayErrors: true,
49
+ });
50
+ } catch (err) {
51
+ console.error(err.message);
52
+ process.exit(1);
53
+ }
app/services/py_sandbox.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ import io
4
+ import os
5
+ import math as _math
6
+ import json as _json
7
+ import re as _re
8
+ import random as _random
9
+ import collections as _collections
10
+ import itertools as _itertools
11
+ import functools as _functools
12
+ import operator as _operator
13
+ import string as _string
14
+ import decimal as _decimal
15
+ import fractions as _fractions
16
+ import statistics as _statistics
17
+ import heapq as _heapq
18
+ import bisect as _bisect
19
+ import copy as _copy
20
+ import typing as _typing
21
+ import textwrap as _textwrap
22
+ import datetime as _datetime
23
+ import hashlib as _hashlib
24
+ import uuid as _uuid
25
+ import secrets as _secrets
26
+ import enum as _enum
27
+ import base64 as _base64
28
+ import time as _time
29
+
30
+ _SAFE_MODULES = {
31
+ 'math': _math, 'json': _json, 're': _re, 'random': _random,
32
+ 'collections': _collections, 'itertools': _itertools,
33
+ 'functools': _functools, 'operator': _operator, 'string': _string,
34
+ 'decimal': _decimal, 'fractions': _fractions, 'statistics': _statistics,
35
+ 'heapq': _heapq, 'bisect': _bisect, 'copy': _copy, 'typing': _typing,
36
+ 'textwrap': _textwrap, 'datetime': _datetime, 'hashlib': _hashlib,
37
+ 'uuid': _uuid, 'secrets': _secrets, 'enum': _enum, 'base64': _base64,
38
+ 'time': _time,
39
+ }
40
+
41
+ _ALLOWED_READ_DIRS = {os.path.realpath(p) for p in ('/tmp', os.getcwd()) if os.path.isdir(p)}
42
+
43
+
44
+ def _safe_import(name, *args, **kwargs):
45
+ if name in _SAFE_MODULES:
46
+ return _SAFE_MODULES[name]
47
+ raise ImportError(f"Module '{name}' is not allowed in sandbox")
48
+
49
+
50
+ def _safe_open(file, mode='r', *args, **kwargs):
51
+ if 'w' in mode or 'a' in mode or '+' in mode or 'x' in mode:
52
+ raise PermissionError("File writing is not allowed in sandbox")
53
+ real = os.path.realpath(os.path.abspath(str(file)))
54
+ allowed = False
55
+ for d in _ALLOWED_READ_DIRS:
56
+ if real.startswith(d + os.sep) or real == d:
57
+ allowed = True
58
+ break
59
+ if not allowed:
60
+ raise PermissionError(f"File '{file}' is outside allowed read directories")
61
+ if 'b' in mode:
62
+ return io.open(file, mode, *args, **kwargs)
63
+ return io.open(file, mode, *args, **kwargs)
64
+
65
+
66
+ def _safe_getattr(obj, name, *args):
67
+ if isinstance(obj, (type, object)) and name in ('__class__', '__bases__', '__subclasses__',
68
+ '__mro__', '__globals__', '__code__',
69
+ '__closure__', '__dict__', '__builtins__'):
70
+ raise AttributeError(f"Access to '{name}' is not allowed in sandbox")
71
+ if args:
72
+ return getattr(obj, name, args[0])
73
+ return getattr(obj, name)
74
+
75
+
76
+ def _safe_setattr(obj, name, value):
77
+ if name.startswith('_'):
78
+ raise AttributeError(f"Setting attribute '{name}' is not allowed in sandbox")
79
+ setattr(obj, name, value)
80
+
81
+
82
+ def _safe_delattr(obj, name):
83
+ if name.startswith('_'):
84
+ raise AttributeError(f"Deleting attribute '{name}' is not allowed in sandbox")
85
+ delattr(obj, name)
86
+
87
+
88
+ _SAFE_BUILTINS = {
89
+ '__import__': _safe_import,
90
+ 'print': print, 'len': len, 'range': range,
91
+ 'int': int, 'float': float, 'str': str, 'bool': bool,
92
+ 'list': list, 'dict': dict, 'tuple': tuple, 'set': set, 'frozenset': frozenset,
93
+ 'bytes': bytes, 'bytearray': bytearray,
94
+ 'True': True, 'False': False, 'None': None,
95
+ 'abs': abs, 'min': min, 'max': max, 'sum': sum, 'pow': pow,
96
+ 'round': round, 'divmod': divmod,
97
+ 'enumerate': enumerate, 'filter': filter, 'map': map, 'zip': zip,
98
+ 'all': all, 'any': any, 'iter': iter, 'next': next,
99
+ 'reversed': reversed, 'sorted': sorted, 'slice': slice,
100
+ 'bin': bin, 'chr': chr, 'hex': hex, 'oct': oct, 'ord': ord,
101
+ 'repr': repr, 'format': format, 'hash': hash, 'id': id,
102
+ 'type': type, 'isinstance': isinstance, 'issubclass': issubclass,
103
+ 'hasattr': hasattr, 'getattr': _safe_getattr, 'setattr': _safe_setattr,
104
+ 'delattr': _safe_delattr, 'callable': callable,
105
+ 'staticmethod': staticmethod, 'classmethod': classmethod,
106
+ 'property': property, 'object': object, 'super': super,
107
+ 'Exception': Exception, 'ValueError': ValueError, 'TypeError': TypeError,
108
+ 'KeyError': KeyError, 'IndexError': IndexError, 'AttributeError': AttributeError,
109
+ 'StopIteration': StopIteration, 'RuntimeError': RuntimeError,
110
+ 'ZeroDivisionError': ZeroDivisionError, 'ArithmeticError': ArithmeticError,
111
+ 'EOFError': EOFError, 'NameError': NameError, 'MemoryError': MemoryError,
112
+ 'NotImplementedError': NotImplementedError, 'OverflowError': OverflowError,
113
+ 'RecursionError': RecursionError, 'AssertionError': AssertionError,
114
+ 'ImportError': ImportError, 'PermissionError': PermissionError,
115
+ 'LookupError': LookupError, 'FileNotFoundError': FileNotFoundError,
116
+ 'input': input, 'open': _safe_open,
117
+ }
118
+
119
+ _FROZEN_ERR = TypeError("Cannot modify sandbox builtins")
120
+
121
+
122
+ class _FrozenDict(dict):
123
+ def __setitem__(self, key, value):
124
+ raise _FROZEN_ERR
125
+ def __delitem__(self, key):
126
+ raise _FROZEN_ERR
127
+ def pop(self, key, *args):
128
+ raise _FROZEN_ERR
129
+ def popitem(self):
130
+ raise _FROZEN_ERR
131
+ def clear(self):
132
+ raise _FROZEN_ERR
133
+ def update(self, *args, **kwargs):
134
+ raise _FROZEN_ERR
135
+ def setdefault(self, key, *args):
136
+ raise _FROZEN_ERR
137
+
138
+
139
+ def execute_code(code: str) -> None:
140
+ try:
141
+ compiled = compile(code, '<sandbox>', 'exec')
142
+ globs = {'__builtins__': _FrozenDict(_SAFE_BUILTINS)}
143
+ exec(compiled, globs)
144
+ except Exception as e:
145
+ print(str(e), file=sys.stderr)
146
+ sys.exit(1)
147
+
148
+
149
+ if __name__ == '__main__':
150
+ if len(sys.argv) > 1:
151
+ with open(sys.argv[1], 'r') as f:
152
+ code = f.read()
153
+ execute_code(code)
154
+ else:
155
+ code = sys.stdin.read()
156
+ execute_code(code)