解引用*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的内存操作逻辑上:
- 先在栈上创建局部变量
vec,此时它的内存地址是栈上的临时位置。 - 把这个临时地址转成指针传给
runner.set_callback,存在runner.data中。 - 随后将
vec移动到WeirdThing结构体的字段中——由于Mutex实现了Movetrait,移动操作会把vec的内容从栈上临时位置转移到结构体的内存位置,原来的栈上临时变量会被销毁。 - 此时
runner.data中存储的还是原来栈上的旧地址,这个地址已经变成悬垂指针(指向已释放或无效的内存)。 - 当调用
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
相关产品推荐
相关产品推荐

