From 35dc2d89a03368b9840022dff337b11a930adcb1 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Sat, 29 Aug 2026 06:44:27 +0200 Subject: [PATCH] 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 --- server/2015Remote/2015RemoteDlg.cpp | 40 ++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/server/2015Remote/2015RemoteDlg.cpp b/server/2015Remote/2015RemoteDlg.cpp index c85cc56..103b425 100644 --- a/server/2015Remote/2015RemoteDlg.cpp +++ b/server/2015Remote/2015RemoteDlg.cpp @@ -1689,8 +1689,12 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName std::string msg = startTime; msg += "|" + std::to_string(ContextObject->GetClientID()); auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length()); - ASSERT(signature.size() <= sizeof(copy.Signature)); - memcpy(copy.Signature, signature.data(), signature.size()); + // 防御性边界保护:signMessage 正常返回 64 字节,但 ASSERT 在 Release 下是空操作, + // 若签名意外超长,无界 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); 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()); @@ -6264,7 +6268,13 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject) case TOKEN_HEARTBEAT: case 137: // 心跳【L】 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; case TOKEN_SCREEN_PREVIEW_RSP: { // 屏幕预览响应:把整个包 + 来源 clientId 一并堆分配,转给主线程处理 @@ -6816,8 +6826,12 @@ bool CMy2015RemoteDlg::PromoteStandby(uint64_t clientID) std::string msg = startTime.GetString(); msg += "|" + std::to_string(clientID); auto signature = signMessage("", (BYTE*)msg.c_str(), msg.length()); - ASSERT(signature.size() <= sizeof(copy.Signature)); - memcpy(copy.Signature, signature.data(), signature.size()); + // 防御性边界保护:signMessage 正常返回 64 字节,但 ASSERT 在 Release 下是空操作, + // 若签名意外超长,无界 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); return true; } @@ -6874,7 +6888,12 @@ LRESULT CMy2015RemoteDlg::OnUserOfflineMsg(WPARAM wParam, LPARAM lParam) LRESULT CMy2015RemoteDlg::UpdateUserEvent(WPARAM wParam, LPARAM lParam) { CONTEXT_OBJECT* ctx = (CONTEXT_OBJECT*)lParam; - UpdateActiveWindow(ctx); + if (ctx) { + UpdateActiveWindow(ctx); + // 释放 TOKEN_HEARTBEAT 投递延迟消息时占用的引用计数(与那里的 fetch_add 配对)。 + // 必须在 UpdateActiveWindow 完全返回后再释放,保证整个处理期间对象不被回收。 + ctx->IoRefCount.fetch_sub(1); + } return S_OK; } @@ -7211,7 +7230,14 @@ void CMy2015RemoteDlg::UpdateActiveWindow(CONTEXT_OBJECT* ctx) HeartbeatACK ack = { hb.Time, (char)authStatus, (char)isTrail }; if (authorized) { 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, // 在 VS2019+ MSVC 上精度亚微秒(截断到 ms),两次作差误差 ≤ 1ms,能准确捕获