PRoot:不需要 root 的 chroot
User-space chroot via ptrace
在 Termux 里装个 proot-distro,一条 login debian 就能在手机上跑起完整的 Debian:有 apt、有 glibc、有 /etc 和 /usr 的标准目录结构,甚至能装桌面环境。手机没解锁、没刷机、没有 root,凭什么?一个普通用户权限的程序,是怎么把一个目录树伪装成整个文件系统的?这就是 PRoot 干的事。
先看 chroot 在干什么
Linux 上有个老机制叫 chroot(change root):它让一个进程看到的”根目录”不再是真的 /,而是你指定的某个目录。进程从此被关在这个目录树里,路径访问出不去。跑发行版靠的就是它——把 Debian 的整个文件系统解压到一个目录里,chroot 进去,里面的程序就以为自己运行在一台正常的 Debian 机器上。
问题在于 chroot 是个特权操作,需要 root 权限。同理,把宿主机的某个目录”挂”进这个假文件系统的 mount –bind,也需要特权。在 Android 上,普通应用永远拿不到这些权限;在没有 sudo 的公司服务器上同样拿不到。PRoot 的目标就是把这三件事——chroot、bind 挂载、以及跑其他 CPU 架构的程序——全部搬到不需要任何特权的用户态实现。
PRoot 的做法:不动内核,只做翻译
PRoot 用到的钥匙是 ptrace——Linux 调试器的底座。gdb 能给程序下断点、单步执行,靠的就是它:父进程通过 ptrace 可以拦下子进程的每一次系统调用,在它真正进入内核前、和内核返回后,各有一次介入机会。这个能力本身不需要特权,父子进程关系天然允许。
PRoot 就当这个”翻译员”。被跟踪的程序调用 open("/etc/hosts"),在系统调用真正发生之前,PRoot 把路径改写成 rootfs 目录里的真实位置再放行,比如 open("~/rootfs/etc/hosts");程序调用 getuid(),PRoot 把返回值改成 0,于是 apt 一类坚持”我必须是 root 才肯干活”的包管理器就被哄住了;程序请求 mount,PRoot 干脆不转发给内核,直接伪造一个成功的结果,然后在后续所有路径翻译里把这个”挂载”兑现。内核全程不知情,被跟踪的程序活在一个被精心维护的错觉里。
这套机制还顺手解决了跨架构问题。配上 QEMU 的用户态模拟(proot 的 -q 参数),x86 机器上可以透明地跑 ARM 发行版:PRoot 负责翻译路径,QEMU 负责翻译指令,两台”翻译员”各管一层。交叉编译的那堆麻烦事,直接被绕开了。
代价都写在 ptrace 里
理解了机制,PRoot 的短板也就一目了然。每个系统调用都要被拦下两次(进入前、返回后),路径还要过一遍翻译,纯 CPU 的活大概慢百分之几到十几,但 I/O 密集的任务——apt 安装、解压 tar 包、大项目编译——会明显变慢,瓶颈不在 CPU 而在文件系统操作的翻译开销。
还有几个结构性限制。ptrace 关系不能中途解除,PRoot 必须一直盯着子进程,所以没法在这里面启动 sshd 之类的守护进程然后退出会话,会话一关进程就得跟着走。gdb 和 strace 自己也依赖 ptrace,和 PRoot 撞在一起经常出问题。新的系统调用如果 PRoot 还没学会翻译,程序就可能撞上一些在原生环境复现不出来的诡异 bug。
要分清的是,fake root 不是真 root。PRoot 只是篡改了”你是谁”这个问题的答案,实际权限一点没变:文件归属还是你的真实用户,写入受真实权限约束,设备节点、FUSE 挂载这些事照样做不了。反过来说,PRoot 也不是安全边界——它提供的是兼容性,不是隔离性,别把它当沙箱用。
它为什么还是无处不在
把这些缺点摆完,PRoot 的生存空间反而更清楚了:它的定位不是”高性能”,也不是”安全隔离”,而是”没有 root 时,让标准 Linux 软件跑起来”。Termux 生态是最大的受益者,proot-distro 靠它支撑了十几种发行版;Ubuntu Touch 用它给受限环境补软件;在没有 sudo 的服务器上,它也能让你拥有一个自己的 Debian 环境。至于速度,编辑文件、跑脚本、SSH 到别处干活这类场景,PRoot 和原生的差距小到感觉不出来。
所以下次在手机上看到一个完整的 Debian,不必惊讶——底下就是一个 ptrace 循环在做同声传译:程序要什么,翻译员就把它带到真实的位置,再把结果原样带回来。内核没被欺骗过一秒,被骗的只有程序自己。
COMMENTS