From 36e0ed15b62cf709b5164ba80bc3a02faf5e863c Mon Sep 17 00:00:00 2001 From: Eternity <1533512157@qq.com> Date: Fri, 30 Jan 2026 12:08:34 +0800 Subject: [PATCH] feat(sandbox): add Node.js code execution support to sandbox --- api/app/core/workflow/nodes/code/node.py | 34 ++++- api/docker-compose.yml | 2 +- sandbox/Dockerfile | 19 ++- sandbox/app/__init__.py | 4 + sandbox/app/config.py | 118 ++++++++------- sandbox/app/controllers/health_controller.py | 2 +- sandbox/app/controllers/sandbox_controller.py | 12 +- sandbox/app/core/runners/__init__.py | 39 +++++ sandbox/app/core/runners/nodejs/__init__.py | 3 + sandbox/app/core/runners/nodejs/env.py | 124 ++++++++++++++++ .../app/core/runners/nodejs/nodejs_runner.py | 138 ++++++++++++++++++ sandbox/app/core/runners/nodejs/prescript.js | 31 ++++ sandbox/app/core/runners/python/__init__.py | 7 +- sandbox/app/core/runners/python/env.py | 70 ++++++++- sandbox/app/core/runners/python/prescript.py | 7 +- .../app/core/runners/python/python_runner.py | 8 +- sandbox/app/core/runners/python/settings.py | 62 -------- sandbox/app/dependencies.py | 8 +- sandbox/app/logger.py | 24 +-- sandbox/app/middleware/concurrency.py | 94 +++++++----- sandbox/app/services/nodejs_service.py | 43 ++++++ sandbox/config.yaml | 10 +- .../nodejs/node_modules/.package-lock.json | 6 + sandbox/dependencies/nodejs/package-lock.json | 6 + sandbox/dependencies/nodejs/package.json | 1 + .../{ => python}/python-requirements.txt | 0 sandbox/lib/seccomp_nodejs/Cargo.lock | 7 - sandbox/lib/seccomp_nodejs/Cargo.toml | 6 - sandbox/lib/seccomp_nodejs/src/lib.rs | 0 .../Cargo.lock | 4 +- .../Cargo.toml | 11 +- .../src/lib.rs | 37 ++++- .../seccomp_redbear/src/nodejs_syscalls.rs | 74 ++++++++++ .../src/python_syscalls.rs} | 38 +++-- sandbox/main.py | 85 ++++------- 35 files changed, 820 insertions(+), 314 deletions(-) create mode 100644 sandbox/app/__init__.py create mode 100644 sandbox/app/core/runners/nodejs/__init__.py create mode 100644 sandbox/app/core/runners/nodejs/env.py create mode 100644 sandbox/app/core/runners/nodejs/nodejs_runner.py create mode 100644 sandbox/app/core/runners/nodejs/prescript.js delete mode 100644 sandbox/app/core/runners/python/settings.py create mode 100644 sandbox/app/services/nodejs_service.py create mode 100644 sandbox/dependencies/nodejs/node_modules/.package-lock.json create mode 100644 sandbox/dependencies/nodejs/package-lock.json create mode 100644 sandbox/dependencies/nodejs/package.json rename sandbox/dependencies/{ => python}/python-requirements.txt (100%) delete mode 100644 sandbox/lib/seccomp_nodejs/Cargo.lock delete mode 100644 sandbox/lib/seccomp_nodejs/Cargo.toml delete mode 100644 sandbox/lib/seccomp_nodejs/src/lib.rs rename sandbox/lib/{seccomp_python => seccomp_redbear}/Cargo.lock (92%) rename sandbox/lib/{seccomp_python => seccomp_redbear}/Cargo.toml (51%) rename sandbox/lib/{seccomp_python => seccomp_redbear}/src/lib.rs (82%) create mode 100644 sandbox/lib/seccomp_redbear/src/nodejs_syscalls.rs rename sandbox/lib/{seccomp_python/src/syscalls.rs => seccomp_redbear/src/python_syscalls.rs} (90%) diff --git a/api/app/core/workflow/nodes/code/node.py b/api/app/core/workflow/nodes/code/node.py index b2a4da32..892708f2 100644 --- a/api/app/core/workflow/nodes/code/node.py +++ b/api/app/core/workflow/nodes/code/node.py @@ -14,7 +14,7 @@ from app.core.workflow.nodes.code.config import CodeNodeConfig logger = logging.getLogger(__name__) -SCRIPT_TEMPLATE = Template(dedent(""" +PYTHON_SCRIPT_TEMPLATE = Template(dedent(""" $code import json @@ -32,6 +32,20 @@ result = "<>" + output_json + "<>" print(result) """)) +NODEJS_SCRIPT_TEMPLATE = Template(dedent(""" +$code +// decode and prepare input object +var inputs_obj = JSON.parse(Buffer.from('$inputs_variable', 'base64').toString('utf-8')) + +// execute main function +var output_obj = main(inputs_obj) + +// convert output to json and print +var output_json = JSON.stringify(output_obj) +var result = `<>$${output_json}<>` +console.log(result) +""")) + class CodeNode(BaseNode): def __init__(self, node_config: dict[str, Any], workflow_config: dict[str, Any]): @@ -83,6 +97,7 @@ class CodeNode(BaseNode): input_variable_dict = {} for input_variable in self.typed_config.input_variables: input_variable_dict[input_variable.name] = self.get_variable(input_variable.variable, state) + code = base64.b64decode( self.typed_config.code ).decode("utf-8") @@ -90,11 +105,18 @@ class CodeNode(BaseNode): input_variable_dict = base64.b64encode( json.dumps(input_variable_dict).encode("utf-8") ).decode("utf-8") - - final_script = SCRIPT_TEMPLATE.substitute( - code=code, - inputs_variable=input_variable_dict, - ) + if self.typed_config.language == "python3": + final_script = PYTHON_SCRIPT_TEMPLATE.substitute( + code=code, + inputs_variable=input_variable_dict, + ) + elif self.typed_config.language == 'nodejs': + final_script = NODEJS_SCRIPT_TEMPLATE.substitute( + code=code, + inputs_variable=input_variable_dict, + ) + else: + raise ValueError(f"Unsupported language: {self.typed_config.language}") async with httpx.AsyncClient() as client: response = await client.post( diff --git a/api/docker-compose.yml b/api/docker-compose.yml index f30220cb..4f855700 100644 --- a/api/docker-compose.yml +++ b/api/docker-compose.yml @@ -69,7 +69,7 @@ services: container_name: sandbox ports: - "8194" - command: /code/.venv/bin/python main.py + command: /code/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8194 --log-level debug restart: unless-stopped networks: - sandbox diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index 677b991c..e34b88dd 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -1,9 +1,10 @@ FROM python:3.12-slim USER root WORKDIR /code -LABEL authors="Eterntiy" -ARG NEED_MIRROR=0 +ARG NEED_MIRROR=1 +ENV DEBIAN_FRONTEND=noninteractive + RUN --mount=type=cache,id=mem_apt,target=/var/cache/apt,sharing=locked \ if [ "$NEED_MIRROR" == "1" ]; then \ @@ -17,11 +18,14 @@ RUN --mount=type=cache,id=mem_apt,target=/var/cache/apt,sharing=locked \ apt --no-install-recommends install -y ca-certificates && \ apt update && \ apt install -y python3-pip pipx nginx unzip curl wget git vim less && \ + apt install -y nodejs npm && \ apt-get install -y --no-install-recommends tzdata libseccomp2 libseccomp-dev && \ ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ echo "Asia/Shanghai" > /etc/timezone && \ apt install -y cargo +ENV PYTHONDONTWRITEBYTECODE=1 + COPY ./app /code/app COPY ./dependencies /code/dependencies COPY ./lib /code/lib @@ -33,10 +37,15 @@ COPY ./requirements.txt /code/requirements.txt RUN python -m venv .venv RUN .venv/bin/python3 -m pip install -r requirements.txt -RUN cargo build --release --manifest-path lib/seccomp_python/Cargo.toml +RUN npm install --prefix=/code/dependencies/nodejs koffi -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ +RUN cargo build --release --manifest-path lib/seccomp_redbear/Cargo.toml --features python3 +RUN mv lib/seccomp_redbear/target/release/libsandbox.so lib/seccomp_redbear/target/release/libpython.so +RUN cargo build --release --manifest-path lib/seccomp_redbear/Cargo.toml --features nodejs +RUN mv lib/seccomp_redbear/target/release/libsandbox.so lib/seccomp_redbear/target/release/libnodejs.so + +HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \ CMD curl 127.0.0.1:8194/health -CMD [".venv/bin/python3", "main.py"] \ No newline at end of file +CMD [".venv/bin/uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8194", "--log-level", "debug"] \ No newline at end of file diff --git a/sandbox/app/__init__.py b/sandbox/app/__init__.py new file mode 100644 index 00000000..1b201ce5 --- /dev/null +++ b/sandbox/app/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: UTF-8 -*- +# Author: Eternity +# @Email: 1533512157@qq.com +# @Time : 2026/1/29 14:33 diff --git a/sandbox/app/config.py b/sandbox/app/config.py index 3fa4cab5..e4930465 100644 --- a/sandbox/app/config.py +++ b/sandbox/app/config.py @@ -4,9 +4,6 @@ from typing import List, Optional from pydantic import BaseModel, Field import yaml -SANDBOX_USER_ID = 1000 -SANDBOX_GROUP_ID = 1000 - DEFAULT_PYTHON_LIB_REQUIREMENTS_AMD = [ "/usr/local/lib/python3.12", "/usr/lib/python3", @@ -15,13 +12,18 @@ DEFAULT_PYTHON_LIB_REQUIREMENTS_AMD = [ "/etc/nsswitch.conf", "/etc/hosts", "/etc/resolv.conf", - "/run/systemd/resolve/stub-resolv.conf", - "/run/resolvconf/resolv.conf", "/etc/localtime", "/usr/share/zoneinfo", "/etc/timezone", ] +DEFAULT_NODEJS_LIB_REQUIREMENTS = [ + "/etc/ssl/certs/ca-certificates.crt", + "/etc/nsswitch.conf", + "/etc/resolv.conf", + "/etc/hosts", +] + class AppConfig(BaseModel): """Application configuration""" @@ -43,83 +45,77 @@ class Config(BaseModel): max_workers: int = 4 max_requests: int = 50 worker_timeout: int = 30 - nodejs_path: str = "node" + enable_network: bool = True enable_preload: bool = False python_path: str = "" python_lib_paths: list = Field(default=DEFAULT_PYTHON_LIB_REQUIREMENTS_AMD) python_deps_update_interval: str = "30m" + + nodejs_path: str = "" + nodejs_lib_paths: list = Field(default=DEFAULT_NODEJS_LIB_REQUIREMENTS) + allowed_syscalls: List[int] = Field(default_factory=list) proxy: ProxyConfig = Field(default_factory=ProxyConfig) + sandbox_user: str = "sandbox" + sandbox_uid: int = 65537 + sandbox_gid: int = 0 + + def set_sandbox_gid(self, gid: int): + """Update sandbox GID dynamically""" + self.sandbox_gid = gid + + def override_with_env(self): + """Override configuration with environment variables""" + env_map = { + "DEBUG": ("app.debug", lambda v: v.lower() in ("true", "1", "yes")), + "MAX_WORKERS": ("max_workers", int), + "MAX_REQUESTS": ("max_requests", int), + "SANDBOX_PORT": ("app.port", int), + "WORKER_TIMEOUT": ("worker_timeout", int), + "API_KEY": ("app.key", str), + "NODEJS_PATH": ("nodejs_path", str), + "ENABLE_NETWORK": ("enable_network", lambda v: v.lower() in ("true", "1", "yes")), + "ENABLE_PRELOAD": ("enable_preload", lambda v: v.lower() in ("true", "1", "yes")), + "ALLOWED_SYSCALLS": ("allowed_syscalls", lambda v: [int(x) for x in v.split(",")]), + "SOCKS5_PROXY": ("proxy.socks5", str), + "HTTP_PROXY": ("proxy.http", str), + "HTTPS_PROXY": ("proxy.https", str), + "PYTHON_PATH": ("python_path", str), + "PYTHON_LIB_PATH": ("python_lib_paths", lambda v: v.split(",")), + "PYTHON_DEPS_UPDATE_INTERVAL": ("python_deps_update_interval", str), + "NODEJS_LIB_PATH": ("nodejs_lib_paths", lambda v: v.split(",")), + } + + for env_var, (attr_path, cast) in env_map.items(): + value = os.getenv(env_var) + if value is not None: + # Support nested attributes like 'app.debug' + parts = attr_path.split(".") + obj = self + for part in parts[:-1]: + obj = getattr(obj, part) + setattr(obj, parts[-1], cast(value)) + # Global configuration instance _config: Optional[Config] = None -def load_config(config_path: str) -> Config: - """Load configuration from YAML file""" +def load_config(config_path: str = "config.yaml") -> Config: + """Load configuration from YAML file and override with env variables""" global _config - - # Load from file if os.path.exists(config_path): with open(config_path, 'r') as f: - data = yaml.safe_load(f) + data = yaml.safe_load(f) or {} _config = Config(**data) else: _config = Config() - # Override with environment variables - if os.getenv("DEBUG"): - _config.app.debug = os.getenv("DEBUG").lower() in ("true", "1", "yes") - - if os.getenv("MAX_WORKERS"): - _config.max_workers = int(os.getenv("MAX_WORKERS")) - - if os.getenv("MAX_REQUESTS"): - _config.max_requests = int(os.getenv("MAX_REQUESTS")) - - if os.getenv("SANDBOX_PORT"): - _config.app.port = int(os.getenv("SANDBOX_PORT")) - - if os.getenv("WORKER_TIMEOUT"): - _config.worker_timeout = int(os.getenv("WORKER_TIMEOUT")) - - if os.getenv("API_KEY"): - _config.app.key = os.getenv("API_KEY") - - if os.getenv("NODEJS_PATH"): - _config.nodejs_path = os.getenv("NODEJS_PATH") - - if os.getenv("ENABLE_NETWORK"): - _config.enable_network = os.getenv("ENABLE_NETWORK").lower() in ("true", "1", "yes") - - if os.getenv("ENABLE_PRELOAD"): - _config.enable_preload = os.getenv("ENABLE_PRELOAD").lower() in ("true", "1", "yes") - - if os.getenv("ALLOWED_SYSCALLS"): - _config.allowed_syscalls = [int(x) for x in os.getenv("ALLOWED_SYSCALLS").split(",")] - - if os.getenv("SOCKS5_PROXY"): - _config.proxy.socks5 = os.getenv("SOCKS5_PROXY") - - if os.getenv("HTTP_PROXY"): - _config.proxy.http = os.getenv("HTTP_PROXY") - - if os.getenv("HTTPS_PROXY"): - _config.proxy.https = os.getenv("HTTPS_PROXY") - - # python - if os.getenv("PYTHON_PATH"): - _config.python_path = os.getenv("PYTHON_PATH") - - if os.getenv("PYTHON_LIB_PATH"): - _config.python_lib_paths = os.getenv("PYTHON_LIB_PATH").split(',') - - if os.getenv("PYTHON_DEPS_UPDATE_INTERVAL"): - _config.python_deps_update_interval = os.getenv("PYTHON_DEPS_UPDATE_INTERVAL") - + # Override from environment + _config.override_with_env() return _config diff --git a/sandbox/app/controllers/health_controller.py b/sandbox/app/controllers/health_controller.py index 4d872e58..882578ec 100644 --- a/sandbox/app/controllers/health_controller.py +++ b/sandbox/app/controllers/health_controller.py @@ -9,4 +9,4 @@ router = APIRouter() @router.get("/health", response_model=HealthResponse) async def health_check(): """Health check endpoint""" - return HealthResponse(status="healthy", version="2.0.0") + return HealthResponse(status="healthy", version="0.1.0") diff --git a/sandbox/app/controllers/sandbox_controller.py b/sandbox/app/controllers/sandbox_controller.py index 1a713f52..c5cce40c 100644 --- a/sandbox/app/controllers/sandbox_controller.py +++ b/sandbox/app/controllers/sandbox_controller.py @@ -2,13 +2,15 @@ from fastapi import APIRouter, Depends from app.middleware.auth import verify_api_key -from app.middleware.concurrency import check_max_requests, acquire_worker +from app.middleware.concurrency import concurrency_guard + from app.models import ( RunCodeRequest, ApiResponse, UpdateDependencyRequest, error_response ) +from app.services.nodejs_service import run_nodejs_code from app.services.python_service import ( run_python_code, list_python_dependencies, @@ -25,16 +27,14 @@ router = APIRouter( @router.post( "/run", response_model=ApiResponse, - dependencies=[Depends(check_max_requests), - Depends(acquire_worker)] + dependencies=[Depends(concurrency_guard)] ) async def run_code(request: RunCodeRequest): """Execute code in sandbox""" if request.language == "python3": return await run_python_code(request.code, request.preload, request.options) elif request.language == "nodejs": - # TODO - return error_response(-400, "TODO") + return await run_nodejs_code(request.code, request.preload, request.options) else: return error_response(-400, "unsupported language") @@ -55,5 +55,3 @@ async def update_dependencies(request: UpdateDependencyRequest): return await update_python_dependencies() else: return error_response(-400, "unsupported language") - - diff --git a/sandbox/app/core/runners/__init__.py b/sandbox/app/core/runners/__init__.py index 96c5e380..b8021009 100644 --- a/sandbox/app/core/runners/__init__.py +++ b/sandbox/app/core/runners/__init__.py @@ -1 +1,40 @@ """Code runners package""" +import pwd +import subprocess + +from app.config import get_config +from app.logger import get_logger + +logger = get_logger() + + +def init_sandbox_user(): + config = get_config() + sandbox_user = config.sandbox_user + sandbox_uid = config.sandbox_uid + try: + pwd.getpwnam(sandbox_user) + logger.info(f"User '{sandbox_user}' already exists") + except KeyError: + try: + subprocess.run( + ["useradd", "-u", str(sandbox_uid), sandbox_user], + check=True, + capture_output=True, + text=True + ) + logger.info(f"Created user '{sandbox_user}' with UID {sandbox_uid}") + except subprocess.CalledProcessError as e: + logger.error(f"Failed to create user: {e.stderr}") + raise RuntimeError(f"Failed to create user '{sandbox_user}': {e.stderr}") from e + + try: + user_info = pwd.getpwnam(sandbox_user) + config.set_sandbox_gid(user_info.pw_gid) + logger.info(f"Sandbox user GID: {config.sandbox_gid}") + except KeyError as e: + logger.error(f"Failed to get GID for user '{sandbox_user}'") + raise RuntimeError(f"Failed to get GID for user '{sandbox_user}'") from e + + + diff --git a/sandbox/app/core/runners/nodejs/__init__.py b/sandbox/app/core/runners/nodejs/__init__.py new file mode 100644 index 00000000..fa5243b7 --- /dev/null +++ b/sandbox/app/core/runners/nodejs/__init__.py @@ -0,0 +1,3 @@ +from app.core.runners.nodejs.env import release_lib_binary + +release_lib_binary(True) diff --git a/sandbox/app/core/runners/nodejs/env.py b/sandbox/app/core/runners/nodejs/env.py new file mode 100644 index 00000000..8c6a55aa --- /dev/null +++ b/sandbox/app/core/runners/nodejs/env.py @@ -0,0 +1,124 @@ +import asyncio +import ctypes +import os +import shutil +import stat +import tempfile +from pathlib import Path + +from app.logger import get_logger +from app.config import get_config + +logger = get_logger() + +RELEASE_LIB_PATH = "./lib/seccomp_redbear/target/release/libnodejs.so" +LIB_PATH = "/var/sandbox/sandbox-nodejs" +LIB_NAME = "libnodejs.so" + +lib = ctypes.CDLL(RELEASE_LIB_PATH) +lib.get_lib_version_static.restype = ctypes.c_char_p +lib.get_lib_feature_static.restype = ctypes.c_char_p +logger.info(f"Seccomp Env: nodejs, " + f"Seccomp Feature: {lib.get_lib_feature_static().decode('utf-8')}, " + f"Seccomp Version: {lib.get_lib_version_static().decode('utf-8')}") + +try: + with open(RELEASE_LIB_PATH, "rb") as f: + _NODEJS_LIB = f.read() +except: + logger.critical("failed to load nodejs lib") + raise + + +def check_lib_avaiable(): + return os.path.exists(os.path.join(LIB_PATH, LIB_NAME)) + + +def release_lib_binary(force_remove: bool): + logger.info("init runtime enviroment") + + lib_file = os.path.join(LIB_PATH, LIB_NAME) + if os.path.exists(lib_file): + if force_remove: + try: + os.remove(lib_file) + except OSError: + logger.critical(f"failed to remove {os.path.join(LIB_PATH, LIB_NAME)}") + raise + + try: + os.makedirs(LIB_PATH, mode=0o755, exist_ok=True) + except OSError: + logger.critical(f"failed to create {LIB_PATH}") + raise + + try: + with open(lib_file, "wb") as f: + f.write(_NODEJS_LIB) + os.chmod(lib_file, 0o755) + except OSError: + logger.critical(f"failed to write {lib_file}") + raise + else: + try: + os.makedirs(LIB_PATH, mode=0o755, exist_ok=True) + except OSError: + logger.critical(f"failed to create {LIB_PATH}") + raise + + try: + with open(lib_file, "wb") as f: + f.write(_NODEJS_LIB) + os.chmod(lib_file, 0o755) + except OSError: + logger.critical(f"failed to write {lib_file}") + raise + + logger.info("nodejs runner environment initialized") + + +async def prepare_nodejs_dependencies_env(): + config = get_config() + + with tempfile.TemporaryDirectory(dir="/") as root_path: + root = Path(root_path) + + env_sh = root / "env.sh" + with open("script/env.sh") as f: + env_sh.write_text(f.read()) + env_sh.chmod(env_sh.stat().st_mode | stat.S_IXUSR) + + shutil.copytree("dependencies/nodejs", os.path.join(LIB_PATH, "node_temp"), dirs_exist_ok=True) + for root, dirs, files in os.walk(os.path.join(LIB_PATH, "node_temp")): + for d in dirs: + os.chmod(os.path.join(root, d), 0o755) + for f in files: + os.chmod(os.path.join(root, f), 0o444) + + for lib_path in config.nodejs_lib_paths: + lib_path = Path(lib_path) + + if not lib_path.exists(): + logger.warning("nodejs lib path %s is not available", lib_path) + continue + + cmd = [ + "bash", + str(env_sh), + str(lib_path), + str(LIB_PATH), + ] + + process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + stdout, stderr = await process.communicate() + retcode = process.returncode + + if retcode != 0: + logger.error( + f"create env error for file {lib_path}: retcode={retcode}, stderr={stderr.decode()}" + ) diff --git a/sandbox/app/core/runners/nodejs/nodejs_runner.py b/sandbox/app/core/runners/nodejs/nodejs_runner.py new file mode 100644 index 00000000..59560eee --- /dev/null +++ b/sandbox/app/core/runners/nodejs/nodejs_runner.py @@ -0,0 +1,138 @@ +"""Nodejs code runner""" +import asyncio +import os +import uuid +from typing import Optional + +from app.core.executor import CodeExecutor, ExecutionResult +from app.core.runners.nodejs.env import check_lib_avaiable, release_lib_binary, LIB_PATH +from app.logger import get_logger +from app.models import RunnerOptions + +# Nodejs sandbox prescript template +with open("app/core/runners/nodejs/prescript.js") as f: + NODEJS_PRESCRIPT = f.read() + +logger = get_logger() + + +class NodejsRunner(CodeExecutor): + """Node.js code runner with security isolation""" + + def __init__(self): + super().__init__() + + @staticmethod + def init_environment(code: str, preload: str) -> str: + if not check_lib_avaiable(): + release_lib_binary(False) + code_file_name = uuid.uuid4().hex.replace("-", "_") + + script = NODEJS_PRESCRIPT.replace("{{preload}}", preload, 1) + + eval_code = f"eval(Buffer.from('{code}', 'base64').toString('utf-8'))" + script = script.replace("{{code}}", eval_code, 1) + + code_path = f"{LIB_PATH}/node_temp/tmp/{code_file_name}.js" + try: + os.makedirs(os.path.dirname(code_path), mode=0o755, exist_ok=True) + with open(code_path, "w", encoding="utf-8") as f: + f.write(script) + os.chmod(code_path, 0o755) + + except OSError as e: + raise RuntimeError(f"Failed to write {code_path}") from e + + return code_path + + async def run( + self, + code: str, + options: RunnerOptions, + preload: str = "", + timeout: Optional[int] = None + ) -> ExecutionResult: + """Run Python code in sandbox + + Args: + options: + code: Base64 encoded encrypted code + preload: Preload code to execute before main code + timeout: Execution timeout in seconds + + Returns: + ExecutionResult with stdout, stderr, and exit code + """ + config = self.config + + if timeout is None: + timeout = config.worker_timeout + + # Check if preload is allowed + if not preload or not config.enable_preload: + preload = "" + script_path = self.init_environment(code, preload) + + try: + # Setup environment + env = { + "UV_USE_IO_URING": "0" + } + + # Add proxy settings if configured + if config.proxy.socks5: + env["HTTPS_PROXY"] = config.proxy.socks5 + env["HTTP_PROXY"] = config.proxy.socks5 + elif config.proxy.https or config.proxy.http: + if config.proxy.https: + env["HTTPS_PROXY"] = config.proxy.https + if config.proxy.http: + env["HTTP_PROXY"] = config.proxy.http + + # Add allowed syscalls if configured + if config.allowed_syscalls: + env["ALLOWED_SYSCALLS"] = ",".join(map(str, config.allowed_syscalls)) + + process = await asyncio.create_subprocess_exec( + config.nodejs_path, + script_path, + LIB_PATH, + str(config.sandbox_uid), + str(config.sandbox_gid), + options.model_dump_json(), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=env, + cwd=LIB_PATH + ) + + # Wait for completion with timeout + try: + stdout, stderr = await asyncio.wait_for( + process.communicate(), + timeout=timeout + ) + + return ExecutionResult( + stdout=stdout.decode('utf-8', errors='replace'), + stderr=stderr.decode('utf-8', errors='replace'), + exit_code=process.returncode + ) + + except asyncio.TimeoutError: + # Kill process on timeout + try: + process.kill() + await process.wait() + except: + pass + + return ExecutionResult( + stdout="", + stderr="Execution timeout", + exit_code=-1, + ) + + finally: + # Cleanup temporary file + self.cleanup_temp_file(script_path) diff --git a/sandbox/app/core/runners/nodejs/prescript.js b/sandbox/app/core/runners/nodejs/prescript.js new file mode 100644 index 00000000..460aa108 --- /dev/null +++ b/sandbox/app/core/runners/nodejs/prescript.js @@ -0,0 +1,31 @@ +let argv = process.argv + +let koffi = require('koffi') + +process.chdir(argv[2]) + +let lib = koffi.load("./libnodejs.so") +/** @type {(uid: number, gid: number, enableNetwork: boolean) => number} */ +let initSeccomp = lib.func('int init_seccomp(int, int, bool)') + +let uid = parseInt(argv[3]) +let gid = parseInt(argv[4]) + +let options = JSON.parse(argv[5]) + +let seccomp_init = initSeccomp(uid, gid, options['enable_network']) +if (seccomp_init !== 0) { + throw `code executor err - ${seccomp_init}` +} + +delete process.argv +argv = undefined +koffi = undefined +lib = undefined +initSeccomp = undefined +uid = undefined +gid = undefined +options = undefined +seccomp_init = undefined + +{{code}} diff --git a/sandbox/app/core/runners/python/__init__.py b/sandbox/app/core/runners/python/__init__.py index 99a56ef7..e1a34906 100644 --- a/sandbox/app/core/runners/python/__init__.py +++ b/sandbox/app/core/runners/python/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: UTF-8 -*- -# Author: Eternity -# @Email: 1533512157@qq.com -# @Time : 2026/1/23 11:27 +from app.core.runners.python.env import release_lib_binary + +release_lib_binary(True) diff --git a/sandbox/app/core/runners/python/env.py b/sandbox/app/core/runners/python/env.py index d82b0522..541acc73 100644 --- a/sandbox/app/core/runners/python/env.py +++ b/sandbox/app/core/runners/python/env.py @@ -1,14 +1,80 @@ import asyncio -import tempfile +import ctypes +import os import stat +import tempfile from pathlib import Path from app.config import get_config -from app.core.runners.python.settings import LIB_PATH from app.logger import get_logger logger = get_logger() +RELEASE_LIB_PATH = "./lib/seccomp_redbear/target/release/libpython.so" +LIB_PATH = "/var/sandbox/sandbox-python" +LIB_NAME = "libpython.so" + +lib = ctypes.CDLL(RELEASE_LIB_PATH) +lib.get_lib_version_static.restype = ctypes.c_char_p +lib.get_lib_feature_static.restype = ctypes.c_char_p +logger.info(f"Seccomp Env: python3, " + f"Seccomp Feature: {lib.get_lib_feature_static().decode('utf-8')}, " + f"Seccomp Version: {lib.get_lib_version_static().decode('utf-8')}") + +try: + with open(RELEASE_LIB_PATH, "rb") as f: + _PYTHON_LIB = f.read() +except: + logger.critical("failed to load python lib") + raise + + +def check_lib_avaiable(): + return os.path.exists(os.path.join(LIB_PATH, LIB_NAME)) + + +def release_lib_binary(force_remove: bool): + logger.info("init runtime enviroment") + + lib_file = os.path.join(LIB_PATH, LIB_NAME) + if os.path.exists(lib_file): + if force_remove: + try: + os.remove(lib_file) + except OSError: + logger.critical(f"failed to remove {os.path.join(LIB_PATH, LIB_NAME)}") + raise + + try: + os.makedirs(LIB_PATH, mode=0o755, exist_ok=True) + except OSError: + logger.critical(f"failed to create {LIB_PATH}") + raise + + try: + with open(lib_file, "wb") as f: + f.write(_PYTHON_LIB) + os.chmod(lib_file, 0o755) + except OSError: + logger.critical(f"failed to write {lib_file}") + raise + else: + try: + os.makedirs(LIB_PATH, mode=0o755, exist_ok=True) + except OSError: + logger.critical(f"failed to create {LIB_PATH}") + raise + + try: + with open(lib_file, "wb") as f: + f.write(_PYTHON_LIB) + os.chmod(lib_file, 0o755) + except OSError: + logger.critical(f"failed to write {lib_file}") + raise + + logger.info("python runner environment initialized") + async def prepare_python_dependencies_env(): config = get_config() diff --git a/sandbox/app/core/runners/python/prescript.py b/sandbox/app/core/runners/python/prescript.py index 950710ea..b694fe9b 100644 --- a/sandbox/app/core/runners/python/prescript.py +++ b/sandbox/app/core/runners/python/prescript.py @@ -17,7 +17,7 @@ sys.excepthook = excepthook # Load security library if available lib = ctypes.CDLL("./libpython.so") lib.init_seccomp.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool] -lib.init_seccomp.restype = None # TODO: raise error info +lib.init_seccomp.restype = ctypes.c_int # Get running path running_path = sys.argv[1] @@ -37,7 +37,10 @@ os.chdir(running_path) {{preload}} # Apply security if library is available -lib.init_seccomp({{uid}}, {{gid}}, {{enable_network}}) +init_status = lib.init_seccomp({{uid}}, {{gid}}, {{enable_network}}) +if init_status != 0: + raise Exception(f"code executor err - {str(init_status)}") +del lib # Decrypt and execute code code = b64decode("{{code}}") diff --git a/sandbox/app/core/runners/python/python_runner.py b/sandbox/app/core/runners/python/python_runner.py index 30792b91..eccd16e0 100644 --- a/sandbox/app/core/runners/python/python_runner.py +++ b/sandbox/app/core/runners/python/python_runner.py @@ -5,10 +5,10 @@ import os import uuid from typing import Optional -from app.config import SANDBOX_USER_ID, SANDBOX_GROUP_ID, get_config +from app.config import get_config from app.core.encryption import generate_key, encrypt_code from app.core.executor import CodeExecutor, ExecutionResult -from app.core.runners.python.settings import check_lib_avaiable, release_lib_binary, LIB_PATH +from app.core.runners.python.env import check_lib_avaiable, release_lib_binary, LIB_PATH from app.logger import get_logger from app.models import RunnerOptions @@ -32,8 +32,8 @@ class PythonRunner(CodeExecutor): config = get_config() code_file_name = uuid.uuid4().hex.replace("-", "_") - script = PYTHON_PRESCRIPT.replace("{{uid}}", str(SANDBOX_USER_ID), 1) - script = script.replace("{{gid}}", str(SANDBOX_GROUP_ID), 1) + script = PYTHON_PRESCRIPT.replace("{{uid}}", str(config.sandbox_uid), 1) + script = script.replace("{{gid}}", str(config.sandbox_gid), 1) script = script.replace( "{{enable_network}}", str(int(options.enable_network and config.enable_network) diff --git a/sandbox/app/core/runners/python/settings.py b/sandbox/app/core/runners/python/settings.py deleted file mode 100644 index aee8827b..00000000 --- a/sandbox/app/core/runners/python/settings.py +++ /dev/null @@ -1,62 +0,0 @@ -import os - -from app.logger import get_logger - -logger = get_logger() - -RELEASE_LIB_PATH = "./lib/seccomp_python/target/release/libpython.so" -LIB_PATH = "/var/sandbox/sandbox-python" -LIB_NAME = "libpython.so" - -try: - with open(RELEASE_LIB_PATH, "rb") as f: - _PYTHON_LIB = f.read() -except: - logger.critical("failed to load python lib") - raise - - -def check_lib_avaiable(): - return os.path.exists(os.path.join(LIB_PATH, LIB_NAME)) - - -def release_lib_binary(force_remove: bool): - logger.info("init runtime enviroment") - lib_file = os.path.join(LIB_PATH, LIB_NAME) - if os.path.exists(lib_file): - if force_remove: - try: - os.remove(lib_file) - except OSError: - logger.critical(f"failed to remove {os.path.join(LIB_PATH, LIB_NAME)}") - raise - - try: - os.makedirs(LIB_PATH, mode=0o755, exist_ok=True) - except OSError: - logger.critical(f"failed to create {LIB_PATH}") - raise - - try: - with open(lib_file, "wb") as f: - f.write(_PYTHON_LIB) - os.chmod(lib_file, 0o755) - except OSError: - logger.critical(f"failed to write {lib_file}") - raise - else: - try: - os.makedirs(LIB_PATH, mode=0o755, exist_ok=True) - except OSError: - logger.critical(f"failed to create {LIB_PATH}") - raise - - try: - with open(lib_file, "wb") as f: - f.write(_PYTHON_LIB) - os.chmod(lib_file, 0o755) - except OSError: - logger.critical(f"failed to write {lib_file}") - raise - - logger.info("python runner environment initialized") diff --git a/sandbox/app/dependencies.py b/sandbox/app/dependencies.py index 6e88aaf2..6fe05ee4 100644 --- a/sandbox/app/dependencies.py +++ b/sandbox/app/dependencies.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import List, Dict from app.config import get_config +from app.core.runners.nodejs.env import prepare_nodejs_dependencies_env from app.core.runners.python.env import prepare_python_dependencies_env from app.logger import get_logger @@ -19,7 +20,10 @@ async def setup_dependencies(): logger.info("Preparing Python dependencies environment...") await prepare_python_dependencies_env() - logger.info("Python dependencies environment ready") + logger.info("Python Environment Ready ....") + logger.info("Preparing Nodejs dependencies environment...") + await prepare_nodejs_dependencies_env() + logger.info("Nodejs Environment Ready ...") except Exception as e: logger.error(f"Failed to setup dependencies: {e}") @@ -36,7 +40,7 @@ async def install_python_dependencies(): config = get_config() # Check if requirements file exists - req_file = Path("dependencies/python-requirements.txt") + req_file = Path("dependencies/python/python-requirements.txt") if not req_file.exists(): logger.warning("Python requirements file not found, skipping installation") return diff --git a/sandbox/app/logger.py b/sandbox/app/logger.py index de2ccc9e..9e63c8e5 100644 --- a/sandbox/app/logger.py +++ b/sandbox/app/logger.py @@ -12,25 +12,27 @@ def setup_logger() -> logging.Logger: """Setup application logger""" global _logger + if _logger is not None: + return _logger + config = get_config() # Create logger _logger = logging.getLogger("sandbox") _logger.setLevel(logging.DEBUG if config.app.debug else logging.INFO) - # Create console handler - handler = logging.StreamHandler(sys.stdout) - handler.setLevel(logging.DEBUG if config.app.debug else logging.INFO) + # 只在 logger 没有 handler 时才添加 + if not _logger.handlers: + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.DEBUG if config.app.debug else logging.INFO) - # Create formatter - formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' - ) - handler.setFormatter(formatter) + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + handler.setFormatter(formatter) - # Add handler to logger - _logger.addHandler(handler) + _logger.addHandler(handler) return _logger diff --git a/sandbox/app/middleware/concurrency.py b/sandbox/app/middleware/concurrency.py index 8d8325a4..e931f846 100644 --- a/sandbox/app/middleware/concurrency.py +++ b/sandbox/app/middleware/concurrency.py @@ -1,48 +1,66 @@ -"""Concurrency control middleware""" +""" +Concurrency control middleware +""" import asyncio +from contextlib import asynccontextmanager + from fastapi import HTTPException, status from app.config import get_config -from app.models import error_response +from app.logger import get_logger + +logger = get_logger() -# Global semaphores -_worker_semaphore: None | asyncio.Semaphore = None -_request_counter = 0 -_request_lock = asyncio.Lock() +class ConcurrencyController: + def __init__(self): + self._worker_semaphore: asyncio.Semaphore | None = None + self._request_counter = 0 + self._lock = asyncio.Lock() + + config = get_config() + self.max_requests = config.max_requests + + def init(self): + config = get_config() + self._worker_semaphore = asyncio.Semaphore(config.max_workers) + + async def _acquire_worker(self): + if self._worker_semaphore is None: + self.init() + async with self._worker_semaphore: + yield + + async def _limit_requests(self): + async with self._lock: + logger.info(f"Current requests: {self._request_counter}/{self.max_requests}") + if self._request_counter >= self.max_requests: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail={ + "code": 503, + "message": "Too many requests", + "data": None, + } + ) + self._request_counter += 1 + try: + yield + finally: + async with self._lock: + self._request_counter -= 1 + + def acquire_worker(self): + return asynccontextmanager(self._acquire_worker)() + + def limit_requests(self): + return asynccontextmanager(self._limit_requests)() -def init_concurrency_control(): - """Initialize concurrency control""" - global _worker_semaphore - config = get_config() - _worker_semaphore = asyncio.Semaphore(config.max_workers) +concurrency = ConcurrencyController() -async def check_max_requests(): - """Check if max requests limit is reached""" - global _request_counter - config = get_config() - - async with _request_lock: - if _request_counter >= config.max_requests: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail=error_response(-503, "Too many requests") - ) - _request_counter += 1 - - try: - yield - finally: - async with _request_lock: - _request_counter -= 1 - - -async def acquire_worker(): - """Acquire a worker slot""" - if _worker_semaphore is None: - init_concurrency_control() - - async with _worker_semaphore: - yield +async def concurrency_guard(): + async with concurrency.limit_requests(): + async with concurrency.acquire_worker(): + yield diff --git a/sandbox/app/services/nodejs_service.py b/sandbox/app/services/nodejs_service.py new file mode 100644 index 00000000..ffd6127b --- /dev/null +++ b/sandbox/app/services/nodejs_service.py @@ -0,0 +1,43 @@ +"""Nodejs execution service""" +import signal + +from app.core.runners.nodejs.nodejs_runner import NodejsRunner +from app.logger import get_logger +from app.models import ( + success_response, + error_response, + RunCodeResponse, + RunnerOptions +) + + +async def run_nodejs_code(code: str, preload: str, options: RunnerOptions): + """Execute Node.js code in sandbox + + Args: + options: + code: Base64 encoded encrypted code + preload: Preload code + + Returns: + API response with execution result + """ + logger = get_logger() + + try: + runner = NodejsRunner() + result = await runner.run(code, options, preload) + if result.exit_code == signal.SIGSYS + 0x80: + return error_response(31, "sandbox security policy violation") + + if result.exit_code != 0: + return error_response(500, result.stderr) + + return success_response(RunCodeResponse( + stdout=result.stdout, + stderr=result.stderr + )) + + except Exception as e: + logger.error(f"Python execution failed: {e}", exc_info=True) + return error_response(-500, str(e)) diff --git a/sandbox/config.yaml b/sandbox/config.yaml index d9581b34..26fb9af3 100644 --- a/sandbox/config.yaml +++ b/sandbox/config.yaml @@ -1,13 +1,11 @@ app: - port: 8194 - debug: true key: redbear-sandbox -max_workers: 4 -max_requests: 50 -worker_timeout: 30 +max_workers: 10 +max_requests: 300 +worker_timeout: 15 python_path: /usr/local/bin/python -nodejs_path: /usr/local/bin/node +nodejs_path: /usr/bin/node enable_network: true enable_preload: false python_deps_update_interval: 30m diff --git a/sandbox/dependencies/nodejs/node_modules/.package-lock.json b/sandbox/dependencies/nodejs/node_modules/.package-lock.json new file mode 100644 index 00000000..28b290ef --- /dev/null +++ b/sandbox/dependencies/nodejs/node_modules/.package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "nodejs", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/sandbox/dependencies/nodejs/package-lock.json b/sandbox/dependencies/nodejs/package-lock.json new file mode 100644 index 00000000..28b290ef --- /dev/null +++ b/sandbox/dependencies/nodejs/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "nodejs", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/sandbox/dependencies/nodejs/package.json b/sandbox/dependencies/nodejs/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/sandbox/dependencies/nodejs/package.json @@ -0,0 +1 @@ +{} diff --git a/sandbox/dependencies/python-requirements.txt b/sandbox/dependencies/python/python-requirements.txt similarity index 100% rename from sandbox/dependencies/python-requirements.txt rename to sandbox/dependencies/python/python-requirements.txt diff --git a/sandbox/lib/seccomp_nodejs/Cargo.lock b/sandbox/lib/seccomp_nodejs/Cargo.lock deleted file mode 100644 index b37698ee..00000000 --- a/sandbox/lib/seccomp_nodejs/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "seccomp_nodejs" -version = "0.1.0" diff --git a/sandbox/lib/seccomp_nodejs/Cargo.toml b/sandbox/lib/seccomp_nodejs/Cargo.toml deleted file mode 100644 index a8bd8932..00000000 --- a/sandbox/lib/seccomp_nodejs/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "seccomp_nodejs" -version = "0.1.0" -edition = "2024" - -[dependencies] \ No newline at end of file diff --git a/sandbox/lib/seccomp_nodejs/src/lib.rs b/sandbox/lib/seccomp_nodejs/src/lib.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/sandbox/lib/seccomp_python/Cargo.lock b/sandbox/lib/seccomp_redbear/Cargo.lock similarity index 92% rename from sandbox/lib/seccomp_python/Cargo.lock rename to sandbox/lib/seccomp_redbear/Cargo.lock index 881ad177..f81d17c0 100644 --- a/sandbox/lib/seccomp_python/Cargo.lock +++ b/sandbox/lib/seccomp_redbear/Cargo.lock @@ -15,8 +15,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60276e2d41bbb68b323e566047a1bfbf952050b157d8b5cdc74c07c1bf4ca3b6" [[package]] -name = "seccomp_python" -version = "0.1.0" +name = "seccomp_redbear" +version = "0.1.1" dependencies = [ "libc", "libseccomp-sys", diff --git a/sandbox/lib/seccomp_python/Cargo.toml b/sandbox/lib/seccomp_redbear/Cargo.toml similarity index 51% rename from sandbox/lib/seccomp_python/Cargo.toml rename to sandbox/lib/seccomp_redbear/Cargo.toml index 07037172..d6535987 100644 --- a/sandbox/lib/seccomp_python/Cargo.toml +++ b/sandbox/lib/seccomp_redbear/Cargo.toml @@ -1,12 +1,17 @@ [package] -name = "seccomp_python" -version = "0.1.0" +name = "seccomp_redbear" +version = "0.1.1" edition = "2024" [lib] -name = "python" +name = "sandbox" crate-type = ["cdylib"] [dependencies] libc = "0.2.180" libseccomp-sys = "0.3.0" + +[features] +default = [] +python3 = [] +nodejs = [] diff --git a/sandbox/lib/seccomp_python/src/lib.rs b/sandbox/lib/seccomp_redbear/src/lib.rs similarity index 82% rename from sandbox/lib/seccomp_python/src/lib.rs rename to sandbox/lib/seccomp_redbear/src/lib.rs index 08b46c54..9de38a56 100644 --- a/sandbox/lib/seccomp_python/src/lib.rs +++ b/sandbox/lib/seccomp_redbear/src/lib.rs @@ -1,13 +1,25 @@ -mod syscalls; +#[cfg(all(feature = "python3", feature = "nodejs"))] +compile_error!("Only one feature can be enabled: either python3 or nodejs, not both!"); -use crate::syscalls::*; -use libc::{chdir, chroot, gid_t, uid_t, c_int}; +#[cfg(not(any(feature = "python3", feature = "nodejs")))] +compile_error!("You must enable one feature: either python3 or nodejs"); + +#[cfg(feature = "python3")] +mod python_syscalls; +#[cfg(feature = "python3")] +use crate::python_syscalls::*; + +#[cfg(feature = "nodejs")] +mod nodejs_syscalls; +#[cfg(feature = "nodejs")] +use crate::nodejs_syscalls::*; + +use libc::{c_char, c_int, chdir, chroot, gid_t, uid_t}; use libseccomp_sys::*; use std::env; use std::ffi::CString; use std::str::FromStr; - /* * get_allowed_syscalls - retrieve allowed syscalls for the sandbox * @enable_network: enable network-related syscalls if non-zero @@ -193,3 +205,20 @@ pub unsafe extern "C" fn init_seccomp(uid: uid_t, gid: gid_t, enable_network: i3 Err(code) => code, } } + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn get_lib_version_static() -> *const c_char { + concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn get_lib_feature_static() -> *const c_char { + #[cfg(feature = "python3")] + let s = b"python3\0"; + #[cfg(feature = "nodejs")] + let s = b"nodejs\0"; + #[cfg(not(any(feature = "python3", feature = "nodejs")))] + let s = b"none\0"; + + s.as_ptr() as *const c_char +} diff --git a/sandbox/lib/seccomp_redbear/src/nodejs_syscalls.rs b/sandbox/lib/seccomp_redbear/src/nodejs_syscalls.rs new file mode 100644 index 00000000..7cf36664 --- /dev/null +++ b/sandbox/lib/seccomp_redbear/src/nodejs_syscalls.rs @@ -0,0 +1,74 @@ +// src/nodejs_syscalls.rs + +pub static ALLOW_SYSCALLS: &[i32] = &[ + // File IO + libc::SYS_open as i32, + libc::SYS_write as i32, + libc::SYS_close as i32, + libc::SYS_read as i32, + libc::SYS_openat as i32, + libc::SYS_newfstatat as i32, + libc::SYS_ioctl as i32, + libc::SYS_lseek as i32, + libc::SYS_fstat as i32, + libc::SYS_readlink as i32, + libc::SYS_dup3 as i32, + libc::SYS_fcntl as i32, + libc::SYS_fsync as i32, + // Memory + libc::SYS_mprotect as i32, + libc::SYS_mmap as i32, + libc::SYS_munmap as i32, + libc::SYS_mremap as i32, + libc::SYS_brk as i32, + libc::SYS_madvise as i32, + // Signal + libc::SYS_rt_sigaction as i32, + libc::SYS_rt_sigprocmask as i32, + libc::SYS_sigaltstack as i32, + libc::SYS_rt_sigreturn as i32, + libc::SYS_tgkill as i32, + // Thread + libc::SYS_futex as i32, + libc::SYS_sched_yield as i32, + libc::SYS_set_robust_list as i32, + libc::SYS_rseq as i32, + // User / Group + libc::SYS_getuid as i32, + // Process + libc::SYS_getpid as i32, + libc::SYS_gettid as i32, + libc::SYS_exit as i32, + libc::SYS_exit_group as i32, + libc::SYS_sched_getaffinity as i32, + // Time + libc::SYS_clock_gettime as i32, + libc::SYS_gettimeofday as i32, + libc::SYS_nanosleep as i32, + libc::SYS_time as i32, + // Epoll / Event (I/O multiplexing) + libc::SYS_epoll_ctl as i32, + libc::SYS_epoll_pwait as i32, +]; + +pub static ALLOW_ERROR_SYSCALLS: &[i32] = &[libc::SYS_clone as i32, libc::SYS_clone3 as i32]; + +pub static ALLOW_NETWORK_SYSCALLS: &[i32] = &[ + libc::SYS_socket as i32, + libc::SYS_connect as i32, + libc::SYS_bind as i32, + libc::SYS_listen as i32, + libc::SYS_accept as i32, + libc::SYS_sendto as i32, + libc::SYS_recvfrom as i32, + libc::SYS_getsockname as i32, + libc::SYS_recvmsg as i32, + libc::SYS_getpeername as i32, + libc::SYS_setsockopt as i32, + libc::SYS_ppoll as i32, + libc::SYS_uname as i32, + libc::SYS_sendmsg as i32, + libc::SYS_getsockopt as i32, + libc::SYS_fcntl as i32, + libc::SYS_fstatfs as i32, +]; diff --git a/sandbox/lib/seccomp_python/src/syscalls.rs b/sandbox/lib/seccomp_redbear/src/python_syscalls.rs similarity index 90% rename from sandbox/lib/seccomp_python/src/syscalls.rs rename to sandbox/lib/seccomp_redbear/src/python_syscalls.rs index 961fffac..998ae390 100644 --- a/sandbox/lib/seccomp_python/src/syscalls.rs +++ b/sandbox/lib/seccomp_redbear/src/python_syscalls.rs @@ -1,7 +1,7 @@ -// src/syscalls.rs +// src/python_syscalls.rs pub static ALLOW_SYSCALLS: &[i32] = &[ - // file io + // File IO libc::SYS_read as i32, libc::SYS_write as i32, libc::SYS_openat as i32, @@ -11,48 +11,44 @@ pub static ALLOW_SYSCALLS: &[i32] = &[ libc::SYS_lseek as i32, libc::SYS_getdents64 as i32, libc::SYS_fstat as i32, - - // thread + // Signal + libc::SYS_rt_sigreturn as i32, + libc::SYS_rt_sigaction as i32, + libc::SYS_rt_sigprocmask as i32, + libc::SYS_sigaltstack as i32, + libc::SYS_tgkill as i32, + // Thread libc::SYS_futex as i32, - - // memory + // Memory libc::SYS_mmap as i32, libc::SYS_brk as i32, libc::SYS_mprotect as i32, libc::SYS_munmap as i32, - libc::SYS_rt_sigreturn as i32, libc::SYS_mremap as i32, - - // user / group - libc::SYS_setuid as i32, - libc::SYS_setgid as i32, + // User / Group libc::SYS_getuid as i32, - - // process + // Process libc::SYS_getpid as i32, libc::SYS_getppid as i32, libc::SYS_gettid as i32, libc::SYS_exit as i32, libc::SYS_exit_group as i32, - libc::SYS_tgkill as i32, - libc::SYS_rt_sigaction as i32, libc::SYS_sched_yield as i32, libc::SYS_set_robust_list as i32, libc::SYS_get_robust_list as i32, libc::SYS_rseq as i32, - - // time + // Time libc::SYS_clock_gettime as i32, libc::SYS_gettimeofday as i32, + libc::SYS_time as i32, libc::SYS_nanosleep as i32, + libc::SYS_clock_nanosleep as i32, + // Epoll / Event (I/O multiplexing) libc::SYS_epoll_create1 as i32, libc::SYS_epoll_ctl as i32, - libc::SYS_clock_nanosleep as i32, libc::SYS_pselect6 as i32, - libc::SYS_rt_sigprocmask as i32, - libc::SYS_sigaltstack as i32, + // Randomness libc::SYS_getrandom as i32, - ]; pub static ALLOW_ERROR_SYSCALLS: &[i32] = &[ diff --git a/sandbox/main.py b/sandbox/main.py index fc417563..99b7b0a6 100644 --- a/sandbox/main.py +++ b/sandbox/main.py @@ -11,51 +11,15 @@ from fastapi import FastAPI from app.config import get_config from app.controllers import manager_router +from app.core.runners import init_sandbox_user from app.dependencies import setup_dependencies, update_dependencies_periodically from app.logger import setup_logger, get_logger +setup_logger() +config = get_config() logger = get_logger() -@asynccontextmanager -async def lifespan(app: FastAPI): - """Application lifespan manager""" - logger = get_logger() - - # Startup - logger.info("Starting RedBear Sandbox...") - - # Setup dependencies in background - asyncio.create_task(setup_dependencies()) - - # Start periodic dependency updates - config = get_config() - if config.python_deps_update_interval: - asyncio.create_task(update_dependencies_periodically()) - - yield - - # Shutdown - logger.info("Shutting down Redbear Sandbox...") - - -def create_app() -> FastAPI: - """Create FastAPI application""" - config = get_config() - - app = FastAPI( - title="Sandbox", - description="Secure code execution sandbox", - version="2.0.0", - lifespan=lifespan, - debug=config.app.debug - ) - - app.include_router(manager_router) - - return app - - def check_root_privileges(): """Check if running with root privileges""" if os.geteuid() != 0: @@ -63,35 +27,38 @@ def check_root_privileges(): sys.exit(1) -def main(): - """Main entry point""" - # Check root privileges - check_root_privileges() +check_root_privileges() - # Setup logging - setup_logger() - config = get_config() +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan manager""" logger = get_logger() - + config = get_config() + # Startup + logger.info("Starting RedBear Sandbox...") logger.info(f"Starting server on port {config.app.port}") logger.info(f"Debug mode: {config.app.debug}") logger.info(f"Max workers: {config.max_workers}") logger.info(f"Max requests: {config.max_requests}") logger.info(f"Network enabled: {config.enable_network}") + init_sandbox_user() + await setup_dependencies() - # Create app - app = create_app() + if config.python_deps_update_interval: + asyncio.create_task(update_dependencies_periodically()) - # Run server - uvicorn.run( - app, - host="0.0.0.0", - port=config.app.port, - log_level="debug" if config.app.debug else "info", - access_log=config.app.debug - ) + yield + # Shutdown + logger.info("Shutting down Redbear Sandbox...") -if __name__ == "__main__": - main() +app = FastAPI( + title="Sandbox", + description="Secure code execution sandbox", + version="0.1.0", + lifespan=lifespan, + debug=config.app.debug +) + +app.include_router(manager_router)