Spaces:
Running on Zero
Running on Zero
Download sandbox.py from YugJ/python-to-rust-cpp: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/spaces/YugJ/python-to-rust-cpp/resolve/97a0ef4ef161cdfe3c01d01f61c07c19507aded0/sandbox.py
- Command line
-
hf download hf://spaces/YugJ/python-to-rust-cpp@97a0ef4ef161cdfe3c01d01f61c07c19507aded0/sandbox.py
-
curl -L -o sandbox.py https://huggingface.co/spaces/YugJ/python-to-rust-cpp/resolve/97a0ef4ef161cdfe3c01d01f61c07c19507aded0/sandbox.py
15.7 kB
| """Isolated execution of untrusted Python and Rust. | |
| The app compiles and runs code that arbitrary visitors typed into a web page. | |
| Running that in-process would hand every visitor the host's environment - which | |
| on a deployed instance includes the API keys. Everything here exists to keep | |
| those two worlds apart. | |
| Guarantees, strongest first: | |
| 1. Untrusted code runs in a *separate process* with an explicitly rebuilt | |
| environment. No API key, token, or secret is inherited - the child gets a | |
| hand-written allowlist of variables (PATH and the few the OS needs to | |
| start a process at all), never a copy of os.environ. | |
| 2. Every run is wall-clock bounded and the whole process tree is killed on | |
| timeout, so an infinite loop cannot pin a worker forever. | |
| 3. Work happens in a throwaway temp directory that is deleted afterwards, so | |
| nothing can accumulate on disk or read the repository. | |
| 4. On Linux (how this deploys) POSIX rlimits additionally cap CPU seconds, | |
| address space, file size, and forked processes. | |
| Item 4 is unavailable on Windows, so local dev is protected by 1-3 only. That | |
| is fine for running your own code; the deployed container is where hostile | |
| input actually arrives, and there the full set applies. | |
| What this is NOT: a security boundary strong enough to run genuinely malicious | |
| code with no further thought. A determined attacker inside the container is | |
| still inside the container. The container is therefore the real boundary - the | |
| process-level work here reduces blast radius, it does not replace isolation at | |
| the platform layer. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import platform | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import time | |
| from dataclasses import dataclass | |
| IS_WINDOWS = platform.system() == "Windows" | |
| # Wall-clock ceilings, seconds. | |
| PYTHON_TIMEOUT = int(os.getenv("PYTHON_TIMEOUT", "60")) | |
| COMPILE_TIMEOUT = int(os.getenv("COMPILE_TIMEOUT", "90")) | |
| RUN_TIMEOUT = int(os.getenv("RUN_TIMEOUT", "60")) | |
| # POSIX-only resource ceilings. | |
| MAX_CPU_SECONDS = int(os.getenv("MAX_CPU_SECONDS", "60")) | |
| # rustc with -C lto=fat is memory-hungry; 2 GB was tight enough to risk killing | |
| # legitimate compiles. The free Spaces container has 16 GB, so this is generous | |
| # without being unbounded. | |
| MAX_MEMORY_MB = int(os.getenv("MAX_MEMORY_MB", "4096")) | |
| MAX_OUTPUT_CHARS = 20_000 | |
| class Result: | |
| """Outcome of one sandboxed run.""" | |
| ok: bool | |
| stdout: str | |
| stderr: str | |
| seconds: float | |
| stage: str = "run" # "compile" or "run" - which step produced the error | |
| def display(self) -> str: | |
| """Human-readable text for the UI output box.""" | |
| if self.ok: | |
| return self.stdout or "(the program produced no output)" | |
| label = "Compilation failed" if self.stage == "compile" else "Execution failed" | |
| detail = (self.stderr or self.stdout or "").strip() or "no diagnostics available" | |
| return f"{label}\n\n{detail}" | |
| def _safe_env() -> dict[str, str]: | |
| """Build a minimal environment from scratch. | |
| Deliberately constructed by allowlist rather than by copying os.environ and | |
| deleting keys - a denylist silently fails the moment a new provider adds an | |
| env var, and that failure mode leaks a credential. | |
| """ | |
| allowed: dict[str, str] = {} | |
| # The bare minimum for a process to start and find a toolchain. | |
| passthrough = ["PATH"] | |
| if IS_WINDOWS: | |
| # Windows cannot create a process without these. | |
| passthrough += ["SYSTEMROOT", "SYSTEMDRIVE", "COMSPEC", "PATHEXT", | |
| "NUMBER_OF_PROCESSORS", "PROCESSOR_ARCHITECTURE", | |
| "USERPROFILE", "WINDIR"] | |
| else: | |
| passthrough += ["LANG", "LC_ALL", "HOME"] | |
| for name in passthrough: | |
| value = os.environ.get(name) | |
| if value: | |
| allowed[name] = value | |
| # Keep the child's Python quiet and deterministic. | |
| allowed["PYTHONUNBUFFERED"] = "1" | |
| allowed["PYTHONDONTWRITEBYTECODE"] = "1" | |
| return allowed | |
| def assert_safe_to_deploy() -> None: | |
| """Refuse to serve the public if secrets are sitting on disk. | |
| Scrubbing the child's environment stops submitted code from *inheriting* a | |
| key, but it cannot stop it from calling open('.env'). Verified by test: a | |
| sandboxed program read the dotenv file in plaintext. | |
| On a public host that path has to be closed by not having the file at all - | |
| secrets belong in the platform's secret store, injected as environment | |
| variables into the parent only. This turns that requirement into a | |
| startup check instead of a thing to remember. | |
| """ | |
| if os.getenv("PUBLIC_DEPLOYMENT", "0") != "1": | |
| return | |
| here = os.path.dirname(os.path.abspath(__file__)) | |
| offenders = [ | |
| name for name in (".env", ".env.local", ".env.production") | |
| if os.path.exists(os.path.join(here, name)) | |
| ] | |
| if offenders: | |
| raise RuntimeError( | |
| "Refusing to start in public mode with secret files on disk: " | |
| f"{', '.join(offenders)}.\n" | |
| "Submitted code runs on this filesystem and can read them. " | |
| "Delete the file and supply secrets as environment variables " | |
| "(on Hugging Face Spaces: Settings -> Variables and secrets)." | |
| ) | |
| def _preexec(): | |
| """Apply POSIX resource limits in the child before exec. Linux/macOS only.""" | |
| import resource | |
| resource.setrlimit(resource.RLIMIT_CPU, (MAX_CPU_SECONDS, MAX_CPU_SECONDS)) | |
| mem = MAX_MEMORY_MB * 1024 * 1024 | |
| resource.setrlimit(resource.RLIMIT_AS, (mem, mem)) | |
| resource.setrlimit(resource.RLIMIT_FSIZE, (64 * 1024 * 1024, 64 * 1024 * 1024)) | |
| # RLIMIT_NPROC is deliberately NOT set. | |
| # | |
| # It counts processes per *user*, not per process tree, so any cap here also | |
| # counts every thread the host container already runs under this uid. In | |
| # practice that starved the compilers of threads: | |
| # rustc: Os { code: 11, kind: WouldBlock, ... } | |
| # g++: cannot execute cc1plus: vfork: Resource temporarily unavailable | |
| # Both are EAGAIN from the fork itself. Because the limit is shared with | |
| # processes we did not start and cannot count, any value is either too low | |
| # to compile or too high to matter - it is not a usable control here. | |
| # | |
| # A fork bomb is still bounded by RLIMIT_CPU, RLIMIT_AS, and the wall-clock | |
| # kill, which terminates the entire process group. | |
| # New process group so a timeout kill takes any children with it. | |
| os.setsid() | |
| def _popen_kwargs() -> dict: | |
| kwargs: dict = {} | |
| if IS_WINDOWS: | |
| # Lets us terminate the whole tree on timeout. | |
| kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | |
| else: | |
| kwargs["preexec_fn"] = _preexec | |
| return kwargs | |
| def _truncate(text: str) -> str: | |
| if text is None: | |
| return "" | |
| if len(text) <= MAX_OUTPUT_CHARS: | |
| return text | |
| return text[:MAX_OUTPUT_CHARS] + f"\n\n... output truncated at {MAX_OUTPUT_CHARS:,} characters ..." | |
| def _kill_tree(proc: subprocess.Popen) -> None: | |
| """Kill a process and everything it spawned.""" | |
| try: | |
| if IS_WINDOWS: | |
| subprocess.run(["taskkill", "/F", "/T", "/PID", str(proc.pid)], | |
| capture_output=True, timeout=10) | |
| else: | |
| import signal | |
| os.killpg(os.getpgid(proc.pid), signal.SIGKILL) | |
| except Exception: | |
| pass | |
| try: | |
| proc.wait(timeout=5) | |
| except Exception: | |
| pass | |
| def _run(cmd: list[str], cwd: str, timeout: int, stage: str) -> Result: | |
| """Run one command under the full set of restrictions.""" | |
| started = time.perf_counter() | |
| try: | |
| proc = subprocess.Popen( | |
| cmd, | |
| cwd=cwd, | |
| env=_safe_env(), | |
| stdin=subprocess.DEVNULL, # never block waiting on input | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| text=True, | |
| **_popen_kwargs(), | |
| ) | |
| except FileNotFoundError as e: | |
| return Result(False, "", f"Toolchain not found: {e}", 0.0, stage) | |
| try: | |
| stdout, stderr = proc.communicate(timeout=timeout) | |
| except subprocess.TimeoutExpired: | |
| _kill_tree(proc) | |
| elapsed = time.perf_counter() - started | |
| return Result( | |
| False, "", | |
| f"Timed out after {timeout}s and was terminated.\n" | |
| f"The sandbox caps how long submitted code may run.", | |
| elapsed, stage, | |
| ) | |
| elapsed = time.perf_counter() - started | |
| return Result(proc.returncode == 0, _truncate(stdout), _truncate(stderr), elapsed, stage) | |
| # -------------------------------------------------------------------------- | |
| # public API | |
| # -------------------------------------------------------------------------- | |
| def run_python(code: str) -> Result: | |
| """Execute untrusted Python in a subprocess. | |
| The notebook original used exec() in the web server's own process with full | |
| builtins, which let any visitor read os.environ - and therefore every API | |
| key on the host. This runs it out-of-process against a scrubbed environment | |
| instead. | |
| """ | |
| if not code.strip(): | |
| return Result(False, "", "No Python code to run.", 0.0) | |
| with tempfile.TemporaryDirectory(prefix="pyrun_") as tmp: | |
| path = os.path.join(tmp, "program.py") | |
| with open(path, "w", encoding="utf-8") as f: | |
| f.write(code) | |
| # -I: isolated mode. Ignores PYTHON* env vars and keeps the script's | |
| # directory off sys.path, so it cannot import anything from this repo. | |
| return _run([sys.executable, "-I", "-S", path], tmp, PYTHON_TIMEOUT, "run") | |
| class Language: | |
| """A compiled target the app can translate Python into.""" | |
| key: str # internal id | |
| display: str # shown in the UI | |
| extension: str # source file suffix | |
| highlight: str # gradio Code component language | |
| compilers: tuple[str, ...] # executables to look for, in order of preference | |
| install_hint: str | |
| def find_compiler(self) -> str | None: | |
| for name in self.compilers: | |
| found = shutil.which(name) | |
| if found: | |
| return found | |
| return None | |
| LANGUAGES: dict[str, Language] = { | |
| "rust": Language( | |
| key="rust", | |
| display="Rust", | |
| extension="rs", | |
| highlight="cpp", # gradio has no rust mode; cpp reads acceptably | |
| compilers=("rustc",), | |
| install_hint="https://rustup.rs", | |
| ), | |
| "cpp": Language( | |
| key="cpp", | |
| display="C++", | |
| extension="cpp", | |
| highlight="cpp", | |
| compilers=("g++", "clang++", "c++"), | |
| install_hint="https://gcc.gnu.org or https://winlibs.com on Windows", | |
| ), | |
| } | |
| DEFAULT_LANGUAGE = "rust" | |
| def build_compile_command(language: Language, compiler: str, source: str, | |
| binary: str) -> list[str]: | |
| """Optimisation flags per toolchain. | |
| Both sets deliberately trade compile time for runtime - whole-program | |
| optimisation, native CPU targeting, no unwinding machinery where it can be | |
| dropped. Compile time is excluded from the measurement, so this is free. | |
| """ | |
| if language.key == "rust": | |
| return [ | |
| compiler, source, | |
| "-C", "opt-level=3", | |
| "-C", "target-cpu=native", | |
| "-C", "codegen-units=1", | |
| "-C", "lto=fat", | |
| "-C", "panic=abort", | |
| "-C", "strip=symbols", | |
| "-o", binary, | |
| ] | |
| return [ | |
| compiler, source, | |
| "-std=c++17", | |
| "-O3", | |
| "-march=native", | |
| "-funroll-loops", | |
| "-fno-plt", | |
| "-flto", | |
| "-o", binary, | |
| ] | |
| def run_compiled(language_key: str, code: str) -> tuple[Result, str]: | |
| """Compile and execute untrusted code. Returns (result, compile command).""" | |
| language = LANGUAGES.get(language_key) | |
| if language is None: | |
| return Result(False, "", f"Unknown language: {language_key}", 0.0, "compile"), "" | |
| if not code.strip(): | |
| return Result(False, "", f"No {language.display} code to compile.", | |
| 0.0, "compile"), "" | |
| compiler = language.find_compiler() | |
| if not compiler: | |
| return Result( | |
| False, "", | |
| f"No {language.display} compiler found on PATH " | |
| f"(looked for: {', '.join(language.compilers)}).\n" | |
| f"Install one from {language.install_hint} to run generated code.", | |
| 0.0, "compile", | |
| ), "" | |
| binary = "program.exe" if IS_WINDOWS else "program" | |
| source_name = f"main.{language.extension}" | |
| with tempfile.TemporaryDirectory(prefix=f"{language.key}run_") as tmp: | |
| with open(os.path.join(tmp, source_name), "w", encoding="utf-8") as f: | |
| f.write(code) | |
| cmd = build_compile_command(language, compiler, source_name, binary) | |
| pretty = " ".join([os.path.basename(cmd[0])] + cmd[1:]) | |
| compiled = _run(cmd, tmp, COMPILE_TIMEOUT, "compile") | |
| if not compiled.ok: | |
| return compiled, pretty | |
| # Time execution only - trading compile time for runtime is the entire | |
| # premise of these flags, so charging the result for it measures wrong. | |
| return _run([os.path.join(tmp, binary)], tmp, RUN_TIMEOUT, "run"), pretty | |
| def available_languages() -> list[str]: | |
| """Language keys with a working compiler on this machine.""" | |
| return [key for key, lang in LANGUAGES.items() if lang.find_compiler()] | |
| _probe_cache: dict[str, bool] = {} | |
| def cpp_has_int128() -> bool: | |
| """Whether this C++ toolchain supports the __int128 extension. | |
| Worth probing rather than assuming. It is a GCC/Clang extension present on | |
| 64-bit Linux (so the deployed container has it) but missing from MinGW on | |
| Windows. Models reach for it unprompted when a benchmark needs wide | |
| accumulators, and telling them "avoid non-portable extensions" is too weak | |
| to stop it - stating the capability as a fact is not. | |
| """ | |
| if "int128" in _probe_cache: | |
| return _probe_cache["int128"] | |
| lang = LANGUAGES["cpp"] | |
| compiler = lang.find_compiler() | |
| if not compiler: | |
| _probe_cache["int128"] = False | |
| return False | |
| snippet = "unsigned __int128 x = 1; int main(){ return (int)(x & 1) - 1; }" | |
| with tempfile.TemporaryDirectory(prefix="probe_") as tmp: | |
| path = os.path.join(tmp, "probe.cpp") | |
| with open(path, "w", encoding="utf-8") as f: | |
| f.write(snippet) | |
| out = os.path.join(tmp, "probe.exe" if IS_WINDOWS else "probe") | |
| try: | |
| proc = subprocess.run( | |
| [compiler, "-std=c++17", "-fsyntax-only", path], | |
| capture_output=True, text=True, timeout=25, env=_safe_env(), | |
| ) | |
| ok = proc.returncode == 0 | |
| except Exception: | |
| ok = False | |
| _probe_cache["int128"] = ok | |
| return ok | |
| def _version_of(path: str) -> str: | |
| try: | |
| out = subprocess.run([path, "--version"], capture_output=True, | |
| text=True, timeout=5) | |
| return (out.stdout or out.stderr).strip().splitlines()[0] | |
| except Exception: | |
| return os.path.basename(path) | |
| def toolchain_status() -> dict[str, str]: | |
| """Report what the sandbox can actually run, for the UI to display.""" | |
| status: dict[str, str] = {} | |
| for key, lang in LANGUAGES.items(): | |
| found = lang.find_compiler() | |
| status[key] = _version_of(found) if found else "" | |
| status["python"] = f"Python {platform.python_version()}" | |
| status["isolation"] = ( | |
| "full (rlimits + scrubbed env)" if not IS_WINDOWS | |
| else "partial (scrubbed env, no rlimits on Windows)" | |
| ) | |
| return status | |