模块间必须用进程隔离而非线程隔离,因线程共享地址空间易导致单点失陷后横向移动;进程具备VM隔离、独立信号处理与资源限制,能有效遏制ROP攻击扩散,核心服务应通过fork()+execve()启动并严格管控IPC与权限。

模块间用进程隔离而非线程隔离是底线
单点失陷后横向移动的前提,往往是共享地址空间。C++ 线程共用堆、全局变量、异常栈帧,一个模块 free() 了别人的指针,或 std::terminate() 被意外触发,整个进程就挂了。进程则天然有 VM 隔离、独立信号处理、独立 setrlimit() 控制。哪怕某个模块被 ROP 攻击劫持,也很难直接篡改其他模块内存。
实操建议:
- 核心服务(如密钥管理、鉴权)必须运行在独立进程,通过
fork()+execve()启动,不继承父进程的 fd 表和环境变量 - 避免用
std::thread或pthread_create()实现“模块”,那是并发单位,不是安全边界 - Windows 下对应使用
CreateProcess(),禁用CREATE_SUSPENDED和INHERIT_HANDLES
IPC 接口必须做严格输入校验和沙箱化反序列化
进程间通信(IPC)是权限提升的高危通道。很多团队用 protobuf 或 JSON 做跨模块消息,但没意识到:反序列化本身可能触发虚函数调用、STL 容器构造、甚至 operator new——这些都在目标进程上下文中执行,攻击者可借机绕过进程隔离。
实操建议:
- 禁止在 IPC 入口直接调用
ParseFromString()或json::parse();先用白名单正则或长度+字段数预检原始字节流 - 对每个字段做显式范围检查:比如
user_id字段不能是负数、不能超INT32_MAX,字符串长度强制截断并 null-terminate - 关键模块(如支付网关)只接受 flatbuffer 的
Verify()通过的数据,且flatbuffers::Verifier构造时传入max_depth=2和max_tables=16
Capability-based 权限模型比 UID/GID 更适合 C++ 模块
Linux 的 UID/GID 是粗粒度的,一个模块拿到 root 就等于拿到全部设备节点访问权。而 C++ 模块通常只需要某几个 syscalls:比如日志模块只需 write() 到特定 socket,不需要 openat()。用 libcap 或 seccomp-bpf 绑定能力更精准。
实操建议:
- 启动子进程前,用
cap_set_flag()只赋予CAP_NET_BIND_SERVICE(仅当需绑定 1024 以下端口),立刻cap_set_proc()生效,然后cap_free() - 配合
prctl(PR_SET_NO_NEW_PRIVS, 1)防止后续execve()提权 - 对高危模块(如解析 PDF 的渲染器)加载 seccomp-bpf 过滤器,只放行
read、write、close、exit_group四个 syscall
崩溃传播链必须被显式切断
默认情况下,一个模块进程 abort() 会发 SIGABRT 给整个进程组,父进程若没忽略,也会退出。更隐蔽的是:主模块用 waitpid() 等待子模块时,若子模块因 SIGSEGV 退出,父模块若未检查 WIFSIGNALED() 就继续调用 sendmsg(),可能把损坏的消息发给下游。
实操建议:
- 所有模块进程启动后立即调用
signal(SIGPIPE, SIG_IGN)和signal(SIGABRT, SIG_DFL),避免信号被继承污染 - 父模块用
waitpid(pid, &status, WNOHANG)轮询,检测到WIFSIGNALED(status)时,立刻关闭对应 IPC socket 并标记该模块为不可用,不重试、不转发请求 - 关键路径上禁用
std::set_terminate(),改用std::set_unexpected()(C++17 已弃用,但某些旧编译器仍支持)加std::abort(),确保异常逃逸时进程干净终止
真正难的不是写隔离代码,而是让每个模块的启动脚本、systemd service 文件、Dockerfile 都一致地应用 no-new-privs、SeccompProfile 和 ReadOnlyPaths。漏掉任意一层,最小权限就形同虚设。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











