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 验证时间与规模

形式化验证最大的敌人是状态空间爆炸。以下是实际项目中常见的工程策略:

  1. 分层验证:只对核心模块(10% 代码)做完整验证,外围模块依赖类型系统保证
  2. 抽象化:用未解释函数(uninterpreted functions)替代复杂实现的依赖
  3. 有界验证 + 压力测试:Kani 默认探索 50 步,配合 proptest 做大规模随机测试
  4. 验证驱动开发(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 形式化验证领域正在快速发展:

  1. Kani 正向更高阶特性迈进:支持 async/await、dyn trait 等之前无法验证的特性
  2. Prusti 与 Rust 编译器更深集成:计划成为 rustc 官方验证后端的候选
  3. Verus 的扩散效应:Amazon 的 Firecracker microVM、AWS Nitro 已经大量使用 Verus 验证
  4. 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

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部