diff --git a/20260815-tasks.md b/20260815-tasks.md index eee5b0b..79e09b0 100644 --- a/20260815-tasks.md +++ b/20260815-tasks.md @@ -229,6 +229,7 @@ Kết quả review toàn bộ diff G2 (`git diff 1fe2d26~1..b59e416`, 15 files; - **G2.6** (2026-08-16, Windows): verify tong. - **G2.0–G2.6 review + verify** (2026-08-16, Linux — máy Linux chỉ review/ghi chú, KHÔNG sửa code): đọc toàn bộ diff G2 (`1fe2d26~1..b59e416`, 15 files) — Vst3Instrument base64 + loadSerializedState (bounds-check đủ), NativeInstrumentEngine snapshot (mu_ chặn race với audio loop, skip reloading), main.cpp saveStateJob/restore/LOAD-guard/DUMP/debounce, lib.rs flush_bridge_state + env SF_STATE_FILE, test_g2_stress.py + test_state_store.cpp + test_g15_stress.py. Verify Linux: py_compile 2 stress scripts PASS; g++ compile + run test_state_store PASS (StateStore portable); node --check app.precompiled.js PASS; rustc/cargo không có default toolchain → không cargo check (Windows đã check PASS). Findings: F1 double flush_bridge_state khi window-close (minor), F2 SF_STATE_FILE thiếu Linux fallback (portability, không ảnh hưởng target Windows), F3 log "bad ctrlLen" trùng cho 2 check (cosmetic). Không sửa code (đúng phân máy). Chi tiết ở mục 5.1. G1.5 regression PASS 5/5 (LOAD/OPEN_GUI/PLAY/STOP/WM_CLOSE, writeIndex delta 307-551, bridge alive). G2.5 PASS 5+5+10 (20 lan kill/respawn, preset giu, khong EXCEPTION/FAILED). Probe G0.4 (ban cap nhat: PID dong, LOAD guard-aware, OPEN_GUI full path): PASS — LOAD skip -> OPEN_GUI hwnd -> WM_CLOSE -> "state captured ch=5 component=2628B" -> reload -> "state restored ch=5 component=2628B" -> OPEN_GUI lai attach OK, khong restart. Bug tim duoc: khong co bug san pham; 2 bug test-script (probe PID int vs list -> bao restart gia; preset EZkeys byte-diff do PlayHeadPos runtime). Commit `G2.6: verify — regression G1.5 + stress G2.5 + probe G0.4 all PASS`. - **Audit bảo mật/bộ nhớ/conflict child process** (2026-08-16, Linux — chỉ review/ghi chú, KHÔNG sửa code): đọc toàn bộ main.cpp/NativeInstrumentEngine/Vst3Instrument/StateStore/SandboxVst3Host/plugin_host_main/SharedMemoryIPC/shm.rs/lib.rs/nativeBridgeService.js/app.precompiled.js. Findings: F-MEM-5 UAF audio-thread get()+deref vs assign() destroy oldInst ngoài lock sau 10ms (MEDIUM, sites 794/884/909/927/953/1290); F-MEM-8 double-buffer không handshake (MEDIUM audio); F-PROC-1 command injection `--path "..."` CreateProcessA (MEDIUM); F-PROC-2 race restart×watchdog → 2 bridge cùng SHM (MEDIUM); F-PROC-3 orphan race Destroyed (LOW); F5 open_vst_gui luôn Ok (LOW); F6 comment cap 2 vs 5 (LOW). Luồng chính đúng thiết kế: drain control trước render, LOAD UiThread+g_loadMutex, OPEN_GUI defer PLAY + pending sweep, STOP flush CC64=0, DUMP_STATE trực tiếp, pump drop-guard trước kill, max 1 restart/incident, JS __bridgePlaying + __bridgeLoadedChannels reset. Verify Linux: StateStore -fanalyzer sạch + round-trip PASS; 3 file Windows-only không build được (đúng); node --check 2 JS PASS. Không sửa code (đúng phân máy). Chi tiết mục 5.2. +- **Fix audit 5.2** (2026-08-16, Windows): xử lý toàn bộ findings 5.2. F-PROC-1: SandboxVst3Host.cpp thêm quote_arg (escape \ và " theo Windows CRT) + CreateProcessA(hostExe.c_str(), ...) + bỏ token exe khỏi cmdline -> path thù địch chứa " giữ 1 argv (probe F-PROC-1 PASS: spawn ch=7 cmdline giữ trọn path trong --path, child không map HACKED_SHM, không nhận --sr 1000, load fail như mong đợi). F-MEM-5: NativeInstrumentEngine.h + .cpp thêm has() + template withInstrument() (giữ mu_ quanh call); main.cpp đổi 6 site audio-thread: 784/794 hasAttachedView dedupe, 1073 restore guard, 1283 CC64=0, 1318 pending-GUI, 1337 state-dirty -> has()/withInstrument(); giữ nguyên 8 site UiThread (an toàn cùng thread assign). F-PROC-2: lib.rs thêm static BRIDGE_RESTART_LOCK serialize kill+spawn giữa restart_bridge_with_sample_rate và pump watchdog. F-PROC-3: pump re-check SHUTTING_DOWN sau lock + sau kill trước spawn. F5: open_vst_gui trả Err (lock poisoned / SHM unavailable / queue full), frontend try/catch sẵn. F6: comment Cap 5 -> Cap 2 (hard min(attempts,2)). F-MEM-8: ghi nhận theo dõi, không fix (đúng audit). Build: daw_vst_bridge + plugin_host PASS; cargo build --release PASS (2m53s). Deploy install/ (kill trước, copy2). Regression: G1.5 PASS 3/3 (writeIndex delta 287), G2.5 PASS 2/2 (watchdog restart + preset giữ exact), probe F-PROC-1 PASS (không injection). Commit: fix 5.2 audit findings on Windows. --- diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 25b7032..e5f18c8 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -466,7 +466,7 @@ const loadVstToBridge=async track=>{if(!window.NativeBridgeService||!window.Soni let knownPath=track.synth_engine&&track.synth_engine.plugin_path;if(!knownPath&&window.__bridgePluginPaths)knownPath=window.__bridgePluginPaths[instrumentId];const r=await window.SonicAPI.bridgeLoad({name:instrumentId,path:knownPath||null,instrumentType:'VST3',channel:bch});let p=r&&r.path?r.path:null;if(!p&&knownPath)p=knownPath;if(!p&&window.SonicAPI.listPlugins){try{const pl=await window.SonicAPI.listPlugins();const v=(pl&&pl.vst_instruments||[]).find(x=>x.id===instrumentId||x.name===instrumentId);if(v&&v.path){p=v.path;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=v.path;}}catch(e2){console.warn('[Bridge] listPlugins fallback fail:',e2);}}if(!p)return false;const ok=await window.NativeBridgeService.loadInstrument(p,'VST3',bch);if(ok)window.__bridgeLoadedChannels[bch]=true;console.log('[Bridge] auto-load track',track.id,'-> VST3 ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] loadVstToBridge fail:',track.id,e);return false;}};const ensureAndOpenVstGui=async(trackId,instrumentId)=>{if(!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive()||!window.NativeBridgeService)return;if(!instrumentId||typeof instrumentId==='string'&&instrumentId.startsWith('sf_'))return;if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};const bch=window.SonicMidiRouter.allocateChannel(trackId,false);if(!window.__bridgeLoadedChannels[bch]){const ok=await loadVstToBridge({id:trackId,instrumentId});if(!ok)return;}try{await openVstGuiRetry(instrumentId,bch,20);}catch(e){console.warn('[Bridge] openNativeGUI fail:',e);}};// V8 bug 4 + V9 bug 3/6/7: C++ load instrument ASYNC (worker thread) — OPEN_GUI // som → C++ defer va tu fulfil khi assign() xong (pending-GUI sweep) → khong // can spam. 20 lan x 500ms tao deferred storm (50+ event/moi restart) va -// re-attach view dang attached (crash USER32 0xc000041d). Cap 5. +// re-attach view dang attached (crash USER32 0xc000041d). Cap 2 (hard min(attempts,2)). const openVstGuiRetry=async(instrumentId,bch,attempts)=>{if(window.__bridgePlaying)return true;// G0.2: khong retry khi PLAY (G0.1 da defer) const n=Math.min(attempts&&attempts>0?attempts:2,2);for(let i=0;isetTimeout(r,800));}return true;};// Module-level so both ProfileModal (open project) and App (auto-restore) can // warm the FluidSynth font cache for each track's instrument. This only loads diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index 2878318..98c047e 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/install/sonicforge-daw.exe b/install/sonicforge-daw.exe index 4791407..50b1ce0 100644 Binary files a/install/sonicforge-daw.exe and b/install/sonicforge-daw.exe differ diff --git a/native_bridge/include/NativeInstrumentEngine.h b/native_bridge/include/NativeInstrumentEngine.h index 2f8747f..ed653ea 100644 --- a/native_bridge/include/NativeInstrumentEngine.h +++ b/native_bridge/include/NativeInstrumentEngine.h @@ -79,6 +79,18 @@ public: INativeInstrument* get(uint32_t channel); + // F-MEM-5 (audit 5.2): get() releases mu_ before the caller derefs, so a + // worker thread's assign()/unload() can destroy the instance mid-use. + // has() is a safe existence check; withInstrument() runs code against the + // instance while mu_ is still held. Use these from non-Ui threads. + bool has(uint32_t channel); + template + auto withInstrument(uint32_t channel, F&& fn) -> decltype(fn((INativeInstrument*)nullptr)) { + std::lock_guard lock(mu_); + auto it = channels_.find(channel); + return fn(it == channels_.end() ? nullptr : it->second.get()); + } + // Real-time MIDI dispatch (audio loop thread). Each call holds mu_ for the // WHOLE call — lookup + reloading check + instrument call under one lock — // so a worker thread's assign()/unload() can never swap the map and destroy diff --git a/native_bridge/src/NativeInstrumentEngine.cpp b/native_bridge/src/NativeInstrumentEngine.cpp index 920cfd4..e598662 100644 --- a/native_bridge/src/NativeInstrumentEngine.cpp +++ b/native_bridge/src/NativeInstrumentEngine.cpp @@ -303,6 +303,11 @@ INativeInstrument* InstrumentEngineManager::get(uint32_t channel) { return it == channels_.end() ? nullptr : it->second.get(); } +bool InstrumentEngineManager::has(uint32_t channel) { + std::lock_guard lock(mu_); + return channels_.count(channel) != 0; +} + void InstrumentEngineManager::setReloading(uint32_t channel, bool on) { std::lock_guard lock(mu_); if (channel < 16) reloadingCh_[channel] = on; diff --git a/native_bridge/src/SandboxVst3Host.cpp b/native_bridge/src/SandboxVst3Host.cpp index 4d0da93..3951b9c 100644 --- a/native_bridge/src/SandboxVst3Host.cpp +++ b/native_bridge/src/SandboxVst3Host.cpp @@ -7,6 +7,24 @@ #include #include +// F-PROC-1 (audit 5.2): Windows CRT command-line parsing escape for an +// argument inside double quotes. A path containing a double quote would +// otherwise break out of --path "..." and inject arbitrary argv (e.g. +// `" && calc`). Metacharacters (& | > <) are inert here: CreateProcessA +// does not run a shell. +static std::string quote_arg(const std::string& s) { + std::string out = "\""; + size_t bs = 0; + for (char c : s) { + if (c == '\\') { ++bs; continue; } + if (c == '"') { out.append(bs * 2 + 1, '\\'); out += '"'; bs = 0; } + else { out.append(bs, '\\'); out += c; bs = 0; } + } + out.append(bs * 2, '\\'); + out += '"'; + return out; +} + static bool proc_alive(HANDLE h) { #ifdef _WIN32 if (!h) return false; @@ -72,8 +90,12 @@ bool SandboxVst3Host::spawnChild() { std::string hostExe = (slash == std::string::npos) ? "plugin_host.exe" : dir.substr(0, slash + 1) + "plugin_host.exe"; - std::string cmd = "\"" + hostExe + "\" --open --shm " + shmName_ + - " --path \"" + path_ + "\" --sr " + std::to_string((int)sampleRate_) + + // F-PROC-1: lpApplicationName = hostExe (exe path is never parsed as + // command line), and the VST path goes through quote_arg so a hostile + // path cannot inject extra arguments. + std::string cmd = "--open --shm " + shmName_ + + " --path " + quote_arg(path_) + + " --sr " + std::to_string((int)sampleRate_) + " --block " + std::to_string(block_) + " --parent " + std::to_string((unsigned long)GetCurrentProcessId()); std::cerr << "[SandboxVst3Host] spawn ch=" << channel_ << " " << cmd << std::endl; @@ -83,7 +105,7 @@ bool SandboxVst3Host::spawnChild() { PROCESS_INFORMATION pi = {}; std::vector buf(cmd.begin(), cmd.end()); buf.push_back('\0'); - if (!CreateProcessA(nullptr, buf.data(), nullptr, nullptr, FALSE, + if (!CreateProcessA(hostExe.c_str(), buf.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) { std::cerr << "[SandboxVst3Host] CreateProcessA failed err=" << (int)GetLastError() << " ch=" << channel_ << std::endl; diff --git a/native_bridge/src/main.cpp b/native_bridge/src/main.cpp index c4f68fd..4861be0 100644 --- a/native_bridge/src/main.cpp +++ b/native_bridge/src/main.cpp @@ -73,11 +73,11 @@ static void disable_ime_contexts(HWND w) { } } - // G1.3: bo pump gate cu (g_juceOwnerTids / g_ownerTid / - // is_teardown_window): teardown (close/LOAD job) va message pump cung chay tren - // 1 thread (UiThread) — job chay thi pump dung, job xong thi editor window da - // destroy nen USER32 tu huy message toi window chet. Khong con "2 thread trong - // 1 DLL" khi teardown (multi-worker cu). + // G1.3: bo pump gate cu (g_juceOwnerTids / g_ownerTid / + // is_teardown_window): teardown (close/LOAD job) va message pump cung chay tren + // 1 thread (UiThread) — job chay thi pump dung, job xong thi editor window da + // destroy nen USER32 tu huy message toi window chet. Khong con "2 thread trong + // 1 DLL" khi teardown (multi-worker cu). // CBT hook: strip the IMC the moment any JUCE_* window is born (JUCE message // window AND editor child) — the post-attach disable_ime_contexts runs too @@ -92,8 +92,8 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) HWND w = (HWND)wParam; char cls[64] = {0}; if (GetClassNameA(w, cls, 63) > 0 && std::strncmp(cls, "JUCE_", 5) == 0) { - ImmAssociateContext(w, nullptr); - + ImmAssociateContext(w, nullptr); + } } return CallNextHookEx(g_cbtHook, nCode, wParam, lParam); @@ -108,13 +108,13 @@ static LRESULT CALLBACK ImeCbtHookProc(int nCode, WPARAM wParam, LPARAM lParam) class ChannelWorker; // fwd — WM_DESTROY posts closeGUI() to the channel worker static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker static void post_resize_view(uint32_t ch, int w, int h); // defined after ChannelWorker - + static InstrumentEngineManager* g_engine = nullptr; static std::mutex g_guiMutex; static std::map g_guiWindows; // channel -> HWND (keep window alive) static std::map g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup) static ChannelWorker* g_uiWorker = nullptr; - + // Same-plugin-DLL reentrancy guards: two threads inside one VST3 DLL (Nexus) // crash or deadlock. g_attachPaths = lowercase plugin paths whose reload/ // createView is running on some worker — a same-path close job must not unmute @@ -125,7 +125,7 @@ static ChannelWorker* g_uiWorker = nullptr; static std::mutex g_attachMutex; static std::vector g_attachPaths; static bool g_closeInFlight[16] = { false }; - + static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_CLOSE) { uint32_t ch = UINT32_MAX; @@ -263,7 +263,7 @@ public: #endif th_ = std::thread([this] { #ifdef _WIN32 - OleInitialize(nullptr); + OleInitialize(nullptr); OleInitialize(nullptr); // Default IMC = none on this thread: new editor windows get // no IME context (see disable_ime_contexts). @@ -295,11 +295,11 @@ public: for (int pumped = 0; pumped < 128; ++pumped) { if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; hadMessages = true; - // G1.3: bo pump gate cu — teardown (close/LOAD job) - // va pump cung 1 thread (UiThread): job chay thi pump dung; - // het job thi editor window da destroy -> message chet bi - // USER32 huy tu dong. Khong can drop theo close_in_flight. - + // G1.3: bo pump gate cu — teardown (close/LOAD job) + // va pump cung 1 thread (UiThread): job chay thi pump dung; + // het job thi editor window da destroy -> message chet bi + // USER32 huy tu dong. Khong can drop theo close_in_flight. + TranslateMessage(&msg); // CRASH FIX (0xc000041d STATUS_FATAL_USER_CALLBACK_EXCEPTION): // a plugin window proc (Nexus throws nlohmann::json::out_of_range @@ -328,7 +328,7 @@ public: } } #ifdef _WIN32 - OleUninitialize(); + OleUninitialize(); OleUninitialize(); #endif }); @@ -364,7 +364,7 @@ public: #endif cv_.notify_all(); } - + private: std::thread th_; @@ -421,24 +421,24 @@ static bool close_in_flight(uint32_t ch) { return g_closeInFlight[ch]; } - - // G1.3: 1 UiThread — editor windows tao trong attach job (UiThread), destroy - // chi goi tu UiThread job (close_editor_now / LOAD) -> ownerTid luon == current - // tid -> destroy truc tiep. Neu owner khac thread (khong con xay ra) -> bo - // (leak > crash): cross-thread DestroyWindow khong an toan. - static void destroy_window_on_owner(HWND hwnd) { - if (!hwnd || !IsWindow(hwnd)) return; - DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr); - DWORD curTid = GetCurrentThreadId(); - if (ownerTid == 0 || ownerTid == curTid) { - if (IsWindow(hwnd)) DestroyWindow(hwnd); - return; - } - std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid - << " != current " << curTid << " - leaving window " << hwnd << std::endl; - } - + + // G1.3: 1 UiThread — editor windows tao trong attach job (UiThread), destroy + // chi goi tu UiThread job (close_editor_now / LOAD) -> ownerTid luon == current + // tid -> destroy truc tiep. Neu owner khac thread (khong con xay ra) -> bo + // (leak > crash): cross-thread DestroyWindow khong an toan. + static void destroy_window_on_owner(HWND hwnd) { + if (!hwnd || !IsWindow(hwnd)) return; + DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr); + DWORD curTid = GetCurrentThreadId(); + if (ownerTid == 0 || ownerTid == curTid) { + if (IsWindow(hwnd)) DestroyWindow(hwnd); + return; + } + std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid + << " != current " << curTid << " - leaving window " << hwnd << std::endl; + } + // Unmute y unless its own close job is still inside createInstance — that job // performs the unmute once its fresh instance is loaded (or, if an attach for @@ -594,9 +594,9 @@ int main(int argc, char* argv[]) { // without an exe manifest. Ignore failure (already aware). SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); // Default IMC = none on the main thread (IME recursion fix). - // Default IMC = none on the main thread (IME recursion fix). - ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT); - + // Default IMC = none on the main thread (IME recursion fix). + ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT); + #endif // 1. Shared memory name: argv --shm | env SF_SHM_NAME | default @@ -781,7 +781,7 @@ int main(int argc, char* argv[]) { ~AttachInFlightGuard() { if (ipc) ipc->attachInFlight = 0; } }; auto handleOpenGui = [&](uint32_t guiCh, uintptr_t arg1, const std::string& pluginId) { - if (!instruments.get(guiCh)) { + if (!instruments.has(guiCh)) { std::cerr << "[NativeBridge] GUI deferred ch=" << guiCh << " plugin=" << pluginId << " (no instrument yet) - queued" << std::endl; std::lock_guard lock(g_pendingGuiMutex); @@ -791,10 +791,10 @@ int main(int argc, char* argv[]) { // Dedupe OPEN_GUI spam (frontend openVstGuiRetry): view dang // attached -> GUI da mo, khong post attach job lan nua. Re-attach // tren view dang attached lam plugin loi (Nexus createView null). - if (auto* i0 = instruments.get(guiCh)) { - if (i0->hasAttachedView()) { - return; - } + if (instruments.withInstrument(guiCh, [](INativeInstrument* i0) { + return i0 && i0->hasAttachedView(); + })) { + return; } // G0.1 (BUG_REPORT): mo GUI khi dang PLAY -> attachView chay tren worker // thread trong luc audio loop process() cung DLL (2 thread 1 plugin) -> @@ -1022,9 +1022,9 @@ int main(int argc, char* argv[]) { #ifdef _WIN32 // Attach that — khong co view (VD: channel la SF2/SFZ hoac plugin // loi). Dong ngay cua so vo nghia de khong con window treo trong - // registry; WM_CLOSE -> UiThread pump destroy (owner = UiThread). - // PostMessage an toan cross-thread (khong nhu DestroyWindow). - + // registry; WM_CLOSE -> UiThread pump destroy (owner = UiThread). + // PostMessage an toan cross-thread (khong nhu DestroyWindow). + PostMessageA((HWND)hwnd, WM_CLOSE, 0, 0); #endif } @@ -1070,7 +1070,7 @@ int main(int argc, char* argv[]) { << " instruments from " << stateFile << std::endl; for (const auto& e : snap.instruments) { uint32_t ch = e.channel & 0xF; - if (e.path.empty() || instruments.get(ch)) continue; // guard: empty channel only + if (e.path.empty() || instruments.has(ch)) continue; // guard: empty channel only InstrumentType t = (InstrumentType)e.type; std::string path = e.path; std::string preset = e.presetBase64; @@ -1191,11 +1191,11 @@ int main(int argc, char* argv[]) { // thay the inst (VST3 -> SF2/inst khac) ma editor con song -> // old inst destructor goi view->removed() tren HWND con hoat // dong -> plugin block -> treo bridge. - // G1.2/G1.3: window tao tren UiThread (handleOpenGui post attach - // job; create_native_vst_window chay trong job). LOAD job cung - // chay tren UiThread -> khong cross-thread destroy. An window, - // giu registry de reuse; editor detach + reload o duoi. - + // G1.2/G1.3: window tao tren UiThread (handleOpenGui post attach + // job; create_native_vst_window chay trong job). LOAD job cung + // chay tren UiThread -> khong cross-thread destroy. An window, + // giu registry de reuse; editor detach + reload o duoi. + HWND hToHide = nullptr; { std::lock_guard lock(g_guiMutex); @@ -1280,7 +1280,9 @@ int main(int argc, char* argv[]) { // khi pedal con down -> note-off cua allNotesOff bi bo qua // -> am treo loop (V8 bug 3). for (uint32_t ch = 0; ch < 16; ++ch) { - if (auto* inst = instruments.get(ch)) inst->controlChange(ch, 64, 0); + instruments.withInstrument(ch, [&](INativeInstrument* inst) { + if (inst) inst->controlChange(ch, 64, 0); + }); } instruments.allNotesOff(); g_transportPlaying = false; @@ -1315,7 +1317,7 @@ int main(int argc, char* argv[]) { std::lock_guard lock(g_pendingGuiMutex); for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) { // G0.1: giu entry khi dang PLAY - handleOpenGui se defer lai - if (transportStopped && instruments.get(it->first)) { + if (transportStopped && instruments.has(it->first)) { readyGui.push_back({ it->first, it->second }); it = g_pendingGui.erase(it); } else ++it; @@ -1334,7 +1336,7 @@ int main(int argc, char* argv[]) { if (std::chrono::duration_cast(nowMark - lastMark).count() >= 2000) { lastMark = nowMark; for (uint32_t ch = 0; ch < 16; ++ch) { - if (instruments.get(ch)) { g_stateDirty = true; break; } + if (instruments.has(ch)) { g_stateDirty = true; break; } } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5dd4cc2..2400975 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -24,6 +24,10 @@ mod shm; /// không respawn bridge mới sau khi sidecar đã bị giết (fix: app còn sống /// sau khi đóng window → pump thấy stall 3s → spawn bridge mới → orphan). static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); +// F-PROC-2 (audit 5.2): serialize kill_bridge -> spawn_bridge across the pump +// watchdog thread and the sample-rate restart command. Without it both threads +// can kill+spawn concurrently and leave two daw_vst_bridge.exe on the same SHM. +static BRIDGE_RESTART_LOCK: Mutex<()> = Mutex::new(()); use shm::{Shm, ShmState}; struct EngineProcess(Mutex>); @@ -513,7 +517,22 @@ pub fn run() { ); // FIX: kill bridge cũ trước — không được để 2 bridge // cùng map SHM (race control queue / double load). + // F-PROC-2: serialize with the sample-rate restart + // command (same BRIDGE_RESTART_LOCK). + let _restart_guard = match BRIDGE_RESTART_LOCK.lock() { + Ok(g) => g, + Err(p) => p.into_inner(), + }; + // F-PROC-3: re-check after acquiring the lock AND + // after kill — Destroyed may have been set while we + // waited/killed; never respawn during app shutdown. + if SHUTTING_DOWN.load(Ordering::Relaxed) { + break; + } kill_bridge(&pump_handle); + if SHUTTING_DOWN.load(Ordering::Relaxed) { + break; + } if spawn_bridge(&pump_handle, &res_dir, &exe_dir, &mut line, &log_path) { // count restarts into bridge.log (same file as stdout redirect) let bridge_log = std::path::Path::new(&log_dir) @@ -672,17 +691,15 @@ fn open_vst_gui(app: AppHandle, plugin_id: String, channel: u8) -> Result<(), St // B9: bridge tự tạo native Win32 window cho editor VST3 (không qua // WebView2 — HTML window cũ vẽ ĐÈ lên GUI plugin). push_control(4,0,0,0) // với hwnd=0 báo bridge tạo window riêng trên ChannelWorker thread. + // F5 (audit 5.2): trả Err khi lệnh không tới được bridge (SHM unavailable / + // lock poisoned / queue full) — frontend openNativeGUI đã try/catch sẵn. let state = app.state::(); - let lock = state.0.lock(); - match lock { - Ok(guard) => match guard.as_ref() { - Some(shm) => { - let ok = shm.push_control(4, 0, 0, channel as u32, &plugin_id); - eprintln!("open_vst_gui: push_control type=4 hwnd=0 ch={} ok={}", channel, ok); - } - None => eprintln!("open_vst_gui: bridge shm unavailable"), - }, - Err(e) => eprintln!("open_vst_gui: shm lock poisoned: {}", e), + let guard = state.0.lock().map_err(|e| e.to_string())?; + let shm = guard.as_ref().ok_or("bridge shm unavailable")?; + let ok = shm.push_control(4, 0, 0, channel as u32, &plugin_id); + if !ok { + eprintln!("open_vst_gui: push_control type=4 hwnd=0 ch={} failed", channel); + return Err("control queue full".into()); } Ok(()) } @@ -770,6 +787,12 @@ fn restart_bridge_with_sample_rate(app: AppHandle, sample_rate: u32) -> Result<( *g = sample_rate; } } + // F-PROC-2: serialize with the pump watchdog restart so two threads can + // never both kill+spawn (two bridges on the same SHM). + let _restart_guard = match BRIDGE_RESTART_LOCK.lock() { + Ok(g) => g, + Err(p) => p.into_inner(), + }; kill_bridge(&app); let res_dir = app.path().resource_dir().unwrap_or_default(); let exe_dir = std::env::current_exe()