Aptos 项目中 Leaner 语言验证器的不安全指针设计:基于预言式所有权模型的 raw pointer 建模
Aptos 项目中 Leaner 语言验证器的不安全指针设计基于预言式所有权模型的 raw pointer 建模【免费下载链接】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导读本文围绕 Aptos 仓库third_party/move/lean下 Leaner 语言验证器Lean-hosted LIR 验证框架的第一份 unsafe-pointer profile 设计文档展开详细解读如何在已验证的 LIRValidated LIR之上、以预言式prophetic所有权模型为底座为 Rust 的 raw pointer*const T/*mut T建立一套可验证、可解释、可形式化证明的内存模型。读者将掌握该设计对 Rust 未定义行为UB的逐条形式化处理、rawPtr/rawAnchor/memory三件套的语义、StoredPtrT动态检查库类型以及 UP1–UP3 三个里程碑的落地路径并能理解这些设计如何与仓库中已有的预言式引用模型prophetic-references和 Rust MIR 前端路线图rust-mir-design衔接。1. 背景Leaner 项目中的 unsafe 指针问题Leaner 是 Aptos 仓库中一个以 Lean 为宿主语言的验证框架其核心思想是通过统一的 Leaner IRLIR把 Move 源码、Leaner 源码与 Rust MIR 汇入同一条验证流水线见 lir-design.md。在 rust-mir-design.md 中项目明确规划了 Rust 前端的 U0–U3 四个 unsafe 支持阶段阶段接受的语义明确排除U0序列化并诊断每一种 unsafe 相关的 MIR 形式任何 unsafe 操作都不进入已验证语义U1派生于活跃局部/堆分配的带类型 raw pointer受检 load/store 与简单 offset安全前置条件整数派生指针、union、FFI、原子操作、内联汇编、任意UnsafeCellU2分配标识、dealloc/realloc、Box/Vec类模型、选定的UnsafeCell库抽象一般共享可变、自定义分配器、并发U3更丰富的 provenance、union、FFI/ABI、原子操作与并发的 profile 特定模型任何没有明确模型和对齐论证的特性而本关联文档 unsafe-pointers.md 正是 U1 阶段的核心设计草案状态2026-08-28 提出、尚未排期它建立在已经实现的 prophetic-references.md 预言式引用模型之上并是 lir-design.md 寄存器中 interior-mutability内部可变性设计的前置条件。从 roadmap.md 可以看到该 unsafe 设计通过 Rust M5 里程碑调度执行。值得说明的是当前仓库中 raw_pointer.exp.lean 的 e2e 基线仍显示 raw pointer 在 M0 阶段被精确拒绝unsupported Rust local type: RigidTy(RawPtr(...))——这正对应 U0先保留/精确拒绝的策略说明本设计是对未来 U1 阶段的完整前瞻规划。2. Rust 中不安全内存的本质2.1 raw pointer 是数据 provenance一个 raw pointer*const T/*mut T本质上是普通数据一个地址外加一个provenance来源标记——一个不可见的令牌指明指针派生自哪个分配、以及它允许的访问方式。文档强调了一个反直觉的核心事实创建、复制、比较指针永远是定义良好的defined无论指针多么垃圾只有访问和偏移运算才带有要求。一次合法的访问就是一次普通的带类型 load 或 storeunsafe { *p 1 }完成写入之后任何持有该内存 provenance 的读取都能观察到它*p v会先丢弃旧值而ptr::write不会。违反任何要求就是未定义行为UB。2.2 UB 是完全未定义的UB 不会触发 panic也不会产生任何可观察的错误——整个执行失去全部意义编译器在优化时假设 UB 不可能发生。因此对验证器而言UB 必须被建模为一种不可达的结果而不是某种错误返回值。文档给出了一个非常直观的判定表这里完整保留并归类示例判定let p 0x10 as *const u32—— 伪造、复制、比较任何指针defined指针是数据只有使用才有要求let p raw mut x; unsafe { *p 1 }对x无活跃引用defined普通带类型 store后续对x的读取可见通过派生自mut x的指针访问但该引用在下次使用前defined引用的下一次使用会使指针失效dealloc、所有者 drop 或帧返回后unsafe { *p }UB—— use-after-deathnull 与越界指针同理p.add(n)越过 one-past-the-end即使从不解引用UBwrapping_add是 defined 的访问必须保持在界内读取未初始化字节、非法值0/1 之外的bool或未对齐的*pUBMaybeUninit、read_unaligned是 defined 路径unsafe { *(x as *const T as *mut T) v }—— 通过T派生指针写入UB即使从未被观察仅在UnsafeCell内才 defined修改活跃T覆盖的内存或在活跃mut T下进行外部访问UB—— 引用保证约束所有访问2.3 从编译器义务到程序员义务遵守每一项要求恰好恢复安全的 Rust 语义unsafe只是把证明义务从编译器转移到程序员。文档同时坦诚指出Rust 还没有一份完成的规范性内存模型操作参考是 Miri 与 Stacked/Tree Borrows因此本设计实现的是候选模型共同认可的保守核心并把每一项要求都做成验证器的证明前提详见下文第 4 节。3. 设计目标验证器要找到滥用建模 unsafe 指针的全部意义在于验证器能够发现滥用并证明其不存在。最典型的滥用就是use-after-death——通过一个分配已经死亡堆内存被释放、或栈上 place 的所有者被 drop、或帧已返回的指针进行访问。文档要求范围内的每一种滥用都必须同时满足两个可观测条件在#leaner_verify下是一个定位到源码位置、失败的证明义务located, failed proof obligation在解释器中是一个被检测到、带定位的结果detected, located outcome。滥用类型检测方式Use-after-death堆释放、所有者 drop、帧退出访问点的 liveness 义务双重死亡对已死分配deallocdealloc点的 liveness 义务越界偏移或访问访问点的 bounds 义务读取未初始化内存load 点的 initializedness 义务通过共享派生或 null 指针写入store 点的 permission 义务上述每一行都是某条操作规则第 4 节的一条安全前提safety premise并携带其Located源码点失败证明会点名滥用及其位置解释器动态检查同一条前提——因此滥用测试夹具misuse fixtures可以以差分方式同时检验两种模式。文档还规定如果一个定理的契约把安全前提假设掉了它就是条件模型定理conditional model theorem必须按 rust-mir-design 的规则明确标注。4. 核心决策raw pointer 不是引用设计的根本性选择是raw pointer 不是引用。在预言式所有权模型中可变借用是携带当前值borrow ℓ v的运行时值贷方 place 留下一个洞loanHole ℓ借用在死亡点执行current prophecy的对账共享借用则被完全擦除为观测值详见 prophetic-references.md 的 P2/P6。而 raw pointer 不参与这套机制没有 loan、没有 hole、没有 prophecy、没有死亡标记。它们构成一个平行域——指针值加上一个显式的memory——只在显式的物化点materialization points与值世界交汇。预言式保证之所以存活是因为别名aliasing只返回到memory内部而那里每一次访问都经过检查。4.1 最小可靠 profile 的四条支柱支柱含义严格分配 provenance指针只在它派生的分配内有效整数到指针的转换、跨分配运算在验证时被拒绝分配标识永不复用悬垂指针仍然命名它已死的分配因此 liveness 在每次访问时都可检查——use-after-death 变得可判定类型化分配无字节一个分配保存一棵值树可选未初始化偏移是元素下标。布局、字节转换、union 保持拒绝U3 材料UB 是第三种结果除returned/threw外增加undefined带 cause 且无可用的最终状态验证证明其不可达4.2 模型的核心代码-- Syntax: new type constructor | rawPointer (pointee : TypeId) (mutable : Bool) -- RuntimeValue: two new constructors | rawPtr (alloc : Option AllocId) (offset : Nat) (mutable : Bool) | rawAnchor (alloc : AllocId) -- RuntimeState gains memory structure Allocation where live : Bool content : Option RuntimeValue -- none uninitialized memory : Array Allocation -- indexed by AllocId, append-only三个关键语义点rawPtr是普通可复制数据。alloc nonenull无 provenance使每次访问都是 UBmutable false共享派生使写入成为 UB——UnsafeCell例外属于 interior-mutability 设计。rawAnchor拥有支撑逃逸 place 的分配。它是仿射的affine只能移动、不能复制验证拒绝复制/共享快照携带 anchor 的值它的死亡会杀死分配。每个存活的物化分配恰好一个 anchor是 well-holed 不变量的内存类比。memory只追加死亡只是把live翻转为 false。解释器与大步关系big-step relation共享全部规则因此一致性定理agreement theorem机械地扩展。4.3 分配死亡与操作集合分配死亡与贷款死亡平行——两者都是同一种语义中的显式事件。分配在两种情况下死亡(1)dealloc p要求一个存活、offset 为零、可变、无 anchor 的指针alloc产生的堆分配没有 anchor所有权是库模型的约定(2) anchor 死亡rawAnchor被 drop、被覆盖、或其帧退出。因此一个逃出其函数的局部指针按 return 的语义本身就是悬垂的——栈 use-after-death 不需要任何特殊机制。已死的分配不会在悬垂指针背后被清理第 3 节中的每一种滥用都是针对该状态的违反前提。操作一个新的共享操作组仅在 unsafe profile 下准入raw checker 在其他地方拒绝它inductive RawOperation where | addrOf (mutable : Bool) (place : PlaceId) -- materialize, yields rawPtr | load -- checked read through rawPtr | store -- checked write through rawPtr | offset -- element offset within allocation | alloc (type : TypeId) -- fresh uninitialized allocation | dealloc | ptrEq | isValid -- decides the access premises; itself never UBraw 访问只通过操作进行Place.deref继续保留给引用raw pointer 永不进入 place 路径前端把(*p).f x降级为显式 raw 操作。每条规则携带自己的安全前提——例如对rawPtr (some a) i true的storea存活否则 use-after-death、i在界内、pointee 类型匹配。前提失败派生undefinedOutcome增加| undefined (cause : UbCause)wp 与契约只对returned/threw量化因此无 UB 是每个证明的一个合取项。语言级throw永不建模 UB反之亦然。4.4 与预言式模型的边界addrOfRust 的raw把 place 的值移动到一个全新分配在 place 中留下rawAnchor a并产出rawPtr (some a) 0 mut。对 anchored place 的安全访问被细化elaborate为穿过 anchor 的受检访问因此安全代码与 raw 别名共享同一个内存单元并正确互见。从活跃mut b逃逸会物化借用borrow的current因为 hole 必须与一个值对账验证要求逃逸必须是括号化的bracketed值读回、分配在 loan 的endLoan之前死亡以 liveness 加 initializedness 作为重新进入义务。预言对账完全不受影响pending、applyPending、引用契约一概不变指针没有死亡点、没有排他性因此没有预言。5.StoredPtrT把动态检查做成知名类型对存储在数据结构中的指针只有两种选择(a) 静态清偿——不变量约束存储的指针使第 4 节的前提在每次访问时可证(b) 动态检查。设计把 (b) 做成一个知名库类型well-known type命名为StoredPtrT它包装一个 raw pointer 并存储检查所需的信息。在模型中这不需要额外的东西——isValid已经针对memory判定访问前提——它的方法就是普通代码read p ≡ if isValid p { load } else { panic }两个关键性质通过StoredPtr的访问按构造不可达 UB其访问不贡献任何undefined义务只有一个 defined 的 panic 结果。规格可以接受 panic也可以证明其不存在——又回到选项 (a)但局部且可选。StoredPtr的数据结构不需要任何内存安全不变量。真实硬件不存 liveness 元数据因此编译后的对应物必须真正携带它一个代际句柄generational handle——分配 id 加 epoch配合 arena 或 registryslotmap/Weak风格——针对 raw 模型验证一次UP3。StoredPtr定理只有在使用该实现时才转移到编译后的 Rust对裸*mut T做isValid是不可实现的。6. 验证与校验机制本节设计把第 2 节的安全语义落实到验证器与校验器的每一层WP 规则wpExprStep为每个RawOperation增加一个分支把其带定位的安全前提与续体合取。规格可见的谓词memory.isLive/memory.initialized沿用holeInGlobals的先例构造子键控的受保护扫描对符号状态安全。生成契约导出 raw-pointer 参数的前提假设为前置条件liveness、bounds、initializedness内存效应为后置条件——一个 unsafefn就是一个其契约携带调用者必须清偿的安全前置条件的函数。滥用夹具两种模式下的负向测试——#leaner_verify在命名的前提和位置失败记录在 expected-failure 基线中并带// error: ...标记解释器报告带定位的undefined结果。校验器Validation/Capability.lean只在 unsafe profile 下准入第 4 节的操作整数派生指针、字节转换、union、FFI、原子操作、内联汇编、UnsafeCell、自定义分配器继续被拒绝并给出带定位的诊断。导出器保持 U0 的 serialize-or-reject 义务mapper 只降级准入的子集。RawUnit以版本化扩展获得新类型与操作组。借用分析认证逃逸点授权对安全访问的 anchor 重写以及第 4 节的括号事实。7. 各模块改动清单文档给出精确到模块的改动表完整保留如下模块改动Syntax.leanTy.rawPointerRawOperation含isValidOperation.rawSemantics/Runtime.leanrawPtr/rawAnchorAllocation、RuntimeState.memoryOutcome.undefined指针类型化死分配在任意 pointee 处类型化如同 holesSemantics/Operations.lean物化、受检访问、alloc/dealloc、anchor 感知的 place 解析与 dropBigStep.leanInterpreter.lean相同的确定性规则undefined传播帧退出杀死 anchored 分配Proofs/*覆盖新结果的一致性不变量每分配一个存活 anchor、memory单调WP 安全分支与memory.*正规形LeanerLang/Contract.leanraw-pointer 签名的安全前/后置条件Validation exporter/mapperprofile 门控逃逸/括号证书事实带精确拒绝的降级Rust profile registryStoredPtrT知名类型降级为isValid保护的 raw 访问8. 里程碑与排期设计的执行被划分为三个里程碑每个都是全矩阵绿的检查点提交UP1 —— 受检内存模型第 4 节覆盖语法、运行时、操作与两个引擎修复一致性与不变量。门禁第 2 节滥用夹具use_after_death栈与堆、double_death、oob_offset、uninit_read、write_through_const产生带定位的undefined结果且一个正向swap_via_raw夹具从 Rust 源码端到端跑通。UP2 —— 滥用发现式验证第 6 节 wp 分支、契约、规格谓词。门禁#leaner_verify无 sorry 地证明swap_via_raw每个滥用夹具恰好在其命名的前提与位置失败并记录在 expected-failure 基线中。UP3 —— 堆所有权与库模型一个Box类模型和StoredPtr实现代际句柄针对该语义验证导出纯值级契约——interior-mutability 设计为UnsafeCell缓存复用的正是这个模式。门禁验证模型的客户端在看不到内存谓词的情况下完成验证一个在 vector 中存储StoredPtr的夹具在无内存安全不变量的情况下验证通过。与 rust-mir-design.md 的对应关系是UP1–UP2 对应 U1UP3 对应 U2 的前半部分U0 仍然是导出器的 serialize-or-reject 义务。9. 与当前仓库实现的呼应虽然该设计仍处于 proposal 状态但其思想与仓库现有实现紧密咬合预言式底座已就绪prophetic-references.md 记录的 P1–P7 已全部落地包括endLoan标记物化、共享引用完全擦除P6、原生返回引用传递P7并留有raw-pointer escapes、two-phase borrows、interior mutability作为验证阶段明确拒绝的边界——本设计正是要填补这条边界中的 raw-pointer 部分。MIR 保留策略已就绪raw pointer 类型与操作在 RawUnit 中自 v1 起就是一等公民标签见 rust-mir-design.md 的 Rust-profile payload 章节即使当前 profile 拒绝它们当前 e2e 基线的精确拒绝、不留部分 JSON行为raw_pointer.exp.lean正是 U0 纪律的执行。验证设施已就位#leaner_verify、wp 符号执行、expected-failure 基线与Located义务机制在预言式引用阶段已经过 P3/P4 验证见 prophetic-references.md 的LeanerLang/Tests/Verification.lean门禁记录UP2 的命名前提失败模式是这套机制的直系复用。结语这份 unsafe-pointer 设计为 Leaner 的 Rust 前端补齐了最关键的一块拼图在预言式所有权模型上用严格分配 provenance 永不复用的分配标识 显式memory构造出一个最小的可靠 raw-pointer profile把 Rust 的 UB 逐条转译为可定位、可解释、可证明的验证义务并用StoredPtrT为数据结构中的指针提供按构造安全的动态检查路径。对读者而言它是理解如何把真实世界的 unsafe 内存语义形式化到验证器里的一份高密度参考——既有逐条 UB 判定表也有 Lean 代码级模型、模块改动清单与可执行里程碑值得与 prophetic-references.md 和 rust-mir-design.md 三份文档对照阅读。【免费下载链接】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),仅供参考