Android App 里跑 proot:一次 fork 被 seccomp 拦截后的排查复盘
这次问题最容易误判:同一份 Alpine rootfs、同一个 proot 二进制,在 adb run-as 里能跑,到了 App 真实终端里却报 /bin/sh: can't fork: Function not implemented。如果只看控制实验,很容易去重做 rootfs、下载脚本或 UI。
真正的分界线在 Android App 进程域。adb run-as 成功只能说明文件和参数大体没坏,不能证明 zygote/seccomp/SELinux 这条真实路径也通。这篇按排查顺序写,重点是如何把验收线放回 App 域里。
左边是容易误导人的控制组,右边才是 App 内终端真正会走到的路径。
一开始误判的地方
最容易误判的是这条控制实验:
1 | adb shell run-as com.vchangxiao.pystudio ` |
它能输出:
1 | OK |
但这只能说明 rootfs、二进制、架构、基础参数大体没坏,不能说明 App 进程域里能跑。Android App 的真实启动路径、SELinux context、zygote/seccomp 策略,和 adb run-as 不是一回事。
后来我把验收门槛改成一个 adb-only 的 App 内探针:
1 | adb shell am broadcast -f 0x20 ` |
这个 receiver 在 App 进程域里跑 DirectProcessHost 和真实终端 JNI/PTY host,所以它才是最终验收线。
不是 rootfs,也不是下载脚本
当时排过很多看起来合理的方向:
- rootfs 是否缺文件;
- proot loader 路径是否错;
PROOT_LOADER、PROOT_LOADER_32是否需要显式指定;PROOT_NO_SECCOMP=1是否能绕过;- 是否要把终端移到独立
:terminal_runtime进程; - 是否是
ProcessBuilder假阴性; - 是否是 runtime manifest 下载错包。
这些都不是关键点。旧包在 App 域里的共同结果是:
1 | [bare-current] |
这说明普通 Termux prefix 终端可以作为主路径继续保留,Alpine/proot 要作为独立能力单独探测,不能把失败都归因到 UI 或下载。
真正的红线:app-domain SIGSYS
这张图是这次修复的关键:不是绕过 Android 的 seccomp,而是在 proot 捕获到 SIGSYS 后把 fork 分支改写到能继续走的 clone 路径。
后来构建了 fork trace 诊断版 proot,终于抓到关键日志:
1 | SIGSYS pid=26687 code=1 syscall=57 arch=3221225534 |
x86_64 上 fork 是 syscall 57。Android App 进程域里的 seccomp 把它拦住后,proot 收到 SIGSYS,识别到了 PR_fork,但旧实现没有恢复分支,最后把结果变成 -ENOSYS,shell 就只能报 Function not implemented。
重要:PROOT_NO_SECCOMP=1 不是万能的。这里触发的不是“proot 自己的 seccomp 加速开关导致的问题”,而是 Android App 进程域本身的 seccomp 行为。关掉 proot 的 seccomp filter,并不能让 Android 不拦 syscall。
最后有效的补丁方向
最终有效路线是 r100 里的 fork-to-clone 方案:
1 | proot 5.1.107.81-3 |
也就是:当 App 域里 x86_64 fork 被 SIGSYS 拦住时,把它改写成:
1 | clone(SIGCHLD, NULL, NULL, NULL, 0) |
然后继续走 proot 已有的 child-event 路径。
同一批里也试过 vfork 方向,但结果是崩:
1 | PTRACE_EVENT_VFORK ... |
所以不要把 vfork 当成运行时包路线。能过 App 域的,是 fork-to-clone。
r100 验收结果
在 x86_64 模拟器上,r100 过了真正的 App 内验收:
1 | [host-init-current] |
更新诊断命令后,其他行也都变绿:
1 | [bare-current] |
这里还顺手修了一个诊断误报:Alpine rootfs 里可能没有 /bin/uname,旧 smoke 命令在打印成功 marker 后继续跑 uname -m,会把成功探针误判成失败。现在改成:
1 | /bin/true && echo PYSTUDIO_ALPINE_PROBE_OK && (/bin/busybox uname -m || uname -m || true) |
这次留下的经验
adb run-as 成功只能当控制组。Android App 内运行 Linux 用户态工具时,必须准备 app-domain 探针;否则你以为自己验证了真实路径,实际上只是在 adb 的另一条路上绕了一圈。
排查时要把终端 UI、runtime manifest、rootfs、proot 二进制分开看。看到 can't fork 就重做下载器或清空 App 数据,通常只会把问题埋得更深。proot 在 Android 上也不是“拿 upstream 编译一下”就完事,Termux proot 值得参考,正是因为它长期处理 seccomp、ptrace、loader、linker、process_vm 这些细节。
验收脚本也要克制:成功 marker 足够判断核心路径,额外信息要允许缺失。发布包时同理,不能只上传 Packages.xz 和 JSON 索引;App 的包管理器最终要下载 .deb,pool 或 release 里的文件如果是 404,用户看到的还是安装失败。
这类问题的麻烦不在某一行参数,而在验收线有没有放对。验证路径放回 App 域,后面才有资格谈修复。