Rust 不安全代码审计器
对 Rust 项目中的不安全代码进行深度审计。分析每个不安全块、函数、特征实现和 FFI 边界的正确性。验证安全不变量是否已记录,原始指针操作是否有界,Send/Sync 实现是否正确。
使用场景:在发布 Rust 包之前进行审查、审计依赖项、准备安全审查或建立不安全代码策略时使用。
分析步骤
cat Cargo.toml 2>/dev/null | head -20
grep -i "libc\|winapi\|bindgen\|cc\|ffi\|sys\b" Cargo.toml 2>/dev/null | head -10
find . -name "
.rs" -not -path '/target/
' | wc -l
项目级不安全代码策略
grep -rn 'forbid(unsafe_code)\|deny(unsafe_code)' --include=".rs" . 2>/dev/null | grep -v 'target/' | head -5
不安全代码统计
echo "=== 不安全代码统计 ==="
echo -n "块:";
grep -rn 'unsafe\s
{' --include=".rs" . 2>/dev/null | grep -v 'target/' | wc -l
echo -n "函数:";
grep -rn 'unsafe\s\+fn' --include="
.rs" . 2>/dev/null | grep -v 'target/' | wc -l
echo -n "特征实现:";
grep -rn 'unsafe\s\+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
没有安全注释的不安全块
for f in $(grep -rl 'unsafe\s{' --include="
.rs" . 2>/dev/null | grep -v 'target/'); do
grep -n 'unsafe\s{' "$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 "未记录:$f:$line";
fi
done
done | head -20
没有安全文档的不安全函数
for f in $(grep -rl 'unsafe\s\+fn' --include="
.rs" . 2>/dev/null | grep -v 'target/'); do
grep -n 'unsafe\s\+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 "未记录函数:$f:$line";
fi
done
done | head -15
标志:缺少 // SAFETY: 注释:每个不安全块必须解释为什么不变量成立(Clippy lint:undocumented_unsafe_blocks)
模糊的安全注释:"这是安全的" 是无效的 —— 必须说明具体的不变量
缺少 # 安全部分的 pub 不安全函数:调用者需要知道合同
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 T 同时指向相同的数据
生命周期正确性:数据在指针生命周期内有效(没有悬垂指针)
所有权清晰:from_raw/into_raw 对必须是 1:1(否则会发生双重释放或泄漏)
grep -rn 'extern\s"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 边界:
跨 FFI 的 panic:Rust panic 跨 extern "C" 是 UB —— 必须使用 catch_unwind
字符串处理:C 字符串是 null 终止的;使用 CString/CStr,检查内部 null
内存所有权:Rust 分配器和 C 分配器是不同的 —— 谁释放?
结构布局:#[repr(C)] 必须用于传递给/从 C 的结构
整数大小:C int 是平台依赖的 —— 使用 c_int,而不是 i32
线程安全:C 函数可能不是线程安全的;记录约束
手动 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
联合类型
grep -rn 'union\s\+[A-Z]' --include="*.rs" . 2>/dev/null | grep -v 'target/' | head -5
对于 Send/Sync,验证:
Send:没有线程本地状态,没有线程亲和的 OS 句柄
Sync:没有内部可变性,没有同步(UnsafeCell 默认使类型 !Sync)
顺序正确性: