
Monty 资源限制机制详解内存、时间、递归与挂起的多层防护体系【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/montyMonty 是一个用 Rust 编写的极简、安全的 Python 解释器专为 AI 场景下的沙箱执行而设计。本篇文章以仓库 limitations/resource_limits.md 为骨架系统讲解 Monty 对不可信代码的四类资源约束——内存max_memory、执行时间max_duration、递归深度max_recursion_depth与主机挂起次数max_suspensions并结合 docs/resource-limits.md 的宿主侧配置说明、crates/monty-types/src/resource.rs 的ResourceTracker实现、crates/monty/src/resource_checks.rs 的预检逻辑以及 crates/monty-alloc/src/lib.rs 的分配器实现说明每项限制的配置方法、底层原理、边界行为与失效模式。读完本文你将掌握如何为 Monty 会话配置安全的资源预算、理解软/硬内存双上限的运作机制以及在资源限制触发后如何正确处理会话。总览谁限制什么Monty 与宿主host在资源管控上分工明确解释器侧限制内存、执行时间和递归深度宿主侧限制挂起suspension事件数量。当超出限制时返回值分别是超出限制宿主可见异常沙箱内可否捕获max_memoryMemoryError否max_durationTimeoutError否max_suspensionsRuntimeError否max_recursion_depthRecursionError是与 CPython 一致这套映射关系在 crates/monty/src/resource_checks.rs 的FromResourceError for RunError实现中固化Memory映射为不可捕获的MemoryErrorTime映射为不可捕获的TimeoutErrorRecursion映射为可捕获的RecursionError。其依据是 crates/monty-types/src/resource.rs 中ResourceError的注释除Recursion外的所有变体在沙箱内都不可捕获因为不可信代码绝不能拦截资源强制手段。五个可配置项的语义与默认值如下出自 docs/resource-limits.md 的“The five settings”一节键含义可否禁用max_memory最大堆内存字节可省略或Nonemax_duration_secs累计执行时间秒可省略或Nonemax_recursion_depth函数调用栈最大深度默认 1000不可gc_interval每 N 次分配运行一次垃圾回收可默认内置每 100,000 次分配回收无法关闭max_suspensions每次会话允许的宿主往返次数默认 1000不可在 JavaScript 中对应字段为maxMemory、maxDurationSecs、maxRecursionDepth、gcInterval与maxSuspensions在 Rust 中则是 crates/monty-types/src/resource.rs 定义的ResourceLimits结构体字段max_duration为Duration类型。ResourceLimits::default()返回“仅递归挂起有界”的默认配置max_duration、max_memory、gc_interval均为Nonemax_recursion_depth与max_suspensions均为DEFAULT_MAX_RECURSION_DEPTH/DEFAULT_MAX_SUSPENSIONS即 1000。之所以递归和挂起不能禁用源码注释给出了原因无界的递归会让沙箱代码撑爆原生栈从而 abort 进程无界的挂起会让代码在max_duration暂停期间无限循环调用宿主见 crates/monty-types/src/resource.rs。宿主侧配置示例Python 侧pydantic_monty通过pool.checkout(limits...)传入限制from pydantic_monty import Monty, MontyRuntimeError limits { max_memory: 10_000_000, max_duration_secs: 1.0, max_recursion_depth: 100, } with Monty() as pool: with pool.checkout(limitslimits) as session: try: session.feed_run(x [0] * 100_000_000) except MontyRuntimeError as exc: print(exc.display(formattype-msg).split(:)[0]) # MemoryErrorTypeScript 侧pydantic/monty写法对称import { Monty, MontyRuntimeError } from pydantic/monty const limits { maxMemory: 10_000_000, maxDurationSecs: 1, maxRecursionDepth: 100, } await using pool await Monty.create() await using session await pool.checkout({ limits }) try { await session.feedRun(x [0] * 100_000_000) } catch (err) { if (!(err instanceof MontyRuntimeError)) throw err console.log(err.display(type-msg).split(:)[0]) // MemoryError }注意示例中x [0] * 100_000_000触发的正是预检机制结果大小可预测因此在实际分配前就被拒绝并抛出MemoryError。编译期不占时间预算但有结构上限max_duration从VM 开始执行那一刻起计因此解析parsing、预处理preparation与字节码编译bytecode compilation都不消耗时间预算。但在 worker 场景下编译产物被保留的分配会计入max_memory瞬态编译分配会在执行到达第一个内存检查点前被释放。编译阶段有自己的结构上限structural caps独立于执行期限制解析器嵌套深度AST 嵌套 200 层字节码操作数大小推导式comprehension嵌套深度重复finally展开一个代码对象若需要超过1,024份finally体副本会被以SyntaxError拒绝——CPython 没有对应的等价限制。正因如此docs/resource-limits.md 提醒接收不可信源码的生产宿主仍应隔离编译过程就像子进程与 WebAssembly 运行时所做的那样。这一点与 docs/limitations/pool-architecture.md 中“worker 与宿主之间通过 protobuf 协议通信、崩溃只波及 worker”的架构一脉相承——编译期无法完全防住的栈溢出与分配器 abort 只杀死 worker而不影响宿主进程。内存限制分配器计数的软上限与硬顶计数口径全局分配器内存用量由worker 进程的全局分配器度量而配置的预算属于单个会话。Worker 统计从其全局分配器请求的每一个字节。直接使用 Rust 的用户必须把monty-alloc安装为全局分配器并在使用max_memory前通过set_limit武装它否则用量恒读为 0限制被静默地不执行。它绑定的是 worker 的分配器而非进程只统计经 Rust 全局分配器请求的字节——即沙箱代码能导致分配的一切——但不包括线程栈、二进制自身的映射镜像或直接mmap获得的内存。它不是内核强制的进程内存上限ulimit -v或 cgroup 限制才是对应工具且二者独立生效内核拒绝分配时worker 报告同样的MemoryError。它统计的是请求的字节而非驻留字节每次分配的开销与碎片位于计数与进程真实占用之间因此 RSS 会略高于限制值。在实现层面crates/monty-alloc/src/lib.rs 用LIVE_MEMORY与BASELINE_MEMORY两个原子变量维护“活动字节数”与“进程最瘦基线”charge()在每次alloc/realloc时累加请求大小refund()在dealloc时归还crates/monty-alloc/src/lib.rs。set_limit()用BASELINE_MEMORY.fetch_min(live)取进程曾达到的最瘦值作为基线再叠加会话预算得到软上限crates/monty-alloc/src/lib.rs。probe_memory()读取LIVE_MEMORY - BASELINE_MEMORYcrates/monty-types/src/resource.rs这正是解释器在执行检查点读取的当前用量。软限制解释器检查点配置的限制是软性的。解释器在执行检查点读取当前分配器用量越过后向宿主报告终止性MemoryError未完成的操作被回卷unwoundworker 与会话存活但沙箱内 Python 无法捕获资源错误。硬顶突发仍可能杀死 worker在配置限制之上还有一道硬顶hard ceiling为异常与回溯机制留出运行空间。两道限制之间的突发分配会以专属 OOM 退出状态结束子进程或在 wasm 上触发 trap池会替换 worker、会话丢失。结果大小已知的大操作通过预检规避此路径。在代码中软/硬两个上限由SOFT_LIMIT与HARD_LIMIT承载charge()只有在live SOFT_LIMIT live HARD_LIMIT时才直接触发out_of_memorycrates/monty-alloc/src/lib.rs。硬顶 基线 软限 固定余量常规为 4 MiBBASE_HEADROOM启用类型检查时为 32 MiBTYPE_CHECK_HEADROOM因为其存根与缓存分配在执行之外。OOM_EXIT_CODE 65取自 BSDsysexits.h的EX_DATAERR使裸退出码在日志中可读。Python 执行之外的路径只有硬顶请求组帧request framing、输入解码、加载快照与类型检查都不会到达解释器检查点。这些路径上一次足够大的分配就可能越过硬顶杀死 worker。跨边界值的“三份拷贝”问题一个值跨界到宿主需要约三份自身的空间返回结果或传给宿主函数的参数会同时持有沙箱值、其宿主侧转换形态以及编码帧。因此单个值的有效上限约为max_memory的三分之一——对普通负载远低于限制但在紧预算下数 MiB 的参数可能在宣布调用时就越过硬顶。其他内存边界行为max_memory单独无法约束 worker 内存硬顶包含 worker 基线加软限之上的固定余量几 MiB类型检查时更多。用max_processes与 OS 级限制来约束宿主。按会话计但基于固定基线一个 worker 服务多次 checkout每次会话都从进程曾达到的最瘦状态重新推导上限。会话之间保留的内存消耗的是余量而非抬高上限残留超出者被杀死并替换而不是无限增长。恢复 dump 受其落地的 checkout 限制load_session/load_snapshot会恢复 dump 自身的限制见 docs/limitations/pool-architecture.md会话存在后上限从它们重新推导但加载本身在checkout()配置应用的限制下运行。把大 dump 恢复到max_memory小得多的 checkout 中加载时可能超出应向checkout()传入相当的限制。wasm worker 无法分类硬性越界软性越界是正常MemoryError但超出硬顶会 trap 实例宿主报告MontyCrashedError。其usize为 32 位接近 4 GiB 的限制会让模块失去上限。WebSocket worker 完全没有分配器强制限制它们是本池不派生的远程进程。分配器拒绝底层失败如何呈现独立于任何限制只要 worker 的分配器拒绝任何分配——普通宿主 OOM或超出可用地址空间的请求如 * (1 60)——都会走同一条路径在有退出状态的 worker 上宿主看到MemoryError且会话消失在 wasm 上同样的拒绝触发 trap报告为MontyCrashedError。CPython 会在进程内抛出可捕获的MemoryError并继续运行Monty 做不到失败发生在解释器之下那里无法抛出任何 Python 级异常因此 worker 将失败归类为专属退出码然后终止。若不这样做进程将以SIGABRT中止而它与栈溢出无法区分docs/limitations/pool-architecture.md 对此有同样的阐述SIGABRT由栈溢出与分配器 abort 共同产生不可分类。大结果预检在分配之前拒绝结果大小可由输入尺寸的简单算术界定的操作在分配前会经过预检。涉及的操作为整数乘法、左移、整数幂、序列重复x * n、替换str.replace、bytes.replace、re.sub、填充str.ljust、str.center、str.zfill、bytes.ljust等、整数除法与divmod、deque 旋转、切片与重复、把迭代器物化为容器以及带动态宽度或精度的字符串格式化——包括 f-stringf{v:{w}}、f{v:.{p}f}与str.format(){0:{1}}.format(v, w)、{0:.{1}f}.format(v, p)。预检阈值为100 KB估算超过该值的操作才会对照剩余预算检查超出即在实际分配前以MemoryError拒绝。因此x * 10**12会立即失败而不是耗尽机器内存后才失败。阈值常量LARGE_RESULT_THRESHOLD 100_000定义于 crates/monty-types/src/resource.rs其文档注释明确这是防止2 ** 10_000_000这类操作在内存检查捕获前分配海量内存的 DoS 防护。核心入口是 crates/monty/src/resource_checks.rs 的check_estimated_size()——仅当估算超过阈值时才调用tracker.check_large_result()避免小操作的开销。各操作的具体估算逻辑同样在 crates/monty/src/resource_checks.rs整数乘法结果最多a_bits b_bits位check_mult_size左移结果约value_bits shift位零值直接跳过check_lshift_size整数除法结果受被除数大小约束check_div_size序列重复item_len * count字节check_repeat_size饱和乘法防溢出替换扩大型替换按最大可能匹配数估算缩小型按input_len估算零匹配也会复制完整输入空模式特殊处理为input_len 1check_replace_sizebigint.pow(base, exp)以bits(base) * exp估算结果大小并带4× 安全乘数覆盖重复平方的中间值——因为BigInt::pow使用重复平方乘法每一步新旧 base 与新旧累加器并存峰值约需最终结果 4 倍内存check_pow_size见 crates/monty/src/resource_checks.rs。位转字节时使用饱和运算溢出意味着结果天文数字般巨大饱和确保限制检查必然触发而非被静默跳过crates/monty/src/resource_checks.rs。fstring 侧还有一个配套细节大容量格式化会延迟到format_builder并逐片复制使截止时间在复制期间被轮询crates/monty/src/fstring.rs。整数专属上限与max_memory无关若干整数操作自带硬性上限pow(base, exp)/base ** exp的指数大于u32::MAX约 4.3 × 10⁹时抛出OverflowError: exponent too large但基数 0、1 与 -1 例外会被正常计算结果恒为小值。pow(base, exp, mod)要求所有参数为整数并拒绝负指数ValueError。int(str_or_bytes, base)在有效基数不是 2 的幂时会在可能二次方的 BigInt 解析前拒绝超过4,300 位的输入。这个固定上限与 CPython 的sys.int_info.default_max_str_digits一致。递归限制唯一可捕获的资源错误Python 级调用深度默认1000 帧第 1001 层嵌套调用抛出RecursionError。宿主通过max_recursion_depth按会话设置上限但无法移除——与时间和内存限制不同它没有“禁用”状态。生产沙箱代码不能修改递归限制。测试构建可能把sys.setrecursionlimit()暴露为仅用于降低限制的 fixture 钩子它无法提高宿主配置的上限。实现上ResourceTracker::lower_recursion_limit在test-hooksfeature 下只能把活动上限调低高于当前上限的请求会被拒绝crates/monty-types/src/resource.rs。异步栈会计入限制但每个await边界算一帧因此await链不会放大深度。解释器自身同步求值的回调在原生 Rust 调用栈上重入而非普通函数调用使用的堆分配帧栈。包括map()、filter()、sorted()/list.sort(key...)、min()/max(key...)、递归__repr__/__str__、构造期间递归的非普通函数__init__值以及调用functools.partial。原生重入以低于1000 帧 Python 限制的固定深度独立设限因此 Monty 会在原生栈溢出 abort 进程之前抛出RecursionError。其主要用户可见差异见 docs/limitations/classes.md 中__repr__/__str__条目。check_recursion_depth的实现crates/monty-types/src/resource.rs在压入新帧前检查current_depth limit即失败报告的深度为current_depth 1。挂起限制宿主的计数职责max_suspensions限制会话向宿主挂起的次数外部函数调用、宿主对象方法调用、属性查找与构造、OS 调用、名字查找以及每次ResolveFutures往返部分 future 解析后重新挂起会再次计数。默认 1000且不可禁用同max_recursion_depth省略或传None都保留默认值合法宿主调用更多的会话应设置更大的数。monty-pool为pydantic_monty、JavaScript napi 池与 monty-server 强制执行它wasm worker 池与 CLI 也执行。直接宿主必须自行计数并在超限时调用abort。超预算的第一次挂起不会返回给调用者。宿主多用一次 worker 往返在挂起点以回溯抛出不可捕获的RuntimeError: suspension limit N exceeded。计数在 checkout 生命周期内持续存在。一旦耗尽之后每次 feed 都在第一次挂起时结束。不挂起的 feed 仍可运行堆保持一致会话仍可 dump。dump 中只携带限制本身。恢复的会话保留max_suspensions但计数归零恢复 checkout 上配置的限制会封顶 dump 的两者取较小者若 worker 回复省略则只用配置值。宿主对恢复会话“重新收养”的限制以 checkout 自身配置为上限因为回复不可信见 docs/limitations/pool-architecture.md。沙箱内无法观测预算或剩余计数。时间限制累计执行时间与非抢占式轮询宿主可设置max_duration预算超限后 VM 在下一个检查点以ResourceError停止。执行是轮询而非抢占式单条字节码指令可能运行很长的原生操作bytes子串扫描、排序、迭代器排空它们以较粗粒度轮询时钟。因此运行可能在停止前超出max_duration。检查点是摊销而非逐指令分发循环每 256 条指令读一次时钟自行轮询的原生循环迭代器推进、序列重复、比较、repr每 64 项轮询一次LOOP_CHECK_INTERVAL 64见 crates/monty-types/src/resource.rs。这两者都是普通max_duration强制之外的无条件超出来源。每个宿主轮次返回时都重查两项限制因此一个未到检查点就结束的轮次仍会失败而非返回结果。两个后果轮次中 Python 代码抛出的异常会被资源错误取代内部吞掉超时的操作如repr以...[timeout]截断仍会使包含它的轮次失败。bytes子序列搜索操作用 bytes-like 探针的in、find、count、split、partition、replace及变体每 64 KiB 轮询一次时钟若被搜索序列更长则每两倍其长度轮询一次。因此搜索超过 64 KiB 的序列时超出量与其长度成正比。相邻的无子序列扫描bytes操作不轮询无论输入多大都运行到完成整型探针的in单字节扫描与默认sepNone空白切分的split()/rsplit()。base64.a85decode()每 64 个不匹配 Ascii85 数字的字节轮询一次即到达ignorechars时每个这样的字节都是一次针对容器的in测试因此大的显式ignorechars会造成与其长度成正比的超时。预算覆盖累计执行时间而非墙钟时间时钟仅在解释器执行字节码时运行在挂起等待宿主外部函数调用、OS 回调以及 REPL feed 之间暂停。它在会话生命周期内跨 feed 累积。宿主侧不会维护第二套时钟——worker 的时钟是唯一事实来源见 docs/limitations/pool-architecture.md。累计时间被序列化进 dump/快照恢复的会话从离开处续用预算而不是从零重启。沙箱内无法观测预算或剩余时间。ResourceTracker的时间实现细节crates/monty-types/src/resource.rson_execution_start/on_execution_stop界定执行窗口running_since仅在窗口打开期间持有Instant累计时间存入total_execution_time并被序列化。elapsed()返回累计值加进行中的窗口。wasm32-unknown-unknown 目标上标准库Instant::now()会 panic因此该目标换用web_time::Instantcrates/monty-types/src/resource.rs。宿主侧的两个兜底沙箱内检查运行于解释器检查点无法捕获卡死解释器本身的代码。两个宿主侧兜底覆盖该场景详见 docs/resource-limits.mdrequest_timeout池上的硬性单轮截止时间。worker 超时即被杀调用抛出带timed_outTrue的MontyCrashedError。每次宿主函数或 mount 调用后的恢复都会开启新截止时间因此频繁挂起的程序可以活过任何单次超时。时长兜底duration backstop对有max_duration_secs限制的会话worker 在每个协议轮次报告执行时间宿主在预算到期后宽限期杀死 worker。宽限期默认 1 秒JavaScript 中是durationLimitGrace池选项null禁用Python 侧目前不可配置。针对可能反复挂起的不可信代码应设置max_duration_secs仅靠request_timeout无法约束整体调用。JSON 与“未覆盖”领域json.loads拒绝嵌套超过200 层的输入抛出json.JSONDecodeError独立于 Python 递归限制。以下资源不在max_memory/max_duration覆盖内docs/resource-limits.md 的“What is not covered”一节编译时间解析与字节码编译发生在 VM 存在之前不计入时长预算worker 中编译代码保留的内存计入max_memory。编译有自身的结构上限见上文。打印收集器CollectString/CollectStreams存活于宿主进程其默认 10 MiB 上限与max_memory相互独立。挂载内存每个 mount 有各自的memory_usage_limit默认 100 MB保留的 overlay 数据与瞬态结果共享该预算CPython 没有等价的默认限制。超过 256 MiB 会重新暴露线上帧上限。宿主实例存储每个送入会话的ClassInstance/ClassType包装含嵌套包装、initTrue构造与convert_value包装在会话结束前保留在宿主进程且不受沙箱max_memory覆盖长时间运行的暴露对象方法的会话应限制分发量或定期回收会话。终止性资源错误之后软性内存或时间限制触发后worker 仍保持响应其会话还能接收下一次 feed但执行不是事务性的堆状态与引用计数不做任何保证——堆上可能残留引用计数错误的孤儿对象。宿主应当丢弃会话worker 本身可复用。而可捕获的RecursionError是例外它不会使任何东西失效沙箱内可正常继续执行。池不会替你收尾checkout 保持打开并接受后续feed_run调用。由于max_duration_secs是累计预算一旦耗尽之后每个 feed 都会立即以同样的TimeoutError失败max_memory触发后后续 feed 可能在你已无法信任的堆上悄悄成功。结束会话是你的职责。这与 docs/limitations/pool-architecture.md 中“资源耗尽对会话是终止性的但 worker 进程为下一个 checkout 复用”的表述一致。参考与延伸阅读本文主体limitations/resource_limits.md宿主侧配置总览五设置、兜底与未覆盖领域docs/resource-limits.mdResourceTracker/ResourceLimits/ResourceError实现crates/monty-types/src/resource.rs大结果预检实现crates/monty/src/resource_checks.rs软/硬内存双上限与 OOM 退出码实现crates/monty-alloc/src/lib.rsWorker 执行模型与资源限制在池中的呈现docs/limitations/pool-architecture.md时间限制的测试覆盖执行时间与 GC 间隔crates/monty/tests/resource_limits.rs【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/monty创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考