runc 1.4.2 超深度分析 — Seccomp安全、CRIU检查点、网络、辅助子系统
源码:seccomp/seccomp_linux.go(350行) +seccomp/patchbpf/enosys_linux.go(746行) +criu_linux.go(1209行) +network_linux.go(232行) + 辅助模块
一、Seccomp — 系统调用过滤
1.1 InitSeccomp 核心流程
funcInitSeccomp(config*configs.Seccomp)(int,error){// ─── Step 1: 解析默认动作 ───defaultAction,_:=getAction(config.DefaultAction,config.DefaultErrnoRet)// SCMP_ACT_KILL / SCMP_ACT_ERRNO / SCMP_ACT_ALLOW / SCMP_ACT_TRACE / SCMP_ACT_LOG / SCMP_ACT_NOTIFY// ─── Step 2: Notify 限制检查 ───// - write 系统调用不能使用 SCMP_ACT_NOTIFY (会死锁)// - 默认动作不能是 SCMP_ACT_NOTIFYfor_,call:=rangeconfig.Syscalls{ifcall.Action==configs.Notify{ifcall.Name=="write"{return-1,errors.New("SCMP_ACT_NOTIFY cannot be used for the write syscall")}}}// ─── Step 3: 创建 BPF Filter ───filter,_:=libseccomp.NewFilter(defaultAction)// ─── Step 4: 添加架构 ───for_,arch:=rangeconfig.Architectures{scmpArch,_:=libseccomp.GetArchFromString(arch)filter.AddArch(scmpArch)}// ─── Step 5: 添加系统调用规则 ───for_,call:=rangeconfig.Syscalls{action,_:=getAction(call.Action,call.ErrnoRet)for_,name:=rangecall.Names{syscallID,_:=libseccomp.GetSyscallFromName(name)iflen(call.Args)==0{// 无参数条件 → 直接添加规则filter.AddRule(syscallID,action)}else{// 有参数条件 → 添加条件规则conditions:=buildConditions(call.Args)filter.AddRuleConditional(syscallID,action,conditions)}}}// ─── Step 6: 设置优化标志 ───filter.SetTsync(true)// 线程同步应用filter.SetLogBit(true)// 记录被拒绝的系统调用// ─── Step 7: 补丁 BPF (ENOSYS stub) ───iferr:=patchbpf.PatchSeccomp(config,filter);err!=nil{return-1,err}// ─── Step 8: 加载 Filter 到内核 ───iferr:=filter.Load();err!=nil{return-1,err}// ─── Step 9: 返回 Notify fd ───seccompFd:=-1ifconfig.HasNotify(){seccompFd,_=filter.GetNotifyFd()// SCMP_ACT_NOTIFY 需要 fd 来接收通知}returnseccompFd,nil}
Seccomp 执行时序
二、patchbpf/enosys — ENOSYS 错误 Stub
2.1 问题
当多架构容器在 64 位系统上运行时,32 位兼容系统调用可能不存在。如果 seccomp 规则阻止了这些调用,进程可能收到错误的 errno 而非 ENOSYS。
2.2 PatchSeccomp 解决方案
PatchSeccomp 的作用: 1. 检查 seccomp 规则是否包含 32-bit 兼容系统调用 2. 为缺失的兼容调用添加 ENOSYS 返回规则 3. 确保程序收到正确的 ENOSYS 而非其他错误
三、CRIU — Checkpoint/Restore
3.1 CRIU 集成架构
3.2 Checkpoint 流程
func(c*Container)Checkpoint(criuOpts*CriuOpts)error{c.m.Lock()deferc.m.Unlock()// ─── Step 1: 状态检查 ───status,_:=c.currentStatus()ifstatus==Stopped||status==Paused{returnfmt.Errorf("container must be running to checkpoint")}// ─── Step 2: 创建 CRIU 管理器 ───criuMgr:=&criuMgr{container:c,criuOpts:criuOpts,allowTCP:criuOpts.AllowOpenTCP,allowUnix:criuOpts.AllowExternalUnixConnections,}// ─── Step 3: 准备检查点目录 ───imageDir:=criuOpts.ImagesDirectoryifimageDir==""{imageDir=getDefaultImagePath()}// ─── Step 4: 运行 CRIU ───returncriuMgr.dump(imageDir)}
3.3 Restore 流程
func(c*Container)Restore(process*Process,criuOpts*CriuOpts)error{c.m.Lock()deferc.m.Unlock()// ─── Step 1: 状态检查 ───status,_:=c.currentStatus()ifstatus!=Stopped{returnfmt.Errorf("container must be stopped to restore")}// ─── Step 2: CRIU 恢复 ───// 恢复进程树 + namespace + cgroup + 内存状态iferr:=c.restore(process,criuOpts);err!=nil{returnerr}// ─── Step 3: 更新状态 ───c.state=&restoredState{c:c,imageDir:criuOpts.ImagesDirectory}returnnil}
CRIU 支持的特性
| 特性 | 条件 | 描述 |
|---|
| TCP 连接 | allowTCP | 保持打开的 TCP 连接 |
| Unix 连接 | allowUnix | 保持打开的 Unix socket |
| Shell Job | shellJob | 恢复 shell 作业 |
| File Locks | fileLocks | 恢复文件锁 |
| Network | 管理 | 恢复网络命名空间 |
四、Network — 网络策略
4.1 网络策略接口
typestrategyinterface{initialize(config*network)error}
4.2 三种网络策略
| 策略 | 函数 | 描述 |
|---|
| veth | (*vethStrategy).initialize() | 创建 veth pair,一端在容器,一端在宿主机 |
| loopback | (*loopbackStrategy).initialize() | 激活 lo 接口 |
| bridge | (未直接实现) | 通过 veth + route 实现 |
veth 策略流程
五、Capabilities — Linux 能力
typeCapabilitiesstruct{Bounding[]string// 限制可获得的能力集Effective[]string// 实际生效的能力集Inheritable[]string// 可继承的能力集Permitted[]string// 允许的能力集Ambient[]string// 环境能力集 (Linux 4.3+)}
Capability 操作顺序
1. ApplyBoundingSet() → prctl(PR_CAPBSET_DROP) 限制 bounding set 2. SetKeepCaps() → prctl(PR_SET_KEEPCAPS) 保留 caps 给 setuid 3. setupUser() → setgid + setuid 切换用户 4. ClearKeepCaps() → prctl(PR_SET_KEEPCAPS,0) 清除保留标记 5. ApplyCaps() → capset() 设置最终能力集
六、exeseal — 二进制克隆安全
6.1 问题
如果容器进程能覆写 /proc/self/exe 指向的 runc 二进制文件,其他容器可能执行恶意代码。
6.2 解决方案
funcCloneSelfExe(stateDirstring)(*os.File,error){// 将 /proc/self/exe 克隆到 stateDir 下的临时文件// 使用 memfd_create 或 overlayfs// runc init 使用克隆后的二进制执行// 容器无法影响克隆后的文件}
exeseal 两种实现
| 实现 | 函数 | 条件 |
|---|
| memfd_create | clonedBinaryMemfdCreate() | 内核 >= 3.17 |
| overlayfs | clonedBinaryOverlayfs() | 降级方案 |
七、pathrs — 安全路径操作
7.1 问题
TOCTOU (Time-of-check to time-of-use) 攻击:检查和操作之间路径可能被替换。
7.2 解决方案
// 使用 open_tree(2) + O_PATH 获取安全的路径引用// 然后通过 /proc/self/fd/N 操作,避免路径解析funcProcSelfOpen(pathstring,flagsint)(*os.File,error){// 1. 打开 /proc/thread-self/ 作为 fd// 2. openat(fd, path, flags|O_NOFOLLOW)// 3. 返回安全的 fd}
八、Intel RDT — 资源监控
typeManagerstruct{config*configs.Config idstringpathstring// resctrl 文件系统路径}func(m*Manager)Set(config*configs.Config)error{// 设置 L3 cache allocation (CAT)// 设置 MBA (Memory Bandwidth Allocation)// 写入 resctrl 文件系统}func(m*Manager)GetStats()(*Stats,error){// 读取 CMT (Cache Monitoring Technology) 统计// 读取 MBM (Memory Bandwidth Monitoring) 统计}
九、辅助工具汇总
| 模块 | 行数 | 核心功能 |
|---|
utils/cmsg.go | 80 | SendFd/RecvFd (SCM_RIGHTS) |
utils/utils.go | 100 | WriteJSON/ResolveRootfs/DecodeState |
utils/utils_unix.go | 287 | CloseExecFrom/UnsafeCloseFrom/NewSockPair |
internal/pathrs/ | 170 | 安全路径操作 (openat/O_PATH) |
internal/sys/ | 60 | WriteSysctls/verifyInode |
internal/userns/ | 160 | User namespace fd + ID 映射 |
system/linux.go | 50 | SetKeepCaps/ClearKeepCaps/GetParentDeathSignal |
system/proc.go | 30 | Stat (进程状态查询) |
keys/keyctl.go | 50 | JoinSessionKeyring/ModKeyringPerm |
logs/logs.go | 30 | 日志管道初始化 |
env.go | 40 | prepareEnv (HOME/PATH 设置) |
十、设计模式总结
| # | 模式 | 体现 |
|---|
| 1 | 策略模式 | Network strategy (veth/loopback/bridge) |
| 2 | BPF 规则链 | Seccomp filter + 多条规则 |
| 3 | Notify 代理 | SCMP_ACT_NOTIFY → seccomp agent |
| 4 | 二进制克隆 | exeseal 防篡改 |
| 5 | 安全路径 | pathrs 防 TOCTOU |
| 6 | RPC 集成 | CRIU protobuf RPC |
| 7 | ENOSYS 补丁 | patchbpf 自动补充缺失调用 |
| 8 | 资源监控 | Intel RDT CAT/MBM/CMT |
| 9 | fd 传递 | SCM_RIGHTS (cmsg) |
| 10 | 线程安全 | LockOSThread + SetTsync |