Rust 形式化验证实战:从 Kani 验证器到 Prusti 契约编程
在安全关键系统中,"编译通过"只是起点,"证明正确"才是目标。Rust 的所有权系统和类型安全为我们提供了坚实的基础,但在航空航天、医疗设备、金融核心等领域,仅有类型安全还不够——我们需要数学意义上的正确性保证。
一、为什么 Rust 需要形式化验证?
Rust 以"无畏并发"和内存安全著称,但这些保证存在边界:
// Rust 编译器这段代码能编译通过吗?
fn divide(a: i32, b: i32) -> i32 {
a / b
}
编译当然通过,但当 b == 0 时,程序会 panic。Rust 阻止了内存不安全问题,但无法阻止逻辑不安全问题。形式化验证的工具恰好填补这一空白。
目前 Rust 生态中三大主流形式化验证工具走向各异:
- Kani:基于 CBMC 的模型检查器,适合验证 unsafe 代码边界和并发正确性
- Prusti:Vienna 大学开发的静态验证器,采用前置/后置条件契约风格,学习曲线平缓
- Verus:Flexus 实验室出品,专为并发系统设计,被 AWS、VMware 用于生产环境验证
二、Kani:模型检查验证器
2.1 快速上手
Kani 将 Rust 代码转换为 LLVM IR,再利用 CBMC(C 有界模型检查)引擎探索所有可能的执行路径:
# Cargo.toml
[dev-dependencies]
kani = "0.44"
// src/lib.rs
#[cfg(kani)]
#[kani::proof]
fn verify_binary_search() {
let size: usize = kani::any();
kani::assume(size > 0 && size <= 1024);
let mut arr: Vec<i32> = Vec::with_capacity(size);
for _ in 0..size {
arr.push(kani::any());
}
// 假设数组已排序(用于演示,实际应验证排序正确性)
arr.sort();
let target: i32 = kani::any();
let result = arr.binary_search(&target);
// 验证:如果返回 Ok(idx),则 arr[idx] == target
if let Ok(idx) = result {
assert_eq!(arr[idx], target);
assert!(idx < arr.len());
}
// 验证:如果返回 Err(idx),则插入位置正确
if let Err(idx) = result {
if idx > 0 {
assert!(arr[idx - 1] < target);
}
if idx < arr.len() {
assert!(arr[idx] > target);
}
}
}
运行验证:
cargo kani --harness verify_binary_search
2.2 验证 unsafe 代码
unsafe Rust 是形式化验证的最大战场。以下示例验证一个裸指针操作的安全性:
/// 基于裸指针的环形缓冲区
pub struct RingBuffer<T> {
ptr: *mut T,
capacity: usize,
head: AtomicUsize,
tail: AtomicUsize,
}
impl<T: Copy + Default> RingBuffer<T> {
pub fn new(capacity: usize) -> Self {
let mut vec = Vec::with_capacity(capacity);
let ptr = vec.as_mut_ptr();
std::mem::forget(vec);
Self {
ptr,
capacity,
head: AtomicUsize::new(0),
tail: AtomicUsize::new(0),
}
}
pub fn push(&mut self, item: T) -> bool {
let tail = self.tail.load(Ordering::Relaxed);
let next_tail = (tail + 1) % self.capacity;
if next_tail == self.head.load(Ordering::Acquire) {
return false; // 缓冲区已满
}
unsafe {
// 验证:ptr 非空、tail 不越界、对齐正确
self.ptr.add(tail).write(item);
}
self.tail.store(next_tail, Ordering::Release);
true
}
}
#[cfg(kani)]
mod verification {
use super::*;
#[kani::proof]
fn verify_push_no_overflow() {
let capacity: usize = kani::any();
kani::assume(capacity > 0 && capacity <= 256);
let mut rb = RingBuffer::<i32>::new(capacity);
let value: i32 = kani::any();
// 连续 push capacity-1 次应成功
for i in 0..capacity - 1 {
assert!(rb.push(value + i as i32),
"第 {} 次 push 不应失败", i);
}
// 第 capacity 次 push 应失败(缓冲区已满)
assert!(!rb.push(value), "缓冲区满时 push 应返回 false");
}
}
2.3 并发验证:检测数据竞争
Kani 能检测所有可能的线程交错执行路径:
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering};
/// 有缺陷的 lock-free 计数器(演示用)
struct BuggyCounter {
value: AtomicI32,
}
impl BuggyCounter {
fn increment(&self) {
// 经典的 read-modify-write 竞态条件
let current = self.value.load(Ordering::Relaxed);
self.value.store(current + 1, Ordering::Relaxed);
}
}
#[cfg(kani)]
#[kani::proof]
fn verify_counter_race() {
let counter = BuggyCounter { value: AtomicI32::new(0) };
// 模拟两个线程并发递增
let c1 = &counter;
let c2 = &counter;
c1.increment();
c2.increment();
// Kani 能发现:最终值可能是 1 而不是 2
let final_value = counter.value.load(Ordering::Relaxed);
assert!(final_value == 2, // FAILURE!
"竞态条件导致最终值 {} != 2", final_value);
}
上述代码运行 cargo kani 会报 FAILURE,精确指出竞态条件漏洞。
三、Prusti:契约驱动的静态验证
3.1 验证即类型契约
Prusti 使用类似 ESC/Java 的契约语法,将不变式直接嵌入 Rust 代码:
use prusti_contracts::*;
/// 计算整数平方根(向下取整)
#[requires(n >= 0)]
#[ensures(result <= n)]
#[ensures((result + 1) * (result + 1) > n || result == n)]
fn isqrt(n: u64) -> u64 {
if n < 2 { return n; }
let mut lo = 1u64;
let mut hi = n;
#[invariant(lo >= 1)]
#[invariant(hi <= n)]
#[invariant(hi * hi > n || hi == n)]
while lo < hi {
let mid = lo + (hi - lo) / 2;
if mid > n / mid {
hi = mid;
} else {
lo = mid + 1;
}
}
hi - 1
}
Prusti 的 #[requires] 是前置条件(caller 义务),#[ensures] 是后置条件(callee 保证),#[invariant] 是循环不变式。
3.2 验证排序算法
#[requires(true)]
#[ensures({
// 输出是输入的一个排列
forall(|j: usize| j < result.len() ==> {
// 每个元素都存在于输入中
let mut count = 0usize;
for i in 0..nums.len() {
if nums[i] == result[j] { count += 1; }
}
let mut orig_count = 0usize;
for i in 0..nums.len() {
if nums[i] == result[j] { orig_count += 1; }
}
count <= orig_count
})
})]
#[ensures({
// 输出是有序的
forall(|i: usize| 1 < i && i < result.len() ==>
result[i-1] <= result[i])
})]
fn merge_sort(nums: &[i32]) -> Vec<i32> {
if nums.len() <= 1 { return nums.to_vec(); }
let mid = nums.len() / 2;
let left = merge_sort(&nums[..mid]);
let right = merge_sort(&nums[mid..]);
merge(&left, &right)
}
3.3 幽灵代码(Ghost Code)
Prusti 允许在验证阶段使用"幽灵代码"——不参与编译的辅助断言:
#[ensures({
let old_sum = old(
// 幽灵变量:记录执行前的状态
ghost_sum = self.items.iter().map(|x| x.price).sum()
);
self.total_revenue > old_sum
})]
fn process_order(&mut self, item_index: usize, qty: u32)
-> Result<(), OrderError>
{
requires!(item_index < self.items.len());
let item = &mut self.items[item_index];
item.stock -= qty;
self.total_revenue += item.price * qty as f64;
Ok(())
}
四、Verus:并发系统的形式化验证利器
4.1 Verus 简介
Verus 由 Amazon、VMware 工程师主导开发,专为验证并发系统设计。它的核心创新在于将推理负担交给 SMT 求解器(z3),同时保持接近原生 Rust 的语法。
4.2 验证并发数据结构
#![allow(unused_imports)]
use builtin::*;
use builtin_macros::*;
verus! {
// 验证无锁栈(Michael-Scott Stack 简化版)
pub struct LockFreeStack<T> {
head: *mut Node<T>,
}
struct Node<T> {
data: T,
next: *mut Node<T>,
}
impl<T> LockFreeStack<T> {
pub fn new() -> Self {
Self { head: core::ptr::null_mut() }
}
// 前置条件:self 是有效的引用
// 后置条件:返回的 Option 正确反映栈状态
pub fn pop(&mut self) -> Option<T> {
// CAS 循环实现
loop {
let head = self.head;
if head.is_null() { return None; }
let next = unsafe { (*head).next };
// 原子 CAS:将 head 从 head 改为 next
// Verus 验证这里的内存安全:
// 1. head 指针有效(非空、对齐)
// 2. 读取 (*head).next 是安全的(所有权未被解除)
// 3. CAS 成功后,head 指针指向的 Node 可安全释放
if self.cas_head(head, next) {
let data = unsafe { Box::from_raw(head).data };
return Some(data);
}
}
}
// CAS 操作的纯函数版本(Verus 内部建模)
fn cas_head(&mut self, old: *mut Node<T>, new: *mut Node<T>) -> bool {
// 平台特定实现...
unimplemented!()
}
}
} // verus!
4.3 形式化规约能力
Verus 支持 Temporal Logic 级别的规约,可以表达"最终必然"等时序性质:
verus! {
/// 验证共识算法的最终性(最终ity)
/// 如果某个值被提交,则最终所有正确节点都会采纳它
pub spec fn committed(v: Value) -> bool;
pub spec fn adopted_by(node: NodeId, v: Value) -> bool;
/// 安全性:如果值 v1 在高度 h 被提交,则不会有冲突值 v2 在同一高度被提交
#[invariant]
fn safety_prop(h: Height, v1: Value, v2: Value) -> bool {
(committed_at(h, v1) && committed_at(h, v2)) ==> v1 == v2
}
/// 活性:每个正确的节点最终会采纳所有已提交的值
#[temporal]
fn liveness_prop(node: NodeId, v: Value) -> bool {
eventually(always(adopted_by(node, v)))
}
}
五、三工具横向对比
| 维度 | Kani | Prusti | Verus |
|---|---|---|---|
| 验证方法 | 有界模型检查(BMC) | SMT + 归结证明 | SMT(z3) |
| 代码侵入性 | 低(仅 #[kani::proof]) |
中(契约标注) | 中(verus! 宏) |
| unsafe 支持 | 强 | 中 | 强 |
| 并发验证 | 强(模拟交错) | 弱 | 强(专项设计) |
| 反例可读性 | 高(带 trace) | 中 | 中 |
| 验证范围 | 有界(默认 50 步) | 无界 | 无界 |
| CI 集成 | easy(cargo kani) | 需安装 Java + z3 | 需预编译 |
| 学习成本 | 低 | 中 | 高 |
选择建议:
- 初创阶段 / 快速验证:Kani,几乎是零配置,cargo kani, 三条命令出结果
- 契约式编程 / 教育场景:Prusti,最适合在团队中推广"契约优先"的设计哲学
- 安全关键并发系统 / 学术界:Verus,被 AWS Nitro 用于验证 hypervisor 代码
六、实战约束与工程权衡
6.1 验证时间与规模
形式化验证最大的敌人是状态空间爆炸。以下是实际项目中常见的工程策略:
- 分层验证:只对核心模块(10% 代码)做完整验证,外围模块依赖类型系统保证
- 抽象化:用未解释函数(uninterpreted functions)替代复杂实现的依赖
- 有界验证 + 压力测试:Kani 默认探索 50 步,配合 proptest 做大规模随机测试
- 验证驱动开发(PDD):先写规约,再写实现,最后用验证器证明,类似于 TDD
6.2 一个生产级案例
/// 经过 Kani 验证的零拷贝网络缓冲区
/// 保证:
/// 1. 无 use-after-free(借用检查 + Kani 验证 unsafe 块)
/// 2. 无数据竞争(AtomicU64 索引 + Relaxed 顺序精心选择)
/// 3. 无 buffer overflow(Kani 有界索引验证)
pub struct NetBuffer {
data: *mut u8,
capacity: AtomicU64,
written: AtomicU64,
}
// 经过验证的安全不变式:
// atomic_read(written) <= atomic_read(capacity) 在任意线程状态下成立
#[cfg(kani)]
#[kani::proof]
fn verify_netbuffer_invariant() {
// Kani 探索所有可能的交错执行
// 证明:无论 read 和 write 如何交错,缓冲区始终满足不变式
}
6.3 与 Rust 类型系统的协作
形式化验证不是替代类型系统,而是在正确的抽象层次上与其协作:
// 类型系统负责:内存安全、别名规则、Send/Sync 线程安全
// 形式化验证负责:业务逻辑正确性、算法正确性、并发安全性
/// 银行账户示例
#[invariant(self.balance >= 0)] // 业务不变式:余额非负
#[invariant(self.total_deposits >= self.total_withdrawals)]
pub struct Account {
balance: u128,
total_deposits: u128,
total_withdrawments: u128,
}
impl Account {
#[requires(amount > 0)]
#[ensures(self.balance == old(self.balance) + amount)]
#[ensures(self.total_deposits == old(self.total_deposits) + amount)]
pub fn deposit(&mut self, amount: u128) { ... }
#[requires(amount > 0 && amount <= self.balance)]
#[ensures(self.balance == old(self.balance) - amount)]
pub fn withdraw(&mut self, amount: u128) { ... }
}
七、展望未来
Rust 形式化验证领域正在快速发展:
- Kani 正向更高阶特性迈进:支持 async/await、dyn trait 等之前无法验证的特性
- Prusti 与 Rust 编译器更深集成:计划成为 rustc 官方验证后端的候选
- Verus 的扩散效应:Amazon 的 Firecracker microVM、AWS Nitro 已经大量使用 Verus 验证
- AI 辅助证明:借助 LLM 自动生成规约和证明,降低使用门槛(例如 Rust Verification 项目的 Copilot 集成)
形式化验证不是银弹——它需要时间投入、学习成本和工程纪律。但对于安全关键代码(密码学原语、hypervisor、加密固件、医疗设备),它已经是不可替代的最后一道防线。
用类型系统阻止灾难,用形式化验证证明正确。
延伸学习: - Kani 模型验证器:https://github.com/model-checking/kani - Prusti 验证工具:https://www.prusti.dev/ - Verus 验证系统:https://verus-lang.dev/ - Rust Verification 研讨会:https://sites.google.com/view/rustverify2025/home

发表评论 取消回复