[toc]

在这里插入图片描述

kernel/seccomp.c 安全计算模式(Secure Computing Mode) 系统调用防火墙

历史与背景

这项技术是为了解决什么特定问题而诞生的?

Seccomp(Secure Computing Mode)是为了解决一个核心的安全问题而诞生的:如何安全地运行不可信的代码。 在复杂的软件环境中,一个进程被赋予了访问数百个系统调用(syscall)的能力,这些系统调用是用户空间程序与内核交互的接口。 然而,大多数程序在其整个生命周期中只需要使用这些系统调用中的一小部分。 如果一个程序(例如,一个处理外部输入的Web浏览器渲染进程)被恶意代码攻陷,攻击者就可以利用那些该程序本不需要、但内核依然暴露给它的系统调用来进一步攻击和破坏整个系统。

Seccomp通过提供一种机制来限制一个进程可以调用的系统调用集合,从而减少内核的攻击面。 它的核心思想是实施“最小权限原则”:只授予程序执行其核心功能所必需的最小权限集合。 这样,即使程序被攻破,攻击者能造成的损害也因为其可用的系统调用极其有限而受到严格控制。 这对于构建应用程序沙箱(sandbox)至关重要。

它的发展经历了哪些重要的里程碑或版本迭代?

Seccomp的发展经历了两个主要阶段:

  1. 模式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. 模式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中的代码实现了系统调用拦截和过滤的逻辑。其核心工作原理如下:

  1. 启用与加载:进程通过prctl()seccomp()系统调用请求进入安全计算模式。在过滤模式下,用户空间会提供一个BPF程序作为过滤器。
  2. 过滤器附加:内核会将这个BPF过滤器附加到当前进程的task_struct结构体上。一旦设置,这个策略是单向的,无法撤销,并且会被所有子进程继承。
  3. 系统调用路径拦截:在进入实际的系统调用处理函数之前,内核的系统调用入口路径上会有一个检查点。 这个检查点会调用__secure_computing()函数。
  4. 执行BPF过滤器__secure_computing()函数会检查当前进程是否设置了seccomp过滤器。如果有,它会准备一个struct seccomp_data结构体,其中包含当前系统调用的编号、参数、指令指针等信息,然后将此结构体作为输入数据,在内核态执行附加的BPF程序。
  5. 返回裁决:BPF程序执行后会返回一个32位的“裁决”值。 这个值的高16位代表要执行的动作(如SECCOMP_RET_ALLOW允许、SECCOMP_RET_KILL终止、SECCOMP_RET_TRAPSECCOMP_RET_ERRNO返回错误码、SECCOMP_RET_LOG记录),低16位是与该动作相关的数据(例如,要返回的错误码)。
  6. 内核执行动作:内核根据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配置文件是标准的安全实践。默认配置文件会禁用unsharekexec_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),从而将具体的转换逻辑与数据分离,提高了代码的可维护性和扩展性。

  1. 核心数据结构 (seccomp_log_name, seccomp_log_names):

    • seccomp_log_name 结构体将一个 u32 类型的位标志 (log) 与其对应的 const char * 字符串名称 (name) 绑定在一起,形成一个原子映射单元。
    • seccomp_log_names 数组是这个机制的中心。它是一个静态常量数组,实例化了一系列的映射关系。这种设计将所有可能的转换关系集中存放在一个地方。数组以一个空成员 {} 作为结束标记(哨兵),这是一种常见的 C 语言编程范式,使得遍历该数组的循环代码无需知道数组的确切大小,只需检查 cur->name 是否为 NULL 即可终止。
  2. 位掩码到字符串的转换 (seccomp_names_from_actions_logged):

    • 此函数实现了从二进制到位图到文本的格式化。它遍历 seccomp_log_names 查找表。
    • 对于表中的每一个条目,它使用按位与操作符 (&) 来测试输入的 actions_logged 位掩码中是否设置了与当前条目对应的位。
    • 如果位被设置,它就将该条目对应的字符串名称复制到输出缓冲区。函数通过一个布尔标志 append_sep 来控制分隔符的插入,确保只有在第一个有效名称被添加之后,才在后续名称前添加分隔符。
    • 函数使用了 strscpy,这是一个安全的字符串复制函数,可以防止缓冲区溢出。
  3. 字符串到原子位标志的转换 (seccomp_action_logged_from_name):

    • 这是一个反向查找函数。它线性遍历查找表,使用 strcmp 函数将输入的 name 字符串与表中的每一个名称进行比较。
    • 一旦找到匹配项,它就将对应的 log 位标志值写入到调用者提供的指针中,并返回成功。如果遍历完整个表都没有找到匹配项,则返回失败。
  4. 字符串到复合位掩码的转换 (seccomp_actions_logged_from_names):

    • 此函数实现了从文本到二进制位图的解析。它使用 strsep 函数来按空格分割输入的字符串,strsep 会在原地修改字符串,用 \0 替换分隔符,并依次返回每个子字符串(token)的指针。
    • 对于每一个 token,它调用 seccomp_action_logged_from_name 来查找其对应的位标志。
    • 查找到的位标志通过按位或赋值操作符 (|=) 被累加到最终的 actions_logged 结果中,从而构建出完整的位掩码。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
* @struct seccomp_log_name
* @brief 将 u32 位标志与其字符串名称关联的结构体。
*/
struct seccomp_log_name {
u32 log; /**< seccomp 日志动作的位标志 */
const char *name;/**< 对应的人类可读名称 */
};

/** @name seccomp 日志动作的位标志定义 */
///@{
/* 用于 seccomp_actions_logged 全局变量 */
#define SECCOMP_LOG_KILL_PROCESS (1 << 0) /**< 记录 kill_process 动作 */
#define SECCOMP_LOG_KILL_THREAD (1 << 1) /**< 记录 kill_thread 动作 */
#define SECCOMP_LOG_TRAP (1 << 2) /**< 记录 trap 动作 */
#define SECCOMP_LOG_ERRNO (1 << 3) /**< 记录 errno 动作 */
#define SECCOMP_LOG_TRACE (1 << 4) /**< 记录 trace 动作 */
#define SECCOMP_LOG_LOG (1 << 5) /**< 记录 log 动作 */
#define SECCOMP_LOG_ALLOW (1 << 6) /**< 记录 allow 动作 */
#define SECCOMP_LOG_USER_NOTIF (1 << 7) /**< 记录 user_notif 动作 */
///@}

/**
* @var seccomp_log_names
* @brief seccomp 日志动作位标志到名称的静态查找表。
*/
static const struct seccomp_log_name seccomp_log_names[] = {
{ SECCOMP_LOG_KILL_PROCESS, SECCOMP_RET_KILL_PROCESS_NAME },
{ SECCOMP_LOG_KILL_THREAD, SECCOMP_RET_KILL_THREAD_NAME },
{ SECCOMP_LOG_TRAP, SECCOMP_RET_TRAP_NAME },
{ SECCOMP_LOG_ERRNO, SECCOMP_RET_ERRNO_NAME },
{ SECCOMP_LOG_USER_NOTIF, SECCOMP_RET_USER_NOTIF_NAME },
{ SECCOMP_LOG_TRACE, SECCOMP_RET_TRACE_NAME },
{ SECCOMP_LOG_LOG, SECCOMP_RET_LOG_NAME },
{ SECCOMP_LOG_ALLOW, SECCOMP_RET_ALLOW_NAME },
{ } /* 哨兵,标记数组结束 */
};

/**
* @brief 从 u32 位掩码生成以分隔符连接的动作名称字符串。
* @param[out] names 目标缓冲区,用于存放生成的字符串。
* @param size 目标缓冲区的大小。
* @param actions_logged 源 u32 位掩码。
* @param sep 名称之间的分隔符字符串。
* @return 成功返回 true,若缓冲区不足则返回 false。
*/
static bool seccomp_names_from_actions_logged(char *names, size_t size,
u32 actions_logged,
const char *sep)
{
const struct seccomp_log_name *cur;
bool append_sep = false;

// 遍历查找表,直到遇到哨兵条目 (cur->name 为 NULL)
for (cur = seccomp_log_names; cur->name && size; cur++) {
ssize_t ret;

// 使用按位与检查 actions_logged 中是否设置了当前动作的标志位
if (!(actions_logged & cur->log))
continue; // 如果未设置,则跳过

// 如果这不是第一个要添加的名称,则先添加分隔符
if (append_sep) {
ret = strscpy(names, sep, size); // 安全地复制分隔符
if (ret < 0) return false; // 检查缓冲区是否已满
names += ret; size -= ret; // 更新缓冲区指针和剩余大小
} else
append_sep = true; // 标记第一个名称已添加

ret = strscpy(names, cur->name, size); // 安全地复制动作名称
if (ret < 0) return false; // 检查缓冲区是否已满
names += ret; size -= ret; // 更新缓冲区指针和剩余大小
}
return true;
}

/**
* @brief 从单个动作名称字符串查找其对应的 u32 位标志。
* @param[out] action_logged 指向 u32 的指针,用于存放找到的位标志。
* @param name 要查找的动作名称。
* @return 找到返回 true,否则返回 false。
*/
static bool seccomp_action_logged_from_name(u32 *action_logged,
const char *name)
{
const struct seccomp_log_name *cur;

// 线性遍历查找表
for (cur = seccomp_log_names; cur->name; cur++) {
// 比较输入名称与表中的名称
if (!strcmp(cur->name, name)) {
*action_logged = cur->log; // 如果匹配,则写入位标志
return true; // 并返回成功
}
}

return false; // 遍历结束仍未找到,返回失败
}

/**
* @brief 从空格分隔的动作名称字符串解析出 u32 位掩码。
* @param[out] actions_logged 指向 u32 的指针,用于存放最终的位掩码。
* @param names 包含动作名称的源字符串(此字符串会被 strsep 修改)。
* @return 所有名称都有效则返回 true,否则返回 false。
*/
static bool seccomp_actions_logged_from_names(u32 *actions_logged, char *names)
{
char *name;

*actions_logged = 0; // 初始化结果位掩码为 0
// strsep 按 " " 分割字符串,直到返回 NULL
while ((name = strsep(&names, " ")) && *name) {
u32 action_logged = 0;

// 查找当前名称对应的位标志
if (!seccomp_action_logged_from_name(&action_logged, name))
return false; // 如果有任何一个名称无效,则整个解析失败

// 使用按位或将找到的位标志合并到结果中
*actions_logged |= action_logged;
}

return true;
}

seccomp_actions_logged_handler: Seccomp 日志配置的读写与审计

本代码片段是 seccomp 子系统 sysctl 接口的核心实现,它为 /proc/sys/kernel/seccomp/actions_logged 文件的读写操作提供了完整的处理逻辑。其核心功能是作为内核内部的 u32 位掩码(seccomp_actions_logged)与用户空间的、人类可读的字符串之间的桥梁,并在此过程中执行严格的权限检查、输入验证和安全审计。

实现原理分析

该实现巧妙地复用了内核的标准 sysctl 处理函数 proc_dostring,并通过一个临时栈缓冲区作为中介,将复杂的双向数据转换、验证和状态更新逻辑封装起来。

  1. 分发与调度 (seccomp_actions_logged_handler):

    • 这是由 sysctl 框架直接调用的顶层处理函数。它扮演一个分发器的角色,根据内核传入的 write 参数,将请求路由到 read_actions_loggedwrite_actions_logged 函数,从而将读写逻辑清晰地分离开。
    • 对于写操作,它遵循一个严谨的“操作-审计”模式:在执行完写操作后,无论成功与否 (ret 的值),都会立即调用 audit_actions_logged 函数记录此次配置变更尝试。
  2. 读操作实现 (read_actions_logged):

    • 状态到文本的转换: 首先,在栈上分配一个足够大的临时缓冲区 names。然后调用 seccomp_names_from_actions_logged 将内核当前的全局位掩码 seccomp_actions_logged 转换为人类可读的字符串,并存入该缓冲区。
    • 复用标准处理器: 这是此实现中最巧妙的部分。它不直接调用 copy_to_user 等底层函数,而是创建了一个临时的 ctl_table 结构体,将它的 .data.maxlen 成员指向栈上的 names 缓冲区及其大小。最后,它调用内核通用的 proc_dostring 函数,并让这个标准函数去处理将 names 缓冲区内容安全地拷贝到用户空间的所有细节。
  3. 写操作实现 (write_actions_logged):

    • 权限检查: 操作的第一步是调用 capable(CAP_SYS_ADMIN),检查当前进程是否拥有系统管理员权限。这是保护关键内核参数不被非授权修改的标准安全措施。
    • 用户数据获取: 与读操作类似,它使用 proc_dostring (此时 write 参数为1) 将用户写入的数据从用户空间拷贝到内核栈上的临时 names 缓冲区。
    • 文本到状态的解析与验证: 调用 seccomp_actions_logged_from_namesnames 缓冲区中的字符串解析成 u32 位掩码。紧接着,进行一项关键的业务逻辑验证:*actions_logged & SECCOMP_LOG_ALLOW。此检查禁止用户启用对 allow 动作的日志记录,因为这通常是默认行为,记录它会产生海量的、无价值的日志信息,可能导致拒绝服务(DoS)。
    • 原子状态更新: 只有在所有检查和验证都通过后,才会执行 seccomp_actions_logged = *actions_logged; 这一步,将新的配置应用到全局状态。
  4. 审计日志 (audit_actions_logged):

    • 此函数提供了配置变更的可追溯性。它将变更前和变更后的 u32 位掩码都转换为字符串形式,然后调用内核审计框架的专用函数 audit_seccomp_actions_logged,生成一条详细的审计日志,记录谁、在何时、尝试将配置从什么改成什么,以及操作是否成功。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* @brief 处理对 actions_logged sysctl 文件的读操作。
* @param ro_table sysctl 框架传入的只读 ctl_table 指针。
* @param buffer 指向用户空间的缓冲区,用于接收数据。
* @param lenp 指向用户空间中表示缓冲区大小的变量的指针,返回时写入实际数据长度。
* @param ppos 文件读写位置的指针。
* @return 成功返回0,失败返回负数错误码。
*/
static int read_actions_logged(const struct ctl_table *ro_table, void *buffer,
size_t *lenp, loff_t *ppos)
{
// 在栈上分配一个临时缓冲区,大小与所有可用动作名称的总长度相同。
char names[sizeof(seccomp_actions_avail)];
struct ctl_table table; // 创建一个可修改的 ctl_table 副本。

// 初始化临时缓冲区。
memset(names, 0, sizeof(names));
// 将全局的 seccomp_actions_logged 位掩码转换为字符串,存入 names。
if (!seccomp_names_from_actions_logged(names, sizeof(names),
seccomp_actions_logged, " "))
return -EINVAL; // 如果转换失败,返回无效参数错误。

// 复制传入的 ctl_table,以便修改其数据指针。
table = *ro_table;
// 将副本的数据指针指向我们栈上的临时缓冲区。
table.data = names;
table.maxlen = sizeof(names);
// 调用内核标准函数 proc_dostring,由它处理将 names 内容拷贝到用户空间 buffer 的所有细节。
return proc_dostring(&table, 0, buffer, lenp, ppos);
}

/**
* @brief 处理对 actions_logged sysctl 文件的写操作。
* @param ... (参数同上)
* @param actions_logged 指向 u32 的指针,用于返回解析出的新位掩码(供审计函数使用)。
* @return 成功返回0,失败返回负数错误码。
*/
static int write_actions_logged(const struct ctl_table *ro_table, void *buffer,
size_t *lenp, loff_t *ppos, u32 *actions_logged)
{
char names[sizeof(seccomp_actions_avail)];
struct ctl_table table;
int ret;

// 检查当前进程是否具有系统管理员权限。
if (!capable(CAP_SYS_ADMIN)) return -EPERM;

memset(names, 0, sizeof(names));
table = *ro_table;
table.data = names;
table.maxlen = sizeof(names);
// 调用 proc_dostring (write=1),将用户空间 buffer 的内容拷贝到内核栈上的 names 缓冲区。
ret = proc_dostring(&table, 1, buffer, lenp, ppos);
if (ret) return ret;

// 将 names 缓冲区中的字符串解析为位掩码。
if (!seccomp_actions_logged_from_names(actions_logged, table.data))
return -EINVAL;
// 业务逻辑验证:不允许记录 SECCOMP_RET_ALLOW 动作。
if (*actions_logged & SECCOMP_LOG_ALLOW)
return -EINVAL;

// 验证通过后,更新全局的 seccomp 日志动作配置。
seccomp_actions_logged = *actions_logged;
return 0;
}

/**
* @brief 将 actions_logged 的变更记录到内核审计日志。
* @param actions_logged 新的 u32 位掩码配置。
* @param old_actions_logged 旧的 u32 位掩码配置。
* @param ret 写操作的返回值 (0表示成功,非0表示失败)。
*/
static void audit_actions_logged(u32 actions_logged, u32 old_actions_logged,
int ret)
{
char names[sizeof(seccomp_actions_avail)];
char old_names[sizeof(seccomp_actions_avail)];
const char *new = names, *old = old_names;

// 如果审计系统未启用,则直接返回。
if (!audit_enabled) return;

memset(names, 0, sizeof(names));
memset(old_names, 0, sizeof(old_names));

// 根据写操作结果和新值,准备用于日志的 "new" 字符串。
if (ret) new = "?"; // 操作失败
else if (!actions_logged) new = "(none)"; // 新值为空
else if (!seccomp_names_from_actions_logged(names, sizeof(names),
actions_logged, ",")) new = "?"; // 转换失败

// 准备用于日志的 "old" 字符串。
if (!old_actions_logged) old = "(none)";
else if (!seccomp_names_from_actions_logged(old_names,
sizeof(old_names),
old_actions_logged, ",")) old = "?";

// 调用内核审计子系统的函数来记录事件。
return audit_seccomp_actions_logged(new, old, !ret);
}

/**
* @brief actions_logged sysctl 的主处理函数,根据操作是读还是写进行分发。
* @param ... (参数同 read_actions_logged)
* @param write 标志位,非0表示写操作,0表示读操作。
* @return 成功返回0,失败返回负数错误码。
*/
static int seccomp_actions_logged_handler(const struct ctl_table *ro_table, int write,
void *buffer, size_t *lenp,
loff_t *ppos)
{
int ret;

if (write) {
u32 actions_logged = 0;
// 在修改前,保存当前的配置以供审计使用。
u32 old_actions_logged = seccomp_actions_logged;
// 执行写操作。
ret = write_actions_logged(ro_table, buffer, lenp, ppos,
&actions_logged);
// 对写操作的结果进行审计。
audit_actions_logged(actions_logged, old_actions_logged, ret);
} else {
// 执行读操作。
ret = read_actions_logged(ro_table, buffer, lenp, ppos);
}

return ret;
}

seccomp_sysctl_init: Seccomp 子系统 sysctl 接口初始化

本代码片段展示了 Linux 内核中 seccomp (安全计算模式) 子系统如何通过 sysctl 机制向用户空间暴露其配置接口。其核心功能是在 /proc/sys/kernel/seccomp/ 目录下创建两个文件:actions_availactions_logged。这使得系统管理员或应用程序能够查询当前内核支持的所有 seccomp 动作,并动态配置哪些动作需要被记录到内核日志中。

实现原理分析

该机制是内核子系统提供运行时配置的标准范例,它利用了内核的 sysctl 框架。

  1. 数据定义:

    • 首先,通过宏定义了一系列人类可读的字符串,分别对应 seccomp 过滤器可以返回的各种处置动作(如 kill_process, trap, allow 等)。
    • seccomp_actions_avail 是一个静态常量字符串,它在编译时将所有可用的动作名称拼接成一个以空格分隔的列表。这个字符串将作为 /proc/sys/kernel/seccomp/actions_avail 文件的内容。
  2. 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 函数来执行相应的逻辑(例如,解析用户输入的字符串并更新内核内部的日志掩码)。
  3. 注册与初始化 (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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* @file
* @brief seccomp 子系统的 sysctl 接口定义。
*/

/** @name 用于 sysctl 交互的人类可读的动作名称 */
///@{
#define SECCOMP_RET_KILL_PROCESS_NAME "kill_process"
#define SECCOMP_RET_KILL_THREAD_NAME "kill_thread"
#define SECCOMP_RET_TRAP_NAME "trap"
#define SECCOMP_RET_ERRNO_NAME "errno"
#define SECCOMP_RET_USER_NOTIF_NAME "user_notif"
#define SECCOMP_RET_TRACE_NAME "trace"
#define SECCOMP_RET_LOG_NAME "log"
#define SECCOMP_RET_ALLOW_NAME "allow"
///@}

/**
* @var seccomp_actions_avail
* @brief 一个静态常量字符串,包含了所有内核支持的 seccomp 动作,以空格分隔。
*
* 此字符串将作为 /proc/sys/kernel/seccomp/actions_avail 文件的内容。
*/
static const char seccomp_actions_avail[] =
SECCOMP_RET_KILL_PROCESS_NAME " "
SECCOMP_RET_KILL_THREAD_NAME " "
SECCOMP_RET_TRAP_NAME " "
SECCOMP_RET_ERRNO_NAME " "
SECCOMP_RET_USER_NOTIF_NAME " "
SECCOMP_RET_TRACE_NAME " "
SECCOMP_RET_LOG_NAME " "
SECCOMP_RET_ALLOW_NAME;

/**
* @var seccomp_sysctl_table
* @brief 定义 seccomp sysctl 接口的表。
*
* 每个条目对应 /proc/sys/kernel/seccomp/ 下的一个文件。
*/
static const struct ctl_table seccomp_sysctl_table[] = {
{
/**
* @property procname
* @brief 在 /proc/sys/ 下的文件名。
*/
.procname = "actions_avail",
/**
* @property data
* @brief 指向要暴露的数据的指针。
*/
.data = (void *) &seccomp_actions_avail,
/**
* @property maxlen
* @brief 数据的大小。
*/
.maxlen = sizeof(seccomp_actions_avail),
/**
* @property mode
* @brief 文件的访问权限 (0444 表示全局只读)。
*/
.mode = 0444,
/**
* @property proc_handler
* @brief 处理此文件读写操作的内核函数。proc_dostring 是用于读取静态字符串的标准处理程序。
*/
.proc_handler = proc_dostring,
},
{
.procname = "actions_logged",
/**
* @brief 权限 (0644 表示 root 可读写,其他用户只读)。
*/
.mode = 0644,
/**
* @brief 指定一个自定义处理函数,用于处理用户对此文件的读写请求,
* 以动态配置哪些 seccomp 动作需要被记录。
*/
.proc_handler = seccomp_actions_logged_handler,
},
};

/**
* @brief seccomp sysctl 初始化函数。
* @return 总是返回 0 表示成功。
*
* 此函数被标记为 __init,意味着它仅在内核启动期间执行,
* 其占用的内存随后会被释放。
*/
static int __init seccomp_sysctl_init(void)
{
// 将 seccomp_sysctl_table 注册到 "kernel/seccomp" 路径下。
register_sysctl_init("kernel/seccomp", seccomp_sysctl_table);
return 0;
}

/**
* @brief 使用 device_initcall 宏来注册初始化函数。
*
* 这会安排 seccomp_sysctl_init 在内核启动过程中的 "device" 初始化阶段被调用。
*/
device_initcall(seccomp_sysctl_init);