[toc]
kernel/seccomp.c 安全计算模式(Secure Computing Mode) 系统调用防火墙
历史与背景
这项技术是为了解决什么特定问题而诞生的?
Seccomp(Secure Computing Mode)是为了解决一个核心的安全问题而诞生的:如何安全地运行不可信的代码。 在复杂的软件环境中,一个进程被赋予了访问数百个系统调用(syscall)的能力,这些系统调用是用户空间程序与内核交互的接口。 然而,大多数程序在其整个生命周期中只需要使用这些系统调用中的一小部分。 如果一个程序(例如,一个处理外部输入的Web浏览器渲染进程)被恶意代码攻陷,攻击者就可以利用那些该程序本不需要、但内核依然暴露给它的系统调用来进一步攻击和破坏整个系统。
Seccomp通过提供一种机制来限制一个进程可以调用的系统调用集合,从而减少内核的攻击面。 它的核心思想是实施“最小权限原则”:只授予程序执行其核心功能所必需的最小权限集合。 这样,即使程序被攻破,攻击者能造成的损害也因为其可用的系统调用极其有限而受到严格控制。 这对于构建应用程序沙箱(sandbox)至关重要。
它的发展经历了哪些重要的里程碑或版本迭代?
Seccomp的发展经历了两个主要阶段:
模式1:严格模式 (Strict Mode)
- 该模式于2005年在Linux内核2.6.12版本中被引入。 最初由Andrea Arcangeli为网格计算场景设计,旨在安全地“出租”CPU算力。
- 在此模式下,一旦启用,进程只能使用四个极其基础的系统调用:
read()、write()、exit()和sigreturn()。 任何尝试调用其他系统调用的行为都会导致进程被内核以SIGKILL信号终止。 - 启用方式最初是通过写入
/proc/self/seccomp文件,后来改为使用prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)系统调用。
模式2:过滤模式 (Filter Mode / seccomp-bpf)
- 由于严格模式的限制过于严苛,实用性有限,社区在2012年的Linux 3.5版本中引入了革命性的过滤模式。
- 这个模式允许进程加载一个**伯克利包过滤器(Berkeley Packet Filter, BPF)**程序。 内核会在每次系统调用发生时执行这个BPF程序,BPF程序可以检查系统调用的编号及其参数,然后决定是允许、拒绝、终止进程还是记录日志等。
- 这一改变极大地提升了seccomp的灵活性和实用性,使其成为现代沙箱技术(如浏览器、容器)的基石。
- 在Linux 3.17中,新增了
seccomp(2)系统调用,为多线程程序的过滤器同步提供了更好的支持。
目前该技术的社区活跃度和主流应用情况如何?
Seccomp是一项非常成熟且被广泛应用的核心内核安全技术。 它的应用遍及:
- 容器技术:Docker、Kubernetes、Podman等容器运行时默认都会使用一个seccomp配置文件,该文件会禁用约44个已知的高风险系统调用,以防止容器逃逸。
- Web浏览器:Google Chrome和Firefox使用seccomp来沙箱化其渲染进程,严格限制这些处理网络不可信内容的进程与系统交互的能力。
- 系统服务:Systemd和OpenSSH等系统关键服务也使用seccomp来限制自身权限,加强安全性。
- 移动平台:Android自8.0版本起在Zygote进程(所有应用的父进程)中使用了seccomp过滤器。
核心原理与设计
它的核心工作原理是什么?
kernel/seccomp.c中的代码实现了系统调用拦截和过滤的逻辑。其核心工作原理如下:
- 启用与加载:进程通过
prctl()或seccomp()系统调用请求进入安全计算模式。在过滤模式下,用户空间会提供一个BPF程序作为过滤器。 - 过滤器附加:内核会将这个BPF过滤器附加到当前进程的
task_struct结构体上。一旦设置,这个策略是单向的,无法撤销,并且会被所有子进程继承。 - 系统调用路径拦截:在进入实际的系统调用处理函数之前,内核的系统调用入口路径上会有一个检查点。 这个检查点会调用
__secure_computing()函数。 - 执行BPF过滤器:
__secure_computing()函数会检查当前进程是否设置了seccomp过滤器。如果有,它会准备一个struct seccomp_data结构体,其中包含当前系统调用的编号、参数、指令指针等信息,然后将此结构体作为输入数据,在内核态执行附加的BPF程序。 - 返回裁决:BPF程序执行后会返回一个32位的“裁决”值。 这个值的高16位代表要执行的动作(如
SECCOMP_RET_ALLOW允许、SECCOMP_RET_KILL终止、SECCOMP_RET_TRAP、SECCOMP_RET_ERRNO返回错误码、SECCOMP_RET_LOG记录),低16位是与该动作相关的数据(例如,要返回的错误码)。 - 内核执行动作:内核根据BPF程序返回的裁决值,决定是继续执行该系统调用,还是立即终止进程,或是返回一个错误给用户空间等。
它的主要优势体现在哪些方面?
- 显著减小攻击面:通过白名单或黑名单的方式,精确控制进程可用的系统调用,极大地限制了漏洞利用的可能性。
- 低性能开销:BPF过滤器直接在内核态执行,非常高效,对系统性能的影响通常很小。 Linux 5.11内核中引入的常量动作位图等优化,进一步将常见过滤场景的开销从O(N)降低到O(1)。
- 灵活性高:过滤模式允许基于系统调用编号及其参数创建复杂的、细粒度的过滤规则。
- 防御TOCTOU攻击:BPF程序不能解引用指针,只能直接评估系统调用参数的值,这使得seccomp能有效防御常见的“检查时-使用时”(Time-of-Check-Time-of-Use)攻击。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 策略创建复杂:为一个复杂的应用程序创建一个精确且最小化的系统调用白名单是一项非常困难的任务。 需要使用
strace等工具仔细分析程序行为,错误的策略可能导致应用崩溃。 - 可移植性问题:系统调用的编号和可用性在不同的CPU架构(如x86_64 vs aarch64)之间存在差异,这使得编写可移植的seccomp策略变得复杂。
- 性能敏感场景:尽管开销很低,但在每秒进行海量系统调用的极端高性能计算(HPC)场景中,任何额外的检查都可能成为瓶颈。
- 非银弹:Seccomp只关注系统调用层面,它无法防御所有类型的攻击,例如内存损坏漏洞、逻辑缺陷或配置错误。 它需要与其他安全机制(如AppArmor, SELinux)结合,形成深度防御。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
Seccomp是实现进程级沙箱、遵循最小权限原则的首选技术。
- 容器安全:在Docker或Kubernetes中,为Pod或容器配置seccomp配置文件是标准的安全实践。默认配置文件会禁用
unshare、kexec_load等危险的系统调用,防止一个被攻陷的容器影响到宿主机或其他容器。 - 沙箱化不可信代码:Web浏览器将处理网页内容的渲染器进程置于seccomp沙箱中。即使渲染器被恶意JavaScript代码利用,seccomp也会阻止它执行文件系统访问、网络连接创建等高风险操作。
- 加固应用程序:对于处理敏感数据的应用程序,可以使用seccomp来限制其行为。例如,一个只进行计算任务的程序可以被禁止所有网络和文件相关的系统调用。
是否有不推荐使用该技术的场景?为什么?
- 通用开发和调试环境:在开发和调试阶段,严格的seccomp策略会干扰
gdb等调试工具(它们依赖ptrace系统调用)的正常工作,并可能因为频繁的合法系统调用被误拦而降低开发效率。 - 需要完整系统功能的场景:对于需要广泛访问系统资源和功能的特权进程或管理工具,使用seccomp会过度限制其能力,使其无法正常工作。
对比分析
请将其 与 其他相似技术 进行详细对比。
Seccomp通常与AppArmor和SELinux这两大Linux安全模块(LSM)进行比较。它们都用于强制访问控制,但作用层面和机制不同。
| 特性 | Seccomp | AppArmor | SELinux |
|---|---|---|---|
| 功能概述 | 系统调用过滤器。它决定一个进程可以或不可以调用哪些系统调用。 | 路径名强制访问控制 (MAC)。它控制一个程序可以访问哪些文件路径以及以何种方式访问(读、写、执行)。 | 标签式强制访问控制 (MAC)。它为所有主体(进程)和客体(文件、套接字等)打上安全标签,通过策略规则控制主体对客体的访问。 |
| 实现方式 | 在系统调用入口处执行BPF程序。 | 基于路径名的规则集,通过Linux安全模块(LSM)钩子实现。 | 基于安全上下文标签,通过LSM钩子实现。 |
| 隔离级别 | 动作/能力级别。关注的是“进程能做什么动作”(如open, socket)。 |
资源级别(基于名称)。关注的是“进程能访问什么资源”(如/etc/passwd)。 |
资源级别(基于标签)。关注的是“user_t类型的进程能否写入etc_t类型的文件”。 |
| 性能开销 | 低。BPF执行非常快。 | 低。开销通常可以忽略。 | 中等。标签匹配和策略查询比AppArmor更复杂,可能带来稍高的开销。 |
| 易用性 | 策略复杂。精确定义一个应用所需的所有系统调用非常困难。 | 相对简单。策略基于文件路径,更直观,且有“complain mode”辅助生成策略。 | 非常复杂。需要理解标签、类型、角色等概念,策略编写和维护难度最大。 |
| 结合使用 | 推荐。Seccomp与LSM(AppArmor/SELinux)是互补的,可以形成深度防御。例如,Seccomp可以允许write系统调用,而AppArmor可以进一步限制该write只能作用于/tmp目录下的文件。 |
Seccomp 日志动作的位掩码与字符串名称转换
本代码片段是 Seccomp (安全计算模式) 子系统的一部分,其核心功能是定义了一套机制,用于在内核内部使用的 u32 位掩码与用户空间交互时使用的人类可读字符串之间进行双向转换。它通过一个静态查找表和一系列辅助函数,实现了对 seccomp 日志动作配置的解析和格式化,是 sysctl 接口实现的基础。
实现原理分析
该机制的核心是基于一个静态的、数据驱动的查找表(Look-Up Table, LUT),从而将具体的转换逻辑与数据分离,提高了代码的可维护性和扩展性。
核心数据结构 (
seccomp_log_name,seccomp_log_names):seccomp_log_name结构体将一个u32类型的位标志 (log) 与其对应的const char *字符串名称 (name) 绑定在一起,形成一个原子映射单元。seccomp_log_names数组是这个机制的中心。它是一个静态常量数组,实例化了一系列的映射关系。这种设计将所有可能的转换关系集中存放在一个地方。数组以一个空成员{}作为结束标记(哨兵),这是一种常见的 C 语言编程范式,使得遍历该数组的循环代码无需知道数组的确切大小,只需检查cur->name是否为NULL即可终止。
位掩码到字符串的转换 (
seccomp_names_from_actions_logged):- 此函数实现了从二进制到位图到文本的格式化。它遍历
seccomp_log_names查找表。 - 对于表中的每一个条目,它使用按位与操作符 (
&) 来测试输入的actions_logged位掩码中是否设置了与当前条目对应的位。 - 如果位被设置,它就将该条目对应的字符串名称复制到输出缓冲区。函数通过一个布尔标志
append_sep来控制分隔符的插入,确保只有在第一个有效名称被添加之后,才在后续名称前添加分隔符。 - 函数使用了
strscpy,这是一个安全的字符串复制函数,可以防止缓冲区溢出。
- 此函数实现了从二进制到位图到文本的格式化。它遍历
字符串到原子位标志的转换 (
seccomp_action_logged_from_name):- 这是一个反向查找函数。它线性遍历查找表,使用
strcmp函数将输入的name字符串与表中的每一个名称进行比较。 - 一旦找到匹配项,它就将对应的
log位标志值写入到调用者提供的指针中,并返回成功。如果遍历完整个表都没有找到匹配项,则返回失败。
- 这是一个反向查找函数。它线性遍历查找表,使用
字符串到复合位掩码的转换 (
seccomp_actions_logged_from_names):- 此函数实现了从文本到二进制位图的解析。它使用
strsep函数来按空格分割输入的字符串,strsep会在原地修改字符串,用\0替换分隔符,并依次返回每个子字符串(token)的指针。 - 对于每一个 token,它调用
seccomp_action_logged_from_name来查找其对应的位标志。 - 查找到的位标志通过按位或赋值操作符 (
|=) 被累加到最终的actions_logged结果中,从而构建出完整的位掩码。
- 此函数实现了从文本到二进制位图的解析。它使用
代码分析
1 | /** |
seccomp_actions_logged_handler: Seccomp 日志配置的读写与审计
本代码片段是 seccomp 子系统 sysctl 接口的核心实现,它为 /proc/sys/kernel/seccomp/actions_logged 文件的读写操作提供了完整的处理逻辑。其核心功能是作为内核内部的 u32 位掩码(seccomp_actions_logged)与用户空间的、人类可读的字符串之间的桥梁,并在此过程中执行严格的权限检查、输入验证和安全审计。
实现原理分析
该实现巧妙地复用了内核的标准 sysctl 处理函数 proc_dostring,并通过一个临时栈缓冲区作为中介,将复杂的双向数据转换、验证和状态更新逻辑封装起来。
分发与调度 (
seccomp_actions_logged_handler):- 这是由
sysctl框架直接调用的顶层处理函数。它扮演一个分发器的角色,根据内核传入的write参数,将请求路由到read_actions_logged或write_actions_logged函数,从而将读写逻辑清晰地分离开。 - 对于写操作,它遵循一个严谨的“操作-审计”模式:在执行完写操作后,无论成功与否 (
ret的值),都会立即调用audit_actions_logged函数记录此次配置变更尝试。
- 这是由
读操作实现 (
read_actions_logged):- 状态到文本的转换: 首先,在栈上分配一个足够大的临时缓冲区
names。然后调用seccomp_names_from_actions_logged将内核当前的全局位掩码seccomp_actions_logged转换为人类可读的字符串,并存入该缓冲区。 - 复用标准处理器: 这是此实现中最巧妙的部分。它不直接调用
copy_to_user等底层函数,而是创建了一个临时的ctl_table结构体,将它的.data和.maxlen成员指向栈上的names缓冲区及其大小。最后,它调用内核通用的proc_dostring函数,并让这个标准函数去处理将names缓冲区内容安全地拷贝到用户空间的所有细节。
- 状态到文本的转换: 首先,在栈上分配一个足够大的临时缓冲区
写操作实现 (
write_actions_logged):- 权限检查: 操作的第一步是调用
capable(CAP_SYS_ADMIN),检查当前进程是否拥有系统管理员权限。这是保护关键内核参数不被非授权修改的标准安全措施。 - 用户数据获取: 与读操作类似,它使用
proc_dostring(此时write参数为1) 将用户写入的数据从用户空间拷贝到内核栈上的临时names缓冲区。 - 文本到状态的解析与验证: 调用
seccomp_actions_logged_from_names将names缓冲区中的字符串解析成u32位掩码。紧接着,进行一项关键的业务逻辑验证:*actions_logged & SECCOMP_LOG_ALLOW。此检查禁止用户启用对allow动作的日志记录,因为这通常是默认行为,记录它会产生海量的、无价值的日志信息,可能导致拒绝服务(DoS)。 - 原子状态更新: 只有在所有检查和验证都通过后,才会执行
seccomp_actions_logged = *actions_logged;这一步,将新的配置应用到全局状态。
- 权限检查: 操作的第一步是调用
审计日志 (
audit_actions_logged):- 此函数提供了配置变更的可追溯性。它将变更前和变更后的
u32位掩码都转换为字符串形式,然后调用内核审计框架的专用函数audit_seccomp_actions_logged,生成一条详细的审计日志,记录谁、在何时、尝试将配置从什么改成什么,以及操作是否成功。
- 此函数提供了配置变更的可追溯性。它将变更前和变更后的
代码分析
1 | /** |
seccomp_sysctl_init: Seccomp 子系统 sysctl 接口初始化
本代码片段展示了 Linux 内核中 seccomp (安全计算模式) 子系统如何通过 sysctl 机制向用户空间暴露其配置接口。其核心功能是在 /proc/sys/kernel/seccomp/ 目录下创建两个文件:actions_avail 和 actions_logged。这使得系统管理员或应用程序能够查询当前内核支持的所有 seccomp 动作,并动态配置哪些动作需要被记录到内核日志中。
实现原理分析
该机制是内核子系统提供运行时配置的标准范例,它利用了内核的 sysctl 框架。
数据定义:
- 首先,通过宏定义了一系列人类可读的字符串,分别对应
seccomp过滤器可以返回的各种处置动作(如kill_process,trap,allow等)。 seccomp_actions_avail是一个静态常量字符串,它在编译时将所有可用的动作名称拼接成一个以空格分隔的列表。这个字符串将作为/proc/sys/kernel/seccomp/actions_avail文件的内容。
- 首先,通过宏定义了一系列人类可读的字符串,分别对应
Sysctl 表定义 (
seccomp_sysctl_table):- 这是
sysctl框架的核心数据结构。它是一个ctl_table结构体数组,每个元素定义了/proc/sys/下的一个文件接口。 actions_avail条目:.procname: 定义了文件名。.data和.maxlen: 将此接口直接绑定到seccomp_actions_avail字符串。.mode = 0444: 设置文件权限为全局只读。.proc_handler = proc_dostring: 指定使用内核提供的标准处理函数,该函数专门用于处理对静态字符串的读操作。
actions_logged条目:.mode = 0644: 设置文件权限为 root 可读写,其他用户只读。.proc_handler = seccomp_actions_logged_handler: 指定一个自定义的处理函数。这意味着当用户读写此文件时,内核会调用seccomp_actions_logged_handler函数来执行相应的逻辑(例如,解析用户输入的字符串并更新内核内部的日志掩码)。
- 这是
注册与初始化 (
seccomp_sysctl_init,device_initcall):seccomp_sysctl_init函数通过调用register_sysctl_init,将上述定义的seccomp_sysctl_table注册到内核的sysctl树中,并指定其路径为kernel/seccomp。device_initcall是一个宏,它将seccomp_sysctl_init函数注册为一个内核初始化回调。这确保了在内核启动过程中的 “device” 阶段,该函数会被自动调用,从而完成sysctl接口的创建。
代码分析
1 | /** |








