# Monty Sandboxing Security Measures: Complete Isolation for Untrusted Python Code

> Discover Monty's complete isolation security for untrusted Python code. Learn how it enforces resource limits and yields OS operations to the host for enhanced safety.

- Repository: [Pydantic/monty](https://github.com/pydantic/monty)
- Tags: deep-dive
- Published: 2026-02-16

---

**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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/src/types/py_trait.rs).

### FrameExit::OsCall Yielding

The VM receives OS call requests as `FrameExit::OsCall` in [`src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/src/resource.rs):

- **`NoLimitTracker`**: Used by `run_no_limits()` (lines 94-96 in [`run.rs`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/run.rs) (lines 71-84). Furthermore, the built-in `os` module ([`src/modules/os.rs`](https://github.com/pydantic/monty/blob/main/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**

```rust
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`](https://github.com/pydantic/monty/blob/main/run.rs) line 211) instead of performing any I/O.*

**Enforcing Strict Resource Limits**

```rust
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**

```python
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`](https://github.com/pydantic/monty/blob/main/monty_python/src/monty_cls.rs) line 396).

## Summary

- Monty uses an **`OsFunction` whitelist** in [`src/os.rs`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/src/modules/os.rs) and [`src/types/path.rs`](https://github.com/pydantic/monty/blob/main/src/types/path.rs) returns `AttrCallResult::OsCall`, which the VM converts to `FrameExit::OsCall` in [`src/bytecode/vm/mod.rs`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/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`](https://github.com/pydantic/monty/blob/main/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.