How to Configure Monty's Resource Limits: Memory, Time, and Recursion Depth
Configure Monty's resource limits by creating a ResourceLimits struct (Rust) or ResourceLimits TypedDict (Python) with values for max_memory, max_duration, max_allocations, and max_recursion_depth, then pass it to LimitedTracker in Rust or the resource_limits parameter in Python to enforce sandbox boundaries.
Monty is a sandboxed Python interpreter developed by Pydantic that executes untrusted code safely through strict resource constraints. To prevent denial-of-service attacks from runaway memory consumption, infinite loops, or stack overflow exploits, you must configure Monty's resource limits before executing arbitrary code. This guide explains the ResourceTracker architecture and provides complete implementation examples for both Rust and Python environments.
Understanding Monty's Resource Tracking Architecture
Monty implements resource limits through the ResourceTracker trait defined in crates/monty/src/resource.rs. This trait provides hooks that the virtual machine calls during critical operations to verify that configured thresholds remain within bounds.
Core Components
The resource management system consists of several key types that work together to enforce constraints:
ResourceLimits– A configuration struct using the builder pattern to set optional caps on memory, time, allocations, and recursion depth.LimitedTracker– The primary implementation ofResourceTrackerthat enforces the constraints defined inResourceLimits.NoLimitTracker– A permissive implementation that only enforces the default recursion limit of 1000 frames without restricting memory or execution time.PySignalTracker– A wrapper available in the Python bindings that adds Ctrl+C handling by checking Python signals periodically.
Configuring Resource Limits in Rust
To configure limits in a Rust application using Monty, instantiate ResourceLimits using the builder pattern and pass it to LimitedTracker.
use monty::{
ResourceLimits, LimitedTracker, DEFAULT_MAX_RECURSION_DEPTH,
ResourceTracker,
};
use std::time::Duration;
// Configure specific resource constraints
let limits = ResourceLimits::new()
.max_allocations(10_000) // Maximum 10,000 heap allocations
.max_memory(5 * 1024 * 1024) // Maximum 5 MiB heap usage
.max_duration(Duration::from_secs(2)) // Maximum 2 seconds CPU time
.gc_interval(500) // Run GC every 500 allocations
.max_recursion_depth(Some(200)); // Maximum 200 call-stack frames
// Create the tracker that enforces these limits
let mut tracker = LimitedTracker::new(limits);
// Execute code with the tracker
let code = "def fib(n):\n return n if n<2 else fib(n-1)+fib(n-2)\nfib(30)";
let mut monty = Monty::new(code)?;
monty.run_with_tracker(&mut tracker)?;
The LimitedTracker provides methods to inspect current usage, including allocation_count(), current_memory(), and elapsed(). You can also adjust time limits dynamically between executions using set_max_duration().
Configuring Resource Limits in Python
When using Monty through its Python bindings, pass a ResourceLimits TypedDict to the Monty constructor. The extraction logic resides in crates/monty-python/src/limits.rs.
from pydantic_monty import Monty, ResourceLimits
# Define resource constraints as a TypedDict
limits = ResourceLimits(
max_allocations=20_000,
max_duration_secs=1.5, # Seconds as float
max_memory=4 * 1024 * 1024, # 4 MiB
gc_interval=250,
max_recursion_depth=300,
)
# Instantiate Monty with these limits
code = """
def fact(n):
return 1 if n==0 else n*fact(n-1)
fact(25)
"""
m = Monty(code, resource_limits=limits)
# Execute - raises MemoryError, TimeoutError, or RecursionError on breach
result = m.run()
print(result)
The Python wrapper automatically maps resource violations to standard Python exceptions. Memory and allocation limits raise MemoryError, time limits raise TimeoutError, and recursion depth limits raise RecursionError.
How Monty Enforces Resource Limits Internally
Understanding the enforcement mechanisms helps you tune limits effectively for specific workloads.
Allocation and Memory Tracking
Every heap allocation in Monty invokes tracker.on_allocate(|| size) before committing memory. The LimitedTracker maintains atomic counters for both allocation count and total bytes. If max_allocations or max_memory would be exceeded, it returns ResourceError::Allocation or ResourceError::Memory, which the VM translates to Python MemoryError.
Execution Time Limits
Time checking uses a sampling strategy to minimize overhead. The VM calls tracker.check_time() after every instruction, but LimitedTracker only evaluates Instant::elapsed() every 10 calls (TIME_CHECK_INTERVAL). When elapsed time exceeds max_duration, it raises ResourceError::Time, converted to Python TimeoutError.
Recursion Depth Limits
Before pushing a new call frame, the VM queries tracker.check_recursion_depth(current_depth). The default limit mirrors CPython's 1000 frames, but you can override this via max_recursion_depth in ResourceLimits. Internal operations like repr() and hash() also respect these limits through DepthGuard structures defined in crates/monty/src/resource.rs.
Large Result Protection
Operations that could generate massive intermediate values—such as x * 10**9 or 2**100000—first call check_large_result(estimated_bytes). The estimate uses bit-size heuristics to prevent accidental denial-of-service from huge integer or string allocations.
Python Signal Integration
When using the Python bindings, PySignalTracker wraps your chosen tracker (typically LimitedTracker). Its check_time implementation forwards to the inner tracker, then every 1000 calls invokes py.check_signals() (wrapping PyErr_CheckSignals). If the user presses Ctrl+C, the next signal check raises KeyboardInterrupt, allowing immediate termination of long-running or infinite loops without waiting for other resource limits to trigger.
Summary
Configuring Monty's resource limits involves these essential steps:
- Define constraints using
ResourceLimitswith the builder pattern in Rust or the TypedDict interface in Python to cap memory, time, allocations, and recursion depth. - Instantiate
LimitedTrackerin Rust or passresource_limitsto theMontyconstructor in Python to activate enforcement. - Understand that the VM checks limits via
ResourceTrackertrait methods:on_allocate,check_time, andcheck_recursion_depth. - Recognize that Python bindings automatically map violations to standard exceptions:
MemoryError,TimeoutError, andRecursionError.
Frequently Asked Questions
What happens when a resource limit is exceeded in Monty?
When a limit is breached, the LimitedTracker returns a ResourceError variant (Allocation, Memory, Time, or Recursion) which the Monty VM converts to a standard Python exception. Memory and allocation limits raise MemoryError, time limits raise TimeoutError, and recursion depth limits raise RecursionError. In Rust, these appear as Err(ResourceError) results that you must handle explicitly in your application code.
Can I change resource limits after creating the Monty instance?
Yes, but with specific constraints. In Rust, you can mutate the LimitedTracker between executions using methods like set_max_duration() to adjust the time limit dynamically. However, the ResourceLimits struct itself is typically consumed during tracker construction. In Python, you must create a new Monty instance with updated resource_limits parameters, as the limits are bound at initialization and cannot be modified on existing instances.
How does Monty handle Ctrl+C interruptions during execution?
Monty supports graceful interruption through the PySignalTracker wrapper available in the Python bindings. When you configure resource limits via Python, the LimitedTracker is automatically wrapped in a PySignalTracker that checks for pending Python signals every 1000 VM instructions. If the user presses Ctrl+C, the next signal check raises KeyboardInterrupt, allowing immediate termination of long-running or infinite loops without waiting for other resource limits to trigger.
What is the default recursion depth limit in Monty?
By default, Monty uses a recursion depth limit of 1000 frames, matching CPython's standard behavior. This default is enforced by the NoLimitTracker when no explicit limits are configured, and by LimitedTracker when max_recursion_depth is set to None or omitted from the configuration. You can override this to any positive integer (or None for unlimited) via the ResourceLimits configuration in both Rust and Python interfaces, though setting unlimited recursion is not recommended for sandboxed environments.
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 →