Rust 中 unsafe 代码的审计清单:从指针别名到未初始化内存的常见 UB 模式
Rust 中 unsafe 代码的审计清单从指针别名到未初始化内存的常见 UB 模式一、unsafe 的隐藏契约编译器不报错不代表正确Rust 的unsafe关键字解锁了五个超能力解引用裸指针、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait、访问 union 字段。但解锁的同时承担了编译器不再检查的契约——违反任一契约都可能导致未定义行为UB。UB 的隐蔽性在于它不一定立即崩溃。常见表现包括Debug 模式下运行正常、Release 模式下结果错误LLVM 优化利用 UB 假设做了不合理的变换x86 上正常、ARM 上段错误严格别名规则在不同架构上执行力度不同单线程正常、多线程下随机崩溃数据竞争的未定义行为。审计 unsafe 代码的核心不是确认是否能编译通过而是确认是否违反了编译器的隐含假设。最危险的 five 类 UB指针别名违反 Strict Aliasing、未初始化内存读取、整数溢出在 Release 模式下、数据竞争、Dangling Pointer。二、unsafe 代码的 UB 分类与检测方法Miri 是检测 unsafe UB 的最优工具。它在 MIRMid-level IR层面解释执行 Rust 代码跟踪每个内存位置的初始化状态、借用的生命周期、指针的来源Provenance。Miri 能检测 95% 以上的 UB 类型但执行速度慢100~1000x无法覆盖生产环境的全量测试。SanitizersASan、TSan、UBSan在编译时插入检查代码运行时检测非法内存访问、数据竞争和未定义算术。开销约 2~5x可在生产预发布环境运行。三、unsafe 审计清单与示例分析use std::mem; use std::ptr; // // 审计项 1指针别名的 Strict Aliasing 违反 // /// ❌ UB两个 mut 指向重叠内存区域 unsafe fn violation_aliasing() { let mut data [0u32; 4]; let ptr data.as_mut_ptr(); // 创建两个指向重叠区域的 mut 引用 let slice_a: mut [u32] std::slice::from_raw_parts_mut(ptr, 3); // [0,1,2] let slice_b: mut [u32] std::slice::from_raw_parts_mut(ptr.add(1), 2); // [1,2] // UBslice_a 和 slice_b 有重叠LLVM 假设两个 mut 不重叠 slice_a[1] 42; slice_b[0] 24; // 写入 slice_a[1] 的同一地址 } /// ✓ 安全使用 *mut 裸指针而非 mut 引用 unsafe fn safe_overlapping() { let mut data [0u32; 4]; let ptr data.as_mut_ptr(); // 裸指针没有别名限制 let ptr_a ptr; let ptr_b ptr.add(1); ptr::write(ptr_a.add(1), 42); ptr::write(ptr_b, 24); // 裸指针允许重叠写入但逻辑上仍需谨慎 } // // 审计项 2未初始化内存读取 // /// ❌ UB读取未初始化的内存 unsafe fn violation_uninit_read() { let mut uninit: mem::MaybeUninitVecu32 mem::MaybeUninit::uninit(); // 忘记调用 write直接 assume_init let vec uninit.assume_init(); // UBvec 的指针/容量/长度都是垃圾值 // 尝试 drop 会导致 free 随机地址 → crash 或更糟 } /// ✓ 安全确保初始化后再读取 unsafe fn safe_maybe_uninit() { let mut uninit: mem::MaybeUninitVecu32 mem::MaybeUninit::uninit(); uninit.write(vec![1, 2, 3]); // 正确初始化 let vec uninit.assume_init(); assert_eq!(vec.len(), 3); } // // 审计项 3transmute 的类型不兼容 // /// ❌ UBtransmute 不兼容类型 unsafe fn violation_transmute() { let x: u32 42; // 将 u32 transmute 为 str — 完全非法 // str 的胖指针需要有效的数据指针和长度 // let s: str mem::transmute(x); // 某些情况下编译通过但 UB // 更隐蔽的错误transmute 不同大小的类型 // let _: [u8; 3] mem::transmute(0u32); // 大小不匹配 } /// ✓ 安全使用 bytemuck 或手工验证 fn safe_transmute_same_size() { let x: u32 0x41424344; let bytes: [u8; 4] unsafe { mem::transmute(x) }; // u32 → [u8; 4] 是同大小同对齐的 transmute安全 println!({:?}, bytes); } // // 审计项 4Dangling Pointer悬挂指针 // /// ❌ UB返回局部变量的引用 fn violation_dangling() - static [u8] { let local vec![1, 2, 3]; // ❌ 编译错误安全代码中 // local[..] // 但 unsafe 中可以绕过 let ptr local.as_ptr(); // 函数返回后 local 被释放ptr 成为悬挂指针 // unsafe { std::slice::from_raw_parts(ptr, 3) } unreachable!() } /// ❌ UBVec 重新分配导致指针失效 fn violation_vec_realloc() { let mut v vec![1, 2, 3]; let ptr v.as_ptr(); // push 可能触发重新分配ptr 失效 for i in 0..100 { v.push(i); } // unsafe { println!({}, *ptr); } // UB } // // 审计项 5FFI 边界的生命周期和 ABI // mod ffi_violations { use std::ffi::CString; use std::os::raw::c_char; /// ❌ UB传递给 C 的指针在 C 使用前就被释放 unsafe fn violation_ffi_lifetime() { let c_str CString::new(hello).unwrap(); let ptr: *const c_char c_str.as_ptr(); // 假设 spawn_c_thread(ptr) 在另一个线程长期使用 ptr // spawn_c_thread(ptr); // c_str 在此处被 drop释放内存 // C 线程仍然持有 ptr → Use-After-Free } /// ✓ 安全确保 C 端使用完毕后释放 unsafe fn safe_ffi_lifetime() { let c_str CString::new(hello).unwrap(); let ptr: *const c_char c_str.as_ptr(); // 同步调用 C 函数在 c_str 存活期间完成 // libc::puts(ptr); // c_str 在 C 函数返回后才被 drop } /// ❌ UB调用约定不匹配 extern C fn c_style_function() {} // 将 extern C 函数指针转换为 extern system 调用 // 在 Windows 上 system stdcallC cdecl → ABI 不匹配 // type SystemFn extern system fn(); // let f: SystemFn unsafe { mem::transmute(c_style_function as *const ()) }; // f(); // UB on Windows } // // 审计项 6并发数据竞争 // mod data_race { use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; /// ❌ UB非原子类型并发写 fn violation_data_race() { let mut shared 0u32; let ptr mut shared as *mut u32; // 两个线程同时写 *ptr没有同步 // thread::scope(|s| { // s.spawn(|| unsafe { *ptr 1; }); // s.spawn(|| unsafe { *ptr 2; }); // }); } /// ✓ 安全使用 Atomic 类型 fn safe_atomic() { let shared AtomicBool::new(false); thread::scope(|s| { s.spawn(|| shared.store(true, Ordering::Release)); s.spawn(|| { while !shared.load(Ordering::Acquire) { std::hint::spin_loop(); } }); }); } } // // 审计清单Checklist // /// unsafe 代码审计清单 struct UnsafeAuditChecklist { items: VecAuditItem, } #[derive(Debug)] struct AuditItem { category: static str, check: static str, passed: bool, notes: String, } impl UnsafeAuditChecklist { fn new() - Self { UnsafeAuditChecklist { items: vec![ // 指针安全 AuditItem { category: 指针, check: 所有裸指针的创建都有对应的内存分配, passed: true, notes: String::new() }, AuditItem { category: 指针, check: 裸指针解引用前检查了非空, passed: true, notes: String::new() }, AuditItem { category: 指针, check: 指针运算结果仍在分配范围内, passed: true, notes: String::new() }, AuditItem { category: 指针, check: 没有创建指向重叠内存的 mut 引用, passed: true, notes: String::new() }, // 类型安全 AuditItem { category: 类型, check: transmute 的源和目标类型大小对齐一致, passed: true, notes: String::new() }, AuditItem { category: 类型, check: MaybeUninit 在 assume_init 前已 write, passed: true, notes: String::new() }, AuditItem { category: 类型, check: union 字段访问的类型确实是活跃变体, passed: true, notes: String::new() }, // 生命周期 AuditItem { category: 生命周期, check: 返回的指针/引用生命周期不超过数据源, passed: true, notes: String::new() }, AuditItem { category: 生命周期, check: FFI 传递的指针在 C 端使用期间 Rust 端不释放, passed: true, notes: String::new() }, // 并发 AuditItem { category: 并发, check: 所有跨线程共享的可变数据使用 Atomic 或 Mutex, passed: true, notes: String::new() }, AuditItem { category: 并发, check: MutexGuard 不会跨越 .await 点, passed: true, notes: String::new() }, // FFI AuditItem { category: FFI, check: extern 函数的调用约定与 C 端匹配, passed: true, notes: String::new() }, AuditItem { category: FFI, check: C 结构体的 repr(C) 布局与 C 端一致, passed: true, notes: String::new() }, ], } } fn audit(mut self) { for item in self.items { let status if item.passed { ✓ } else { ✗ }; println!([{}] {}: {}, status, item.category, item.check); } } } fn main() { println!( Unsafe Code Audit Checklist \n); let mut checklist UnsafeAuditChecklist::new(); checklist.audit(); println!(\n Recommended Tools ); println!(1. Miri: cargo miri test (100% UB detection, 100-1000x slow)); println!(2. ASan: RUSTFLAGS-Zsanitizeraddress (2-5x overhead)); println!(3. TSan: RUSTFLAGS-Zsanitizerthread (5-15x overhead)); println!(4. loom: #[test] fn with loom::model(|| ...) (排列检查)); println!(5. MIRI_TEST: cargo careful test (strict checks)); }from_raw_parts_mut从裸指针创建mut [T]时的别名限制是 audit 清单中最常被违反的条目。Rust 编译器假设mut引用是其指向内存的唯一可写路径。违反这个假设时LLVM 的重排和消除优化可能产生与源代码逻辑不一致的结果。MaybeUninit::assume_init()的误用是第二常见的 UB 来源。关键原则assume_init必须在write或as_mut_ptrptr::write之后调用。对于Vec、String等拥有非 Trivial Drop 的类型错误地assume_init后再 drop 会释放随机地址。Sanitizer 组合策略在 unsafe 代码审计中至关重要。单一 Sanitizer 只能覆盖一类问题但 UB 往往跨类别共生——例如 FFI 边界的 Use-After-Free 会同时触发 ASan堆内存和 TSan如果涉及跨线程。推荐的 CI 矩阵是在 Nightly Rust 上运行cargo miri test覆盖纯 Rust unsafe 路径在-Zsanitizeraddress编译的二进制上运行集成测试覆盖 FFI 交互在-Zsanitizerthread编译的二进制上运行并发测试。这三个 Sanitizer 不能同时启用编译器限制需要分别构建三次。Miri 的 Tree Borrows 模型是目前最严格的别名检测实现它在 Stacked Borrows 基础上增加了一个树形借用结构能检测到跨函数调用的隐式 reborrow 违规——这类 Bug 在编译期完全静默仅在特定 LLVM 优化等级下才会产生错误结果。四、unsafe 代码的测试策略Miri 测试的限制不支持 FFI 调用无法执行外部 C 代码不支持 SIMD intrinsic不支持文件系统操作应在 Miri 可覆盖的范围内充分测试FFI 部分通过 Sanitizers 覆盖unsafe 代码的最小化原则每个unsafe块应尽可能小3~5 行便于人工审查用安全抽象封装 unsafe 操作调用方无需 unsafe在unsafe块前用注释明确写出被调 unsafe 函数的安全契约五、总结Rust unsafe 代码的五大 UB 类别指针别名违反、未初始化内存读取、类型 transmute 错误、生命周期不匹配含 FFI、数据竞争。mut引用有排他性别名限制*mut裸指针没有。重叠内存区域的操作必须使用裸指针而非mut引用。MaybeUninit::assume_init()必须在write后调用对拥有非 Trivial Drop 的类型Vec、String尤其危险。检测工具链Miri100% UB 检测100x 慢→ Sanitizers2~15x 开销→ loom并发模型检查→ cargo-careful严格模式。unsafe 代码的最小化原则每个 unsafe 块 ≤ 5 行、用安全抽象封装、注释明确安全契约。