Fix: hold IoRefCount across deferred heartbeat update to prevent use-after-free

Frequent standby-park churn (duplicate logins sharing one clientID) was
crashing the server with 0xC0000409 (STACK_BUFFER_OVERRUN). The heartbeat
handler posts the raw CONTEXT_OBJECT pointer via PostMessageA
(WM_UPDATE_ACTIVEWND) without holding a reference, so RemoveStaleContext
could recycle the context back to the pool and reuse it before the UI
thread consumed the queued message. UpdateActiveWindow then read a
stale/recycled object, and the unguarded Authorization memcpy overflowed
the 200-byte HeartbeatACK field and clobbered the stack cookie.

Hold the object's IoRefCount across the deferred message: fetch_add
before PostMessageA (with rollback on failure) and a paired fetch_sub in
UpdateUserEvent after UpdateActiveWindow returns. RemoveStaleContext
already waits for IoRefCount == 0 before MoveContextToFreePoolList, so
the object is now guaranteed to stay alive for the whole deferred call.

Also bound the three memcpy sites (two Signature[64], one
Authorization[200]) with explicit truncation so an unexpectedly long
signature or license can no longer overflow its fixed buffer in Release
builds, where ASSERT is a no-op.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-29 06:44:27 +02:00
parent d99122b25f
commit 35dc2d89a0

View File

@@ -1689,8 +1689,12 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
std::string msg = startTime; std::string msg = startTime;
msg += "|" + std::to_string(ContextObject->GetClientID()); msg += "|" + std::to_string(ContextObject->GetClientID());
auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length()); auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length());
ASSERT(signature.size() <= sizeof(copy.Signature)); // 防御性边界保护signMessage 正常返回 64 字节,但 ASSERT 在 Release 下是空操作,
memcpy(copy.Signature, signature.data(), signature.size()); // 若签名意外超长,无界 memcpy 会越界写 MasterSettings.Signature[64]。这里强制截断。
size_t sigLen = signature.size();
if (sigLen > sizeof(copy.Signature))
sigLen = sizeof(copy.Signature);
memcpy(copy.Signature, signature.data(), sigLen);
LeaveCriticalSection(&m_cs); LeaveCriticalSection(&m_cs);
Mprintf("主机[%s]上线: %s[%s][%s]\n", v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), Mprintf("主机[%s]上线: %s[%s][%s]\n", v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(),
std::to_string(id).c_str(), loc, groupName.c_str()); std::to_string(id).c_str(), loc, groupName.c_str());
@@ -6264,7 +6268,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
case TOKEN_HEARTBEAT: case TOKEN_HEARTBEAT:
case 137: // 心跳【L】 case 137: // 心跳【L】
ContextObject->HeartbeatRecvMs.store(GetUnixMs(), std::memory_order_relaxed); ContextObject->HeartbeatRecvMs.store(GetUnixMs(), std::memory_order_relaxed);
g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject); // 为延迟消息占一个引用计数RemoveStaleContext 会等 IoRefCount==0 才回收对象,
// 否则这里经消息队列传出的裸指针在 UI 线程消费时可能已被替补抖动回收复用use-after-free
// UpdateUserEvent 处理完后配对 fetch_sub 释放。
ContextObject->IoRefCount.fetch_add(1);
if (!g_2015RemoteDlg->PostMessageA(WM_UPDATE_ACTIVEWND, 0, (LPARAM)ContextObject)) {
ContextObject->IoRefCount.fetch_sub(1); // 投递失败,退回引用,避免泄漏
}
break; break;
case TOKEN_SCREEN_PREVIEW_RSP: { case TOKEN_SCREEN_PREVIEW_RSP: {
// 屏幕预览响应:把整个包 + 来源 clientId 一并堆分配,转给主线程处理 // 屏幕预览响应:把整个包 + 来源 clientId 一并堆分配,转给主线程处理
@@ -6816,8 +6826,12 @@ bool CMy2015RemoteDlg::PromoteStandby(uint64_t clientID)
std::string msg = startTime.GetString(); std::string msg = startTime.GetString();
msg += "|" + std::to_string(clientID); msg += "|" + std::to_string(clientID);
auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length()); auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length());
ASSERT(signature.size() <= sizeof(copy.Signature)); // 防御性边界保护signMessage 正常返回 64 字节,但 ASSERT 在 Release 下是空操作,
memcpy(copy.Signature, signature.data(), signature.size()); // 若签名意外超长,无界 memcpy 会越界写 MasterSettings.Signature[64]。这里强制截断。
size_t sigLen = signature.size();
if (sigLen > sizeof(copy.Signature))
sigLen = sizeof(copy.Signature);
memcpy(copy.Signature, signature.data(), sigLen);
SendMasterSettings(standby, copy); SendMasterSettings(standby, copy);
return true; return true;
} }
@@ -6874,7 +6888,12 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam)
LRESULT CMy2015RemoteDlg::UpdateUserEvent(WPARAM wParam, LPARAM lParam) LRESULT CMy2015RemoteDlg::UpdateUserEvent(WPARAM wParam, LPARAM lParam)
{ {
CONTEXT_OBJECT* ctx = (CONTEXT_OBJECT*)lParam; CONTEXT_OBJECT* ctx = (CONTEXT_OBJECT*)lParam;
if (ctx) {
UpdateActiveWindow(ctx); UpdateActiveWindow(ctx);
// 释放 TOKEN_HEARTBEAT 投递延迟消息时占用的引用计数(与那里的 fetch_add 配对)。
// 必须在 UpdateActiveWindow 完全返回后再释放,保证整个处理期间对象不被回收。
ctx->IoRefCount.fetch_sub(1);
}
return S_OK; return S_OK;
} }
@@ -7211,7 +7230,14 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx)
HeartbeatACK ack = { hb.Time, (char)authStatus, (char)isTrail }; HeartbeatACK ack = { hb.Time, (char)authStatus, (char)isTrail };
if (authorized) { if (authorized) {
std::string authorization = isV2 ? LoadLicenseAuthorization(hb.SN) : BuildV1Authorization(hb.SN, true); std::string authorization = isV2 ? LoadLicenseAuthorization(hb.SN) : BuildV1Authorization(hb.SN, true);
memcpy(ack.Authorization, authorization.c_str(), authorization.length()); // 防御性边界保护:正常 authorization 约 150~200 字节;一旦异常超长,无界 memcpy
// 会越界写 HeartbeatACK.Authorization[200] 并踩栈金丝雀0xC0000409
// Release 下强制截断并保证 NUL 结尾。
size_t authLen = authorization.length();
if (authLen >= sizeof(ack.Authorization))
authLen = sizeof(ack.Authorization) - 1;
memcpy(ack.Authorization, authorization.c_str(), authLen);
ack.Authorization[authLen] = '\0';
} }
// 在 send 前一刻填进处理耗时毫秒。GetUnixMs 底层是 chrono::system_clock // 在 send 前一刻填进处理耗时毫秒。GetUnixMs 底层是 chrono::system_clock
// 在 VS2019+ MSVC 上精度亚微秒(截断到 ms两次作差误差 ≤ 1ms能准确捕获 // 在 VS2019+ MSVC 上精度亚微秒(截断到 ms两次作差误差 ≤ 1ms能准确捕获