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

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 defines the sandbox boundaries through a builder pattern. You can constrain allocation counts, heap memory, execution duration, and recursion depth independently:

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) 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 (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). 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) 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). 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). 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). 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, 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). 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) converts the Python dictionary into the Rust ResourceLimits struct, handling optional fields and default values.

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).

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
  • 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

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 (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 (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 (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 (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.

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 →