# Monty Resource Limits and Security Controls: A Deep Dive into Sandboxed Execution

> Explore Monty's resource limits and security controls for sandboxed execution. Learn how LimitedTracker intercepts allocations, monitors time, and caps recursion for safe Python code.

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

---

**Monty enforces configurable resource limits and security controls through a Rust-based `LimitedTracker` that intercepts every heap allocation, monitors execution time, and caps recursion depth, converting violations into uncatchable Python exceptions.**

The `pydantic/monty` repository provides a secure Python sandbox by implementing deterministic resource limits and security controls at the Rust level. These safeguards prevent denial-of-service attacks, memory exhaustion, and unbounded computation while ensuring that limit violations surface as proper Python exceptions that cannot be suppressed by untrusted code.

## Core Resource Limits Architecture

Monty's security model centers on the `ResourceTracker` trait, which the virtual machine consults before every memory allocation, at instruction boundaries for time checks, and during call stack manipulation.

### ResourceLimits Configuration

The `ResourceLimits` struct in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs) defines the sandbox boundaries through a builder pattern. You can constrain **allocation counts**, **heap memory**, **execution duration**, and **recursion depth** independently:

```rust
use monty::ResourceLimits;
use std::time::Duration;

let limits = ResourceLimits::new()
    .max_allocations(1_000_000)          // Cap total object allocations
    .max_memory(100 * 1024 * 1024)       // Limit heap to 100 MiB
    .max_duration(Duration::from_secs(2))// Halt after 2 seconds CPU time
    .max_recursion_depth(Some(500));     // Stack depth limit

```

### The ResourceTracker Trait

The `ResourceTracker` trait (defined in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs)) provides hooks for the VM to validate operations:

- `on_allocate` – Called before every heap allocation to check `max_allocations` and `max_memory`
- `check_time` – Invoked at instruction boundaries to enforce `max_duration`
- `check_recursion_depth` – Validates stack depth before pushing new frames
- `check_large_result` – Pre-allocates checks for expensive operations like large exponentiation

## Memory and Allocation Controls

Monty implements defense-in-depth for memory safety by tracking both the number of objects allocated and the total bytes consumed, with additional pre-checks for operations that would produce oversized temporary results.

### Allocation Count Limits

The `LimitedTracker::on_allocate` method checks `max_allocations` before permitting any heap allocation. If the counter exceeds the configured threshold, the tracker returns `ResourceError::Allocations`, which the VM converts to a `MemoryError` that cannot be caught by the sandboxed code.

According to the source in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs) (lines 13-24), this check occurs atomically before the actual memory allocation, preventing resource exhaustion attacks that attempt to allocate millions of small objects.

### Heap Memory Caps

In addition to object count, Monty tracks total bytes allocated. The `on_allocate` method updates `current_memory` and compares it against `max_memory` (lines 25-35 in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs)). If the new allocation would exceed the cap, the tracker returns `ResourceError::Memory`, ensuring the sandbox cannot exhaust the host's RAM.

### Large Result Pre-checks

Monty prevents temporary allocation attacks—such as `2**10_000_000`—through helper functions that estimate result sizes before allocation. Functions like `check_repeat_size`, `check_pow_size`, and `check_mult_size` (lines 20-70 in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs)) calculate the expected byte size of operations. If the estimate exceeds 100 KB, they invoke `tracker.check_large_result`, which returns `ResourceError::LargeResult` if the operation would violate limits.

## Execution Control Mechanisms

Beyond memory safety, Monty enforces temporal and structural limits to prevent infinite loops and stack overflow attacks.

### CPU Time Limits

The `LimitedTracker::check_time` method enforces `max_duration` by sampling the elapsed time every 10 checks (lines 48-64 in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs)). Using `Instant::elapsed()`, the tracker detects when the cumulative execution time exceeds the configured duration, returning `ResourceError::Time`. The VM surfaces this as a `TimeoutError` that terminates execution immediately.

### Recursion Depth Protection

To prevent stack overflow attacks, `check_recursion_depth` validates the current call stack depth against `max_recursion_depth` before pushing new frames (lines 68-78 in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs)). If the limit is reached, the tracker returns `ResourceError::Recursion`, which the VM converts to an uncatchable `RecursionError`.

### Garbage Collection Scheduling

Monty guarantees periodic garbage collection to break reference cycles that could otherwise cause unbounded memory growth. The `LimitedTracker` stores a `gc_interval`, and the VM invokes `heap.maybe_gc()` when the allocation counter reaches this interval (lines 952-960 in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs)). This prevents adversarial code from creating circular references to evade memory limits.

## Signal Handling and Exception Safety

Monty integrates with host signal handling and guarantees that resource violations cannot be suppressed by sandboxed exception handlers.

### Python Signal Integration

The `PySignalTracker` wrapper (defined in [`crates/monty-python/src/limits.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/limits.rs), lines 73-88) decorates any `ResourceTracker` to poll Python signals every 1,000 time checks. This allows the host process to respond to `Ctrl-C` (SIGINT) or other Python signals even while executing untrusted code, safely aborting the sandbox without corrupting the host state.

### Uncatchable Exception Guarantees

When a resource limit is violated, Monty converts the `ResourceError` into an uncatchable Python exception via `ResourceError::into_exception` (lines 1382-1393 in [`crates/monty/src/exception_private.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/exception_private.rs)). These exceptions—`MemoryError`, `TimeoutError`, and `RecursionError`—bypass standard `try/except` blocks in the sandboxed code, ensuring that malicious scripts cannot swallow resource violations to continue execution.

## Implementation in Language Bindings

Monty exposes identical resource limit configurations across its Python and JavaScript bindings, ensuring consistent sandbox behavior regardless of the host language.

### Python Configuration

The `monty-python` crate exposes resource limits through a `TypedDict` named `ResourceLimits`. The `extract_limits` function (lines 16-53 in [`crates/monty-python/src/limits.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/limits.rs)) converts the Python dictionary into the Rust `ResourceLimits` struct, handling optional fields and default values.

```python
from pydantic_monty import Monty, ResourceLimits

limits = ResourceLimits(
    max_allocations=500_000,
    max_memory=50_000_000,          # 50 MiB

    max_duration_secs=1.5,          # 1.5 seconds

    max_recursion_depth=300,
)

m = Monty(
    code="""def fib(n):
    if n <= 1: return n
    return fib(n-1) + fib(n-2)
fib(1000)""",
    limits=limits,
)

try:
    m.run()
except Exception as exc:
    print("Sandbox stopped:", exc)   # RecursionError

```

### JavaScript Configuration

The JavaScript bindings in `monty-js` mirror the Python API, exposing a `ResourceLimits` class that maps to the underlying Rust struct (defined in [`crates/monty-js/src/limits.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-js/src/limits.rs)).

```typescript
import { Monty, ResourceLimits } from "@pydantic/monty";

const limits = new ResourceLimits({
  maxAllocations: 800_000,
  maxMemory: 30_000_000,          // 30 MiB
  maxDurationSecs: 1.0,
  maxRecursionDepth: 250,
});

const m = new Monty(`
def busy():
    while True: pass
busy()
`, { limits });

m.run()
  .then(() => console.log("finished"))
  .catch(err => console.error("Sandbox stopped:", err)); // TimeoutError

```

## Summary

Monty provides comprehensive **resource limits and security controls** for sandboxed Python execution through a multi-layered Rust architecture:

- **Memory safety** enforced via `max_allocations`, `max_memory`, and large-result pre-checks in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs)
- **Temporal controls** limiting CPU time through periodic `check_time` calls in the execution engine
- **Structural limits** preventing stack overflow via `max_recursion_depth` validation before frame pushes
- **Signal integration** allowing host processes to abort execution via `PySignalTracker` in the Python bindings
- **Uncatchable exceptions** ensuring resource violations bypass sandboxed `try/except` blocks through [`exception_private.rs`](https://github.com/pydantic/monty/blob/main/exception_private.rs)

These controls are exposed consistently across Rust, Python, and JavaScript APIs, providing deterministic sandbox behavior that prevents denial-of-service attacks while maintaining safe execution of untrusted code.

## Frequently Asked Questions

### How does Monty prevent infinite loops from consuming all CPU time?

Monty enforces CPU time limits through the `LimitedTracker::check_time` method in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs) (lines 48-64). The tracker samples elapsed time every 10 instruction checks using `Instant::elapsed()`. When the cumulative execution time exceeds `max_duration`, it returns `ResourceError::Time`, which the VM converts to an uncatchable `TimeoutError` that terminates the sandbox immediately.

### Can sandboxed Python code catch and suppress resource limit errors?

No. Monty converts all resource violations into **uncatchable Python exceptions** via `ResourceError::into_exception` in [`crates/monty/src/exception_private.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/exception_private.rs) (lines 1382-1393). These exceptions—`MemoryError`, `TimeoutError`, and `RecursionError`—bypass standard `try/except` blocks in the sandboxed code, ensuring malicious scripts cannot swallow resource violations to continue execution.

### What prevents a sandboxed script from allocating a single massive object to exhaust memory?

Monty implements **large-result pre-checks** through helper functions like `check_repeat_size`, `check_pow_size`, and `check_mult_size` in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs) (lines 20-70). These functions estimate the byte size of expensive operations—such as `2**10_000_000` or `"x" * 1_000_000_000`—before allocation occurs. If the estimate exceeds 100 KB, the tracker returns `ResourceError::LargeResult`, preventing temporary allocation attacks.

### How does Monty handle Ctrl-C or host process signals during execution?

The `PySignalTracker` wrapper in [`crates/monty-python/src/limits.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/limits.rs) (lines 73-88) decorates any `ResourceTracker` to poll Python signals every 1,000 time checks. This allows the host process to respond to `SIGINT` (Ctrl-C) or custom Python signals even while executing untrusted code, safely aborting the sandbox without corrupting the host state or leaving resources locked.