资讯详情

资讯详情

Rust 健全性(Soundness)深度解析:comprehensive-rust 课程中的内存安全基石

Rust 健全性Soundness深度解析comprehensive-rust 课程中的内存安全基石【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust在 Rust 中健全性soundness是决定代码是否安全的核心概念一份 sound 的代码在满足其安全前置条件时不可能触发未定义行为Undefined BehaviorUB或内存安全问题。本文基于 Google Android 团队使用的 Rust 课程仓库 comprehensive-rust 中 rules-of-the-game游戏规则 章节系统讲解 soundness 的形式化定义、三种 sound 代码形态、责任归属burden of proof并通过一个从 Safe Rust 到暴露 unsafe 的copy函数五步演进让你掌握如何写出、审查出 sound 的 unsafe 代码的完整方法。SoundnessRust 的立身之本课程 Rust is sound 一节开宗明义地指出Soundness 是 Rust 的根本Soundness is fundamental to RustSoundness ≈ 不可能触发内存安全问题Sound 的函数具有共同的形态shapes。在此之前课程中已经展示了大量存在问题的代码示例但一直缺乏统一的术语来描述它们。Rules of the game 章节导语 说明了引入这套词汇的动机由于许多安全前置条件本质上是**语义性semantic**的而非语法性syntactic的使用共享的词汇才能让开发者就语义达成一致。整节围绕三个核心术语展开undefined behavior未定义行为、sound健全、unsound不健全最终目标是建立关于 soundness 的心智框架确保包含 unsafe 的 Rust 代码依然保持健全。在给出正式定义之前课程先用直觉语言刻画 sound 的含义sound 代码就是无法触发内存安全问题的代码它由 sound 的函数与 sound 的操作组成而 sound 函数是指其所有可能的输入都不会引发健全性问题的函数。形式化定义Sound、Unsound 与 UBRust is sound 一文承诺稍后将给出 soundness 的正式定义这一承诺由 Soundness健全性 一节兑现Sound 函数一个函数只要其安全前置条件safety preconditions得到满足就绝不会触发未定义行为UB。与之相对Unsoundness不健全性 给出了反面定义Unsound 函数即使你满足了文档中声明的全部安全前置条件它依然可能触发 UB。课程特别强调unsound 代码是坏的Unsound code is bad。即使调用者完全遵守了文档规则unsound 代码仍可能引发 UB因此在代码仓库中不应存在任何 unsound 代码找出 unsound 代码是 code review 的首要目标finding unsound code is the primary goal of the code review。把形式定义翻译成直觉语言sound 函数是守规矩的好函数——它清楚地文档化自己的安全前置条件调用者满足这些条件后函数就表现良好不产生 UB。而 unsound 函数则是不守规矩的坏函数无论调用者多么小心都可能踩雷。责任归属谁为前置条件负责一个容易被忽略的要点是满足安全前置条件的责任在调用者而不在编译器。编译器不会帮你验证这些条件——这正是语义性前置条件的代价。这一点在 soundness 定义一节被反复强调。关键推论纯 Safe Rust 必然 Sound附证明基于上述定义课程给出了一个优雅的推论Soundness Proof Part 2推论所有仅用 Safe Rust 实现的函数都是 sound 的。证明过程QED如下Safe Rust 代码没有任何安全前置条件空集因此任何调用纯 Safe Rust 函数的人都平凡地满足了这个空的前置条件集合Safe Rust 代码不可能触发 UB。由 1、2、3 即得证。直觉解释所有纯 Safe Rust 代码都是好代码——程序员无需为它考虑任何安全前置条件它永远守规矩永远不会触发 UB。这一推论是 Rust 安全故事的核心支柱编译器把 safe 代码不可能 UB 作为一个可证明的属性从而让不安全行为被精确地隔离在unsafe块与unsafe fn之中。Sound 代码的三种形态那么什么样的代码是 sound 的呢3 Shapes of Sound Rust 给出了答案sound 代码只可能有以下三种形态纯 Safe Rust 函数内部不含任何unsafe块完全封装了 unsafe 块的 Safe 函数unsafe块被安全地包裹在内部调用者完全不需要知道它们的存在即不可能被误用未封装 unsafe 块、并将证明负担proof burden转嫁给调用者的 unsafe 函数这类函数必须把安全前置条件文档化。举证责任Burden of Proof的转移三种形态对应的责任主体各不相同这是理解 unsafe 代码审查的关键代码形态由谁保证 soundness纯 Safe Rust 函数编译器compile-time 保证含有 unsafe 块的 Safe 函数函数作者必须保证对任意输入都不会引发内存问题unsafe 函数调用者必须满足文档化的安全前置条件理解这张表之后审查 unsafe 代码时就有了明确的问题清单作者是否封装了 unsafe前置条件是否文档化调用者是否真的满足了这些条件实战案例copy函数的安全形态演进为了把上述抽象概念落到实处课程用同一个功能——从source读取字节并写入dest——演示了五种实现形态起点原型见 Copying memory/// Reads bytes from source and writes them to dest pub fn copy(dest: mut [u8], source: [u8]) { ... }dest是可变的字节切片source是不可变的字节切片。接下来逐一看五种演进。形态一纯 Safe Rust 实现Safe Rust 给出第一种实现pub fn copy(dest: mut [u8], source: [u8]) { for (dest, src) in dest.iter_mut().zip(source) { *dest *src; } } fn main() { let a [114, 117, 115, 116]; let b mut [82, 85, 83, 84]; println!({}, String::from_utf8_lossy(b)); copy(b, a); println!({}, String::from_utf8_lossy(b)); }这个实现只使用 Safe Rust因此对于所有可能的输入都不可能触发内存安全问题。课程借此引导读者思考为什么迭代器方案是安全的因为通过 Rust 的迭代器我们永远不会直接操作指针从而自动规避了空指针、越界检查等指针相关错误。还能想到其他保证吗课程给出的答案包括不会发生别名aliasing问题悬垂指针dangling pointer不可能出现对齐alignment一定是正确的不会意外读取未初始化内存。因此可以说copy是 sound 的——Rust 保证了它的所有安全前置条件都被满足。从程序员视角看这个函数没有任何安全前置条件。不过要注意sound 不等于总是符合调用者意图。如果dest空间不足数据就不会被完整拷贝过去zip会在较短的切片处停止但这只是逻辑层面的限制与内存安全无关。形态二封装 unsafe 的 Safe 函数Encapsulated Unsafe Rust 展示了第二种形态函数签名保持 safe但内部使用 unsafe 手动访问内存pub fn copy(dest: mut [u8], source: [u8]) { let len dest.len().min(source.len()); let mut i 0; while i len { // SAFETY: i must be in-bounds as it was produced by source.len() let new unsafe { source.get_unchecked(i) }; // SAFETY: i must be in-bounds as it was produced by dest.len() let old unsafe { dest.get_unchecked_mut(i) }; *old *new; i 1; } }这次实现绕开了迭代器改为手动内存访问。关键问题随之而来这段代码正确吗有隐患吗由谁负责保证正确性答案由函数作者负责。Safe 函数内部包含 unsafe 块时只要不存在能让某个输入引发内存安全问题的可能它就是 sound 的。这里作者通过dest.len().min(source.len())精确限定了循环上界并在每个 unsafe 块旁用SAFETY:注释说明索引不会越界属于完全封装、调用者无感知的第二类形态。形态三暴露 unsafe 的函数问题代码Exposed Unsafe Rust 改变了游戏规则签名变为接收裸指针要求调用者配合pub fn copy(dest: mut [u8], source: *const u8) { let source { let mut len 0; let mut end source; while unsafe { *end ! 0 } { len 1; end unsafe { end.add(1) }; } unsafe { std::slice::from_raw_parts(source, len 1) } }; for (dest, src) in dest.iter_mut().zip(source) { *dest *src; } }功能与之前相同把字节从一处拷到另一处。但为了从裸指针构造切片必须先找到数据的结尾——既然处理的是文本就采用 C 语言惯例以 null 字节结尾的字符串NUL-terminated string。这段代码可以编译输出也与前两版一致——但课程点破了一个重要事实一个 unsound 的函数在部分输入上依然可能正常工作。测试通过了不代表你的函数就是 sound 的。课程随即引导读者找问题可读性差代码难以快速扫读source指针可能为nullsource指针可能悬垂指向已释放或未初始化的内存source可能没有以 null 字节结尾。假设不能修改函数签名可做的改进包括空指针加空指针检查并提前返回if source.is_null() { return; }可读性不要自己实现寻找第一个 null 字节改用经过充分测试的库。但有些安全需求是无法通过防御性检查来满足的例如悬垂指针、缺少 null 终止字节——这些只有调用者才知道。那么如何让这个函数变得 sound课程给出两条路改变source的类型换成已知长度的类型比如像前面那样用切片把函数标记为 unsafe并文档化安全前置条件。形态四文档化安全前置条件的 unsafe 函数Documented safety preconditions 走第二条路最终形成完整的 sound 形态/// ... /// /// # Safety /// /// This function can easily trigger undefined behavior. Ensure that: /// /// - source pointer is non-null and non-dangling /// - source data ends with a null byte within its memory allocation /// - source data is not freed (its lifetime invariants are preserved) /// - source data contains fewer than isize::MAX bytes pub unsafe fn copy(dest: mut [u8], source: *const u8) { let source { let mut len 0; let mut end source; // SAFETY: Caller has provided a non-null pointer while unsafe { *end ! 0 } { len 1; // SAFETY: Caller has provided a data with length isize:MAX end unsafe { end.add(1) }; } // SAFETY: Caller maintains lifetime and aliasing requirements unsafe { std::slice::from_raw_parts(source, len 1) } }; for (dest, src) in dest.iter_mut().zip(source) { *dest *src; } }与前几版相比变化有三点copy被标记为unsafe fn通过文档注释中的# Safety小节明确文档化安全前置条件每个内部unsafe块旁都有SAFETY:行内注释。课程给出的结论是当一个 unsafe 函数的安全前置条件和内部 unsafe 块都被文档化时它就是 sound 的。相应地main中的调用也必须修改fn main() { let a [114, 117, 115, 116].as_ptr(); let b mut [82, 85, 83, 84, 0]; println!({}, String::from_utf8_lossy(b)); unsafe { copy(b, a); } println!({}, String::from_utf8_lossy(b)); }注意两点修正a指向的[114, 117, 115, 116]并不满足以 null 字节结尾 这一前置条件这正是课程设置的陷阱同时调用 unsafe 函数需要unsafe块并在块中或紧邻处用SAFETY:注释说明调用者已核实各项前置条件。形态五反面教材Crying Wolf 函数Crying Wolf 展示了另一种常见误区——狼来了式函数pub unsafe fn copy(dest: mut [u8], source: [u8]) { for (dest, src) in dest.iter_mut().zip(source) { *dest *src; } }这类函数被标记为unsafe但实际上没有任何需要调用者检查的安全前置条件参数全是对齐良好、生命周期安全的切片。它带来的是纯粹的负担调用者被迫写unsafe块、被迫思考并不存在的风险而编译器本可以替你完成全部保证。课程给出的判断标准很清晰不要为了看起来需要小心而标记 unsafeunsafe 应该与真实存在的、文档化的安全前置条件一一对应。从课程到实践一套可执行的审查清单把这五种形态串起来就得到了一套实用的 unsafe 代码审查清单这也正是课程希望读者建立的心智框架判定形态函数属于三种 sound 形态中的哪一种纯 Safe封装 unsafe还是暴露 unsafe确认责任方若含 unsafe 块作者是否对任意输入都能保证无 UB若为 unsafe 函数前置条件是否被完整文档化核对前置条件每个文档化条件是否都被调用方真正满足条件是否可满足如 dangling 指针、无终止符这类无法防御性检查的条件必须转为文档义务警惕 Crying Wolfunnecessaryunsafe同样是一种代码坏味道回归本质记住形式定义——在满足安全前置条件的前提下函数能否触发 UB如果能它就是 unsound必须修复。另外值得强调课程反复出现的一句话测试通过 ≠ sound。unsound 函数可以在某些输入上行为正常因此单元测试不能替代对 safety preconditions 的严谨推理。小结围绕 Rust is sound 这一核心comprehensive-rust 课程给出了从直觉到证明、再到实战的完整链条直觉sound ≈ 不会触发内存安全问题定义满足安全前置条件即不触发 UB 的函数是 sound 的反之则是 unsound推论带证明纯 Safe Rust 函数必然 sound形态sound 代码只有三种形态责任分别落在编译器、函数作者与调用者身上实战同一个copy函数历经 Safe 实现、封装 unsafe、暴露 unsafe、文档化前置条件、Crying Wolf 五种形态完整演示了如何把不安全代码驯化为 sound 代码。这套规则正是 Rust 生态中大量底层库标准库、std::slice::from_raw_parts等不安全接口能够安全共存的基础。掌握它你不仅能写出可靠的 unsafe 代码也能在 code review 中精准识别出 unsound 的实现。相关章节还可以进一步研读 3 Shapes of Sound Rust 与 Copying memory 系列以及本章后续的 Copying memory 深度讲解 与 Safety preconditions 章节。【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →