fix(sandbox): isolate LLM-generated code execution in a subprocess - #6
Open
pydev42 wants to merge 1 commit into
Open
fix(sandbox): isolate LLM-generated code execution in a subprocess#6pydev42 wants to merge 1 commit into
pydev42 wants to merge 1 commit into
Conversation
core/sandbox.py ran every LLM-generated snippet (code_gen, radiomics, monai_infer, idc_query, bih_query, midrc_query) via a bare exec() with full builtins and the real os module in scope, in the same process as the server. No isolation, no timeout, and the EXECUTION_MODE=subprocess toggle it exposed pointed at a dead, never-implemented stub - the only mode that actually ran was in-process. Practical effect: anything that influences what code gets generated has a path to full host-level compromise - read/write any file the process user can touch, spawn subprocesses, and read the app's own .env secrets via os.environ, which the sandbox handed to generated code directly. Also, because run_user_code is called synchronously from inside async tool handlers, a hung or infinite-looping snippet blocks the whole server's event loop indefinitely - asyncio's per-tool timeout can't preempt a blocking call, so it never actually fires. This implements the subprocess mode the code already gestured at: - runs generated code in a separate spawned process (own address space, no inherited DB/API client objects reachable via sys.modules) - enforces a real wall-clock timeout via process termination, the only mechanism that can actually stop a runaway/malicious snippet given the sync-call-inside-async-handler shape above - strips anything that looks like a credential (KEY/SECRET/TOKEN/ PASSWORD/CREDENTIAL/_AUTH) from the child's environment - keeps optional, off-by-default resource limits (CPU/memory/nproc) for deployments with a known workload profile; not on by default since the six tools span lightweight pandas queries and GPU torch/MONAI inference, and a strict RLIMIT_AS in particular tends to break CUDA context creation regardless of actual memory used - flips the EXECUTION_MODE default from inproc to subprocess; inproc stays available for trusted, already-sandboxed deployments local_env dicts pass module objects (pd, os, plt, nib, ...), which plain pickle can't serialize - the child re-imports them by name instead. monai_infer.py's two closures (_save_pred_as_nifti, _normalize_to_HWD) are hoisted to module level so they're picklable by reference; neither closes over anything instance-specific, so this is a pure move with no behavior change. Scope: this stops the two most severe gaps (zero isolation, zero timeout) and closes the credential-exfiltration-via-os.environ path. It does not add filesystem or network confinement - the six tools intentionally need real file I/O and CLI subprocess access (per prompts/agent_systems/code_gen.txt), so that would need a container/ chroot boundary, which is a separate, larger change. Tests: tests/test_sandbox.py (stdlib unittest, no new dependency) - covers basic execution, module/data values in local_env, exception propagation, env secret redaction, and timeout enforcement. All pass locally; also manually verified DataFrame and matplotlib Figure return values round-trip through the subprocess boundary (both are explicitly permitted res_query types per prompts/agent_systems/shared/output_contract.txt). Prepared with AI assistance under my review; reproduction, the isolation design, and tests were verified locally before opening this PR.
pydev42
force-pushed
the
fix/sandbox-process-isolation
branch
from
August 24, 2026 03:36
0785aa2 to
6af5aba
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
core/sandbox.pyis the execution path for every LLM-generated Python snippet in the app — used bycode_gen,radiomics,monai_infer,idc_query,bih_query, andmidrc_query. Currently it's a bareexec()with full builtins, running in the same process as the server, with the realosmodule handed to generated code directly:There's an
EXECUTION_MODEenv var that implies asubprocessisolation mode exists, butrun_user_code_subprocesswas a dead, commented-out stub — the only mode that actually ran was in-process, unsandboxed.Impact: anything that influences what code gets generated (a crafted prompt, or content the model reads back from a queried dataset) has a path to full host-level compromise — read/write any file the process user can touch, spawn subprocesses, and read the app's own
.envsecrets viaos.environ. Separately, becauserun_user_codeis called synchronously from insideasync deftool handlers, a hung or infinite-looping snippet blocks the server's entire event loop indefinitely — the per-toolasyncio.wait_fortimeout can't preempt a blocking synchronous call, so it never actually fires. There's noSECURITY.md, so this is being reported via PR rather than a private channel.Fix
Implements the subprocess mode the code already gestured at:
sys.modulesthe way an in-process/forked child would have.KEY/SECRET/TOKEN/PASSWORD/CREDENTIAL/_AUTH) from the child's environment before running user code.RLIMIT_ASin particular tends to break CUDA context creation regardless of actual memory used.tools/idc_python_worker.pyalready sets a precedent for a narrower, CPU-only worker (RLIMIT_CPU+ a 4GBRLIMIT_AS) if useful reference.EXECUTION_MODEdefault frominproctosubprocess.inprocstays available (opt-in) for trusted, already-sandboxed deployments that want to avoid the subprocess overhead.local_envdicts pass module objects (pd,os,plt,nib, ...) into generated code, and plainpicklecan't serialize module objects — the child re-imports them by name instead (cheap; already loaded).monai_infer.py's two closures (_save_pred_as_nifti,_normalize_to_HWD) are hoisted to module level so they're picklable by reference — neither closes over anything instance-specific, so this is a pure move, no behavior change.Scope
This closes the two most severe gaps (zero isolation, zero timeout) and the credential-exfiltration-via-
os.environpath. It does not add filesystem or network confinement — the six tools intentionally need real file I/O and CLI subprocess access (perprompts/agent_systems/code_gen.txt, e.g. callingTotalSegmentator/dcm2niix), so full containment would need a container/chroot boundary, which is a separate, larger change. Happy to follow up on that if it's wanted.Tests
tests/test_sandbox.py(stdlibunittest, no new dependency — the project doesn't have a test runner configured yet) covers: basic execution, module/data values inlocal_env, exception propagation, env secret redaction, and timeout enforcement. All pass locally:Also manually verified a
pandas.DataFramequery and amatplotlib.figure.Figureboth round-trip correctly through the subprocess boundary — both are explicitly permittedres_queryreturn types perprompts/agent_systems/shared/output_contract.txt.Prepared with AI assistance under my review; reproduction, the isolation design, and tests were verified locally before opening this PR.