limitations/resource_limits.md
Monty enforces limits on memory, time, and recursion to keep untrusted code
bounded. Memory limits surface to the host as MemoryErrors and time limits as
TimeoutErrors; sandboxed code cannot catch either resource error.
RecursionError is catchable, as in CPython.
max_duration starts when the VM executes, so parsing, preparation, and
bytecode compilation do not consume it. In workers, allocations retained by
compiled code do count toward max_memory; transient compilation allocations
are released before execution reaches its first memory checkpoint.
Compilation has separate structural caps for parser nesting, bytecode operand
sizes, comprehension nesting, and repeated finally expansion. In particular,
a code object requiring more than 1,024 emitted copies of finally bodies is
rejected with SyntaxError; CPython has no equivalent limit. Production hosts
should still isolate compilation when accepting untrusted source, as the
subprocess and WebAssembly runtimes do.
monty-alloc as the global allocator and arm it with
set_limit before using max_memory; without it usage always reads as
zero and the limit is silently not enforced.'x' * n), replacement
(str.replace, bytes.replace), padding (str.ljust, str.center,
str.zfill, bytes.ljust, …), and f-string formatting
(both dynamic width f"{v:>{w}}" and dynamic precision on float
formats f"{v:.{p}f}" / e / %). The pre-check threshold is 100 KB —
estimates above that are checked against the remaining budget and rejected
with MemoryError before allocation when they would exceed it.bigint.pow(base, exp) estimates result size as bits(base) * exp with
a 4× safety multiplier to cover repeated-squaring intermediate values.max_memory in a worker (pools)A worker counts every byte requested from its global allocator. Nothing extra is
enabled by the host: setting max_memory on a session applies it, and a session
without one is unlimited.
MemoryError to the
host after crossing it. The incomplete operation is unwound and the worker
and session survive, although sandboxed Python cannot catch resource errors.mmap. It is not a kernel-enforced
bound on process memory — an inherited ulimit -v or cgroup limit is the tool
for that, and still applies independently (a worker whose allocation the
kernel then refuses reports the same MemoryError).max_memory alone does not bound worker memory. The hard ceiling includes
the worker's baseline plus a fixed gap above the soft limit — a few MiB, more
with type checking. Use max_processes and an OS-level limit to bound a host.load_session /
load_snapshot restore the dump's own limits (see
limitations/pool-architecture.md), and the cap is re-derived from them
once the session exists — but the load itself runs under the limit the
checkout() config applied. Restoring a large dump into a checkout with a
much smaller max_memory can therefore exceed it while loading; pass a
comparable limit to checkout().MemoryError, but exceeding the hard limit traps the instance and the host
reports MontyCrashedError. Its usize is also 32 bits, so a limit near
4 GiB leaves the module uncapped.Independently of any limit, any allocation a worker's allocator refuses —
plain host OOM, or a request beyond the usable address space such as
' ' * (1 << 60) — takes this same path: on a worker with an exit status the
host sees that MemoryError with its session gone, and on wasm the same
refusal traps, reported as MontyCrashedError per the bullet above. CPython
raises a catchable MemoryError in-process and carries on. Monty cannot: the failure
happens below the interpreter, where no Python-level exception can be raised, so
the worker classifies the failure into a dedicated exit code and dies. (Without
that, the process would abort with SIGABRT — indistinguishable from a stack
overflow, which is why the sandbox exits deliberately instead.)
pow(base, exp) / base ** exp with an exponent larger than u32::MAX
(≈ 4.3 × 10⁹) raises OverflowError: "exponent too large".pow(base, exp, mod) requires all integer arguments and rejects negative
exponents (ValueError).int(str_or_bytes, base) rejects inputs over 4,300 digits before the
potentially quadratic BigInt parse when the effective base is not a power
of two. The fixed cap matches CPython's
sys.int_info.default_max_str_digits.RecursionError.sys.setrecursionlimit() as a lowering-only fixture hook; it cannot
raise the host-configured ceiling.await boundary is treated
as one frame, so await-chains do not amplify depth.map(), filter(),
sorted()/list.sort(key=...), min()/max(key=...), recursive
__repr__/__str__, and non-plain-function __init__ values that recurse
during construction. Native re-entry is capped independently at a lower
fixed depth than the 1000-frame Python limit, so Monty raises
RecursionError before a native stack overflow would abort the process. See
limitations/classes.md's __repr__/__str__ entry for the main
user-visible divergence this causes.max_duration budget; if exceeded the VM stops on
the next bytecode boundary with ResourceError.bytes substring scan, a sort, an iterator
drain), and those poll the clock at a coarse granularity. A run can
therefore overshoot max_duration before stopping.bytes operations that search for a sub-sequence (in with a bytes-like
probe, find, count, split, partition, replace and their
variants) poll the clock every 64KiB, or every two lengths of the
searched-for sequence if that is longer. Searching for a
sequence over 64KiB therefore overshoots max_duration in proportion to
its length.bytes operations that scan without a sub-sequence are
not polled and run to completion however large the input: in with an
integer probe (a single-byte scan) and split()/rsplit() left to their
default sep=None (whitespace splitting).json.loads rejects input nested deeper than 200 levels with
json.JSONDecodeError (independent of the Python recursion limit).A worker remains responsive after a soft memory or time limit and its session
can receive another feed, but execution is not transactional and no guarantees
are made about heap state or reference counts. Hosts should discard the session;
the worker itself remains reusable. A caught RecursionError may continue
normally inside the sandbox.