暗色模式

Linux chroot 沙盒隔离:从根目录切换到轻量环境构建

技术教程
2026-09-10
10
0
本文要点
  • chroot 把进程看到的根目录 / 换成另一个目录:进程从此「看不见」主机文件系统,只能看到沙盒里放进去的东西——实测进入沙盒后 ls / 只剩自己放进去的 binls /root 直接报 No such file or directory
  • 零依赖搭沙盒:一个静态编译的 busybox(实测 2.1MB 单文件,本机系统自带;没有的话 apt-get install busybox-static 即可)复制进目录就是完整用户态
  • 沙盒默认没有 /proc/etc、任何主机文件,ps 会报 can't open '/proc';需要时可用 unshare -m 挂载私有 proc
  • chroot 只隔离文件系统视图,不隔离网络:实测在沙盒里起 busybox httpd,宿主 curl 127.0.0.1:8080 正常返回内容
  • chroot 不是安全边界:root 用户可以在进入沙盒前保存真实根目录的文件描述符,之后借 fchdir 逃出——本文实测三行 Python 完成逃逸
  • 现代容器 = 命名空间(unshare)+ 换根目录 + cgroup 资源限制:实测 unshare -u 改主机名只影响命名空间内

Linux chroot 沙盒隔离:从根目录切换到轻量环境构建

假设你想在一台干净服务器上跑某个来路不明的程序,又不想让它碰到 /home/etc 里的任何东西;或者你想测试一个软件在「空系统」里能不能跑——不需要装虚拟机,也不需要 Docker 镜像,Linux 自带一个最朴素的隔离原语:chroot

chroot 的作用只有一句话:把当前进程(及其子进程)看到的根目录 /,换成你指定的目录。换完以后,进程访问 /etc/passwd/root/home,实际上访问的都是新根目录里对应路径——主机上那些目录它一概看不见。本文在一台 Ubuntu 22.04 云服务器(KVM,内核 5.15.0-30-generic)上用静态 busybox 从零构建一个轻量沙盒,实测文件隔离、网络行为,最后做一个「逃逸」实验——顺便说清楚 chroot 为什么不是安全边界。全部命令真实执行,截图即真实输出。

原料:一个静态 busybox 就够

沙盒里要有可执行文件,最简单的是放一个静态编译的 busybox——单文件集成 sh、ls、cp、httpd 等几百个常用命令,不依赖任何动态库,复制进沙盒即可运行。先确认本机这个二进制:

ls -lh /usr/bin/busybox && busybox 2>&1 | head -1
-rwxr-xr-x 1 root root 2.1M Feb  4  2022 /usr/bin/busybox
BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) multi-call binary.

静态与否决定它能不能脱离宿主运行环境:ldd /usr/bin/busybox 输出「not a dynamic executable」即为静态。如果你的发行版没有 busybox,一条命令补上(约 1MB;本文演示机系统自带 busybox,此步未执行):

apt-get install -y busybox-static

构建沙盒并进入

沙盒目录就放一个 bin/busybox,完事。下面这条命令为了演示可重复,链首先用 rm -rf 重置沙盒目录,然后 mkdir + cp 构建,最后 chroot 进入并环顾四周:

rm -rf /opt/jail && mkdir -p /opt/jail/bin && cp /usr/bin/busybox /opt/jail/bin/ && chroot /opt/jail /bin/busybox sh -c 'echo "== in jail =="; pwd; ls -la /; ls /root 2>&1'

进入沙盒:根目录只剩自己放进去的内容,主机的 /root 不可见

输出值得逐行看:

  • pwd 显示 /——进程的根目录已经被换成 /opt/jail,所以它在沙盒里看到的一切都以这里为根;
  • ls -la / 只有 ...bin——主机上几十个目录一概不存在,因为沙盒的根目录里只放了我们拷贝进去的 busybox
  • ls /rootNo such file or directory——主机的 /root/etc/home/proc 在沙盒视角里全都「不存在」。

这就是 chroot 的全部魔法:不是把目录藏起来,而是换了一个根,进程从根上就够不着外面的东西。代价是沙盒里缺的东西全得自己放——比如想看进程列表,得先有 /proc,直接跑 ps 会报错:

chroot /opt/jail /bin/busybox ps
PID   USER     COMMAND
ps: can't open '/proc': No such file or directory

在沙盒里跑服务:文件隔离,网络不隔离

chroot 只管文件系统视图,管不着网络。沙盒里的进程照样能监听端口、收发网络包。这一点用 busybox 自带的迷你 Web 服务器 httpd 实测最直观:先在沙盒里放一个首页文件,再从沙盒里启动 httpd,然后在宿主上用 curl 访问:

mkdir -p /opt/jail/www; printf 'hello from chroot jail\n' > /opt/jail/www/index.html; chroot /opt/jail /bin/busybox httpd -f -p 127.0.0.1:8080 -h /www >/dev/null 2>&1 & sleep 1; curl -s http://127.0.0.1:8080/; echo; kill $!; echo httpd-stopped

沙盒内跑 busybox httpd:文件隔离但网络照常可达

说明几点:

  • -h /www 是沙盒内的相对路径——httpd 在 chroot 之后启动,它的工作目录已经在沙盒根里,/www 指向沙盒里的 index.html
  • httpd 绑定 127.0.0.1:8080,宿主 curl 一次成功,证明进程虽然被关了「文件系统禁闭」,网络栈仍是宿主同一个
  • 这也给出一个实用场景:把不信任的静态站点、旧版程序丢进沙盒跑,文件访问面被锁死,网络功能照常可用(沙盒服务若只服务本机,记得绑 127.0.0.1 而不是 0.0.0.0)。

逃逸实验:chroot 不是安全边界

chroot 这么好用,能不能拿它当「沙箱」关犯人?答案是否定的,原因很本质:chroot 只改变了路径解析的起点,进程手里的文件描述符(fd)不受影响。root(准确说持有 CAP_SYS_CHROOT)的进程可以在进入沙盒前先打开真实根目录,事后用 fd 把当前目录「拽」回去——经典逃逸手法,man 手册里白纸黑字写着(chroot(2) NOTES)。

把逃逸脚本写到沙盒外(放沙盒目录里会被 rm -rf 一起清掉,放 /opt/escape.py):

cat > /opt/escape.py <<'PY'
import os
fd = os.open("/", os.O_RDONLY)   # 进入沙盒前, 先抓住真实根目录的句柄
os.chroot("/opt/jail")           # 切换根目录, 进入沙盒
os.fchdir(fd)                    # 用句柄把当前目录拉回真实根目录
os.chdir("..")
print("真实根目录内容(逃出后):", sorted(os.listdir("."))[:6])
print("沙盒视图内 /etc/hostname 存在:", os.path.exists("/etc/hostname"))
print("真实视图内 etc/hostname 存在:", os.path.exists("etc/hostname"))
PY

执行它:

python3 /opt/escape.py

用保存的目录句柄逃出沙盒:chroot 不是安全边界

三行输出把逃逸过程讲得明明白白:

  1. 逃出后 os.listdir(".") 看到的又是真实根目录(bin boot dev etc home lib……);
  2. 用绝对路径 /etc/hostname 访问——还是被沙盒拦着(False),因为绝对路径仍从沙盒根开始解析;
  3. 用相对路径 etc/hostname(从被拽回真实根目录的 cwd 出发)——真实文件系统触手可及(True)。

注意这个脚本是 root 跑的:非 root 用户连 chroot 本身都调不动(缺 CAP_SYS_CHROOT)。但反过来也说明:凡是有 root 的地方,chroot 都拦不住——历史上真实世界用 chroot 关押「叛逃」进程被越狱的案例一抓一大把。所以 chroot 的定位是环境隔离(测试、救援、依赖打包),不是安全隔离(防恶意进程)。要安全,得靠下一节的能力组合。

从 chroot 到容器:命名空间补齐隔离

容器技术本质上就是补 chroot 缺的那几块:换根目录 + 一堆命名空间 + cgroup 资源限制。命名空间由 unshare/clone 创建,每一种命名空间隔离一类全局资源。实测最直观的 UTS 命名空间——隔离主机名:

hostname && unshare -u sh -c 'hostname jail-demo && echo "in-ns: $(hostname)"' && echo "out-ns: $(hostname)"
MFY001832765701
in-ns: jail-demo
out-ns: MFY001832765701

沙盒里把主机名改成 jail-demo,命名空间一退出,宿主主机名纹丝不动。类似地,挂载命名空间(unshare -m)可以给沙盒单独挂一个干净的 /proc——这正是上节 ps 报错的标准解法:

unshare -m sh -c 'mkdir -p /opt/jail/proc && mount -t proc proc /opt/jail/proc && echo MNT-PROC-OK && umount /opt/jail/proc'
MNT-PROC-OK

加上 cgroup 限 CPU/内存(本站 cgroup 资源控制 一文有完整实测),「文件隔离 + 资源隔离 + 系统视图隔离」齐了,这就是 Docker 容器 在底层干的事(网络命名空间的玩法另见本站 网络命名空间)。所以想深入容器原理,chroot + unshare 是最好的起点——它们全是内核原语,不依赖任何镜像层。

清理

演示产物只有两个目录/文件,确认后删掉即可:

rm -rf /opt/jail /opt/escape.py

收个尾:chroot 的价值在于它把「隔离」这个概念拆到了最小可理解单元——一个系统调用,一个目录,进程的世界就变了。日常里它最常见的两个用途:一是救援(系统起不来时 chroot 进挂载好的磁盘修引导、改配置),二是给软件一个干净的家(构建环境、测试环境、老软件运行环境)。记住它的边界——能隔离文件视图,隔离不了网络,更拦不住 root——你就不会在错误的地方依赖它了。

发表评论

暂无评论,快来抢沙发吧!