Monty Sandboxing Security Measures: Complete Isolation for Untrusted Python Code
Monty isolates untrusted Python code by yielding to the host for all OS operations and enforcing strict resource limits via a ResourceTracker that monitors memory, allocations, execution time, and recursion depth.
The pydantic/monty repository provides a secure Python virtual machine designed specifically for sandboxing untrusted code. Unlike standard Python interpreters that grant full system access, Monty implements a defense-in-depth strategy that combines OS function indirection, host-mediated I/O, and configurable resource constraints to guarantee code isolation.
OS Function Indirection and Host Mediation
The OsFunction Whitelist
All built-in operations requiring host system access are represented by the OsFunction enum in crates/monty/src/os.rs. This enum contains an explicit whitelist of safe, supported operations such as Path.read_text and os.getenv. When Python code invokes a method mapped to these functions, the type implementation returns AttrCallResult::OsCall(os_fn, args) as defined in src/types/py_trait.rs.
FrameExit::OsCall Yielding
The VM receives OS call requests as FrameExit::OsCall in src/bytecode/vm/mod.rs. Rather than executing the operation directly, the VM pauses execution and yields control back to the host application through the RunProgress enum in crates/monty/src/run.rs. This ensures the interpreter never performs I/O without explicit host approval.
Resource Tracking and Enforcement
LimitedTracker vs NoLimitTracker
Monty injects a ResourceTracker implementation into the VM's heap via Heap::new(..., tracker). Two concrete implementations exist in src/resource.rs:
NoLimitTracker: Used byrun_no_limits()(lines 94-96 inrun.rs), applies only default recursion limits (~1000) with no memory or time constraints.LimitedTracker: Enforces configurable limits on allocations, memory, duration, and recursion depth (lines 66-84 inresource.rs).
Memory and Allocation Limits
The LimitedTracker::on_allocate method (lines 15-24) intercepts every heap allocation to check against max_memory and max_allocations limits. Similarly, on_free (lines 44-46) maintains accurate memory estimates. For operations that could allocate massive objects (like 2 ** 10_000_000), check_pow_size and check_repeat_size (lines 24-70) estimate result sizes before allocation, raising MemoryError preemptively.
Execution Time and Recursion Controls
Time enforcement occurs in LimitedTracker::check_time (lines 48-63), executed every TIME_CHECK_INTERVAL VM instructions (constant defined at line 34). Recursion limits are enforced via check_recursion_depth (lines 68-78) before each new call frame. If limits are exceeded, ResourceError::into_exception (lines 31-51) converts violations into appropriate Python exceptions (TimeoutError, RecursionError, etc.).
VM Design Safeguards
DepthGuard for Data Structures
When formatting objects via repr, hash, or equality comparisons, Monty uses a DepthGuard (lines 16-71 in resource.rs) that tracks traversal depth independently of the Python call stack limit. This prevents host stack overflow when processing deeply nested containers (e.g., recursive lists), returning truncated representations (...) instead of crashing.
I/O Abstraction and Strict Module Exposure
All I/O paths use the PrintWriter abstraction in src/io.rs, which can be replaced with dummy writers to prevent output leakage. The VM only exits to the host via FrameExit::OsCall (whitelisted OS functions) or FrameExit::ExternalCall (user callbacks), both captured by RunProgress in run.rs (lines 71-84). Furthermore, the built-in os module (src/modules/os.rs, lines 67-108) only exports functions mapping to OsFunction variants, ensuring no other stdlib module can access host resources directly.
Practical Implementation Examples
The following examples demonstrate how to configure Monty's sandboxing mechanisms in practice.
Running Sandboxed Code with OS Request Handling
use monty::{MontyRun, MontyObject, PrintWriter};
use monty::resource::{LimitedTracker, ResourceLimits};
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a runner for code that tries to read a file
let runner = MontyRun::new(
"Path('secret.txt').read_text()".to_owned(),
"example.py",
vec![],
vec![],
)?;
// Configure resource limits (no file system access, tiny memory budget)
let limits = ResourceLimits::new()
.max_memory(10_000) // 10 KB max heap
.max_duration(Duration::from_secs(2));
let tracker = LimitedTracker::new(limits);
// Start execution – it will pause at the OS call
let mut print = PrintWriter::Stdout;
match runner.start(vec![], tracker, &mut print)? {
// OS operation requested → host decides what to do
monty::RunProgress::OsCall { function, args, .. } => {
println!("Sandbox requested OS function: {function:?} with args {args:?}");
// Reject the request by raising NotImplementedError
Err(monty::ExcType::not_implemented("filesystem access disabled"))?;
}
// Normal completion
monty::RunProgress::Complete(value) => {
println!("Result: {value:?}");
}
_ => unreachable!(),
}
Ok(())
}
The VM yields a RunProgress::OsCall (see run.rs line 211) instead of performing any I/O.
Enforcing Strict Resource Limits
use monty::resource::{LimitedTracker, ResourceLimits};
use std::time::Duration;
// 1 MiB memory, 5k allocations, 1 second runtime limit
let limits = ResourceLimits {
max_memory: Some(1 << 20),
max_allocations: Some(5_000),
max_duration: Some(Duration::from_secs(1)),
..Default::default()
};
let tracker = LimitedTracker::new(limits);
// Pass `tracker` to `MontyRun::run` or `start`
If the sandbox tries 2 ** 10_000_000, the pre-check in check_pow_size (lines 28-38) estimates the required bytes and aborts with a MemoryError before allocating the huge integer.
Python Binding Usage
from pydantic_monty import Monty
code = "Path('tmp.txt').write_text('hello')"
m = Monty(code, inputs=[], external_functions=[])
# Run with no limits – will raise NotImplementedError because the host
# binding does not permit filesystem writes.
try:
m.run()
except Exception as e:
print("Sandbox error:", e)
The binding forwards the OsCall to the Python side, where the default handler raises NotImplementedError (see monty_python/src/monty_cls.rs line 396).
Summary
- Monty uses an
OsFunctionwhitelist insrc/os.rsto ensure only explicitly permitted host operations are requested, never executed directly. - The VM yields control via
FrameExit::OsCallandRunProgress, forcing host mediation for all I/O and system calls. LimitedTrackerenforces configurable caps on memory, allocation count, execution time, and recursion depth, with pre-checks preventing oversized object creation.DepthGuardprevents stack overflow during data structure traversal independently of the call stack limit.- The
osmodule is the sole bridge to host resources, andPrintWriterabstractions prevent unauthorized output leakage.
Frequently Asked Questions
How does Monty prevent sandboxed code from accessing the filesystem?
Monty never executes filesystem operations inside the VM. Instead, code in src/modules/os.rs and src/types/path.rs returns AttrCallResult::OsCall, which the VM converts to FrameExit::OsCall in src/bytecode/vm/mod.rs. The interpreter yields to the host via RunProgress::OsCall, allowing the host to reject the request with NotImplementedError or sanitize arguments before allowing access.
What happens when sandboxed code exceeds memory or time limits?
The LimitedTracker in src/resource.rs intercepts every allocation via on_allocate and checks execution time every TIME_CHECK_INTERVAL instructions via check_time. If limits are exceeded, it returns a ResourceError that into_exception converts to MemoryError, TimeoutError, or RecursionError, terminating execution safely without crashing the host.
Can sandboxed code perform network operations or spawn processes?
No. The OsFunction enum in src/os.rs contains an explicit whitelist of safe operations. Network sockets and process spawning are not included in this whitelist, so any attempt to invoke them results in an immediate AttributeError or NotImplementedError when the host rejects the OsCall yield, guaranteeing these capabilities remain inaccessible to sandboxed code.
How does Monty protect against deeply nested data structures causing stack overflows?
Monty implements a DepthGuard in src/resource.rs (lines 16-71) that operates independently of the Python call stack limit. When performing operations like repr, hash, or equality comparisons on containers, the guard tracks traversal depth and truncates output with ... if the limit is reached, preventing host stack overflow while allowing safe inspection of deeply nested objects.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →