Hotdry.

Article

Linux 内核 Killswitch:函数级短路 CVE 缓解机制工程指南

基于 Killswitch 原语实现 Linux 内核函数的粒度短路缓解机制,在单个系统调用入口插入守卫提前终止风险路径,与 io_uring CVE 缓解思路互补。

2026-05-09security

当一个内核安全漏洞公开后,fleet(多台机器集群)在补丁构建、分发和重启完成之前会持续暴露在风险窗口中。在这段时间内,最简单的缓解手段就是停止调用存在 bug 的函数 —— 这正是 Linux 内核 Killswitch 机制的核心设计目标。Killswitch 提供了一个管理员层面的运行时原语,可以让选定的内核函数直接返回一个固定值、跳过函数体的执行,从而在无需重启的情况下切断漏洞利用路径,直到真正的补丁上线。

技术原理:kprobe 与函数体短路

Killswitch 的实现依赖于 kprobe 基础设施。当管理员发起一次 engage 操作时,内核会在目标函数的入口点注册一个 kprobe,其 pre-handler 的工作非常直接:设置返回寄存器为预设值,然后通过 override_function_with_return() 将控制流直接导向函数出口。换言之,函数体自始至终没有被执行,所有调用者收到的都是同一个固定返回值。

这种设计的优势在于透明性:只要返回值类型与调用者预期一致(如 -EPERM-EFAULT),用户空间的错误处理逻辑可以无感知地处理这个 "失败的 syscall"。同时,由于使用了 text patching 机制(text_poke_bp()),engage 操作在所有 CPU 确认新执行路径之前不会返回给用户,确保了 SMP 下的内存可见性。

控制接口与命令格式

Killswitch 的运维接口位于 /sys/kernel/security/killswitch/,核心文件为 control(仅限 CAP_SYS_ADMIN 写操作)。支持三条命令:

启用(engage):

echo "engage <symbol_name> <retval>" > /sys/kernel/security/killswitch/control

例如,针对 AF_ALG 相关 CVE,管理员可以执行:

echo "engage af_alg_sendmsg -1" > /sys/kernel/security/killswitch/control

此后,af_alg_sendmsg() 的每次调用都会立即返回 -EPERM,任何经由该路径触发的下游漏洞都无法到达。

停用(disengage):

echo "disengage <symbol_name>" > /sys/kernel/security/killswitch/control

全量停用:

echo "disengage_all" > /sys/kernel/security/killswitch/control

除了控制接口之外,还有三个只读文件:engaged 列出当前所有启用的 killswitch 及其返回值和调用计数;taint 指示内核是否被 Killswitch 修改过;每个启用函数下还会在 fn/<name>/ 目录中创建 retvalhits 两个属性文件,允许动态修改返回值并读取累计调用次数。

目标选择的核心原则与反模式

最重要的一条原则:选择包含 bug 的最高层入口函数。 这个原则背后的逻辑是:如果跳过的是中间层而非最顶层,调用者可能会看到半初始化的状态,从而引入比原始漏洞更严重的新 bug。

以 Linux 内核 crypto/af_alg.c 中的 AF_ALG 子系统为例,文档明确列出了两个反模式:

反模式一:af_alg_count_tsgl 这个函数返回 TX SG 条目的数量(unsigned int)。如果将它短路并返回 0,调用者会分配一个 1 条目的 scatterlist(因为存在 if (!entries) entries = 1 的保护),但随后会通过 af_alg_pull_tsgl 将真实的 TX SGL 写入这个空间不足的目的地,产生越界写操作。这比被缓解的原始 bug 危害更大。

反模式二:af_alg_pull_tsgl 这个函数返回 void,任何返回值都被接受。但它的调用者依赖它来完成 per-request SGL 的填充。跳过函数体会导致 SGL 中的页面指针保持 NULL,下一阶段的 memcpy_sglist 对其解引用时内核直接 oops。

正确模式:af_alg_sendmsg 这是 AF_ALG 发送路径的最顶层入口函数,返回值已经是标准的错误码(-EPERM-ENOMEM 等)。将它短路后,所有尝试发送的用户空间请求都会收到一个合理的错误,用户空间的错误处理逻辑可以正常运作,而下游任何子路径中的 bug 都不会被触发。

总结而言,canonical pattern 就是选一个 "syscall-handler 形状" 的函数,其返回值已经编码了 "操作未发生" 的语义,把错误交给用户空间处理即可。

工程化参数与运维要点

内核污染标记(taint flag): 首次成功 engage 后,内核会设置 TAINT_KILLSWITCH(bit 20,字符 H)。这个标记会在 oops 横幅中显示为 Tainted: ... H,用于标识内核已被 killswitch 修改。如果在调查一个崩溃时看到 H,第一步应该查看 /sys/kernel/security/killswitch/engaged 确认哪些函数被短路。有趣的是,这个 taint 在 disengage不会清除,它会持续到下一次重启 —— 因为一旦运行时的执行路径被修改,即使撤销了 killswitch,内核镜像也不再是初始状态。

调用计数监控(hits counter): 每个启用函数都有一个 per-CPU 累计的调用计数器,通过 fn/<name>/hits 文件读取。在 hot path 上逐条记录日志会产生大量噪音,因此 Killswitch 没有提供 per-call 日志功能,只能在事后读取总计值。建议在紧急启用 killswitch 后立即记录一次 hits 初始值,后续每小时采集一次增量,用于判断该函数在生产负载中的活跃程度 —— 如果一个被短路的函数在数小时内 hits 为零,说明它可能不是一个被广泛使用的入口,重新评估目标选择的正确性是合理的。

模块卸载自动 disengage: 如果一个 engaged 的目标函数位于某个可卸载内核模块中,当该模块被卸载时,Killswitch 会自动对该函数执行 disengage,并在 dmesg 中记录一条警告消息。这是因为当模块代码从内存中移除后,继续保持指向已释放代码地址的 kprobe 会导致系统不稳定。重新加载模块后,killswitch 不会自动重新启用,管理员需要手动 re-engage。这是一个需要注意的运维陷阱:对于依赖内核模块暴露功能的系统,手动跟踪哪些 killswitch 需要在模块重新加载后恢复非常重要。

启动参数(boot parameter): Killswitch 支持通过内核命令行在启动时自动应用缓解,格式为 killswitch=fn1=<val>,fn2=<val>,...。这对于 fleet rollout 场景特别有价值:当某个 CVE 被公开后,可以在 bootloader 或 PXE 配置中预先写入 killswitch 启动参数,将整个 fleet 通过重启滚动到受保护状态,同时真正的内核补丁正在准备中。解析失败时会输出警告并跳过有问题的条目,不会导致 panic,保证了启动过程的健壮性。

与 io_uring CVE 缓解的互补关系

io_uring 子系统近年频繁出现提权类漏洞(如 ZCRX 漏洞),其缓解策略通常是在特定操作码路径上添加验证守卫或限制 sqpoll 模式的功能。这类缓解往往针对的是具体的漏洞利用原语(如特定系统调用的参数验证)。相比之下,Killswitch 提供的是一种防御性的通用能力:无论漏洞是通过哪个具体操作码触发的,只要该漏洞的触发路径经过某个可识别的函数入口,管理员就可以在该入口处切断整条路径。

两者形成互补的层次结构:io_uring 特定缓解解决的是 "这个操作码有问题该怎么修",而 Killswitch 解决的是 "在这个 CVE 的正式补丁到达之前,如何让 fleet 保持安全运行"。将 Killswitch 视为 CVE 响应窗口中的临时防波堤,而非长期解决方案,是理解其设计意图的关键。

实践清单:启用 killswitch 的操作规程

第一步:确认配置。 需要内核启用 CONFIG_KILLSWITCH(依赖 SECURITYFSKPROBESHAVE_FUNCTION_ERROR_INJECTION)。可以通过检查 /sys/kernel/security/killswitch/ 是否存在来验证功能可用性。

第二步:确定目标函数。 仔细分析 CVE 的触发路径,识别包含 bug 的最高层入口函数。避免选择有副作用依赖的中间层函数。如果不确定,查阅 CVE 公告中提到的受影响函数,并在测试环境中先验证短路后系统基本功能是否正常。

第三步:执行 engage 并记录。 执行 engage 命令时,建议在命令中附加 reason 字段(虽然当前实现中 reason 不是必需参数,但善用注释有助于后续审计)。立即检查 /sys/kernel/security/killswitch/engaged 确认函数已被列出,并读取 fn/<name>/hits 记录初始值。

第四步:验证与监控。 开启对 dmesg 的监控,Killswitch 会在每次 engage/disengage 时向内核日志写入一行 KERN_WARNING,包含操作者身份(uid/auid/sessionid/comm)和相关信息。在生产工作负载上观察一段时间,确认该函数在业务流量中的实际调用模式与预期一致。

第五步:准备回滚。 始终保持对 control 文件的写权限(CAP_SYS_ADMIN),确保在补丁上线后可以快速执行 disengage。如果补丁需要重启才能完全生效,记得在重启后移除启动参数中的 killswitch 配置。

第六步:追踪 taint 影响。 在 oops 或其他内核崩溃调查时,第一时间检查 taint 标记中是否存在 H。如果存在,查看 engaged 列表,评估崩溃是否与某个被短路的函数相关联。

Killswitch 不是银弹。它的存在意义在于缩短 CVE 响应窗口中的 "暴露期",让运维团队在不打乱生产节奏的前提下争取到补丁上线的时间。在正确的场景下 —— 目标函数是一个相对独立的入口、返回值已经编码了标准的错误语义、且该功能在大多数部署中是可选的 ——Killswitch 可以提供一种精准、即时且可回滚的缓解手段。结合针对特定漏洞利用原语的 io_uring 类缓解措施,两者共同构成了 Linux 内核在面对 CVE 压力时的纵深防御体系。

资料来源:LWN.net "[PATCH] killswitch: add per-function short-circuit mitigation primitive",Sasha Levin,2026-05-07,https://lwn.net/Articles/1071706/

security

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com