You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解引用*const Mutex触发panic问题排查

FFI回调悬垂指针导致panic的原因分析

问题场景

我正在开发一个FFI库的安全绑定,以下是触发panic的最小复现代码:

fn test_weird_thing() {
    use std::sync::Mutex;
                
    struct WeirdThing<'a> {
        vec: Mutex<Vec<i32>>,
        callback_runner: &'a CallbackRunner,
    }

    // Hypothetical FFI Struct///
    struct CallbackRunner {
        callback: extern "C" fn(*const Mutex<Vec<i32>>),
        data: *const Mutex<Vec<i32>>,
    }

    impl CallbackRunner {
        extern "C" fn default_callback(_vec: *const Mutex<Vec<i32>>) {}

        fn new() -> Self {
            CallbackRunner {
                callback: Self::default_callback,
                data: std::ptr::null(),
            }
        }

        fn set_callback(
            &mut self, callback: extern "C" fn(*const Mutex<Vec<i32>>), 
            data: *const Mutex<Vec<i32>>
        ) { 
            self.data = data;
            self.callback = callback;
        }

        fn run(&self) {
            (self.callback)(self.data);
        }
    }
    /////////////////////////////

    impl<'a> WeirdThing<'a> {
        fn new(runner: &'a mut CallbackRunner) -> Self {
            let vec = Mutex::new(Vec::new());
            runner.set_callback(Self::callback, &vec as *const Mutex<Vec<i32>>);
            WeirdThing { vec, callback_runner: runner }
        }

        extern "C" fn callback(vec: *const Mutex<Vec<i32>>) {
            let vec = unsafe { &*vec };
            let mut vec = vec.lock().unwrap();
            vec.push(1);
        }

        fn do_thing(&self) -> Vec<i32> {
            self.callback_runner.run();
            self.vec.lock().unwrap().clone()
        }
    }

    let mut runner = CallbackRunner::new();
    let thing = WeirdThing::new(&mut runner);
    debug!("Thing: {:?}", thing.do_thing());
}

原本认为thing存活期间,thing.vec始终有效,但调用runner.run()时触发了panic。

核心原因:悬垂指针

问题出在WeirdThing::new的内存操作逻辑上:

  1. 先在栈上创建局部变量vec,此时它的内存地址是栈上的临时位置。
  2. 把这个临时地址转成指针传给runner.set_callback,存在runner.data中。
  3. 随后将vec移动到WeirdThing结构体的字段中——由于Mutex实现了Move trait,移动操作会把vec的内容从栈上临时位置转移到结构体的内存位置,原来的栈上临时变量会被销毁。
  4. 此时runner.data中存储的还是原来栈上的旧地址,这个地址已经变成悬垂指针(指向已释放或无效的内存)。
  5. 当调用do_thing执行self.callback_runner.run()时,回调函数通过悬垂指针解引用内存,属于未定义行为,最终导致lock()操作panic。

修复方案

要确保传给FFI回调的指针始终指向有效内存,有两种可靠的方式:

方案1:直接在结构体中初始化vec后再取指针

先创建WeirdThing实例,确保vec处于最终内存位置,再将其地址传给回调:

impl<'a> WeirdThing<'a> {
    fn new(runner: &'a mut CallbackRunner) -> Self {
        let mut thing = WeirdThing {
            vec: Mutex::new(Vec::new()),
            callback_runner: runner,
        };
        // 此时thing.vec的地址是最终有效地址
        runner.set_callback(Self::callback, &thing.vec as *const Mutex<Vec<i32>>);
        thing
    }

    // 其他代码保持不变
}

注意:如果后续WeirdThing实例被移动(比如从栈上移到堆上),thing.vec的地址会变化,仍可能出现悬垂指针。如果需要保证地址绝对稳定,推荐方案2。

方案2:用Box将Mutex分配在堆上

堆上的内存地址不会因为结构体的移动而改变,因为Box本身只是指向堆内存的指针,移动Box不会影响底层堆内存的位置:

impl<'a> WeirdThing<'a> {
    fn new(runner: &'a mut CallbackRunner) -> Self {
        let vec_box = Box::new(Mutex::new(Vec::new()));
        // 取堆上Mutex的地址
        let vec_ptr = &*vec_box as *const Mutex<Vec<i32>>;
        runner.set_callback(Self::callback, vec_ptr);
        // 将Box中的内容解包到结构体字段
        WeirdThing { vec: *vec_box, callback_runner: runner }
    }

    // 其他代码保持不变
}

这种方式下,无论WeirdThing如何移动,vec的底层堆内存地址始终不变,传给FFI的指针会一直有效,直到WeirdThing被销毁。


内容的提问来源于stack exchange,提问作者assort

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 11:52:46