From 0ba3817ceefe00393928f95ad8ba945a19e82464 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Tue, 9 Jun 2026 20:13:59 +0800 Subject: [PATCH] fix: guard qwen_engine Mutex poison + null ctx FFI calls Two crash-risk fences on the dictation hot path: 1. set_token_handler: Replace std::sync::Mutex::lock().expect() with unwrap_or_else(|poisoned| poisoned.into_inner()) to survive a poisoned mutex instead of panicking the process. This is the ONLY std Mutex in the codebase (all others use parking_lot). If token_trampoline's C FFI callback panics, the Mutex poisons and the next dictation crashes. 2. transcribe_audio / transcribe_stream: Add is_null() guard before C FFI calls to prevent UB if engine is somehow used after Drop (defense- in-depth under normal Arc ownership). --- .../app/src-tauri/src/asr/local/qwen_engine.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/asr/local/qwen_engine.rs b/openless-all/app/src-tauri/src/asr/local/qwen_engine.rs index eb6f565a..b126d600 100644 --- a/openless-all/app/src-tauri/src/asr/local/qwen_engine.rs +++ b/openless-all/app/src-tauri/src/asr/local/qwen_engine.rs @@ -59,7 +59,13 @@ impl QwenAsrEngine { where F: FnMut(&str) + Send + 'static, { - let mut slot = self.token_handler.lock().expect("token_handler poisoned"); + // 用 std::sync::Mutex(非 parking_lot)是因为这个锁可能在 C FFI 回调 + // token_trampoline 中被 handler 间接访问;若 handler panic,std Mutex + // 会 poison。此处用 into_inner() 恢复而非 panic,避免进程崩溃。 + let mut slot = self + .token_handler + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); // 先把 C 端那一侧切干净,再 drop 旧 Box,避免 C 在替换瞬间还持有旧指针。 unsafe { qwen_set_token_callback(self.ctx, None, ptr::null_mut()) }; @@ -78,6 +84,9 @@ impl QwenAsrEngine { /// 批式转写:一次性给完整音频(mono f32 16kHz)。 pub fn transcribe_audio(&self, samples: &[f32]) -> Result { + if self.ctx.is_null() { + anyhow::bail!("engine already freed — cannot transcribe"); + } // SAFETY: samples 在调用期间存活;返回是 C `malloc` 出的字符串。 let raw = unsafe { qwen_transcribe_audio(self.ctx, samples.as_ptr(), samples.len() as i32) }; @@ -94,6 +103,9 @@ impl QwenAsrEngine { /// 流式转写:内部按 2s chunk 切片,token 通过 `set_token_handler` 注册的 /// 回调实时吐出;返回值是最终完整文本。 pub fn transcribe_stream(&self, samples: &[f32]) -> Result { + if self.ctx.is_null() { + anyhow::bail!("engine already freed — cannot transcribe"); + } let raw = unsafe { qwen_transcribe_stream(self.ctx, samples.as_ptr(), samples.len() as i32) }; if raw.is_null() {