From deb55d770e9aa6ccc78460256974ea8aa3de46fb Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Thu, 24 Sep 2026 16:11:13 +0800 Subject: [PATCH] fix(oak-plugin): serialize OFX binary loading across threads OFX binaries run their own global init on the first OfxGetNumberOfPlugins query, and that init is not thread-safe: two threads racing to load the CImg fixture corrupted its static map (SIGSEGV/SIGABRT, and a hang when the loader deadlocked). Tests create their own PluginCache instances, so per-cache locks cannot serialize it; add a process-wide LOAD_LOCK around dlopen + collect_plugins. Loading is a startup-time operation, so the lock is free in production and makes concurrent scans safe. --- crates/oak-plugin/src/host.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/oak-plugin/src/host.rs b/crates/oak-plugin/src/host.rs index 2ce110205..6635d5f6d 100644 --- a/crates/oak-plugin/src/host.rs +++ b/crates/oak-plugin/src/host.rs @@ -718,6 +718,13 @@ pub struct PluginCache { binaries: Mutex>, } +/// 进程级串行化 dlopen + `OfxGetNumberOfPlugins`:OFX 二进制在首次 +/// 查询时会跑自身的全局初始化,而这个初始化不是线程安全的(CImg 的 +/// 静态 map 被两个线程同时进入时会损坏内存,见测试并发加载时的 +/// SIGSEGV/SIGABRT)。加载是启动期操作,锁的开销可忽略,却让任何 +/// 调用方(多个 `PluginCache` 实例、后台扫描线程)并发扫描也安全。 +static LOAD_LOCK: Mutex<()> = Mutex::new(()); + /// bundle 目录判定:目录名以 `.bundle` 或 `.plugin` 结尾。 fn is_bundle_dir(path: &Path) -> bool { let Some(name) = path.file_name().and_then(|n| n.to_str()) else { @@ -873,6 +880,8 @@ impl PluginCache { /// 加载一个 bundle(幂等:按 bundle 路径去重)。每个早退分支都打 /// 诊断日志——静默失败会让效果库毫无线索地缺插件。 fn load_bundle(&self, bundle: &Path) { + // 串行化插件二进制的首次查询(见 [`LOAD_LOCK`])。 + let _load = LOAD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let Some(binary) = find_binary_in_bundle(bundle) else { eprintln!("[ofx] {}: no plugin binary in bundle", bundle.display()); return;