事情背景
补丁链接:a4ae32c71fe9 (“exec: Always set cap_ambient in cap_bprm_set_creds”)
补丁信息中提到,该补丁是为了确保 cap_bprm_set_creds()
每次都会设置 cap_ambient
变量。另外还提到了下述内容:
… if there is a suid or sgid script with an interpreter that has neither the suid nor the sgid bits set the interpreter should be able to accept ambient credentials. Unfortuantely because cap_ambient is not reset to it’s original value the interpreter can not accept ambient credentials.
因此错误场景应该是与解释器(如 Bash、Python 这类)和脚本执行有关。例如我们创建如下脚本文件 run.sh
:
1#!/bin/bash
2
3# bash script code
4# bla bla bla ...
赋予其可执行权限,并以执行一般可执行文件的方式执行它:
1chmod +x ./run.sh
2./run.sh
这部分内容令我联想起了自己曾经读过的一篇文章
,里面主要讲的是 Bash 脚本中常见的 #!
的实际作用,并涉及到 Linux 中脚本文件的实际执行方式。当时的我对文章的内容感到十分惊奇,但苦于对内核了解不多,无法结合代码深入探究这部分的相关机理。今时不同往日,既然再次碰上了,我便决定一探究竟,打算将这部分关于 Linux 如何执行脚本文件的内容彻底搞清。
代码分析
在执行脚本的场景下,内核1存在以下执行路径:
1__do_execve_file
2 bprm = kzalloc(...)
3 prepare_bprm_creds(bprm)
4 bprm->cred = prepare_exec_creds()
5 new = prepare_creds()
6 new = kmem_cache_alloc(cred_jar, GFP_KERNEL)
7 memcpy(new, old, sizeof(struct cred)) // `old` is `current->cred`
8 prepare_binprm(bprm)
9 security_bprm_set_creds(bprm) ~> cap_bprm_set_creds
10 (1)
11 exec_binprm(bprm)
12 search_binary_handler(bprm)
13 fmt->load_binary(bprm) ~> load_script
14 // replace script's filename with interpreter's filename (awa.
15 // the binary program being executed), and bla bla bla
16 prepare_binprm(bprm)
17 security_bprm_set_creds(bprm) ~> cap_bprm_set_creds
18 (2)
Linux 内核中,一个 struct linux_binprm
(常简称 bprm
)代表一个用户态程序,而一个 struct linux_binfmt
实例代表一种内核可接受的用户态程序的类型。在 search_binary_handler()
中,Linux 内核会遍历所有注册了的 linux_binfmt
,逐个调用它们中的 load_binary()
钩子成员函数,来尝试加载当前的用户态程序(注意不是运行)。与脚本文件相对应的 load_binary()
的实现,就是 fs/binfmt_script.c
中的 load_script()
函数。这个函数按顺序做了以下几件事情:
- 看文件的头两个字节是不是
#
和!
,若不是则说明不是脚本文件,直接退出。 - 随后解析
#!
后面的内容,将解释器名称和参数解析出来。 - 用解析出来的解释器文件名称和参数调整当前进程的
bprm
。
经调整后,当前进程就从“运行当前脚本文件”变成了“运行解释器并解释执行当前脚本文件”。
最后解释一下上述补丁修复的错误场景:在脚本文件有 suid/sgid
而解释器文件没有的情况下,子进程应该获取到父进程的 cap_ambient
2(因为本质上是解释器文件在被执行)。但由于当前的 cap_bprm_set_creds()
没有主动设置 cap_ambient
,导致在 (1) 处被清除的 cap_ambient
在 (2) 处无法恢复成父进程的。
这与上面描述的执行脚本文件的过程是相关的:从逻辑上讲,bprm
自创建后被修改了两次,第一次是根据脚本文件来设置,另一次是根据解释器文件来调整。此为该补丁修复的问题所产生的基础。在 5.8 及以上版本中,该流程被重构。