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。引入这个例外需要满足三个条件:
- ADR 审批:ADR 0007 §I-7.8 明确论证了必要性
- 独立 crate:vigil-sandbox-linux 单独承载 unsafe,不污染其他 crate
- 局部 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 的类型系统还提供了其他编译期安全:
- 所有权系统:SecretLease 的 value 不会被意外 Clone 或 Send 到不安全的地方
- 生命周期检查:lease 的有效期在编译期就能被跟踪
- 枚举穷尽匹配:match 核心枚举必须显式处理 _ => 分支,fail-closed
- Result 传播:所有可能失败的操作都返回 Result,不允许裸 unwrap
性能与安全的平衡
有人担心"零 unsafe"会影响性能。实际上:
- SHA256 计算使用 ring crate(纯 Rust,性能接近 OpenSSL)
- ONNX 推理使用 ort crate(C++ 后端,但 Rust 绑定安全)
- SQLite 使用 rusqlite(C 绑定,但参数绑定防止 SQL 注入)
"零 unsafe"不等于"零 C 依赖",而是"所有 C 依赖都有安全的 Rust 边界"。
红队测试
Vigils 的红队测试套件包含专门针对内存安全的攻击:
- 尝试通过 panic 信息泄露 secret → 失败,panic hook 已脱敏
- 尝试通过 Debug 格式化泄露 secret → 失败,Debug 只输出 alias
- 尝试通过日志注入绕过 redaction → 失败,所有日志都走 redaction 模块
- 尝试触发缓冲区溢出 → 失败,Rust 的边界检查在运行时生效
安全不是信任,是验证。