- 容器运行时
- 云原生
【免费下载链接】youki
A container runtime written in Rust
本文以 docs/src/user/libseccomp.md 为骨架,结合 youki 仓库中 libcontainer 的 seccomp 模块源码、初始化流程与实验性纯 Rust 实现,系统讲解 seccomp 内核特性、
libseccompFFI 绑定的由来与边界,以及 youki 如何在容器 init 进程中把 OCI seccomp 配置翻译成真实的内核过滤器。读完你可以掌握:seccomp 在容器场景下的作用机制、youki 中libseccompcrate 的依赖与特性开关、initialize_seccomp的完整实现链路,以及仓库中围绕该主题的测试与替代方案。
一、先理解背景:seccomp 是什么
在深入代码之前,先明确 seccomp 这一内核特性的本质。正如开发者文档 docs/src/developer/libseccomp.md 所描述的:
Seccomp 是 Linux 内核提供的一项特性,它允许进程进行一次性、不可逆地切换到安全模式。在该模式下,进程能够发起的系统调用(syscall)受到限制,对文件描述符的操作也受到约束。具体来说,进程被限制为只能执行
exit、sigreturn,以及读写已打开的文件描述符。
也就是说,seccomp 让一个进程在内核层面被"隔离"起来,限制它与系统其余部分交互的方式——即使进程本身被攻破、代码被恶意篡改,它也无法随意调用execve、socket、open等危险系统调用,攻击面被大幅收窄。
seccomp 的完整定义可参考其 man page(man 2 seccomp),其核心机制是:
- 过滤器(filter):以经典的 BPF(cBPF)程序形式描述"哪些系统调用允许、哪些拒绝";
- 动作(action):命中过滤器后执行的动作,如
KILL、ERRNO、TRACE、ALLOW、NOTIFY等; - 单向性:过滤器一旦安装(通过
prctl(PR_SET_SECCOMP)或seccomp(2)系统调用)便无法撤销,除非进程先设置了no_new_privileges或有相应特权。
二、libseccompcrate 的定位:绑定层而非实现层
原文档开篇即点明该 crate 的本质:
这个 crate 并没有实际实现任何特定功能,而是为 seccomp 模块提供 Rust FFI 绑定。这些绑定主要通过 rust-bindgen 对 seccomp 的 C 头文件生成,随后针对发现的问题进行人工修正。由于 rust-bindgen 在处理 C 函数宏(function macros)时存在一些问题,这些宏被手工修复。
翻译成工程语言:
- 它不重新实现 seccomp 策略引擎,而是把 C 库 libseccomp 的 API(
seccomp_init、seccomp_rule_add、seccomp_load、seccomp_arch_add等)以unsafe extern函数和对应数据结构的形式暴露给 Rust; - 绑定来源是 rust-bindgen:即对 libseccomp 提供的 C 头文件自动生成
FFI声明,属于典型的"自动生成 + 人工修补"路线; - 人工修正集中在 C 函数宏:C 头文件中大量使用宏(如把
SCMP_ACT_ERRNO(x)、SCMP_ARCH_X86_64等定义为带参数的宏),rust-bindgen 无法直接翻译它们,youki 团队在生成结果上手工补齐了这些类型常量与构造函数的映射。
因此,在 youki 的代码中,你永远不会直接写seccomp_init(...)这样的 C 调用,而是使用libseccomp::ScmpFilterContext、ScmpAction、ScmpArch、ScmpArgCompare、ScmpCompareOp、ScmpSyscall这些 Rust 安全的封装类型。这正是 FFI 绑定层的价值:把 C 的指针与错误码语义,包装成 Rust 的类型安全接口。
从仓库结构看,工作区根 Cargo.toml 将libseccomp声明为共享依赖:
libseccomp = "0.4.0"即使用当前 crates.io 上最新的 0.4.x 版本。
三、依赖关系与特性开关:如何引入与关闭
libseccomp绑定并非直接挂在 youki 主 crate 下,而是作为libcontainer(容器控制核心库)的可选依赖。看 crates/libcontainer/Cargo.toml:
[features] default = ["systemd", "v2", "v1", "libseccomp"] libseccomp = ["dep:libseccomp"]同时在依赖段中它是 optional 的:
libseccomp = { workspace = true, optional = true }这带来两个重要工程含义:
- 默认开启:
default特性集合包含libseccomp,因此常规构建(cargo build)会自动启用 seccomp 支持; - 可裁剪:通过
--no-default-features -F v2之类的组合可以关闭它。关闭后,libcontainer 中所有 seccomp 相关代码都会被#[cfg(feature = "libseccomp")]条件编译掉,例如 crates/libcontainer/src/lib.rs 中的mod seccomp;声明。
3.1 为什么需要关闭它:musl 静态编译的限制
libseccomp 是一个 C 库,无法在纯 musl 静态环境下链接。这一点在 crates/libcontainer/README.md 中有明确说明:
要以 musl 构建,必须先移除 libseccomp 依赖,因为它会引用无法用 musl 构建的共享库(
libseccomp)。
具体做法是使用--no-default-features关闭默认特性,再用-F显式开启需要的特性(如v2)。README 给出了完整的 nightly + musl 构建命令:
# 为 rustup +nightly 添加 musl 目标 rustup +nightly target add $(uname -m)-unknown-linux-musl # 安装 nightly musl 工具链 rustup +nightly toolchain install nightly-$(uname -m)-unknown-linux-musl # 使用 -Zbuild-std 构建 musl 标准库 cargo +nightly build -Zbuild-std --target $(uname -m)-unknown-linux-musl --no-default-features -F v2 cargo +nightly build --target $(uname -m)-unknown-linux-musl --no-default-features -F v2这也解释了 why:当目标平台(或出于体积、无 glibc 环境考虑)无法链接 C 库时,youki 仍可编译运行,只是 seccomp 能力被降级——运行时会输出seccomp not available之类的告警(见下文初始化流程)。
四、核心实现剖析:initialize_seccomp全流程
youki 把"OCI seccomp 配置 → 内核过滤器"的翻译工作全部封装在 crates/libcontainer/src/seccomp/mod.rs 的initialize_seccomp函数中。它的输入是oci_spec::runtime::LinuxSeccomp(OCI 运行时规范中linux.seccomp的结构化 Rust 表示),输出是Result<Option<io::RawFd>>——当配置中包含NOTIFY动作时返回监听 fd。
整个流程可以拆成七个步骤:
4.1 前置校验:check_seccomp
在创建任何过滤器之前,先做合法性检查(mod.rs#L119-L147):
NOTIFY不允许作为默认动作:如果default_action == ScmpActNotify,直接返回NotifyAsDefaultAction错误。原因在源码注释中写得很清楚:过滤器一旦以 notify 创建,容器进程就必须把返回的 fd 交给另一个进程去处理;而这又依赖write系统调用,若默认动作就是 NOTIFY,write 也会被过滤拦截,进程将直接卡死。runc同样禁止这种做法。NOTIFY不允许用于write系统调用:遍历所有 syscall 规则,若某条规则动作为ScmpActNotify且名字是write,返回NotifyWriteSyscall错误,理由同上(read/close 也被刻意保留,因为接收方处理完通知后需要正常放行这些调用)。
4.2 创建过滤器上下文
let default_action = translate_action(seccomp.default_action(), seccomp.default_errno_ret())?; let mut ctx = ScmpFilterContext::new(default_action).map_err(...)?;对应 C 层的seccomp_init(action):以一个默认动作创建过滤器上下文。默认动作决定"未命中任何规则时怎么办",通常容器配置为SCMP_ACT_ALLOW(默认放行,仅拦截黑名单)或SCMP_ACT_ERRNO(默认拒绝,仅放行白名单)。
4.3 设置过滤器 flag
OCI 规范的seccomp.flags支持四种内核 flag,翻译逻辑(mod.rs#L161-L176):
OCI flag(LinuxSeccompFilterFlag) | libseccomp 调用 | 内核含义 |
|---|---|---|
SECCOMP_FILTER_FLAG_LOG | set_ctl_log(true) | 未命中的 syscall 记录到审计日志(SECCOMP_FILTER_FLAG_LOG) |
SECCOMP_FILTER_FLAG_TSYNC | set_ctl_tsync(true) | 同步应用到进程组所有线程(SECCOMP_FILTER_FLAG_TSYNC) |
SECCOMP_FILTER_FLAG_SPEC_ALLOW | set_ctl_ssb(true) | 允许使用被 SSB(Spectre v2)缓解禁用的 spec 存储绕过缓解(SECCOMP_FILTER_FLAG_SPEC_ALLOW) |
SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV | set_ctl_waitkill(true) | 处于SECCOMP_RET_USER_NOTIF等待中的进程可被 kill 信号打断(SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV) |
注意这里set_ctl_*系列正是 libseccomp 提供的"上下文属性设置"API 的绑定,属于文档所述"FFI 绑定"在 youki 中的典型用法。
4.4 添加架构
for &arch in architectures { ctx.add_arch(translate_arch(arch))... }通过translate_arch把 OCI 的Arch枚举映射为 libseccomp 的ScmpArch。完整的映射表见 mod.rs#L55-L82,覆盖了Native、X86、X86_64、X32、Arm、Aarch64、Mips/Mips64/Mips64n32/Mipsel*、Ppc/Ppc64/Ppc64le、S390/S390x、Riscv64、Parisc*、Loongarch64、M68k、Sh、Sheb等全部 OCI 规范定义的架构,为跨架构容器镜像(如 x86_64 上跑 arm64 用户态)提供过滤支持。
4.5 关闭自动 NNP:set_ctl_nnp(false)
这是 youki 一个精心设计的细节(mod.rs#L186-L194):
ctx.set_ctl_nnp(false)libseccomp 的SCMP_FLTATR_CTL_NNP属性控制seccomp_load时是否自动通过prctl设置no_new_privileges位。正常情况下这很方便,但对 OCI 运行时而言:如果 spec 的process.noNewPrivileges没有显式开启,运行时就不应该替用户设置它(这属于进程语义的一部分)。所以 youki 主动关掉自动行为,把 NNP 的决策权交还给 init 流程自身(见第五节)。如果因特权不足导致 load 失败,就让它失败——不悄悄放宽安全语义。
4.6 添加 syscall 规则:无参数与带参数
遍历seccomp.syscalls()中的每条规则(mod.rs#L196-L298):
- 跳过冗余规则:如果某条规则的动作与默认动作相同,说明它不改变任何行为,直接
continue并打 warn——这既省一次内核过滤器空间,也避免 libseccomp 在部分内核上对"与默认动作相同的规则"报错。 - 按名解析 syscall:
ScmpSyscall::from_name(name)。若解析失败(例如当前内核版本不支持该 syscall),源码选择skip 并告警而非报错:"如果无法按名解析,很可能内核不支持这个 syscall,跳过是安全的"。 - 无参数规则:
ctx.add_rule(action, sc),对应 C 层seccomp_rule_add,对 syscall 的所有调用形态生效。 - 带参数规则:
ctx.add_rule_conditional(action, sc, &comparators),对应seccomp_rule_add_exact_array带条件版本。每个条件用ScmpArgCompare::new(index, op, value)描述"第 index 个参数需满足某比较关系"。
参数规则中还有一个与 runc 对齐的细节:libseccomp 允许一条规则里有多个参数比较,但每个参数索引只能比较一次。当 OCI 配置里出现同一参数索引被比较多次时(源码用HashSet检测重复 index),youki 遵循 runc 的行为——把每个条件拆成独立的规则分别添加(add_rule_conditional每次只传单个 comparator)。源码注释直接引用了 libseccomp 的seccomp_rule_add(3)手册和 runc 的seccomp_linux.go作为依据。
4.7 加载过滤器并(可选)返回 notify fd
ctx.load()...; // 对应 seccomp_load,安装过滤器,此后不可撤销 let fd = if is_notify(seccomp) { // 配置中存在 ScmpActNotify 规则时 Some(ctx.get_notify_fd()...) // 拿到 seccomp 通知 fd } else { None };load()通过SECCOMP_SET_MODE_FILTER安装过滤器。源码注释指出其前置条件:调用线程要么在其用户命名空间内拥有CAP_SYS_ADMIN,要么已经设置了no_new_privs位——这正是 4.5 节中 NNP 决策的意义所在。
五、动作与比较符:OCI 语义到 libseccomp 的翻译
5.1 动作(Action)翻译表
translate_action(mod.rs#L84-L105)处理"动作 + errno 返回值"两个维度。errno 缺省时默认使用EPERM(libc::EPERM):
| OCI 动作 | libseccomp 动作 | 行为 |
|---|---|---|
SCMP_ACT_KILL | ScmpAction::KillThread | 终止触发过滤器的线程 |
SCMP_ACT_KILL_PROCESS | ScmpAction::KillProcess | 终止整个进程(内核 4.14+) |
SCMP_ACT_KILL_THREAD | ScmpAction::KillThread | 同 KILL,仅杀线程 |
SCMP_ACT_TRAP | ScmpAction::Trap | 向触发线程发送SIGSYS |
SCMP_ACT_ERRNO | ScmpAction::Errno(errno) | 返回指定 errno(默认EPERM) |
SCMP_ACT_TRACE | ScmpAction::Trace(errno) | 通知 tracer(ptrace),errno 需转为 i16,失败时报TraceAction |
SCMP_ACT_ALLOW | ScmpAction::Allow | 放行 |
SCMP_ACT_NOTIFY | ScmpAction::Notify | 挂起 syscall,等待用户态通知处理(需要SECCOMP_FILTER_FLAG_NEW_LISTENER) |
SCMP_ACT_LOG | ScmpAction::Log | 记录审计日志后放行(内核 4.14+) |
注意ScmpActKill在 OCI 规范中被映射为 libseccomp 的KillThread,这是为了与 runc 保持一致的语义取舍。
5.2 比较符(Operator)翻译表
translate_op(mod.rs#L107-L117)把 OCI 比较运算符映射为ScmpCompareOp:
| OCI 运算符 | libseccomp 比较 | 语义 |
|---|---|---|
SCMP_CMP_NE | NotEqual | 不等于 |
SCMP_CMP_LT | Less | 小于 |
SCMP_CMP_LE | LessOrEqual | 小于等于 |
SCMP_CMP_EQ | Equal | 等于 |
SCMP_CMP_GE | GreaterEqual | 大于等于 |
SCMP_CMP_GT | Greater | 大于 |
SCMP_CMP_MASKED_EQ | MaskedEqual(datum_b) | (value & mask) == datum_b,掩码取自 OCI 规则的valueTwo |
六、在容器 init 流程中的集成:时机与顺序的艺术
seccomp 过滤器一旦安装就不可撤销,且安装本身依赖特权状态,因此安装时机直接决定容器安全模型。youki 在 crates/libcontainer/src/process/init/process.rs 中按process.noNewPrivileges是否显式设置分两条路径:
noNewPrivileges未设置(is_none())时——提前安装:在丢弃 capabilities 之前初始化 seccomp。源码注释:"没有 no_new_privileges,seccomp 就是特权操作,必须在丢 capabilities 之前做。"否则后续 load 会因权限不足失败。此时若libseccomp特性被关闭,则打印seccomp not available, unable to enforce no_new_privileges!告警。noNewPrivileges已设置(is_some())时——尽量晚安装:放到"马上要 exec payload 之前",让过滤器和 exec 之间的 syscall 数量最小化,避免容器进程自身的启动代码意外撞上自己的过滤器。
6.1 NOTIFY 场景:init 进程与主进程的握手
当 seccomp 配置包含NOTIFY规则时,initialize_seccomp返回 notify fd。此时 init 进程通过 sync_seccomp 与主进程协作:
main_sender.seccomp_notify_request(fd):把 fd 通过进程间 channel 发送给主进程(内部走 SCM_RIGHTS 完成 fd 的跨进程复制);- 等待
wait_for_seccomp_request_done():确认主进程已经拿到 fd 并交给 seccomp 监听器; - 确认后 init 进程才安全地
close(fd)(fd 已在主进程侧复制)。
主进程侧的处理在 crates/libcontainer/src/process/container_main_process.rs:handle_seccomp_notify构建容器进程状态(seccomp_listener.rs#L86 的build_container_process_state,将容器映射为 OCI 状态机中的creating或running),连同 notify fd 一起通过sync_seccomp_send_msg(SCM_RIGHTS 消息)发送给用户配置的 seccomp 监听器(seccompListenerPath)。
更值得注意的设计是 InitRequestSequence:主进程把 init 侧的一次性请求按阶段排序——Setup(hooks 与网络设备,无顺序依赖,可乱序到达)→Seccomp(必须在 hooks/网络之后,因为过滤器一旦应用,init 自身后续 syscall 都将受其约束)→Ready。任何乱序或重复请求都会触发UnexpectedInitMessage错误。这个状态机用源码内注释与单元测试(init_request_sequence_rejects_seccomp_while_setup_is_pending等)双重保障了协议顺序。
七、测试与验证:仓库里如何证明它工作
7.1 单元测试(mod.rs#L327-L559)
seccomp 测试很难写:默认的 kill/errno 动作会杀死测试进程本身(Rust 测试框架也依赖 syscall)。youki 的解法是全部在子进程中执行(test_utils::test_in_child_process),并刻意选用getcwd这个"在 seccomp 规则下会干净地返回错误"的 syscall:
test_basic:默认动作ALLOW,对getcwd应用SCMP_ACT_ERRNO且 errno 设为EAGAIN(getcwd自身永远不会返回 EAGAIN,从而可以断定失败来自过滤器)。子进程先prctl::set_no_new_privileges(true),加载过滤器后调用getcwd,断言其错误码恰为EAGAIN;test_moby:直接加载仓库内的真实 fixture crates/libcontainer/src/seccomp/fixture/config.json——一份 Moby 风格的完整 OCI spec(含noNewPrivileges: true、受限 capabilities、/proc掩码路径等),在子进程中加载其 seccomp 配置,验证真实配置可被正确翻译;test_seccomp_notify:getcwd配SCMP_ACT_NOTIFY,断言initialize_seccomp返回了 notify fd;test_seccomp_conditional_rule_multiple_distinct_args:对socket同时约束arg0 == AF_INET与arg1 == SOCK_STREAM两条不同索引条件,验证组合规则;test_seccomp_conditional_rule_duplicate_arg_index:对socket的arg0施加两个条件(== AF_INET且!= AF_UNIX),验证重复索引被拆分为独立规则(与 runc 对齐)的逻辑;test_seccomp_multiple_syscall_entries_for_same_name:同一个socket名字出现两条不同规则的场景。
这些测试全部带#[serial]注解——seccomp 过滤器是进程级的,并行测试会互相污染,串行执行是必要的。
7.2 真实配置样例
fixture 中 seccomp 相关配置的形态(摘自 crates/libcontainer/src/seccomp/fixture/config.json,节选):
{ "linux": { "seccomp": { "defaultAction": "SCMP_ACT_ALLOW", "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"], "syscalls": [ { "names": ["personality"], "action": "SCMP_ACT_ALLOW" }, { "names": ["clone", "clone3"], "action": "SCMP_ACT_ALLOW", "args": [ { "index": 0, "value": 2114060288, "valueTwo": 0, "op": "SCMP_CMP_MASKED_EQ" } ] } ] } } }这正是 OCI 运行时规范中linux.seccomp的 JSON 形态,也是 youki 经oci-speccrate 解析后交给initialize_seccomp的输入格式。
八、面向未来的替代实验:纯 Rust 的 seccomp 实现
仓库中还存在一个名为 experiment/seccomp 的实验性项目,其 README 明确写道:
这是一个为了摆脱 libseccomp 而做的实验项目。
也就是说,libseccompFFI 绑定虽然好用,但毕竟依赖外部 C 库。youki 团队正在探索一条完全用 Rust 实现的路径:直接生成 cBPF 指令、直接调用seccomp(2)系统调用,绕开 libseccomp。该实验位于 experiment/seccomp/src/seccomp.rs:
Seccomp::apply()直接以SECCOMP_SET_MODE_FILTER+SECCOMP_FILTER_FLAG_NEW_LISTENER调用内核,并把 BPF 程序(Vec<Instruction>)提交给内核;NotifyFd封装通知 fd,支持recv()接收通知与success()应答(SECCOMP_IOCTL_NOTIF_SEND等 ioctl);- 定义了
SeccompData、SeccompNotif、SeccompNotifResp、SeccompNotifSizes、SeccompNotifAddfd等repr(C)内核交互结构。
配套的测试展示了它的两种用法(experiment/seccomp/README.md):
# 应用示例 seccomp 过滤器(包含 notify 接收与应答的完整演示) $ cargo test --test filter -- --show-output # 从 OCI JSON 读取配置并导出 BPF 指令 $ cargo test --test readjson -- --show-output其中 tests/filter.rs 通过send_fd/recv_fd(SCM_RIGHTS)演示了 notify fd 的跨进程传递,handle_notifications循环打印每个被拦截 syscall 的 id/pid/nr 并应答;tests/libseccomp_readjson.rs 则展示了"从 OCI 配置 → 过滤器 →export_bpf导出 BPF 字节码"的完整链路,逐 8 字节打印code/jt/jf/k字段。这个实验既是对libseccomp绑定层的对照,也是理解 cBPF 过滤器格式的最佳入门材料——它与 crates/libcontainer/src/seccomp/mod.rs 中translate_action/translate_op/translate_arch等翻译逻辑形成互文,可以对照阅读。
九、总结
从 docs/src/user/libseccomp.md 的三段式描述出发,可以看到 youki 中"seccomp 安全能力"的完整技术栈:
- 绑定层:
libseccompcrate(rust-bindgen 生成 + 人工修复 C 宏)提供类型安全的 FFI 接口,版本 0.4.0,作为 libcontainer 的可选依赖受libseccomp特性开关控制; - 翻译层:
initialize_seccomp把 OCI 规范的LinuxSeccomp(动作、errno、flags、架构、带参数规则)逐项翻译为 libseccomp 调用,并对NOTIFY默认动作、write通知、冗余规则、重复参数索引等边界情况做了与 runc 对齐的精细化处理; - 集成层:init 进程根据
noNewPrivileges是否存在选择"丢特权前"或"exec 前"两个安装时机,NOTIFY 场景通过进程间 channel + SCM_RIGHTS 与主进程握手,再转交用户配置的 seccomp 监听器; - 未来方向:experiment/seccomp 正在探索不依赖 C 库的纯 Rust 实现。
对容器安全感兴趣的同学,可以按此顺序深入:先读 crates/libcontainer/src/seccomp/mod.rs 理解翻译逻辑,再看 crates/libcontainer/src/process/init/process.rs 理解安装时机,最后对照 experiment/seccomp 的纯 Rust 实现理解 cBPF 与内核通知机制的底层细节。
- 容器运行时
- 云原生
【免费下载链接】youki
A container runtime written in Rust
相关推荐
youki架构深度解析:理解Rust容器运行时的核心设计
youki架构深度解析:理解Rust容器运行时的核心设计 youki是用Rust语言实现的OCI容器运行时,它遵循开放容器倡议标准,提供了高效、安全的容器管理能
容器运行时云原生libgit-sys 深度解析:用 Rust FFI 绑定 Git 内部 C API 的概念验证 crate
libgit sys 深度解析:用 Rust FFI 绑定 Git 内部 C API 的概念验证 crate libgit sys 是 Git 仓库 contr
版本控制开发工具CLI5层防护构建容器运行时安全屏障:从内核隔离到应用沙箱的深度防御实践
5层防护构建容器运行时安全屏障:从内核隔离到应用沙箱的深度防御实践 容器技术在现代软件开发中扮演着关键角色,而确保容器运行时安全是保障整个系统稳定的核心环节。本
操作系统虚拟化系统编程网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考