- P1-3: introduce ConnectionLock (mutex + atomic active) to close the residual CWE-362 race where _remove_client closes fd before reply/broadcast acquires the lock; senders now check active after locking and abort if fd was closed - P2-7: make _server_fd std::atomic<int> to remove the data-race UB between stop() and _event_loop - P2-8: validate on_message is non-null in MsgServer::start() - P3-2: distinguish missing _route vs wrong-typed _route in RpcServer - P3-3: set TCP_NODELAY/SO_SNDTIMEO before registering fd in _clients - doc: add SO_SNDTIMEO precondition note to pmsg_send
30 KiB
libmsg 模块安全审计报告
审计日期: 2026-08-07(第四轮审计)
首次审计: 2026-08-06
审计范围: libmsg/inc/us/MsgServer.hpp、libmsg/inc/us/RpcServer.hpp、libmsg/inc/us/Msg.hpp、libmsg/inc/us/RpcClient.hpp、libmsg/inc/us/AsyncQueue.hpp、libmsg/inc/us/Msg_Basic.hpp、libmsg/src/Msg_Basic.cpp
代码版本: main 分支(基于 e92ef05 进行修复)
审计工具: 人工代码审查 + Qoder Security SAST 原则
问题严重等级
| 等级 | 说明 | 响应时间 |
|---|---|---|
| P0 🔴 | 可导致服务崩溃、DoS 或远程接管、数据发往错误客户端 | 立即修复 |
| P1 🟠 | 可导致资源耗尽、信息泄露、拒绝服务 | 72小时内 |
| P2 🟡 | 健壮性/配置问题、校验缺失 | 一周内 |
| P3 🟢 | 优化建议 | 版本迭代 |
修复进度汇总
| 审计轮次 | 等级 | 总数 | 已修复 | 待修复 | 修复率 |
|---|---|---|---|---|---|
| 第一轮 (2026-08-06) | P0 | 1 | 1 | 0 | 100% |
| 第一轮 (2026-08-06) | P1 | 1 | 1 | 0 | 100% |
| 第一轮 (2026-08-06) | P2 | 1 | 1 | 0 | 100% |
| 第二轮 (2026-08-07) | P0 | 1 | 1 | 0 | 100% |
| 第二轮 (2026-08-07) | P2 | 4 | 4 | 0 | 100% |
| 第三轮 (2026-08-07) | P1 | 1 | 1 | 0 | 100% |
| 第三轮 (2026-08-07) | P2 | 1 | 1 | 0 | 100% |
| 第三轮 (2026-08-07) | P3 | 1 | 1 | 0 | 100% |
| 第四轮 (2026-08-07) | P1 | 1 | 1 | 0 | 100% |
| 第四轮 (2026-08-07) | P2 | 2 | 2 | 0 | 100% |
| 第四轮 (2026-08-07) | P3 | 2 | 2 | 0 | 100% |
| 合计 | — | 16 | 16 | 0 | 100% |
🔴 P0 — 严重问题
P0-1: broadcast/reply 竞态条件导致数据发送到错误客户端 ✅ 已修复(第一轮)
- CWE: CWE-362 (Race Condition)
- OWASP: A01:2021
- 位置:
MsgServer.hpp:228(原始行号) - 置信度: 80%
问题描述:
broadcast() 在 _clients_mtx 保护下拷贝 fd + send_lock 列表后释放锁,再无锁执行 pmsg_send(fd, msg)。此窗口期内 _remove_client() 可关闭该 fd,OS 可将其分配给新连接。broadcast()/reply() 随后向复用的 fd 发送数据,导致消息到达错误的客户端。_remove_client() 原实现关闭 fd 前不获取 per-connection send_lock,因此 send_lock 无法阻止此竞态。
修复方案(commit e92ef05):
_remove_client() 先从 _send_locks 取出 send_lock 并从 map 删除,持有 send_lock 后再执行 ::close(fd),确保关闭与正在进行的发送操作互斥。
P0-2: _close_all_clients() 批量关闭路径存在同类竞态回归 ✅ 已修复(第二轮)
- CWE: CWE-362 (Race Condition)
- OWASP: A01:2021
- 位置:
MsgServer.hpp::_close_all_clients()(析构/stop路径) - 置信度: 90%
问题描述:
第一轮修复只覆盖了单客户端移除路径 _remove_client(),但批量关闭路径 _close_all_clients()(在 start() 异常退出或正常退出时调用)直接遍历 _clients 并 ::close(fd),未持有每连接 send_lock。在竞态窗口内,reply/broadcast 可能在 _clients_mtx 释放后、close(fd) 执行前获取到 send_lock,或 close(fd) 后 OS 复用 fd 给新连接,导致数据发送到错误客户端。
触发场景:
- 服务调用
stop()或start()异常退出,进入_close_all_clients() - 另一线程正在调用
reply()/broadcast(),已在_clients_mtx下拷贝了 send_lock 但尚未持有 send_lock 发送数据 _close_all_clients()无锁直接 close(fd),OS 复用 fd- 发送操作向复用的 fd 写入数据,到达错误客户端
修复方案(第二轮修复):
_close_all_clients() 改为与 _remove_client() 一致的安全模式:先在 _clients_mtx 保护下收集所有 (fd, send_lock) 对并清空 map,释放锁后对每个 fd 持有对应的 send_lock 再执行 shutdown()+close():
void _close_all_clients() {
std::vector<std::pair<int, std::shared_ptr<std::mutex>>> to_close;
{
std::lock_guard<std::mutex> clk(_clients_mtx);
to_close.reserve(_clients.size());
for (auto& [fd, addr] : _clients) {
auto it = _send_locks.find(fd);
if (it != _send_locks.end()) {
to_close.emplace_back(fd, it->second);
} else {
to_close.emplace_back(fd, nullptr);
}
}
_clients.clear();
_send_locks.clear();
}
for (auto& [fd, send_lock] : to_close) {
if (send_lock) {
std::lock_guard<std::mutex> slk(*send_lock);
::shutdown(fd, SHUT_RDWR);
::close(fd);
} else {
::shutdown(fd, SHUT_RDWR);
::close(fd);
}
}
// ... 清理 _busy/_failed
}
🟠 P1 — 高危问题
P1-1: 事件循环顺序阻塞接收导致 DoS(Slowloris 变体)✅ 已修复(第一轮)
- CWE: CWE-400 (Uncontrolled Resource Consumption)
- OWASP: A05:2021
- 位置:
MsgServer.hpp:424(原始行号) - 置信度: 80%
问题描述:
_event_loop() 对每个就绪 fd 顺序调用 _handle_client(),后者在事件循环主线程中阻塞调用 pmsg_recv(fd, recv_timeout)(默认 10 秒)。线程池仅处理回调执行,不处理消息接收,即使配置了 workers > 0 也无法缓解。攻击者用 max_connections(默认 1000)个连接各发送不完整帧头(如 3 字节),可使事件循环阻塞最长 1000 × 10 = 10000 秒(约 2.8 小时),期间服务完全无法处理任何新连接或已有消息。
修复方案(commit e92ef05):
在 workers > 0 模式下,_handle_client() 不再在事件循环线程调用 pmsg_recv(),而是提交 needs_recv=true 的任务给 worker 线程,由 worker 线程负责完整的 recv + callback 流程。同步模式(workers=0)行为不变。
🟡 P2 — 中危问题
P2-1: 错误信息路径脱敏正则不完整,可能泄露内部路径 ✅ 已修复(第一轮)
- CWE: CWE-209 (Information Exposure Through an Error Message)
- OWASP: A04:2021
- 位置:
RpcServer.hpp:184(原始行号) - 置信度: 75%
问题描述:
RpcServer::_sanitize_error() 使用正则 /[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)* 替换绝对路径,含空格、括号、@、~ 等特殊字符的路径不被匹配,会原文返回给远程客户端。
修复方案(commit e92ef05):
正则扩展为 /\S+(?:/\S*)*,覆盖所有非空白字符路径。
P2-2: pmsg_recv(int) 依赖隐式SO_RCVTIMEO前置条件,误用可导致永久阻塞 ✅ 已修复(第二轮)
- CWE: CWE-400 (Uncontrolled Resource Consumption) / CWE-672 (Operation on a Resource after Expiration)
- 位置:
Msg.hpppmsg_recv(int sockfd) - 置信度: 70%
问题描述:
单参数版本 pmsg_recv(int sockfd)(阻塞模式)依赖调用方预先通过 setsockopt(SO_RCVTIMEO) 设置接收超时,函数本身既不设置超时,也不检查超时是否已设置。如果外部用户直接使用此 API 而忘记设置超时,recv() 会永久阻塞,构成拒绝服务风险。虽然所有内部调用点(MsgClient::listen())都正确设置了超时,但作为公共 API 存在误用风险。
修复方案(第二轮修复):
- 在 Doxygen 注释中明确标注前置条件,推荐使用 deadline 模式
pmsg_recv(sockfd, timeout_sec) - 统一了接收路径的 UTF-8 校验(见 P2-5)
P2-3: 公共API参数无边界校验,异常输入可导致未定义行为 ✅ 已修复(第二轮)
- CWE: CWE-20 (Improper Input Validation)
- 位置:
Msg.hpp、MsgServer.hpp、AsyncQueue.hpp、RpcClient.hpp、RpcServer.hpp各构造函数和公共方法 - 置信度: 85%
问题描述:
多数公共入口函数未校验参数合法性:
timeout_sec传入 0 或负数时,deadline立即到期导致异常行为;传入极大值(如 > 1小时)可绕过 Slowloris 防护port传入 0 或 >65535 时,htons()截断产生错误端口MsgServerConfig中workers、max_connections、max_queue_size、poll_timeout、recv_timeout、listen_backlog无范围校验,传入负值或极大值可导致内存分配失败、CPU 100% 或绕过安全限制AsyncQueueConfig中batch_size、flush_interval、poll_timeout无校验,batch_size=0导致死循环或异常RpcClient的timeout、retry_interval无校验RpcServer::add_route()接受空字符串路由名和空 handler,导致后续调用崩溃AsyncQueue构造接受 null handler,调用时崩溃
修复方案(第二轮修复):
新增 detail::validateTimeout() 和 detail::validatePort() 辅助函数,并在以下入口点添加边界校验:
| 位置 | 校验项 |
|---|---|
detail::recvFrameDeadline() |
0 < timeout_sec ≤ 300 秒 |
detail::openConnection() |
1 ≤ port ≤ 65535,0 < timeout ≤ 300 秒 |
MsgClient::MsgClient() |
port 范围 |
MsgClient::call() |
timeout、retry_interval 范围 |
MsgServer::MsgServer() |
port/workers/recv_timeout/max_connections/max_queue_size/listen_backlog/poll_timeout 全部校验 |
AsyncQueue::AsyncQueue() |
handler 非空、batch_size>0、flush_interval/poll_timeout 合理范围 |
RpcClient::RpcClient() |
timeout、retry_interval 范围 |
RpcServer::add_route() |
name 非空、handler 非空 |
P2-4: 消息载荷缺少UTF-8合法性校验 ✅ 已修复(第二轮)
- CWE: CWE-20 (Improper Input Validation)
- 位置:
Msg.hpppmsg_recv()两个重载版本 - 置信度: 65%
问题描述:
Python 版 pymsg.recv() 在接收载荷后调用 payload.decode("utf-8") 进行 UTF-8 合法性校验,非法字节序列会抛出 UnicodeDecodeError 并被包装为 ConnectionError。C++ 版直接将原始字节存入 std::string 返回,不校验 UTF-8 合法性,可能导致上层 JSON 解析器或业务逻辑出现未定义行为(如 null 字节截断、非法序列解析异常、安全绕过)。
修复方案(第二轮修复):
- 新增
detail::isValidUtf8()函数,实现完整的 UTF-8 校验:- 正确识别 1/2/3/4 字节序列
- 拒绝 overlong 编码(如 0xC0 0x80 表示 NUL)
- 拒绝 UTF-16 代理对(0xD800-0xDFFF)
- 拒绝超 U+10FFFF 的编码
- 拒绝非法续字节、截断序列
pmsg_recv(int)和pmsg_recv(int, double)在接收完 payload 后调用 isValidUtf8 校验,非法载荷抛出MsgConnectionException("invalid UTF-8 payload")断开连接
P2-5: 缺少参数校验导致无效配置可绕过安全限制 ✅ 已修复(第二轮)
- CWE: CWE-20 (Improper Input Validation)
- 说明: 此问题已合并入 P2-3 修复。MsgServer 构造函数现在对所有配置项实施范围校验:
recv_timeout: (0, 300] 秒,防止传入超大值绕过 Slowloris 防护max_connections: [1, 100000],防止 0 或负值导致所有连接被拒绝或无限制max_queue_size: [1, 1000000],防止 0 或负值导致无法提交任务workers: [0, 256],防止负值或过大线程数耗尽资源poll_timeout: (0, 5] 秒,防止 0 导致 select 忙轮询 CPU 100%listen_backlog: [1, 4096]
🟠 P1 — 高危问题(第三轮)
P1-2: 服务端 socket 未设置发送超时,慢客户端不读数据可致 send 永久阻塞(DoS) ✅ 已修复(第三轮)
- CWE: CWE-400 (Uncontrolled Resource Consumption)
- OWASP: A05:2021
- 位置:
MsgServer.hpp::_accept_client()(新连接只设 TCP_NODELAY,未设 SO_SNDTIMEO) - 置信度: 85%
问题描述:
服务端每个客户端 socket 在 _accept_client() 中仅设置了 TCP_NODELAY,未设置发送超时 SO_SNDTIMEO。若攻击者连接后停止读取数据,发送缓冲区会被填满,此时 reply()/broadcast() 调用的 pmsg_send() → sendAll() 中的 send() 因无法写入而永久阻塞(无超时则不会返回 EAGAIN)。
- 同步模式(workers=0):
reply()/broadcast()在主线程(事件循环线程)内执行,send 永久阻塞即卡死整个事件循环,服务完全无法处理任何连接 → 服务级 DoS。 - 线程池模式(workers>0): 阻塞的 send 占用 worker 线程,攻击者可用少量慢连接耗尽全部 worker。
触发场景:
- 攻击者建立连接但不调用 recv 读取数据
- 服务端向其 reply() 大量数据(如日志/广播),发送缓冲区填满
send()永久阻塞,卡死主线程(同步模式)或占用 worker 线程
修复方案(第三轮修复):
在 _accept_client() 中对新连接设置 SO_SNDTIMEO = 5.0 秒。发送超时后 sendAll() 检测到 EAGAIN/EWOULDBLOCK 抛 MsgTimeoutException,被 reply()/broadcast() 捕获并标记为 _failed,由主循环统一清理连接(配合 P2-6 修复)。
🟡 P2 — 中危问题(第三轮)
P2-6: reply() 发送失败静默忽略且不标记 failed,连接残留不清理 ✅ 已修复(第三轮)
- CWE: CWE-703 (Improper Check or Handling of Exceptional Conditions)
- 位置:
MsgServer.hpp::reply()(catch (const MsgException&)分支) - 置信度: 80%
问题描述:
broadcast() 发送失败时会将该 fd 加入 _failed 集合,由主循环统一清理;而 reply() 发送失败时仅 catch 静默忽略,不标记 _failed。若 reply 的目标连接已断开或发送超时,该连接会一直残留在 _clients 中,直到主循环 select 检测到读端异常才被清理,造成资源滞留和清理路径不一致。
修复方案(第三轮修复):
reply() 的失败分支改为与 broadcast() 一致:捕获 MsgException 后向 _failed 集合插入该 fd,由主循环 _reap_failed() 统一清理。
🟢 P3 — 优化建议(第三轮)
P3-1: RpcClient::call() 未校验 success 字段类型,非 bool 时抛未捕获的 boost 异常 ✅ 已修复(第三轮)
- CWE: CWE-20 (Improper Input Validation)
- 位置:
RpcClient.hpp::call()(obj.at("success").as_bool()) - 置信度: 70%
问题描述:
call() 已检查响应包含 success 字段,但未校验其类型。若远端返回 {"success": "false"}(字符串)等非 bool 值,as_bool() 会抛出未捕获的 boost::system::system_error,而非统一的 RpcError,导致调用方无法按既定异常层次处理,且错误信息可能泄露内部细节。
修复方案(第三轮修复):
调用 as_bool() 前先检查 success.is_bool(),类型不符时抛出 RpcError("非法 RPC 响应(success 字段不是 bool)"),与其余非法响应处理保持一致。
🟠 P1 — 高危问题(第四轮)
P1-3: reply/broadcast 与 _remove_client 之间存在残余竞态,fd 复用可致数据发往错误客户端 ✅ 已修复(第四轮)
- CWE: CWE-362 (Race Condition)
- OWASP: A01:2021
- 位置:
MsgServer.hppreply()/broadcast()/_remove_client() - 置信度: 85%
问题描述:
第一轮 P0-1 修复确保了"发送期间不会被 close"(reply/broadcast 先获取 send_lock → _remove_client 等待),但未覆盖反向场景:_remove_client 先获取 send_lock 并 close(fd) 后,reply/broadcast 后获取 send_lock 时 fd 已关闭。如果 OS 将该 fd 复用给新 accept 的连接,reply/broadcast 的 pmsg_send() 会向复用的 fd 写入数据,导致消息到达错误客户端。
触发场景:
- reply/broadcast 在
_clients_mtx下获取了send_lock的 shared_ptr 副本 - 释放
_clients_mtx _remove_client()获取_clients_mtx,erase fd,获取send_lock副本,释放_clients_mtx_remove_client()先于 reply/broadcast 获取send_lock_remove_client()close(fd),释放send_lock- OS 将 fd 复用给新 accept 的连接
- reply/broadcast 获取
send_lock,调用pmsg_send(fd, msg)→ 数据到达新连接
步骤 2-4 的窗口期虽然很短(仅几条指令),但在高并发场景(大量 broadcast + 连接断开同时发生)下可被触发。
修复方案(第四轮修复):
引入 ConnectionLock 结构,将 send_lock(mutex)和 active(atomic<bool>)组合在一起:
struct ConnectionLock {
std::mutex send_mtx;
std::atomic<bool> active{true};
};
reply()/broadcast()在获取send_mtx后检查active:若为 false 则放弃发送(fd 已被关闭)_remove_client()/_close_all_clients()在获取send_mtx后先设置active=false,再shutdown()+close(fd)
这样无论谁先获取锁:
- 若 reply/broadcast 先获取锁:
active=true,正常发送,_remove_client等待发送完成后再 close - 若
_remove_client先获取锁:设置active=false,close(fd),reply/broadcast 后获取锁时检查active=false直接 return
🟡 P2 — 中危问题(第四轮)
P2-7: _server_fd 非原子访问构成数据竞争(UB) ✅ 已修复(第四轮)
- CWE: CWE-362 (Race Condition) / CWE-662 (Improper Synchronization)
- 位置:
MsgServer.hpp_server_fd成员 - 置信度: 90%
问题描述:
_server_fd 是普通 int,在主线程(_event_loop)读、stop() 线程写,构成 C++ 标准定义的数据竞争(Undefined Behavior)。虽然在实际 64 位系统上 int 读写通常是原子的(最坏情况读取旧值导致 accept 失败),但严格来说是 UB,编译器优化可能产生意外行为。
修复方案(第四轮修复):
将 _server_fd 改为 std::atomic<int>,所有读写使用 .load() / .store() / .exchange()。stop() 使用 exchange(-1) 原子地认领 fd 并关闭。
P2-8: start() 未校验 on_message 回调非空 ✅ 已修复(第四轮)
- CWE: CWE-20 (Improper Input Validation)
- 位置:
MsgServer.hppstart() - 置信度: 85%
问题描述:
start(on_message) 接受 std::function 参数但未校验是否为空。若传入空回调(默认构造的 std::function),后续 _on_message(fd, addr, msg) 会抛出 std::bad_function_call,该异常在 worker 线程中被 catch 但在同步模式下会导致连接断开;在 _dispatch(RpcServer)中会导致单个请求失败。虽然不会崩溃服务,但行为不符合预期且错误信息不明确。
修复方案(第四轮修复):
start() 开头添加校验:if (!on_message) throw MsgException("on_message callback must not be null");
🟢 P3 — 优化建议(第四轮)
P3-2: RpcServer _route 非 string 类型时错误信息不准确 ✅ 已修复(第四轮)
- CWE: CWE-209 (Information Exposure Through an Error Message)
- 位置:
RpcServer.hpp_dispatch() - 置信度: 70%
问题描述:
_dispatch() 中检查 _route 字段时,若字段存在但不是 string 类型(如数字、数组),错误信息统一返回 "missing _route",误导客户端以为字段缺失而非类型错误。
修复方案(第四轮修复):
区分两种情况:字段不存在返回 "missing _route",类型不符返回 "_route must be a string"。
P3-3: _accept_client 中 setsockopt 在加入 _clients 之后执行 ✅ 已修复(第四轮)
- 位置:
MsgServer.hpp_accept_client() - 置信度: 60%
问题描述:
原实现先将 client_fd 加入 _clients 和 _send_locks(持锁),释放锁后再设置 TCP_NODELAY 和 SO_SNDTIMEO。虽然新连接此时尚未收到消息,不会有 reply/broadcast 发送数据,但最佳实践是先完成所有 socket 配置再暴露给其他线程。
修复方案(第四轮修复):
调整顺序:先设置 TCP_NODELAY 和 SO_SNDTIMEO,再持锁加入 _clients。
✅ 已有安全机制(更新后)
| 机制 | 位置 | 说明 |
|---|---|---|
| 消息大小上限 64 MiB | Msg_Basic.hpp kMaxMsgSize |
防止超大消息耗尽内存 |
| deadline 模式 recv 超时 | Msg.hpp recvFrameDeadline |
header+payload 共享总超时,防 Slowloris |
| 入口参数边界校验 | Msg.hpp/MsgServer.hpp/AsyncQueue.hpp/Rpc*.hpp |
timeout/port/配置项全范围校验,拒绝非法输入 |
| UTF-8 载荷校验 | Msg.hpp isValidUtf8() |
与 Python decode('utf-8') 对齐,拒绝非法字节序列 |
| 最大连接数限制 | MsgServer.hpp max_connections=1000 |
超限拒绝新连接 |
| 有界任务队列 | MsgServer.hpp max_queue_size=1000 |
队列满时断开连接,防 OOM |
| per-connection 发送锁 + active 标志 | MsgServer.hpp ConnectionLock |
reply/broadcast 线程安全防帧交错 + active 检查防 fd 复用后发送 |
| 服务端发送超时 SO_SNDTIMEO | MsgServer.hpp _accept_client() |
慢客户端不读数据时 send 超时抛异常,防拒绝服务(P1-2) |
| reply() 失败标记 failed | MsgServer.hpp reply() |
与 broadcast 一致,失败连接由主循环清理(P2-6) |
| RpcClient success 类型校验 | RpcClient.hpp call() |
非 bool 时抛 RpcError,而非未捕获的 boost 异常(P3-1) |
| _close_all_clients 批量关闭也持锁 | MsgServer.hpp _close_all_clients() |
修复批量关闭路径的竞态(P0-2) |
| 新增: ConnectionLock active 标志 | MsgServer.hpp ConnectionLock::active |
reply/broadcast 获取锁后检查 active,彻底消除残余竞态(P1-3) |
| 新增: _server_fd atomic 访问 | MsgServer.hpp std::atomic<int> _server_fd |
消除 stop() 与 _event_loop 的数据竞争 UB(P2-7) |
| 新增: start() 空回调校验 | MsgServer.hpp start() |
防止 bad_function_call 异常(P2-8) |
| 新增: _accept_client 先配置后注册 | MsgServer.hpp _accept_client() |
socket 选项在加入 _clients 前设置完整(P3-3) |
| 同一连接消息串行 | MsgServer.hpp _busy 集合 |
回调执行中暂停该连接 select 监听 |
| JSON 解析深度限制 | RpcServer.hpp opts.max_depth=32 |
防深度嵌套 JSON 导致栈溢出 |
| 异常信息脱敏 | RpcServer.hpp _sanitize_error() |
过滤绝对路径,完整异常由 on_error 钩子记录 |
| RpcClient 响应格式校验 | RpcClient.hpp |
校验 success 字段,非法响应抛 RpcError |
| worker 线程异常隔离 | MsgServer.hpp _worker_loop() |
handler 异常仅断开该连接,不影响服务 |
| select FD_SETSIZE 检查 | MsgServer.hpp _accept_client() |
fd >= FD_SETSIZE 时拒绝连接,防 select 越界 |
| TCP_NODELAY | MsgServer.hpp _accept_client() |
新连接禁用 Nagle 算法,降低小包延迟 |
| MSG_NOSIGNAL 兼容 | Msg.hpp |
macOS 兼容,防止 send 到关闭连接触发 SIGPIPE |
| AsyncQueue 背压保护 | AsyncQueue.hpp max_queue_size |
队列满时丢最旧数据,防内存无限增长 |
| AsyncQueue fail-safe | AsyncQueue.hpp |
handler 异常触发 on_error,不传播到业务线程 |
待修复 / 已知限制(架构层面,非代码缺陷)
| # | 问题 | 优先级 | 说明 |
|---|---|---|---|
| L-1 | 无认证/鉴权机制 | P0(已缓解) | 仅依赖绑定 127.0.0.1 网络层隔离,任意本地进程可调用所有路由。设计如此,内网微服务信任模型。 |
| L-2 | 无 TLS 加密传输 | P1 | 内网明文 TCP,进程间通信可被本地 root 用户抓包。如跨主机部署需增加 TLS 层。 |
| L-3 | 同步模式(workers=0)仍在主线程阻塞 recv | P2 | 无线程池时无法并发,Slowloris 防护依赖 recv_timeout 截止时间(10秒)。建议生产环境使用 workers>0。 |
| L-4 | select 模型上限 FD_SETSIZE(通常 1024) | P2 | 高并发场景建议改用 epoll/kqueue,或在 fd 超限时告警/拒绝。已在 _accept_client 加检查防止越界。 |
| L-5 | 安全事件无结构化日志 | P3 | 超时、超大消息、UTF-8 校验失败、JSON 解析失败等仅 fprintf(stderr),无可观测性。建议接入结构化日志。 |
| L-6 | pmsg_recv(int) 单参数版本仍依赖外部超时设置 | P3 | 已添加文档警告,推荐使用 deadline 模式。考虑未来版本弃用单参数版本或内部设置安全默认超时。 |
| L-7 | AsyncQueue 关闭时 flush 失败数据只记 on_error | P3 | _flush_all() 中 handler 失败后数据不重试,符合非关键路径设计,但应确保 on_error 有持久化/告警机制。 |
| L-8 | 半连接(只发部分帧头)仍可占用一个 worker 线程 recv_timeout 秒 | P2 | workers>0 时消息接收在 worker 线程完成,慢连接可占用 worker 最多 recv_timeout(默认10秒)。数量受 max_connections 与 recv_timeout 有界约束,已缓解。可用更小 recv_timeout 或独立接收线程池进一步收紧。 |
| L-9 | pmsg_send 发送端不校验 UTF-8,与 Python 版 encode('utf-8') 不对称 | P3 | 接收端已在 pmsg_recv 校验 UTF-8(P2-4),非法载荷会被远端断开。发送端未校验,若业务传入二进制可能被远端拒绝。若存在纯二进制传输场景,发送端校验会破坏兼容,故暂不强制,仅记录。 |
| L-10 | pmsg_send 公共 API 依赖 SO_SNDTIMEO | P3 | sendAll() 在 send() 返回 EAGAIN 时抛超时异常,但前提是 socket 已设置 SO_SNDTIMEO。已添加文档警告,MsgClient 和 MsgServer 内部均已设置。直接使用 pmsg_send 的外部调用方需自行设置。 |
| L-11 | MsgServer 未设置 TCP keepalive | P3 | 客户端崩溃(不发送 FIN)时连接会残留直到 select 检测异常或 send 失败。建议生产环境设置 SO_KEEPALIVE 以更快检测死连接。 |
| L-12 | _remove_client 同步模式下无总超时限制 | P3 | 同步模式下 _handle_client 中 pmsg_recv 有 recv_timeout 限制,但回调执行无超时。慢回调会阻塞事件循环。建议将慢操作移至线程池模式或业务层自行限时。 |
变更记录
2026-08-07(第四轮审计修复)
代码变更文件:
| 文件 | 变更内容 |
|---|---|
| MsgServer.hpp | P1修复: 引入 ConnectionLock 结构(mutex + atomic active),reply()/broadcast() 获取锁后检查 active,_remove_client()/_close_all_clients() close 前设置 active=false,彻底消除残余竞态(P1-3);P2修复: _server_fd 改为 std::atomic<int>,所有访问使用 .load()/.store()/.exchange()(P2-7);P2修复: start() 校验 on_message 非空(P2-8);P3修复: _accept_client() 中 setsockopt 移到加入 _clients 之前(P3-3) |
| RpcServer.hpp | P3修复: _dispatch() 区分 _route 字段缺失和类型不符的错误信息(P3-2) |
| Msg.hpp | 文档: pmsg_send 添加 SO_SNDTIMEO 前置条件警告(L-10) |
2026-08-07(第三轮审计修复)
代码变更文件:
| 文件 | 变更内容 |
|---|---|
| MsgServer.hpp | P1修复: _accept_client() 对新连接设置 SO_SNDTIMEO=5s,慢客户端不读数据时 send 超时抛异常,防服务级 DoS(P1-2);P2修复: reply() 发送失败标记 _failed,由主循环清理连接(P2-6) |
| RpcClient.hpp | P3修复: call() 调用 as_bool() 前校验 success 字段为 bool,类型不符抛 RpcError(P3-1) |
2026-08-07(第二轮审计修复)
代码变更文件:
| 文件 | 变更内容 |
|---|---|
| Msg.hpp | 新增 isValidUtf8()/validateTimeout()/validatePort();recvFrameDeadline 添加参数校验和UTF-8校验;openConnection 添加 port/timeout 校验;pmsg_recv(int) 添加UTF-8校验和文档警告;MsgClient 构造/call 添加参数校验 |
| MsgServer.hpp | P0修复: _close_all_clients() 改为持 send_lock 关闭并 shutdown;构造函数添加全量配置校验 |
| AsyncQueue.hpp | 添加 / 头文件;构造函数校验 handler 非空、batch_size、flush_interval、poll_timeout 范围 |
| RpcClient.hpp | 构造函数校验 timeout、retry_interval 范围 |
| RpcServer.hpp | add_route() 校验 name 非空、handler 非空 |
审计结论
四轮审计累计发现 16 个问题,全部已修复(100%)。
- 第一轮(2026-08-06): 3 个问题(P0 竞态、P1 Slowloris、P2 脱敏正则)已修复。
- 第二轮(2026-08-07): 5 个问题(P0 批量关闭竞态回归、4 个 P2 健壮性)已修复。
- 第三轮(2026-08-07): 3 个新问题(P1 服务端发送超时缺失、P2 reply 失败未清理、P3 RpcClient success 类型校验)已修复。
- 第四轮(2026-08-07): 5 个新问题(P1 reply/broadcast 残余竞态、P2 _server_fd 数据竞争、P2 start 空回调校验、P3 _route 错误信息、P3 setsockopt 顺序)已修复。
结合四轮审计结果,libmsg 核心通信层的安全状况良好:
- 内存安全: 无缓冲区溢出、无 use-after-free(fd 生命周期通过 ConnectionLock + shared_ptr + atomic active 严格管理)
- DoS 防护: 64MiB 消息上限、deadline 模式接收超时(防Slowloris)、服务端发送超时 SO_SNDTIMEO(防慢客户端耗尽资源)、max_connections 连接数限制、max_queue_size 队列上限、poll_timeout 防忙轮询、全量参数边界校验
- 竞态安全: 所有 fd 关闭路径均持有 per-connection send_mtx 并设置 active=false,reply/broadcast 获取锁后检查 active,彻底消除 CWE-362 竞态(含正向和反向场景);_server_fd 使用 atomic 消除数据竞争 UB
- 协议健壮性: UTF-8 收发端校验与 Python 版对齐、RpcClient 响应字段类型校验统一抛 RpcError、RpcServer _route 类型校验
- 异常安全: worker 线程异常隔离、AsyncQueue fail-safe、Handler 异常不崩溃服务、RPC 错误脱敏、reply/broadcast 失败连接统一清理、start 空回调校验
遗留的 L-1~L-12 项为架构层面的已知限制/设计决策,非代码缺陷,可根据实际部署需求决定是否增强。