// native_bridge/src/SandboxVst3Host.cpp #include "SandboxVst3Host.h" #include #include #include #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; DWORD code = 0; if (!GetExitCodeProcess(h, &code)) return false; return code == STILL_ACTIVE; #else return true; #endif } SandboxVst3Host::SandboxVst3Host() {} SandboxVst3Host::~SandboxVst3Host() { stop_.store(true); if (watchdog_.joinable()) watchdog_.join(); #ifdef _WIN32 if (childProc_) { TerminateProcess(childProc_, 0); CloseHandle(childProc_); childProc_ = nullptr; } #endif if (shm_) { shm_close(shm_); shm_ = nullptr; ipc_ = nullptr; } } bool SandboxVst3Host::loadPlugin(const std::string& path, double sampleRate, uint32_t channel) { path_ = path; sampleRate_ = sampleRate; channel_ = channel; char name[128]; snprintf(name, sizeof(name), "SonicForge_PluginHost_%lu_%u", (unsigned long)GetCurrentProcessId(), channel); shmName_ = name; if (!spawnChild()) return false; watchdog_ = std::thread([this]() { watchdogLoop(); }); return true; } bool SandboxVst3Host::spawnChild() { // Mapping created by the bridge (parent); the child opens it with --open. if (shm_) { shm_close(shm_); shm_ = nullptr; ipc_ = nullptr; } shm_ = shm_create(shmName_.c_str(), sizeof(SandboxHostIPC)); if (!shm_) { std::cerr << "[SandboxVst3Host] shm_create failed ch=" << channel_ << std::endl; return false; } ipc_ = shm_sandbox_ptr(shm_); // plugin_host.exe sits next to daw_vst_bridge.exe (same dir, install/). char exePath[MAX_PATH] = {}; GetModuleFileNameA(nullptr, exePath, MAX_PATH); std::string dir(exePath); size_t slash = dir.find_last_of("\\/"); std::string hostExe = (slash == std::string::npos) ? "plugin_host.exe" : dir.substr(0, slash + 1) + "plugin_host.exe"; // 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; STARTUPINFOA si = {}; si.cb = sizeof(si); PROCESS_INFORMATION pi = {}; std::vector buf(cmd.begin(), cmd.end()); buf.push_back('\0'); 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; return false; } CloseHandle(pi.hThread); #ifdef _WIN32 if (childProc_) CloseHandle(childProc_); childProc_ = pi.hProcess; #endif // Wait for the child heartbeat (Nexus load can take 30s+). uint32_t hb = ipc_->base.heartbeat; auto t0 = std::chrono::steady_clock::now(); while (std::chrono::duration_cast( std::chrono::steady_clock::now() - t0).count() < 60) { if (!proc_alive(childProc_)) { std::cerr << "[SandboxVst3Host] child exited during load ch=" << channel_ << std::endl; return false; } if (ipc_->base.heartbeat != hb) { alive_.store(true); return true; } Sleep(200); } std::cerr << "[SandboxVst3Host] child heartbeat timeout ch=" << channel_ << std::endl; return false; } void SandboxVst3Host::watchdogLoop() { int fails = 0; while (!stop_.load()) { Sleep(500); if (stop_.load()) break; if (!alive_.load()) continue; if (proc_alive(childProc_)) { fails = 0; continue; } alive_.store(false); DWORD ec = 0; GetExitCodeProcess(childProc_, &ec); std::cerr << "[SandboxVst3Host] child died ch=" << channel_ << " rc=" << ec << " — respawning" << std::endl; ++fails; if (fails >= 3) { std::cerr << "[SandboxVst3Host] ch=" << channel_ << " respawn limit hit — muted until reload" << std::endl; continue; // stays dead; processAudioBlock returns silence } Sleep(1000); if (stop_.load()) break; if (spawnChild()) { fails = 0; std::cerr << "[SandboxVst3Host] ch=" << channel_ << " respawned" << std::endl; } } } bool SandboxVst3Host::init(double sampleRate, uint32_t maxBlockSize) { sampleRate_ = sampleRate; block_ = maxBlockSize; return true; } void SandboxVst3Host::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) { if (!alive_.load() || !ipc_) return; shm_write_midi(shm_, 0x9, (uint8_t)channel, (uint8_t)pitch, (uint8_t)(velocity * 127.0f), sampleOffset); } void SandboxVst3Host::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) { if (!alive_.load() || !ipc_) return; shm_write_midi(shm_, 0x8, (uint8_t)channel, (uint8_t)pitch, 0, sampleOffset); } void SandboxVst3Host::controlChange(uint32_t channel, uint32_t cc, uint32_t value) { if (!alive_.load() || !ipc_) return; shm_write_midi(shm_, 0xB, (uint8_t)channel, (uint8_t)cc, 0, 0, (uint8_t)value, 0); } void SandboxVst3Host::programChange(uint32_t channel, uint32_t program) { if (!alive_.load() || !ipc_) return; shm_write_midi(shm_, 0xC, (uint8_t)channel, 0, 0, 0, (uint8_t)program, 0); } void SandboxVst3Host::pitchBend(uint32_t channel, uint32_t bend14) { if (!alive_.load() || !ipc_) return; shm_write_midi(shm_, 0xE, (uint8_t)channel, 0, 0, 0, (uint8_t)(bend14 & 0x7F), (uint8_t)((bend14 >> 7) & 0x7F)); } void SandboxVst3Host::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) { if (!alive_.load() || !ipc_) { std::memset(outputL, 0, numSamples * sizeof(float)); std::memset(outputR, 0, numSamples * sizeof(float)); return; } // G4.3: the child publishes the slot it finished rendering (writeSlot). // Copy that block. A stale slot means the child is still working on the // other one — we repeat the previous block instead of tearing. uint32_t slot = ipc_->writeSlot & 1u; for (uint32_t i = 0; i < numSamples; ++i) { outputL[i] = ipc_->audioLeft[slot][i]; outputR[i] = ipc_->audioRight[slot][i]; } }