在这里插入图片描述


0 双重释放:看似小 bug,实则核弹

// C 代码:一次手误,两次 free
char *p = malloc(64);
free(p);
free(p);        // 未定义行为:可能 crash,也可能破坏 malloc 元数据

传统系统语言靠「程序员自律 + 运行时检测」;
Rust 把这一问题 前移编译期,用 所有权系统双重释放成为编译错误


1 Rust 的三级防火墙

层级机制双重释放保护
语言规则单一所有权 + move 语义一个值只能被 drop 一次
类型系统Drop trait & 线性类型值离开作用域时 恰好 调用一次析构
MIR 检查borrow checker + drop elaboration保证 move 后再访问 = 编译错误

2 Move 语义:把「释放」从运行时搬到编译期

2.1 最小示例:move 后不可再使用

let s = String::from("hello");
let t = s;          // s 的所有权 **move** 到 t
println!("{}", s);  // ❌ 编译错误:value borrowed here after move

编译器输出:

error[E0382]: borrow of moved value: `s`

背后 MIR(简化):

bb0:
    _1 = String::from("hello");
    _2 = move _1;        // move
    _3 = _1;             // ❌ use after move
  • move 在 MIR 中表现为 变量标记为 uninit
  • 任何后续读取都会触发 线性类型检查失败

3 Drop 的线性化保证

3.1 单一路径,单一析构

struct Bomb(*mut u8);

impl Drop for Bomb {
    fn drop(&mut self) {
        unsafe { std::alloc::dealloc(self.0, std::alloc::Layout::new::<u8>()); }
    }
}

fn demo() {
    let bomb = Bomb(Box::into_raw(Box::new(0u8)));
    // bomb 离开作用域时,drop 被调用 **一次**
}
  • 编译器插入的 drop elaboration 确保 恰好一条路径 走到 drop
  • 任何提前 move、return、panic,都会被 MIR 生成器 正确插入 dropdrop_in_place

4 深度实验:手写智能指针,验证「双重释放 = 编译期错误」

目标:写一个 最小 Box,故意触发双重释放,观察编译器如何拦截。

4.1 故意错误版本(无法编译)

struct MyBox<T>(*mut T);

impl<T> MyBox<T> {
    fn new(value: T) -> Self {
        Self(Box::into_raw(Box::new(value)))
    }
}

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        unsafe { Box::from_raw(self.0); } // 正确释放
    }
}

fn double_drop() {
    let b = MyBox::new(42);
    let _c = b;           // move
    drop(b);              // ❌ 编译错误:use of moved value
}

编译器:

error[E0382]: use of moved value: `b`

4.2 允许二次释放的唯一途径:unsafe

fn evil() {
    let raw = Box::into_raw(Box::new(42u8));
    unsafe {
        Box::from_raw(raw);
        Box::from_raw(raw); // UB:双重释放
    }
}

unsafe 是逃生舱,但所有权系统依旧生效
在 safe API 边界外,责任回到程序员;
工具链提供 Miri / AddressSanitizer动态补票


5 FFI 边界:C 裸指针如何继承 Rust 所有权?

5.1 借用守卫(Borrow Guard)模式

#[repr(C)]
pub struct FfiSlice {
    data: *const u8,
    len: usize,
}

impl FfiSlice {
    pub fn new(s: &str) -> Self {
        Self { data: s.as_ptr(), len: s.len() }
    }
}

// 禁止返回 &'static str,防止悬垂
pub struct Guard<'a> {
    _marker: PhantomData<&'a str>,
}

pub fn with_slice<R>(s: &str, f: extern "C" fn(FfiSlice) -> R) -> R {
    let slice = FfiSlice::new(s);
    let _guard = Guard { _marker: PhantomData };
    f(slice)
}
  • Guard 保证 Rust 字符串生命周期 ≥ C 调用区间
  • C 侧 无法 保留指针越过回调边界,否则 Miri 报错

6 工具链:双重释放的「雷达网」

工具用法捕获示例
Miricargo +nightly miri run未初始化读、double free
AddressSanitizerRUSTFLAGS="-Zsanitizer=address"运行期检测
-Zdrop-trackingnightlyMIR 级 drop 路径可视化

7 极端场景:手动管理内存时的「线性类型」技巧

7.1 标记类型(marker type)

struct Allocation(*mut u8, Layout);

impl Allocation {
    fn new(layout: Layout) -> Self {
        let ptr = unsafe { std::alloc::alloc(layout) };
        if ptr.is_null() { std::alloc::handle_alloc_error(layout); }
        Self(ptr, layout)
    }
}

impl Drop for Allocation {
    fn drop(&mut self) {
        unsafe { std::alloc::dealloc(self.0, self.1); }
    }
}

7.2 禁止复制 / 克隆

impl !Clone for Allocation {}
impl !Copy for Allocation {}
  • 编译期 保证 Allocation 只能 move,无法 clone → 杜绝双重释放
  • 与 C++ 的 std::unique_ptr 等价,但 零运行时开销

8 终极挑战:故意写出一个「可编译的双重释放」

结论:在 safe Rust 中,不可能
在 unsafe Rust 中,需要显式 复制裸指针

fn really_evil() {
    let b = Box::new(42u8);
    let raw1 = Box::into_raw(b);
    let raw2 = raw1; // 复制裸指针,合法
    unsafe {
        Box::from_raw(raw1);
        Box::from_raw(raw2); // UB
    }
}
  • 即使如此,工具链依旧能捕获
    Miri 输出:
error: Undefined Behavior: double free detected

9 工程 Checklist:如何在团队中彻底杜绝双重释放

  1. 禁止裸指针逃逸 safe API 边界
  2. 所有 FFI 返回指针必须配合借用守卫 RAII
  3. CI 强制跑 Miri + AddressSanitizer
  4. 对 unsafe 代码块 100 % code review + miri test

10 结语:双重释放从 bug 变成「编译期不可能事件」

Rust 的所有权系统把「何时释放」从 运行时策略 升级为 编译期定理
一旦你把值交给 safe Rust,编译器就替你证明:
• 它一定会被释放
• 且只会被释放一次

剩下的 unsafe 角落,则交给 Miri、ASan、review 三道闸门。
从此,双重释放不再是程序员的噩梦,而是编译器的日常作业
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐