Linux 脚本执行:由一个补丁分析说起

事情背景

补丁链接: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() 函数。这个函数按顺序做了以下几件事情:

  1. 看文件的头两个字节是不是 #!,若不是则说明不是脚本文件,直接退出。
  2. 随后解析 #! 后面的内容,将解释器名称和参数解析出来。
  3. 用解析出来的解释器文件名称和参数调整当前进程的 bprm

经调整后,当前进程就从“运行当前脚本文件”变成了“运行解释器并解释执行当前脚本文件”。


最后解释一下上述补丁修复的错误场景:在脚本文件有 suid/sgid 而解释器文件没有的情况下,子进程应该获取到父进程的 cap_ambient2(因为本质上是解释器文件在被执行)。但由于当前的 cap_bprm_set_creds() 没有主动设置 cap_ambient,导致在 (1) 处被清除的 cap_ambient 在 (2) 处无法恢复成父进程的。

这与上面描述的执行脚本文件的过程是相关的:从逻辑上讲,bprm 自创建后被修改了两次,第一次是根据脚本文件来设置,另一次是根据解释器文件来调整。此为该补丁修复的问题所产生的基础。在 5.8 及以上版本中,该流程被重构。

参考资料

  1. man capabilities(7)
  2. capabilities: Ambient capabilities – LWN.net
  3. What The @#$%&! (Heck) is this #! (Hash-Bang) Thingy In My Bash Script | Linux Journal

  1. 分析代码路径时用的是 4.19 代码。到了 5.8 版本,security_bprm_set_creds() 被更名;下述执行路径也被修改,从而该函数只会被调用一次。 ↩︎

  2. 关于 Ambient Capabilities 的详细信息,见参考资料 12 。 ↩︎


进程切换与抢占
Spinlock 代码及故障模型分析