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 checkmax_allocationsandmax_memorycheck_time– Invoked at instruction boundaries to enforcemax_durationcheck_recursion_depth– Validates stack depth before pushing new framescheck_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 incrates/monty/src/resource.rs - Temporal controls limiting CPU time through periodic
check_timecalls in the execution engine - Structural limits preventing stack overflow via
max_recursion_depthvalidation before frame pushes - Signal integration allowing host processes to abort execution via
PySignalTrackerin the Python bindings - Uncatchable exceptions ensuring resource violations bypass sandboxed
try/exceptblocks throughexception_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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →