Skip to content
RustUnsafeMemory SafetyEngineering

Rust 零 unsafe 工程实践:安全从编译期开始

Workspace 安全策略

Vigils 的 Cargo.toml 配置了全局 lint:

[workspace.lints.rust]
unsafe_code = "forbid"

这意味着任何引入 unsafe 的代码都会在编译期报错。当前 12 个 vigil-* crate 全部遵守这一规则。

唯一的例外:Landlock

Linux Landlock LSM 需要在 pre_exec 钩子中调用 restrict_self(),这是 async-signal-safe 的 unsafe API。引入这个例外需要满足三个条件:

  1. ADR 审批:ADR 0007 §I-7.8 明确论证了必要性
  2. 独立 crate:vigil-sandbox-linux 单独承载 unsafe,不污染其他 crate
  3. 局部 allow:#[allow(unsafe_code)] 仅在 landlock.rs 中使用,附带完整的不变量分析注释

代码审查时,任何新增的 unsafe 都会被 CI 拒绝。

类型系统防泄漏

SecretLease 类型是防止凭据泄漏的核心设计:

pub struct SecretLease {
    id: LeaseId,
    alias: String,
    // 注意:没有 value 字段
}

impl Display for SecretLease {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "[REDACTED {}]", self.alias)
    }
}

impl Debug for SecretLease {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "SecretLease {{ id: {:?}, alias: {:?} }}", self.id, self.alias)
    }
}

关键点:

  • Display 和 Debug 都只输出 alias,不输出 value
  • 如果开发者意外地在错误消息中包含 SecretLease,用户看到的只是 [REDACTED github_token]
  • thiserror 派生的错误类型也遵守这一规则

编译期保证

Rust 的类型系统还提供了其他编译期安全:

  1. 所有权系统:SecretLease 的 value 不会被意外 Clone 或 Send 到不安全的地方
  2. 生命周期检查:lease 的有效期在编译期就能被跟踪
  3. 枚举穷尽匹配:match 核心枚举必须显式处理 _ => 分支,fail-closed
  4. Result 传播:所有可能失败的操作都返回 Result,不允许裸 unwrap

性能与安全的平衡

有人担心"零 unsafe"会影响性能。实际上:

  • SHA256 计算使用 ring crate(纯 Rust,性能接近 OpenSSL)
  • ONNX 推理使用 ort crate(C++ 后端,但 Rust 绑定安全)
  • SQLite 使用 rusqlite(C 绑定,但参数绑定防止 SQL 注入)

"零 unsafe"不等于"零 C 依赖",而是"所有 C 依赖都有安全的 Rust 边界"。

红队测试

Vigils 的红队测试套件包含专门针对内存安全的攻击:

  1. 尝试通过 panic 信息泄露 secret → 失败,panic hook 已脱敏
  2. 尝试通过 Debug 格式化泄露 secret → 失败,Debug 只输出 alias
  3. 尝试通过日志注入绕过 redaction → 失败,所有日志都走 redaction 模块
  4. 尝试触发缓冲区溢出 → 失败,Rust 的边界检查在运行时生效

安全不是信任,是验证。