news 2026/7/17 15:27:57

Rust 中 unsafe 代码的审计清单:从指针别名到未初始化内存的常见 UB 模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 的最优工具。它在 MIR(Mid-level IR)层面解释执行 Rust 代码,跟踪每个内存位置的初始化状态、借用的生命周期、指针的来源(Provenance)。Miri 能检测 95% 以上的 UB 类型,但执行速度慢(100~1000x),无法覆盖生产环境的全量测试。

Sanitizers(ASan、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] // UB:slice_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::MaybeUninit<Vec<u32>> = mem::MaybeUninit::uninit(); // 忘记调用 write,直接 assume_init let vec = uninit.assume_init(); // UB!vec 的指针/容量/长度都是垃圾值 // 尝试 drop 会导致 free 随机地址 → crash 或更糟 } /// ✓ 安全:确保初始化后再读取 unsafe fn safe_maybe_uninit() { let mut uninit: mem::MaybeUninit<Vec<u32>> = mem::MaybeUninit::uninit(); uninit.write(vec![1, 2, 3]); // 正确初始化 let vec = uninit.assume_init(); assert_eq!(vec.len(), 3); } // ============================================================ // 审计项 3:transmute 的类型不兼容 // ============================================================ /// ❌ UB:transmute 不兼容类型 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); } // ============================================================ // 审计项 4:Dangling 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!() } /// ❌ UB:Vec 重新分配导致指针失效 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! } // ============================================================ // 审计项 5:FFI 边界的生命周期和 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 = stdcall,C = 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: Vec<AuditItem>, } #[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='-Zsanitizer=address' (2-5x overhead)"); println!("3. TSan: RUSTFLAGS='-Zsanitizer=thread' (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必须在writeas_mut_ptr+ptr::write之后调用。对于VecString等拥有非 Trivial Drop 的类型,错误地assume_init后再 drop 会释放随机地址。

Sanitizer 组合策略在 unsafe 代码审计中至关重要。单一 Sanitizer 只能覆盖一类问题,但 UB 往往跨类别共生——例如 FFI 边界的 Use-After-Free 会同时触发 ASan(堆内存)和 TSan(如果涉及跨线程)。推荐的 CI 矩阵是:在 Nightly Rust 上运行cargo miri test覆盖纯 Rust unsafe 路径,在-Zsanitizer=address编译的二进制上运行集成测试覆盖 FFI 交互,在-Zsanitizer=thread编译的二进制上运行并发测试。这三个 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 函数的安全契约

五、总结

  1. Rust unsafe 代码的五大 UB 类别:指针别名违反、未初始化内存读取、类型 transmute 错误、生命周期不匹配(含 FFI)、数据竞争。
  2. &mut引用有排他性别名限制,*mut裸指针没有。重叠内存区域的操作必须使用裸指针而非&mut引用。
  3. MaybeUninit::assume_init()必须在write后调用,对拥有非 Trivial Drop 的类型(Vec、String)尤其危险。
  4. 检测工具链:Miri(100% UB 检测,100x 慢)→ Sanitizers(2~15x 开销)→ loom(并发模型检查)→ cargo-careful(严格模式)。
  5. unsafe 代码的最小化原则:每个 unsafe 块 ≤ 5 行、用安全抽象封装、注释明确安全契约。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/17 15:27:49

蓝奏云网盘客户端:高效管理云文件的终极解决方案

蓝奏云网盘客户端&#xff1a;高效管理云文件的终极解决方案 【免费下载链接】lanzouyun-disk 蓝奏云网盘、客户端&#xff0c;实现了蓝奏云大部分功能并添加了许多增强型功能&#xff0c;可作为网盘的日常管理工具使用。项目用 electron 构建&#xff0c;支持 macos 和 window…

作者头像 李华
网站建设 2026/7/17 15:27:38

Arachni终极指南:如何用Ruby框架轻松实现Web应用安全扫描

Arachni终极指南&#xff1a;如何用Ruby框架轻松实现Web应用安全扫描 【免费下载链接】arachni Web Application Security Scanner Framework 项目地址: https://gitcode.com/gh_mirrors/ar/arachni 在当今数字化时代&#xff0c;Web应用安全已成为每个开发者和安全工程…

作者头像 李华
网站建设 2026/7/17 15:27:12

BugTraceAI-Apex-G4-26B-Q4部署指南:3种高效配置的终极对比方案

BugTraceAI-Apex-G4-26B-Q4部署指南&#xff1a;3种高效配置的终极对比方案 【免费下载链接】BugTraceAI-Apex-G4-26B-Q4 项目地址: https://ai.gitcode.com/hf_mirrors/BugTraceAI/BugTraceAI-Apex-G4-26B-Q4 BugTraceAI-Apex-G4-26B-Q4是一款基于Gemma 4架构的高性能…

作者头像 李华
网站建设 2026/7/17 15:26:45

2026年标杆网站建设公司精选推荐,四大类网站设计开发公司选型详解

据艾瑞咨询等机构发布的行业报告显示&#xff0c;2026年国内高端定制网站开发市场规模已超570亿元&#xff0c;企业对于能深度融合业务逻辑的网站制作公司需求愈发迫切。然而&#xff0c;面对市场上超过10万家服务商&#xff0c;如何找到真正懂行业、能落地、可托付的伙伴&…

作者头像 李华