审计 Rust代码中的 unsafe 块使用——验证安全不变式、检查 FFI 边界、审查裸指针操作、校验 Send/Sync 实现,...
粗糙的不安全审计师是一项面向实际任务的技能,主要用于对 Rust 项目中的不安全代码进行深入审计;分析每个不安全的块、 函数、 特征 和 FFI 边界, 以求安全;
该技能适合需要稳定复用相关能力的场景,可作为自动化工作流的一部分,也便于后续检查、调整和扩展。从功能定位来看,该技能强调把分散的操作要求整理成清晰、可复用的处理流程,使用户能够围绕既定目标快速准备输入、选择执行方式并获得结构化结果。实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;
若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。
对 Rust 项目中的 unsafe 代码进行深度审计。全面分析每个 unsafe 代码块、函数、trait 实现以及 FFI 边界,验证其内存安全性(soundness)。确保安全不变式(safety invariants)已明确文档化、原始指针操作具有明确边界、且 Send/Sync 实现正确无误。
适用场景:发布 Rust crate 前的审查、依赖项审计、安全审查准备,或制定 unsafe 使用策略。
cat Cargo.toml 2>/dev/null | head -20
grep -i "libc|winapi|bindgen|cc|ffi|sysb" Cargo.toml 2>/dev/null | head -10
find . -name "*.rs" -not -path '*/target/*' | wc -l
# 项目级 unsafe 策略
grep -rn 'forbid(unsafe_code)|deny(unsafe_code)' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -5
# 普查
echo "=== Unsafe 普查 ==="
echo -n "代码块数量: "; grep -rn 'unsafes*{' --include="*.rs" . 2>/dev/null | grep -v 'target/' | wc -l
echo -n "函数数量: "; grep -rn 'unsafes+fn' --include="*.rs" . 2>/dev/null | grep -v 'target/' | wc -l
echo -n "trait 实现数量: "; grep -rn 'unsafes+impl' --include="*.rs" . 2>/dev/null | grep -v 'target/' | wc -l
# SAFETY 注释(Rust 社区惯例)
grep -B1 -A3 'unsafe' --include="*.rs" . 2>/dev/null | grep -i 'SAFETY' | head -20
# 缺少 safety 注释的 unsafe 代码块
for f in $(grep -rl 'unsafes*{' --include="*.rs" . 2>/dev/null | grep -v 'target/'); do
grep -n 'unsafes*{' "$f" | while read match; do
line=$(echo "$match" | cut -d: -f1); prev=$((line - 1))
if ! sed -n "${prev}p" "$f" | grep -qi 'safety'; then echo "UNDOCUMENTED: $f:$line"; fi
done
done | head -20
# 缺少 safety 文档的 unsafe 函数
for f in $(grep -rl 'unsafes+fn' --include="*.rs" . 2>/dev/null | grep -v 'target/'); do
grep -n 'unsafes+fn' "$f" | while read match; do
line=$(echo "$match" | cut -d: -f1); prev=$((line - 1))
if ! sed -n "${prev}p" "$f" | grep -q '///|//!'; then echo "UNDOC_FN: $f:$line"; fi
done
done | head -15
需标记的问题:
// SAFETY: 注释:每个 unsafe 代码块都必须说明为何安全不变式成立(Clippy 检查项:undocumented_unsafe_blocks)# Safety 文档段落:调用者需清楚了解函数契约grep -rn 'as *const|as *mut' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -20
grep -rn '.offset(|.add(|.sub(' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -15
grep -rn 'from_raw|into_raw|from_raw_parts' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -15
grep -rn 'ManuallyDrop|MaybeUninit|mem::forget|mem::transmute' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -15
对每个原始指针操作,需验证以下几点:
&T 和 &mut Tfrom_raw/into_raw 调用必须成对出现(否则将导致双重释放或内存泄漏)grep -rn 'externs*"C"' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -15 grep -rn '#[no_mangle]' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -10 grep -rn 'CString|CStr|c_char|c_int|c_void' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -15 find . -name "bindings.rs" -o -name "*_ffi.rs" -not -path '*/target/*' 2>/dev/null | head -5
逐个检查每个 FFI 边界,关注以下问题:
extern "C" 边界属于未定义行为(UB)——必须使用 catch_unwind 封装CString/CStr,并检查内部是否含 null 字节#[repr(C)] 属性int 类型尺寸随平台而异——应使用 c_int,而非硬编码的 i32# 手动实现的 Send/Sync
grep -rn 'unsafe impl.*Send|unsafe impl.*Sync' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -15
# 原子操作
grep -rn 'AtomicBool|AtomicUsize|AtomicPtr|Ordering::' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -10
# transmute(最危险的操作)
grep -rn 'mem::transmute|transmute(' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -10
# mem::zeroed / mem::uninitialized(对多数类型而言属于 UB)
grep -rn 'mem::zeroed|mem::uninitialized' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -10
# 联合体(union)类型
grep -rn 'unions+[A-Z]' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -5
针对 Send/Sync,需验证:
UnsafeCell 默认使类型变为 !Sync)Ordering(常见错误:本该用 Acquire/Release 却误用 Relaxed)针对 transmute,需标记如下问题:
0u8 转为 bool,或生成非法枚举判别值——立即触发 UBmem::zeroed:对 NonNull、bool、&T 或枚举类型调用 mem::zeroed 属于 UBmem::uninitialized:自 Rust 1.38 起已被弃用,始终为 UB —— 应改用 MaybeUninit# Rust Unsafe 审计报告 — [Crate Name]
## 概览
- 文件数:N | Rust Edition:2021 | unsafe 代码块:N | unsafe 函数:N | unsafe trait 实现:N
- 未文档化的 unsafe:N(目标值:0)
- 审计结论:PASS / CONDITIONAL / FAIL
## unsafe 项清单
| # | 文件:行号 | 类别 | 是否已文档化 | 结论 |
|---|-----------|----------|-----------|---------|
| 1 | src/lib.rs:45 | 原始指针解引用 | 是 | Sound |
| 2 | src/ffi.rs:23 | extern "C" 调用 | 否 | REVIEW |
| 3 | src/pool.rs:89 | Send 实现 | 是 | Sound |
| 4 | src/convert.rs:12 | transmute | 否 | UNSOUND |
## 关键问题(潜在 UB)
### [C1] transmute 构造非法枚举值
- **文件**:src/convert.rs:12
- **代码**:`unsafe { mem::transmute::(byte) }` —— byte 值未经校验
- **修复建议**:根据 byte 值进行模式匹配,并返回 Result
### [C2] extern "C" 回调中发生 panic
- **文件**:src/ffi.rs:67
- **代码**:extern "C" 函数内调用 `.unwrap()`
- **修复建议**:替换为 `match` + 错误码,或用 `catch_unwind` 包裹
## 文档缺失项
| 文件 | 行号 | 类型 | 缺失内容 |
|------|------|------|---------|
| src/lib.rs:45 | unsafe 代码块 | `// SAFETY:` 注释 |
| src/ffi.rs:23 | unsafe 函数 | `# Safety` 文档段落 |
## 建议措施
1. 为 N 个未文档化的 unsafe 代码块补充 `// SAFETY:` 注释
2. 修复 N 处潜在 UB 问题
3. 在 N 个 extern "C" 回调中添加 `catch_unwind`
4. 运行 `cargo +nightly miri test` 动态检测未定义行为
5. 添加 `#![deny(unsafe_op_in_unsafe_fn)]`,强制 unsafe 函数内所有 unsafe 操作均需显式标注作用域
6. 对无需 unsafe 的 crate,考虑启用 `#![forbid(unsafe_code)]`
| 当前 unsafe 实现 | 安全替代方案 |
|---|---|
mem::transmute 用于枚举类型转换 |
实现 TryFrom trait |
| 原始指针数组索引 | slice::get_unchecked(仍属 unsafe,但支持运行时边界检查) |
from_raw_parts 构造缓冲区视图 |
bytemuck::cast_slice(安全、零开销) |
手动实现 Send/Sync |
将内部类型包装进 Arc |
cargo +nightly miri test 动态检测未定义行为cargo clippy -- -W clippy::undocumented_unsafe_blocks 强制要求 unsafe 代码块提供 safety 注释cargo geiger 统计整个依赖树中 unsafe 的使用频次cargo audit 检查已知漏洞NonNull 替代 *mut T,将非空性约束编码进类型系统bytemuck 安全地对 POD 类型执行类型重解释(type punning)#相关专题
热门下载
相关下载
精品课程
共6课时 | 54.7万人学习
共89课时 | 134.1万人学习
共49课时 | 82.6万人学习
最新文章