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 by run_no_limits() (lines 94-96 in run.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 in resource.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 OsFunction whitelist in src/os.rs to ensure only explicitly permitted host operations are requested, never executed directly.
  • The VM yields control via FrameExit::OsCall and RunProgress, forcing host mediation for all I/O and system calls.
  • LimitedTracker enforces configurable caps on memory, allocation count, execution time, and recursion depth, with pre-checks preventing oversized object creation.
  • DepthGuard prevents stack overflow during data structure traversal independently of the call stack limit.
  • The os module is the sole bridge to host resources, and PrintWriter abstractions 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →