Harden plugin runtime: TLS verify, LSP deadlock, path traversal, ABI exception safety (W14)
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled

W14 addresses the five most critical findings from the W13 plugin audits:

- W14.1 network: enable ssl::verify_peer + SSL_set1_host SNI hostname
  verification (fixes TLS bypass, W13.3 CVSS 7.4); add steady_timer DNS
  timeout and bottom-up catch(...) hardening (engineer-zhou)
- W14.2 lsp: fix reader_loop/stop mutex deadlock via stop_nolock/stop_locked
  split (W13.4); wrap 11 vtable/entry functions in try/catch with cv
  notification on reader exit (engineer-sun)
- W14.3 tools: add is_safe_path() rejecting empty/absolute/.. paths before
  file_io calls (fixes path traversal, W13.5 CVSS 7.5); guard g_tools and
  g_session/g_history under mutex; 9 vtable try/catch (security-cao)
- W14.4 host: add fallback plugin search (../plugins/) so binaries run from
  build/tests/ load current DLLs, resolving the W13.6 R2 stale-DLL false
  alarm (architect-lin)
- W14.5 anthropic+deepseek: wrap 12 ABI boundary functions in try/catch with
  log-guard, preventing exceptions from crossing the C ABI (engineer-chen)

Verified: cmake build 0 error 0 warning, ctest 4/4 pass, smoke R2 now
passes naturally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 12:03:50 +08:00
parent 47082376ef
commit 102cd3e141
12 changed files with 1230 additions and 702 deletions

View File

@@ -373,42 +373,48 @@ static void send_notification(const std::string& method, const json::object& par
// ============================================================================
static void handle_message(const std::string& body) {
json::value val;
try { val = json::parse(body); }
catch (...) { return; }
json::object msg;
try { msg = val.as_object(); }
catch (...) { return; }
if (msg.contains("id") && !msg.contains("method")) {
// 响应 (有 id, 无 method)
int id = static_cast<int>(msg["id"].as_int64());
std::lock_guard<std::mutex> lock(g_lsp.mutex);
g_lsp.pending_responses[id] = body;
g_lsp.cv.notify_all();
} else if (msg.contains("method") && !msg.contains("id")) {
// 通知 (有 method, 无 id)
std::string method;
try { method = json::value_to<std::string>(msg["method"]); }
try {
json::value val;
try { val = json::parse(body); }
catch (...) { return; }
if (method == "textDocument/publishDiagnostics") {
if (!msg.contains("params")) return;
auto params = msg["params"].as_object();
if (!params.contains("uri")) return;
std::string uri = json::value_to<std::string>(params["uri"]);
std::string diag_json;
if (params.contains("diagnostics"))
diag_json = json::serialize(params["diagnostics"]);
else
diag_json = "[]";
json::object msg;
try { msg = val.as_object(); }
catch (...) { return; }
if (msg.contains("id") && !msg.contains("method")) {
// 响应 (有 id, 无 method)
int id = static_cast<int>(msg["id"].as_int64());
std::lock_guard<std::mutex> lock(g_lsp.mutex);
g_lsp.diagnostics[uri] = diag_json;
g_lsp.pending_responses[id] = body;
g_lsp.cv.notify_all();
} else if (msg.contains("method") && !msg.contains("id")) {
// 通知 (有 method, 无 id)
std::string method;
try { method = json::value_to<std::string>(msg["method"]); }
catch (...) { return; }
if (method == "textDocument/publishDiagnostics") {
if (!msg.contains("params")) return;
auto params = msg["params"].as_object();
if (!params.contains("uri")) return;
std::string uri = json::value_to<std::string>(params["uri"]);
std::string diag_json;
if (params.contains("diagnostics"))
diag_json = json::serialize(params["diagnostics"]);
else
diag_json = "[]";
std::lock_guard<std::mutex> lock(g_lsp.mutex);
g_lsp.diagnostics[uri] = diag_json;
}
}
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] handle_message: %s", e.what());
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] handle_message: unknown exception");
}
}
@@ -417,40 +423,46 @@ static void handle_message(const std::string& body) {
// ============================================================================
static void reader_loop() {
while (g_lsp.running) {
int content_length = -1;
bool pipe_ok = true;
try {
while (g_lsp.running) {
int content_length = -1;
bool pipe_ok = true;
// 状态机式读取 header 块:循环 read_line 直到读到空行
// LSP 3.17: header 块以空行(\r\n)结束,允许 Content-Type 等其他 header
while (pipe_ok) {
std::string line;
if (!g_lsp.proc.read_line(line)) {
pipe_ok = false;
break;
// 状态机式读取 header 块:循环 read_line 直到读到空行
// LSP 3.17: header 块以空行(\r\n)结束,允许 Content-Type 等其他 header
while (pipe_ok) {
std::string line;
if (!g_lsp.proc.read_line(line)) {
pipe_ok = false;
break;
}
// header 块以空行结束
auto sv = trim(std::string_view(line));
if (sv.empty()) break;
// 累积 Content-Length遇到其他 header 不丢弃,继续读取下一行
int len = parse_content_length(line);
if (len >= 0) content_length = len;
}
// header 块以空行结束
auto sv = trim(std::string_view(line));
if (sv.empty()) break;
if (!pipe_ok) break;
// 累积 Content-Length;遇到其他 header 不丢弃,继续读取下一行
int len = parse_content_length(line);
if (len >= 0) content_length = len;
// 空行前都没读到 Content-Length,协议错误——记日志并跳过这一帧
if (content_length < 0) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] Invalid LSP frame: missing Content-Length header");
continue;
}
std::string body;
if (!g_lsp.proc.read_bytes(body, content_length)) break;
handle_message(body);
}
if (!pipe_ok) break;
// 空行前都没读到 Content-Length协议错误——记日志并跳过这一帧
if (content_length < 0) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] Invalid LSP frame: missing Content-Length header");
continue;
}
std::string body;
if (!g_lsp.proc.read_bytes(body, content_length)) break;
handle_message(body);
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] reader_loop: %s", e.what());
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] reader_loop: unknown exception");
}
std::lock_guard<std::mutex> lock(g_lsp.mutex);
@@ -463,106 +475,131 @@ static void reader_loop() {
// ============================================================================
static void g_lsp_impl_stop();
static void g_lsp_impl_stop_nolock();
static void g_lsp_impl_stop_locked(std::unique_lock<std::mutex>& lock);
static int g_lsp_impl_start(const char* server_cmd, const char* language) {
if (!server_cmd || !server_cmd[0]) return -1;
// 如果已在运行, 先停止
if (g_lsp.running) {
g_lsp_impl_stop();
}
g_lsp.language = language ? language : "";
// 启动进程
if (!g_lsp.proc.start(server_cmd)) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] failed to start: %s", server_cmd);
return -1;
}
// 重置 ID 计数器
g_lsp.next_id = 1;
// 启动读取线程
g_lsp.running = true;
g_lsp.reader_thread = std::thread(reader_loop);
// 构建 initialize 参数
json::object text_doc_caps;
{
json::object hover;
hover["dynamicRegistration"] = false;
text_doc_caps["hover"] = hover;
json::object completion;
completion["dynamicRegistration"] = false;
text_doc_caps["completion"] = completion;
json::object diagnostic;
diagnostic["dynamicRegistration"] = false;
text_doc_caps["diagnostic"] = diagnostic;
}
json::object capabilities;
capabilities["textDocument"] = text_doc_caps;
json::object init_params;
init_params["processId"] = nullptr;
init_params["rootUri"] = nullptr;
init_params["capabilities"] = capabilities;
// 发送 initialize 请求
int init_id = send_request("initialize", init_params);
// 等待 initialize 响应 (最多 10 秒)
{
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [init_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(init_id) > 0;
});
if (!got || !g_lsp.running) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] initialize timed out");
try {
// 如果已在运行, 先停止
if (g_lsp.running) {
g_lsp_impl_stop();
}
g_lsp.language = language ? language : "";
// 启动进程
if (!g_lsp.proc.start(server_cmd)) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] failed to start: %s", server_cmd);
return -1;
}
g_lsp.pending_responses.erase(init_id);
// 重置 ID 计数器
g_lsp.next_id = 1;
// 启动读取线程
g_lsp.running = true;
g_lsp.reader_thread = std::thread(reader_loop);
// 构建 initialize 参数
json::object text_doc_caps;
{
json::object hover;
hover["dynamicRegistration"] = false;
text_doc_caps["hover"] = hover;
json::object completion;
completion["dynamicRegistration"] = false;
text_doc_caps["completion"] = completion;
json::object diagnostic;
diagnostic["dynamicRegistration"] = false;
text_doc_caps["diagnostic"] = diagnostic;
}
json::object capabilities;
capabilities["textDocument"] = text_doc_caps;
json::object init_params;
init_params["processId"] = nullptr;
init_params["rootUri"] = nullptr;
init_params["capabilities"] = capabilities;
// 发送 initialize 请求
int init_id = send_request("initialize", init_params);
// 等待 initialize 响应 (最多 10 秒)
{
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [init_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(init_id) > 0;
});
if (!got || !g_lsp.running) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] initialize timed out");
g_lsp_impl_stop_locked(lock);
return -1;
}
g_lsp.pending_responses.erase(init_id);
}
// 发送 initialized 通知
send_notification("initialized", json::object{});
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server started: %s", server_cmd);
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] start: %s", e.what());
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] start: unknown exception");
return -1;
}
}
// 发送 initialized 通知
send_notification("initialized", json::object{});
static void g_lsp_impl_stop_nolock() {
try {
if (!g_lsp.running) return;
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server started: %s", server_cmd);
return 0;
// 发送 shutdown 请求
int shutdown_id = send_request("shutdown", json::object{});
// 等待 shutdown 响应 (最多 2 秒)
{
std::unique_lock<std::mutex> lock(g_lsp.mutex);
g_lsp.cv.wait_for(lock, std::chrono::seconds(2), [shutdown_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(shutdown_id) > 0;
});
g_lsp.pending_responses.clear();
}
// 发送 exit 通知
send_notification("exit", json::object{});
// 停止读取线程
g_lsp.running = false;
g_lsp.proc.stop();
if (g_lsp.reader_thread.joinable())
g_lsp.reader_thread.join();
g_lsp.diagnostics.clear();
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server stopped");
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] stop: %s", e.what());
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] stop: unknown exception");
}
}
static void g_lsp_impl_stop() {
if (!g_lsp.running) return;
g_lsp_impl_stop_nolock();
}
// 发送 shutdown 请求
int shutdown_id = send_request("shutdown", json::object{});
// 等待 shutdown 响应 (最多 2 秒)
{
std::unique_lock<std::mutex> lock(g_lsp.mutex);
g_lsp.cv.wait_for(lock, std::chrono::seconds(2), [shutdown_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(shutdown_id) > 0;
});
g_lsp.pending_responses.clear();
}
// 发送 exit 通知
send_notification("exit", json::object{});
// 停止读取线程
g_lsp.running = false;
g_lsp.proc.stop();
if (g_lsp.reader_thread.joinable())
g_lsp.reader_thread.join();
g_lsp.diagnostics.clear();
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] server stopped");
static void g_lsp_impl_stop_locked(std::unique_lock<std::mutex>& lock) {
lock.unlock();
g_lsp_impl_stop_nolock();
}
static int g_lsp_impl_open_document(const char* uri, const char* content,
@@ -570,131 +607,177 @@ static int g_lsp_impl_open_document(const char* uri, const char* content,
if (!g_lsp.running) return -1;
if (!uri || !content || !lang_id) return -1;
json::object text_doc;
text_doc["uri"] = uri;
text_doc["languageId"] = lang_id;
text_doc["version"] = 1;
text_doc["text"] = content;
try {
json::object text_doc;
text_doc["uri"] = uri;
text_doc["languageId"] = lang_id;
text_doc["version"] = 1;
text_doc["text"] = content;
json::object params;
params["textDocument"] = text_doc;
json::object params;
params["textDocument"] = text_doc;
send_notification("textDocument/didOpen", params);
return 0;
send_notification("textDocument/didOpen", params);
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] open_document: %s", e.what());
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] open_document: unknown exception");
return -1;
}
}
static int g_lsp_impl_close_document(const char* uri) {
if (!g_lsp.running) return -1;
if (!uri) return -1;
json::object text_doc;
text_doc["uri"] = uri;
try {
json::object text_doc;
text_doc["uri"] = uri;
json::object params;
params["textDocument"] = text_doc;
json::object params;
params["textDocument"] = text_doc;
send_notification("textDocument/didClose", params);
return 0;
send_notification("textDocument/didClose", params);
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] close_document: %s", e.what());
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] close_document: unknown exception");
return -1;
}
}
static int g_lsp_impl_get_diagnostics(const char* uri, char** json_out) {
if (!g_lsp.running) return -1;
if (!uri || !json_out) return -1;
std::lock_guard<std::mutex> lock(g_lsp.mutex);
auto it = g_lsp.diagnostics.find(uri);
if (it == g_lsp.diagnostics.end()) {
*json_out = g_host->strdup("[]");
} else {
*json_out = g_host->strdup(it->second.c_str());
try {
std::lock_guard<std::mutex> lock(g_lsp.mutex);
auto it = g_lsp.diagnostics.find(uri);
if (it == g_lsp.diagnostics.end()) {
*json_out = g_host->strdup("[]");
} else {
*json_out = g_host->strdup(it->second.c_str());
}
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_diagnostics: %s", e.what());
*json_out = nullptr;
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_diagnostics: unknown exception");
*json_out = nullptr;
return -1;
}
return 0;
}
static int g_lsp_impl_get_hover(const char* uri, int line, int col, char** json_out) {
if (!g_lsp.running) return -1;
if (!uri || !json_out) return -1;
json::object position;
position["line"] = line;
position["character"] = col;
try {
json::object position;
position["line"] = line;
position["character"] = col;
json::object text_doc;
text_doc["uri"] = uri;
json::object text_doc;
text_doc["uri"] = uri;
json::object params;
params["textDocument"] = text_doc;
params["position"] = position;
json::object params;
params["textDocument"] = text_doc;
params["position"] = position;
int req_id = send_request("textDocument/hover", params);
int req_id = send_request("textDocument/hover", params);
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0;
});
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0;
});
if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) {
if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) {
return -1;
}
std::string response_body = g_lsp.pending_responses[req_id];
g_lsp.pending_responses.erase(req_id);
json::value val;
try { val = json::parse(response_body); }
catch (...) { return -1; }
json::object resp;
try { resp = val.as_object(); }
catch (...) { return -1; }
if (!resp.contains("result")) return -1;
*json_out = g_host->strdup(json::serialize(resp["result"]).c_str());
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_hover: %s", e.what());
*json_out = nullptr;
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_hover: unknown exception");
*json_out = nullptr;
return -1;
}
std::string response_body = g_lsp.pending_responses[req_id];
g_lsp.pending_responses.erase(req_id);
json::value val;
try { val = json::parse(response_body); }
catch (...) { return -1; }
json::object resp;
try { resp = val.as_object(); }
catch (...) { return -1; }
if (!resp.contains("result")) return -1;
*json_out = g_host->strdup(json::serialize(resp["result"]).c_str());
return 0;
}
static int g_lsp_impl_get_completion(const char* uri, int line, int col, char** json_out) {
if (!g_lsp.running) return -1;
if (!uri || !json_out) return -1;
json::object position;
position["line"] = line;
position["character"] = col;
try {
json::object position;
position["line"] = line;
position["character"] = col;
json::object text_doc;
text_doc["uri"] = uri;
json::object text_doc;
text_doc["uri"] = uri;
json::object params;
params["textDocument"] = text_doc;
params["position"] = position;
json::object params;
params["textDocument"] = text_doc;
params["position"] = position;
int req_id = send_request("textDocument/completion", params);
int req_id = send_request("textDocument/completion", params);
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0;
});
std::unique_lock<std::mutex> lock(g_lsp.mutex);
bool got = g_lsp.cv.wait_for(lock, std::chrono::seconds(10), [req_id]() {
return !g_lsp.running || g_lsp.pending_responses.count(req_id) > 0;
});
if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) {
if (!got || !g_lsp.running || g_lsp.pending_responses.count(req_id) == 0) {
return -1;
}
std::string response_body = g_lsp.pending_responses[req_id];
g_lsp.pending_responses.erase(req_id);
json::value val;
try { val = json::parse(response_body); }
catch (...) { return -1; }
json::object resp;
try { resp = val.as_object(); }
catch (...) { return -1; }
if (!resp.contains("result")) return -1;
*json_out = g_host->strdup(json::serialize(resp["result"]).c_str());
return 0;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_completion: %s", e.what());
*json_out = nullptr;
return -1;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] get_completion: unknown exception");
*json_out = nullptr;
return -1;
}
std::string response_body = g_lsp.pending_responses[req_id];
g_lsp.pending_responses.erase(req_id);
json::value val;
try { val = json::parse(response_body); }
catch (...) { return -1; }
json::object resp;
try { resp = val.as_object(); }
catch (...) { return -1; }
if (!resp.contains("result")) return -1;
*json_out = g_host->strdup(json::serialize(resp["result"]).c_str());
return 0;
}
// ============================================================================
@@ -722,11 +805,19 @@ static int on_init(const dstalk_host_api_t* host) {
}
static void on_shutdown() {
if (g_lsp.running) {
g_lsp_impl_stop();
try {
if (g_lsp.running) {
g_lsp_impl_stop();
}
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] shutdown");
g_host = nullptr;
} catch (const std::exception& e) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] on_shutdown: %s", e.what());
g_host = nullptr;
} catch (...) {
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] on_shutdown: unknown exception");
g_host = nullptr;
}
if (g_host) g_host->log(DSTALK_LOG_INFO, "[lsp] shutdown");
g_host = nullptr;
}
// ============================================================================