Spaces:
Running
Running
Commit ·
273d53f
1
Parent(s): d6a3dd3
feat: add code sandbox
Browse files- Dockerfile +3 -0
- app/api/v1/code_executor.py +84 -0
- app/api/v1/router.py +2 -1
- app/config.py +7 -0
- app/models/schemas.py +28 -0
- app/services/code_executor_service.py +256 -0
- app/services/js_sandbox.js +53 -0
- app/services/py_sandbox.py +156 -0
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)
|