Aptos MonoMove VM 安全与正确性设计指南:漏洞分类、关键不变量与工程落地
2026/9/18 3:06:39 网站建设 项目流程

Aptos MonoMove VM 安全与正确性设计指南:漏洞分类、关键不变量与工程落地

【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core

MonoMove 是 Aptos 仓库(third_party/move/mono-move)中新一代 Move VM 的实现代号,本文围绕其安全设计文档 vm_security_and_correctness.md 展开,系统梳理 VM 层必须坚守的漏洞分类体系与关键不变量。读完本文,你将掌握 MonoMove 在算术安全、内存安全、气体计量、确定性、缓存一致性等维度上的硬性约束,以及这些约束如何在 global-context、runtime、alloc 等模块中落地实现。

文档定位:一份给 VM 实现的"安全宪法"

该文档是 MonoMove 设计与实现系列文档之一,与 closure_design.md、gas_design.md、heap_and_gc.md、value_representation.md 等并列,共同定义新一代 VM 的运行时语义。它的作用不是描述某个具体算法,而是列出 VM 实现任何阶段都不得违反的安全与正确性契约——从漏洞分类、关键不变量到实现层面的强制规则,是代码审查、模糊测试和形式化验证的对照基准。

核心设计原则

文档开篇给出两条奠基性设计原则:

  1. 假定所有漏洞终将被发现(Assume all vulnerabilities will be discovered)。任何可被利用的缺陷最终都会被攻击者发现并利用,或者被安全研究者报告——在 AI 辅助审计工具日益普及的背景下,这种趋势只会加速。因此,不能寄希望于"没人发现",而必须默认漏洞会被曝光并提前防御。
  2. 安全必须内建于核心设计(Security must be integral to the core design)。事后向已有实现"打补丁式"地追加安全保证,远比从第一天起就把安全设计进去困难得多、也更容易出错。这也解释了为什么该文档先于实现、或与实现同步地规定不变量——例如 global-context/src/context.rs 开篇就声明了两阶段状态机的安全契约。

常见漏洞分类

文档将 VM 漏洞归纳为五大类,按严重程度排列:

类别危害描述严重性
铸造 / 数据变造 / 资金损失(Minting / Data Transmutation / Loss of Funds)未经授权地创建、销毁或篡改链上资产最严重的一类
非确定性(Non-determinism)导致链停止出块(chain halt)
崩溃(Crashing)未处理的错误使节点进程终止
重入(Reentrancy)意外的重入调用导致合约级绕过或失败
慢化(Slow-down)恶意输入使执行性能劣化(DoS)

其中,"铸造/数据变造/资金损失"之所以被列为最严重类别,是因为 Move 的全局存储模型下,资源(resource)即资产,任何绕过类型系统的字节级操作(见下文"类型与内存安全")都可能直接改变资产的创建与归属。而"非确定性"之所以会造成链停,是因为所有验证节点必须对同一输入产生完全一致的结果,一旦分歧出现,共识即失效——文档在"严格确定性"一节对这一点给出了更细的约束。

关键不变量(Key Invariants)

以下不变量贯穿 VM 实现全程,是本文档的核心章节。

算术安全

所有算术运算与数值类型转换必须经过检查。除非能证明正确性(例如输入已因先前的边界检查或不变量而确定在合法范围内),否则不允许未经检查的算术(wrapping/overflow)。这意味着 u64/u128 加减乘除、整数移位、整数类型之间的收窄转换等,都必须显式处理溢出与截断,不能依赖 Rust 在 release 模式下的默认 wrapping 行为。

类型与内存安全

MonoMove 中值以无类型的原始字节存储,VM 必须时刻防止类型混淆(type confusion)与数据变造(data transmutation)。这一点与 value_representation.md 中的扁平内存布局设计直接呼应——原始字节没有自描述的类型信息,安全性完全依赖 VM 的布局计算与描述符表正确。

指针有效性(Pointer validity)。每次指针解引用都必须满足:

  • 指针必须指向该交易被授权访问的内存区域——即交易自身的内存区域,或由先前已提交交易共享的冻结全局状态;
  • 禁止 use-after-free:不得解引用已释放或被回收的内存指针;
  • 禁止 off-by-one 或其他错误的偏移计算;
  • 禁止越界访问容器(vector、struct 等)内部。

分配失败处理。栈溢出(stack overflow)与堆分配失败必须被当作错误并优雅处理——即中止交易(abort the transaction),绝不能忽略。这与 heap_and_gc.md 中关于 per-block 内存上限的讨论互为表里:分配失败是内存受限下的预期事件,而不是崩溃条件。

集中式内存管理。所有重要的内存分配都必须经由 VM 的内存管理器完成。原生函数(native functions)尤其不应维护独立的影子内存空间,否则难以强制全局内存上限,并为不受追踪的资源消耗敞开大门。从源码结构看,MonoMove 将分配器独立成 crate:alloc(内含GlobalArenaPoolglobal_arena.rs等),global-context 与 runtime 均依赖它,这正是"集中式"约束的体现。

气体计量(Gas Metering)

VM 执行的每一单位工作都必须计费。文档给出两条核心要求:

  • 渐近安全(Asymptotic safety):执行过程中任意时刻的累计 gas 消耗必须与累计工作量成正比。这是不可谈判的底线——如果某个操作复杂度是 O(n) 但只收常量费用,攻击者就能以极低成本放大工作量,形成 DoS。
  • 先计费后工作(Charge-before-work):一般原则是 gas 应在对应工作执行之前收取。该规则并非总能满足,必要时可被短暂违反,但任何违反都必须由一个小的常数界住——即"欠账"量必须有上界。

MonoMove 的气体设计在 gas_design.md 中有详细展开:计量被放在stackless execution IR 层面做基本块(basic block)粒度计费,每个块的成本在 lowering 期间静态计算,控制流转入某块时由转移指令(jump)顺带收取该块成本(entry_gasgas_takengas_fallthrough字段),从而在热路径上省去独立的计费指令。这正体现了"先计费后工作"与"渐近安全"两条不变量在实现层的落地策略。

有界性(Boundedness)

VM 内部所有数据结构、算法与资源消耗都必须显式有界。任何维度(空间或时间)上的无界都是潜在的拒绝服务向量。

数据与存储上界。以下各项都必须有强制上限:

  • Loader 数据,包括代码大小(需计入单态化/泛型展开的膨胀);
  • 内存消耗:栈、堆、代码区;
  • 类型名长度与数据布局大小;
  • 缓存大小(全局上下文与每交易上下文中的所有缓存);
  • 二进制格式中的所有条目(注意:现有上限由反序列化器执行,但必须审查其完备性);
  • 写集(write set)大小。

递归上界。递归是最重要也最难防御的方向,文档明确列出三个不同的无界递归来源:

  1. Move 层递归:栈内存耗尽时必须中止交易;
  2. Rust 层递归:任何遍历深度嵌套数据的 Rust 算法都易栈溢出。其中尤须警惕:
    • 递归类型上的Drop实现——因为程序员对它的调用时机与控制能力有限;
    • 递归类型上派生的 trait(CloneHashEqPartialEqDisplayDebug)——它们生成递归实现,在深度嵌套数据上可溢出栈,且极易在看似无害的代码路径中悄然发生;
    • 目标是彻底消除递归算法,至少在上生产前要有清晰、文档化的缓解方案。
  3. 递归库调用:任何内部递归的库函数调用(例如拓扑排序、强连通分量分析)也必须遵守深度限制。

算法运行时间。loader、单态化器(monomorphizer)、字节码验证器及其他所有算法,相对于输入规模必须有界运行时间。

值上界。当前倾向是:只要没有递归遍历作用于值之上(即不存在递归的 display、drop 或其他遍历值结构的递归算法),值(包括闭包)就不需要显式的深度或大小上界。若某类遍历不可避免,则该遍历自身必须做深度限制,而不是依赖值级上界。该决策待值上的操作集合完全确定后最终拍板。

禁止未记录的 Panic

VM 代码中的 panic 等价于崩溃,而崩溃即漏洞。文档给出两条硬性规则:

  • unwrap()禁止
  • 任何可能 panic 的 API 必须使用expect()unreachable!()或等价形式,并附带一条说明"为何该 panic 条件被相信为不可达"的消息。

从源码看,这一要求在 runtime/src/verifier.rs 等模块的错误处理风格中有所体现:静态检查器以返回Vec<VerificationError>(而非 panic)的方式报告函数体不合法问题,错误消息包含函数名与可选的 pc 位置,便于定位。

严格确定性(Strict Determinism)

VM 必须在所有节点、所有平台、所有运行中对相同输入产生相同结果。三条禁令:

  • 禁止浮点运算(IEEE 754):跨平台浮点行为无法保证一致;
  • 禁止操作系统或线程局部随机源:唯一允许的随机性来源是区块上下文(如区块级随机种子);
  • 禁止无序枚举:哈希表等迭代顺序不确定的数据结构,除非显式保证确定性顺序,否则绝不枚举。文档给出的可能缓解方案是:为 VM 提供经过审查的安全数据结构 crate,例如对HashMap的包装——完全禁止迭代,或要求unsafe才能枚举。

缓存一致性(Cache Consistency)

所有缓存必须时刻保持一致:静默地对外提供过期缓存条目是正确性 bug,最坏情况下是安全漏洞。VM 维护多层缓存状态——已验证模块、结构体布局、interned 类型、类型标签等,文档点名以下常见风险区:

  • 代码升级(Code upgrades):发布或升级模块会使所有派生自它的缓存数据失效:模块本身、其定义、类型布局,以及任何跨模块依赖方;
  • 块内可见性(Intra-block visibility):在 Block-STM 下,某交易发布模块时,仍必须对所有投机执行(speculatively executed)、读取了该模块的交易保证正确性。全局缓存、每块缓存与每交易读集三者的交互,是当前 VM 复杂性的主要来源之一;
  • 跨块缓存生命周期(Cross-block cache lifecycle):跨块存活的缓存必须在条件变化时保持有效(非连续交易切片、VM 配置变更、大小上限被突破)。每一项被遗漏都是一致性隐患。MonoMove 的MaintenanceGuard(见下)为这一场景提供了单一的强制点;
  • 派生数据一致性(Derived data coherence):缓存常存储由其他缓存数据派生而来的数据(结构体布局依赖类型定义,类型定义依赖已加载模块)。只失效某一层而不传播到依赖方,将导致不一致;
  • 枚举布局(Enum layouts):枚举需要特别关注——增加一个变体是合法的模块升级,但它改变了类型的布局。因此即使升级本身合法,布局缓存也必须失效。当前 VM 的做法是:任何模块发布时整体刷新布局缓存;
  • 读集追踪(Read-set tracking):执行期间读取的每个模块或代码工件都必须记录到交易的读集中(即使由缓存提供),以保证 Block-STM 正确性。缓存命中在这一点上必须与存储读取不可区分

文档提到的MaintenanceGuard在代码中确实存在:global-context/src/context.rs 定义了该结构,第 360-392 行 显示它通过&mut GlobalContext独占获取,与执行期可并发的ExecutionGuard互斥,从而保证维护阶段没有任何执行上下文存活、不存在悬垂指针,使缓存的 reset 与去分配安全——这正是"跨块缓存生命周期"一致性问题的工程解法。维护行为由 maintenance_config.rs 中的MaintenanceConfig配置(当前仍为占位实现)。

引用别名(Reference Aliasing)

Move 的线性类型系统禁止可变引用的别名(aliasing)。这一不变量必须在运行时得到维持,否则违反可直接导致资金损失——例如通过对同一 coin 资源的别名可变访问实现双重花费。这意味着即使字节码验证器在发布时已保证借用安全,运行时仍不能假设这一点可以放松。

安全格式化与显示(Safe Formatting and Display)

格式化值或内部数据结构(例如用于错误消息或日志)是危险操作:值可能非常大,甚至深度嵌套,导致过度内存分配、栈溢出或执行停滞。文档要求:

  • 错误消息不应包含值的完整转储或复杂的内部状态;
  • 对值执行Clone具有相同风险,须同等谨慎对待;
  • 考虑禁止在特定 VM 内部类型上使用#[derive(...)],以免意外引入递归或高开销的 trait 实现。

交易参数校验(Transaction Argument Validation)

交易参数不得豁免于校验。所有输入——包括交易 payload 中提供的参数——都必须与进入 VM 的任何其他数据一样,通过相同的安全检查。换句话说,参数解析路径不能成为绕过验证器的旁路。

文档结尾以TBA: Closure-specific things标注了待定项——闭包相关的安全事项尚未完全敲定。MonoMove 已支持闭包字节码指令(PackClosure/CallClosure),其语义约束(捕获即 move、调用即消费、禁止捕获引用等)详见 closure_design.md,而闭包相关的安全与正确性不变量仍在设计中。

从文档到实现:不变量如何在代码中落地

安全文档的价值最终体现在实现上。结合仓库源码,可以看到上述不变量与 MonoMove 各组件的对应关系:

不变量对应实现/文档证据
集中式内存管理、分配失败处理alloc、heap_and_gc.md(bump 分配 + Cheney 复制式 GC、per-block 内存上限)
缓存一致性、跨块生命周期global-context/src/context.rs 的两阶段状态机(ExecutionGuard / MaintenanceGuard)
禁止未记录 panic、运行时健全性runtime/src/verifier.rs 的静态 well-formedness 检查(帧边界、指针槽有效性、非法跳转目标等)
气体计量gas_design.md(stackless IR 基本块计费、静态成本)
类型与内存安全value_representation.md(统一 8 字节堆对象头[desc_id, size]、描述符表驱动 GC 追踪)

以"类型与内存安全"为例:MonoMove 的所有堆对象共享统一头部[desc_id: u32 | size: u32]desc_id索引描述符表,GC 借此获知内部指针的偏移(struct 字段、enum 按 tag 索引的变体指针表、vector 长度与元素区),从而在复制式 GC 中只追踪真正的堆内指针——这正是指针有效性约束得以机械执行的基础设施。

再以"缓存一致性"为例:GlobalContext内部用DashMap维护 identifiers、module_ids、types、type_lists、function_refs 等 intern 缓存,外加 module_cache 与 script_cache(context.rs 第 116-141 行)。文档要求的"读集追踪",则由 loader 模块中的 read_set.rs 等承担——缓存命中与存储读取在执行结果上不可区分,正是 Block-STM 正确性的前提。

总结与阅读建议

MonoMove 的这份安全与正确性文档是一份"设计期安全规格":它不追求覆盖每个 API 的用法,而是锚定 VM 最致命的五类漏洞(铸造/变造、非确定性、崩溃、重入、慢化),并把防御责任分解为一条条可审计、可测试、可形式化验证的不变量。对 VM 开发者而言,它是实现时的红线清单;对审计者而言,它是漏洞排查的检查表;对研究者而言,它展示了"把安全内建于核心设计"在区块链 VM 中的具体含义。

建议按以下顺序继续深入仓库:

  1. 先读 vm_security_and_correctness.md 本文档,建立不变量框架;
  2. 再读 heap_and_gc.md 与 value_representation.md,理解内存安全不变量赖以执行的布局与 GC 设计;
  3. 读 gas_design.md,对照"气体计量"不变量看基本块计费方案;
  4. 最后对照 global-context/src/context.rs 与 runtime/src/verifier.rs 的源码注释,体会两阶段状态机与静态健全性检查如何在代码层面兑现文档承诺。

【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询