SELinux:系统启动时通过用户态配置动态关闭的流程

要在系统启动时彻底关闭 SELinux(即设为 Disabled 状态),已知有几种办法:

  • 内核启动参数 selinux=0
  • 配置 /etc/selinux/config

关于 /etc/selinux/config,我们知道可以通过它来对 SELinux 进行初始设置,包括运行状态以及要加载的策略文件。那么,这是怎么做到的呢?又是谁实际执行了这些操作?在开闭 SELinux 上,通过它与通过内核启动参数有何不同?

首先,内核对该文件完全是不感知的:查遍 security/selinux 下的所有代码,都没有关于该文件的内容。随后在 Fedora 的 Wiki 上查到:

When booting up the machine, init uses libselinux to read this file, and determines which policy to load and what mode to put the machine in.

那么理所当然地,就要去查一下用户态的 libselinux 和(当前以 systemd 为代表的)init 程序了。通过一番探寻,终于将整个流程梳理清楚了。

代码分析

libselinux

首先是 libselinux 库 。简言之,该库对 SELinux 的内核态接口做了封装,向用户态提供操作 SELinux 相关的 API。通过搜索 /etc/selinux/config 关键词,可以发现该文件路径被硬编码到了代码中:

1/* libselinux/src/selinux_internal.h */
2#define SELINUXDIR "/etc/selinux/"
3#define SELINUXCONFIG SELINUXDIR "config"

基于 SELINUXCONFIG 继续搜索,最终发现读取该配置文件和操作 SELinux 内核态接口以关闭 SELinux 的流程。可以看到,这里实际上就是利用了 SELinux 的动态关闭机制,即通过写入 selinuxfs/disable 接口来实现的关闭 SELinux:

 1/* libselinux/src/load_policy.c */
 2selinux_init_load_policy(int *enforce) {
 3    selinux_getenforcemode(enforce:&seconfig) {
 4        /* libselinux/src/selinux_config.c */
 5        FILE *cfg = fopen(SELINUXCONFIG, "re")
 6        // if "disabled" is found
 7        *enforce = -1
 8    }
 9    if (seconfig == -1) {
10        security_disable() {
11            /* libselinux/src/selinux_config.c */
12            snprintf(path, ..., "%s/disable", selinux_mnt)
13            fd = open(path, ...)
14            buf[0] = '1'
15            buf[1] = '\0'
16            write(fd, buf, ...)
17        }
18        umount(selinux_mnt)
19        goto noload
20    }
21  noload:
22    return -1
23}

上述代码中用到了 selinux_mnt 全局变量。该变量由动态库的初始化函数1在动态库加载完毕后设置:

 1/* libselinux/src/init.c */
 2init_lib(void) __attribute__((constructor)) {
 3    init_selinuxmnt() {
 4        verify_selinuxmnt(mnt:SELINUXMNT) {
 5            statfs(mnt, &sfbuf)
 6            sfbuf.f_type == SELINUX_MAGIC
 7            set_selinuxmnt(mnt) {
 8                selinux_mnt = strdup(mnt)
 9            }
10        }
11        verify_selinuxmnt(OLDSELINUXMNT)
12        fp = fopen("/proc/mounts", "re")
13        // check if selinuxfs appears in /proc/mounts
14    }
15}

该过程会去检查 selinuxfs 的新、旧默认挂载点(即 SELINUXMNTOLDSELINUXMNT)是否真的挂有 selinuxfs,若这两个点没找到则继续去 /proc/mounts 里面找。确认 selinuxfs 已被挂载的方式是通过 statfs() 系统调用获取文件系统信息,并检查其中的 magic 值。确认后,记录实际挂载点至全局变量 selinux_mnt,供后续其他 API 使用。从下面的代码中可以看出,selinuxfs 的新、旧默认挂载点也是硬编码的文件系统路径:

1/* libselinux/src/policy.h */
2#define SELINUXMNT "/sys/fs/selinux"
3#define OLDSELINUXMNT "/selinux"

systemd

随后,在 systemd 中找到了调用 libselinux 的 selinux_init_load_policy() 函数以配置 SELinux 的过程:

 1/* src/core/main.c */
 2main() {
 3    loaded_policy = false
 4    initialize_security(&loaded_policy, ...) {
 5        r = mac_selinux_setup(loaded_policy) {
 6            /* src/core/selinux-setup */
 7            r = selinux_init_load_policy(&enforce)
 8            if (r == 0) {
 9                *loaded_policy = true
10            }
11            return 0
12        }
13        if (r < 0) { /* set error message*/ ; return r }
14        r = mac_smack_setup(loaded_policy);
15        if (r < 0) { /* set error message*/ ; return r }
16        r = mac_apparmor_setup();
17        if (r < 0) { /* set error message*/ ; return r }
18        r = ima_setup();
19        if (r < 0) { /* set error message*/ ; return r }
20    }
21}

有意思的是,此处代码显示 systemd 在启动后会去尝试配置的 LSM 模块不仅有 SELinux,还包括 Smack、AppArmor 和 IMA。啥它都要管,颇有些“上面千条线,下面一根针”的意思。

总结

所以说,若是通过 /etc/selinux/config 配置文件来关闭的 SELinux,实际上系统会短暂地运行 SELinux 一段时间:在内核启动完毕至 systemd 执行上述流程之间的一小段时间内。


  1. 带有 constructor 属性的函数会在 main() 函数之前自动执行,用户态的动态库一般会利用这类函数做一些初始化的工作。关于该属性的更多详情请参见 GCC 手册 。 ↩︎


Slab Merging 特性
关于 SELinux 策略的各种操作