Unsafe 审计清单能跑过测试不代表内存模型站得住Rust 里的 unsafe 有时不可避免FFI、SIMD、共享内存、无锁结构、手写 allocator。但 unsafe 能跑过测试不代表内存模型站得住。测试只能覆盖样本unsafe 要靠不变量证明。我审计 unsafe 代码时不先看它“快不快”先看它声明了哪些不变量以及代码有没有逐条守住。不变量写不出来unsafe 块就不该合并。一、每个 unsafe 块都要有理由unsafe 前面应该写清楚为什么需要 unsafe调用方必须满足什么条件本函数保证什么。// Safety: // ptr must be valid for len bytes, aligned to u8, and not mutated // while the returned slice is alive. unsafe fn slice_from_rawa(ptr: *const u8, len: usize) - a [u8] { std::slice::from_raw_parts(ptr, len) }这不是形式主义。写不清 safety comment说明脑子里也没完全想清。二、审计指针和生命周期指针是否非空、是否对齐、是否指向有效内存、生命周期是否足够、是否存在别名可变引用这些是基本项。flowchart TD A[unsafe 块] -- B[指针有效性] A -- C[生命周期] A -- D[别名规则] A -- E[线程安全] A -- F[panic 后状态]尤其是把 raw pointer 包成引用时Rust 编译器默认你已经证明了一切。证明错了后果就是 UB。指针审计中最容易被忽视的是别名规则。Rust 的mut T要求唯一访问权但 raw pointer 没有这个承诺。当 unsafe 块中同时存在多个指向同一内存的 raw pointer其中一个被转换成了mut就进入了危险区域。典型陷阱是自引用结构或侵入式链表的节点操作在 iter 或 insert 时你可能持有前驱节点的*mut Node同时在当前节点构造了mut Node。这在编译期完全合法——raw pointer 和mut共存不会触发借用检查——但运行时如果修改了mut指向的字段而前驱的指针仍在读取同一块内存就产生了 Stacked Borrows 或 Tree Borrows 违例。Miri 配合-Zmiri-tag-raw-pointers可以部分检测但最可靠的手段是在设计阶段就明确每个阶段哪些指针还活跃、哪些已经退役用状态枚举或 scope guard 把活跃窗口约束住让不变量显式化而非靠记忆。别名分析还有一个实操难点当 unsafe 代码和 safe 代码混合时safe 代码中的未知第三方 trait 实现可能会触发隐式 Drop而 Drop 的实现里 if 访问了你要操作的 raw pointer就会在不知情的情况下制造别名。Rust 的安全代码不会导致 UB保证在这里变成了一把双刃剑——你在 unsafe 块里假设了不变量但 safe 代码的演化可能无声地破坏它。因此审计 unsafe 时还要看调用方是否会传入带有自定义 Drop 的类型以及这些 Drop 是否会触碰你操作的指针。更保守的做法是尽量用NonNullT而非*mut T因为NonNull在类型层面标记了非零和协变能减少一部分误用。三、线程安全不能靠感觉unsafe impl Send和unsafe impl Sync要特别谨慎。底层 C 库是否线程安全内部是否有全局状态Drop 是否可能和调用并发// 只有确认底层 handle 可跨线程移动才允许 Send unsafe impl Send for ModelHandle {}这类 impl 必须有外部文档或源码证据支撑。不能因为编译器抱怨就用 unsafe 把它按下去。四、panic 路径也要看unsafe 代码里如果中途 panic是否会留下半初始化对象Drop 是否会 double free锁是否释放这些都要检查。可以用 Miri、sanitizer、loom 辅助但工具不能替代不变量审查。工具抓的是一部分执行审计看的是所有可能。审计结果最好形成清单并随代码提交。哪些 unsafe 块依赖外部库保证哪些依赖调用方传入对齐指针哪些已经由测试覆盖要写清楚。以后改动触碰这些不变量时reviewer 才知道重点在哪里。unsafe_audit: block: tensor_view_from_raw invariant: ptr aligned, len within allocation, no mutable alias tests: miri, asan, fuzz_shapeunsafe 代码不是不能写而是不能没有账本。五、总结Unsafe 审计的核心是内存模型和不变量。每个 unsafe 块要说明理由检查指针、生命周期、别名、线程安全和 panic 路径。能跑过测试只是开始。unsafe 代码要能讲清为什么不会错才配进入系统核心路径。
Unsafe 审计清单:能跑过测试,不代表内存模型站得住
Unsafe 审计清单能跑过测试不代表内存模型站得住Rust 里的 unsafe 有时不可避免FFI、SIMD、共享内存、无锁结构、手写 allocator。但 unsafe 能跑过测试不代表内存模型站得住。测试只能覆盖样本unsafe 要靠不变量证明。我审计 unsafe 代码时不先看它“快不快”先看它声明了哪些不变量以及代码有没有逐条守住。不变量写不出来unsafe 块就不该合并。一、每个 unsafe 块都要有理由unsafe 前面应该写清楚为什么需要 unsafe调用方必须满足什么条件本函数保证什么。// Safety: // ptr must be valid for len bytes, aligned to u8, and not mutated // while the returned slice is alive. unsafe fn slice_from_rawa(ptr: *const u8, len: usize) - a [u8] { std::slice::from_raw_parts(ptr, len) }这不是形式主义。写不清 safety comment说明脑子里也没完全想清。二、审计指针和生命周期指针是否非空、是否对齐、是否指向有效内存、生命周期是否足够、是否存在别名可变引用这些是基本项。flowchart TD A[unsafe 块] -- B[指针有效性] A -- C[生命周期] A -- D[别名规则] A -- E[线程安全] A -- F[panic 后状态]尤其是把 raw pointer 包成引用时Rust 编译器默认你已经证明了一切。证明错了后果就是 UB。指针审计中最容易被忽视的是别名规则。Rust 的mut T要求唯一访问权但 raw pointer 没有这个承诺。当 unsafe 块中同时存在多个指向同一内存的 raw pointer其中一个被转换成了mut就进入了危险区域。典型陷阱是自引用结构或侵入式链表的节点操作在 iter 或 insert 时你可能持有前驱节点的*mut Node同时在当前节点构造了mut Node。这在编译期完全合法——raw pointer 和mut共存不会触发借用检查——但运行时如果修改了mut指向的字段而前驱的指针仍在读取同一块内存就产生了 Stacked Borrows 或 Tree Borrows 违例。Miri 配合-Zmiri-tag-raw-pointers可以部分检测但最可靠的手段是在设计阶段就明确每个阶段哪些指针还活跃、哪些已经退役用状态枚举或 scope guard 把活跃窗口约束住让不变量显式化而非靠记忆。别名分析还有一个实操难点当 unsafe 代码和 safe 代码混合时safe 代码中的未知第三方 trait 实现可能会触发隐式 Drop而 Drop 的实现里 if 访问了你要操作的 raw pointer就会在不知情的情况下制造别名。Rust 的安全代码不会导致 UB保证在这里变成了一把双刃剑——你在 unsafe 块里假设了不变量但 safe 代码的演化可能无声地破坏它。因此审计 unsafe 时还要看调用方是否会传入带有自定义 Drop 的类型以及这些 Drop 是否会触碰你操作的指针。更保守的做法是尽量用NonNullT而非*mut T因为NonNull在类型层面标记了非零和协变能减少一部分误用。三、线程安全不能靠感觉unsafe impl Send和unsafe impl Sync要特别谨慎。底层 C 库是否线程安全内部是否有全局状态Drop 是否可能和调用并发// 只有确认底层 handle 可跨线程移动才允许 Send unsafe impl Send for ModelHandle {}这类 impl 必须有外部文档或源码证据支撑。不能因为编译器抱怨就用 unsafe 把它按下去。四、panic 路径也要看unsafe 代码里如果中途 panic是否会留下半初始化对象Drop 是否会 double free锁是否释放这些都要检查。可以用 Miri、sanitizer、loom 辅助但工具不能替代不变量审查。工具抓的是一部分执行审计看的是所有可能。审计结果最好形成清单并随代码提交。哪些 unsafe 块依赖外部库保证哪些依赖调用方传入对齐指针哪些已经由测试覆盖要写清楚。以后改动触碰这些不变量时reviewer 才知道重点在哪里。unsafe_audit: block: tensor_view_from_raw invariant: ptr aligned, len within allocation, no mutable alias tests: miri, asan, fuzz_shapeunsafe 代码不是不能写而是不能没有账本。五、总结Unsafe 审计的核心是内存模型和不变量。每个 unsafe 块要说明理由检查指针、生命周期、别名、线程安全和 panic 路径。能跑过测试只是开始。unsafe 代码要能讲清为什么不会错才配进入系统核心路径。