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:
@@ -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,能准确捕获
|
||||||
|
|||||||
Reference in New Issue
Block a user