Monty Resource Limits: How the Python Sandbox Enforces Security Boundaries
Monty enforces resource limits through the ResourceTracker trait defined in crates/monty/src/resource.rs, capping heap allocations, execution duration, memory consumption, and recursion depth to prevent untrusted Python code from exhausting host resources.
Monty is a secure Python sandbox developed by Pydantic that executes untrusted code while preventing resource exhaustion attacks. Understanding Monty resource limits is essential for configuring appropriate security boundaries when running arbitrary Python code in production environments.
Core Resource Limits Defined in Monty
Monty defines resource constraints through the ResourceLimits struct in crates/monty/src/resource.rs. These limits are enforced by the ResourceTracker trait, which provides hooks for the interpreter to validate resource consumption at allocation sites, statement boundaries, and recursion points.
Maximum Heap Allocations
The ResourceLimits::max_allocations field (line 73) defines the total number of heap allocations permitted during execution. The LimitedTracker::on_allocate method checks self.allocation_count against this limit on every allocation. If the count exceeds the configured maximum, the tracker returns ResourceError::Allocation, which Monty converts into a Python MemoryError.
Execution Time Limits
Time-based resource limits are controlled by ResourceLimits::max_duration (line 76). The LimitedTracker::check_time method samples Instant::elapsed() every 10 VM steps (defined by TIME_CHECK_INTERVAL). When elapsed time exceeds max_duration, the tracker raises ResourceError::Time, which becomes a Python TimeoutError.
Memory Usage Caps
The ResourceLimits::max_memory field (line 78) restricts total heap memory consumption in bytes. Both LimitedTracker::on_allocate and LimitedTracker::check_large_result verify that new memory usage stays below this threshold. If an allocation would exceed the limit, ResourceError::Memory is returned and converted to MemoryError.
Recursion Depth Restrictions
Monty enforces two distinct recursion limits:
-
Call Stack Depth:
ResourceLimits::max_recursion_depth(line 82) controls Python function call recursion.LimitedTracker::check_recursion_depthvalidates the depth before pushing a new frame, returningResourceError::Recursion(converted toRecursionError) if exceeded. -
Data Structure Recursion:
MAX_DATA_RECURSION_DEPTH(lines 99‑115) limits recursion during data structure operations likerepr()or equality checks. TheDepthGuardstruct (lines 121‑172) manages this counter, ensuring that operations on deeply nested containers raise aRecursionErrorbefore causing a native stack overflow.
Large Result Pre-checks
Before performing operations that could allocate large contiguous blocks (such as huge exponentiation or string repetition), Monty calls ResourceTracker::check_large_result. The threshold LARGE_RESULT_THRESHOLD (line 18) is set to 100 KB. While the default NoLimitTracker permits any size, LimitedTracker verifies the requested size against max_memory and rejects it with ResourceError::Memory if it would exceed the cap.
Runtime Enforcement: LimitedTracker vs. NoLimitTracker
Monty provides two implementations of the ResourceTracker trait:
LimitedTracker: Enforces all configuredResourceLimitsincluding allocations, time, memory, and recursion. Used when running untrusted code.NoLimitTracker(lines 22‑64): A permissive tracker that only enforces the default recursion limit of 1000 (matching CPython behavior). All other resource limits are disabled unless a customLimitedTrackeris supplied.
The ResourceError enum (lines 85‑94) defines concrete error variants (Allocation, Time, Memory, Recursion) that are turned into Python exceptions via into_exception (lines 122‑156).
Configuring Resource Limits in Python
The pydantic_monty package exposes resource limits through the ResourceLimits class defined in crates/monty-python/src/limits.rs. You can configure limits using a fluent API:
from pydantic_monty import Monty, ResourceLimits
import datetime
# Limit execution to 200 milliseconds
limits = ResourceLimits().max_duration(datetime.timedelta(milliseconds=200))
m = Monty("while True: pass", limits=limits)
try:
m.run()
except TimeoutError as e:
print("Timeout triggered:", e)
Memory and allocation limits work similarly:
from pydantic_monty import Monty, ResourceLimits
# Allow at most 10,000 heap allocations and 5 MiB memory
limits = ResourceLimits().max_allocations(10_000).max_memory(5 * 1024 * 1024)
m = Monty("a = [0] * 10_000_000", limits=limits)
try:
m.run()
except MemoryError as e:
print("Memory limit hit:", e)
Configuring Resource Limits in Rust
For internal use or custom integrations, you can construct a LimitedTracker directly in Rust:
use monty::{LimitedTracker, ResourceLimits};
use std::time::Duration;
let limits = ResourceLimits::new()
.max_allocations(5_000)
.max_memory(2 * 1024 * 1024) // 2 MiB
.max_duration(Duration::from_secs(1))
.max_recursion_depth(Some(200));
let tracker = LimitedTracker::new(limits);
// Pass `tracker` into the VM execution context
The execution loop in crates/monty/src/run.rs calls check_time, check_recursion_depth, and other tracker hooks at statement boundaries to ensure continuous enforcement.
Summary
Monty implements a comprehensive resource limiting system to safely execute untrusted Python code:
- Allocation caps prevent excessive object creation via
max_allocationsenforced inon_allocate. - Time limits interrupt infinite loops using
max_durationchecked every 10 VM steps. - Memory boundaries restrict heap usage through
max_memoryverified during allocations and large-result operations. - Recursion guards protect both the Python call stack (
max_recursion_depth) and data structure operations (MAX_DATA_RECURSION_DEPTHviaDepthGuard). - Large-result pre-checks block operations that would allocate >100 KB contiguous blocks if they exceed memory limits.
These limits are configurable through the ResourceLimits struct and enforced by the LimitedTracker implementation, while NoLimitTracker provides a permissive mode for trusted code.
Frequently Asked Questions
What happens when a Monty resource limit is exceeded?
When a limit is exceeded, the LimitedTracker returns a ResourceError variant (such as ResourceError::Allocation, ResourceError::Time, or ResourceError::Memory). These errors are converted into Python exceptions via the into_exception method in crates/monty/src/resource.rs. For example, exceeding the time limit raises TimeoutError, while exceeding memory or allocation limits raises MemoryError.
Can I disable specific resource limits while keeping others active?
Yes. Monty provides the NoLimitTracker which disables all limits except the default recursion depth of 1000. To selectively disable limits, you can instantiate ResourceLimits with None for specific fields (where applicable) or use NoLimitTracker for unrestricted execution. For fine-grained control, implement a custom ResourceTracker trait that selectively enforces only the limits you require.
How does Monty prevent stack overflow from deeply nested data structures?
Monty uses a separate MAX_DATA_RECURSION_DEPTH constant (defined at lines 99‑115 in crates/monty/src/resource.rs) to limit recursion during data structure operations like repr() or equality checks. The DepthGuard struct (lines 121‑172) manages this counter, ensuring that operations on deeply nested containers raise a RecursionError before causing a native stack overflow. This complements the call-stack limit enforced by max_recursion_depth.
What is the performance impact of Monty's resource tracking?
The performance impact is minimal due to strategic sampling. For example, time checks occur only every 10 VM steps (TIME_CHECK_INTERVAL) rather than at every instruction. Allocation counters increment on every heap allocation but use simple integer arithmetic. The NoLimitTracker provides zero-overhead execution for trusted code by disabling all optional checks. According to the implementation in crates/monty/src/run.rs, the tracker hooks are called at statement boundaries and allocation sites, ensuring safety without excessive overhead.
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 →