之前有说过
chroot
这个系统调用, pivot_root
这个系统调用看起来和它很像,但实际上有很大的不同,来看看两者的区别。chroot
是更改进程的根目录,所以它的更改只对执行的进程及其子进程有效,这种隔离并不完全,因为它并没有更改实际的挂载。关键就是这个挂载,因为
chroot
没有对挂载做任何改动,虽然从进程的视角而言,根目录是新指定的,但事实上它并不是一个真正的根文件系统,这就意味着如果有办法绕过 chroot
视角的限制,就能访问到外部。 而
pivot_root
是替换根文件系统,会将目标作为新的根文件系统挂载。这种隔离级别明显高于 chroot
,因为它是从根文件系统这一层面来达成效果的。下面我们看看
pivot_root
的使用。pivot_root(8)
接收两个参数:pivot_root new_root put_old
它会将
new_root
作为新的根文件系统,并将当前的根文件系统挂载到 put_old
。它还有一系列的限制:
new_root
和put_old
必须都是目录
new_root
和put_old
不能是当前的根
put_old
必须在new_root
目录下
new_root
必须是一个挂载点
new_root
和当前根的父挂载点的传播类型不能是MS_SHARED
,并且如果put_old
也是一个挂载点,它的传播类型也不能是MS_SHARED
。这些限制是为了保证pivot_root
不会传播改动到其他的命名空间。
- 当前的根目录必须是一个挂载点
按照上面的限制,我们来实验一下怎么调用
pivot_root(8)
:# 创建 new 目录 mkdir new # 挂载一个临时文件系统到 new 目录 mount -t tmpfs mytmpfs new # 创建 old 目录 mkdir new/old
一般而言,根目录都是一个挂载点。所以目前,我们的环境除了第 5 个限制,已经都满足了,先试一下能不能执行了。
root@j1800:~/new# pivot_root . old/ pivot_root: failed to change root from `.' to `old/': Invalid argument
报错了,看来是第 5 点限制未能满足,那就查看一下:
findmnt -o TARGET,PROPAGATION TARGET PROPAGATION / shared /root/new shared
果不其然,那怎么才能让它们不再是
MS_SHARED
传播类型呢?在执行 mount
时是可以指定传播类型的:mount --make-private / mount --make-private -t tmpfs mytmpfs new
这样之后,我们就能执行
pivot_root(8)
了,但是。如果你已经执行了,那可能要重启系统了,因为我们的新根文件系统中什么都没有,切换后将失去所有的操控能力。
为了更安全的实验,我们下面将使用命名空间的功能来隔离挂载的影响,这里不会去介绍命名空间的作用,因为计划在后面专门去写命名空间的使用。
unshare --mount --fork
执行完上面的命令后,我们会进入一个隔离了挂载点的命名空间中,所有的挂载点都会变更为
private
,这样我们后续所有的挂载操作都不会影响到命名空间外,这样就能安全的实验了。为了切换根文件系统后能够进行操作,我们还需要准备一个可用的新根文件系统,这里利用已有的
docker
镜像,比如 busybox
。docker pull buxybox docker run --rm -d --name busybox busybox top -b docker export -o busybox.tar busybox docker stop busybox mkdir busybox tar -xzf busybox.tar -C busybox mount -t tmpfs mytmpfs new cp -r busybox/* new mkdir new/old cd new pivot_root . old
到现在为止,我们已经能够切换根文件系统了。为了尽可能的安全,我们还需执行两个操作,就是
cd
、 exec
和 umount
。因为 pivot_root
不一定会切换当前程序的目录,比如可能还处在旧的根文件系统中,所以最好手动切换一下。另外我们的旧根还在,这也可能导致问题,所以需要将旧的根文件系统 umount
。cd / && exec /bin/sh # 由于 umount 依赖 /proc mount -t proc myproc /proc umount -l /old
前面我们都是新建了一个
old
目录来挂载旧的根文件系统,一般用于安全隔离的话,我们又会手动 umount
掉这个旧的根文件系统,那这个 old
目录往往是一个临时目录。于是,有人就在思考有没有办法节省掉这个临时目录的创建。参考自 lxc
的实现,能发现他们采用了一种特殊的方式,这个方式并没有在文档中说明,所以事实上这是一种内部机制的利用,且这种机制有可能在后面的内核版本中被更改。当我们把
put_old
指定为 .
时,并不是指定当前的目录,而是 /proc/self/cwd
。于是,利用这个特性,我们就可以不用创建临时目录了。cp -r busybox/* new unshare -mf mount --bind new new cd new pivot_root . . # 此时我们就仍然处在旧的根文件系统中,umount 旧的根文件系统 umount -l . cd / && exec /bin/sh # 此时就切换完毕了 # 下面退出 exit rm -rf new/*
相关链接