这次问题最容易误判:同一份 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
2
3
4
5
6
7
adb shell run-as com.vchangxiao.pystudio `
/data/data/com.vchangxiao.pystudio/files/usr/bin/proot `
-r /data/data/com.vchangxiao.pystudio/files/alpine `
-0 -b /dev -b /proc -b /sys `
-b /storage -b /sdcard:/sdcard `
--kill-on-exit --link2symlink -w /root `
/bin/sh -c "/bin/true && echo OK && uname -m"

它能输出:

1
2
OK
x86_64

但这只能说明 rootfs、二进制、架构、基础参数大体没坏,不能说明 App 进程域里能跑。Android App 的真实启动路径、SELinux context、zygote/seccomp 策略,和 adb run-as 不是一回事。

后来我把验收门槛改成一个 adb-only 的 App 内探针:

1
2
3
4
5
6
adb shell am broadcast -f 0x20 `
-n com.vchangxiao.pystudio/.terminal.TerminalDiagnosticsReceiver `
-a com.vchangxiao.pystudio.action.PROBE_ALPINE

adb shell run-as com.vchangxiao.pystudio `
cat /data/data/com.vchangxiao.pystudio/files/diagnostics/alpine-proot-diagnostics-latest.txt

这个 receiver 在 App 进程域里跑 DirectProcessHost 和真实终端 JNI/PTY host,所以它才是最终验收线。

不是 rootfs,也不是下载脚本

当时排过很多看起来合理的方向:

  • rootfs 是否缺文件;
  • proot loader 路径是否错;
  • PROOT_LOADERPROOT_LOADER_32 是否需要显式指定;
  • PROOT_NO_SECCOMP=1 是否能绕过;
  • 是否要把终端移到独立 :terminal_runtime 进程;
  • 是否是 ProcessBuilder 假阴性;
  • 是否是 runtime manifest 下载错包。

这些都不是关键点。旧包在 App 域里的共同结果是:

1
2
3
4
5
6
7
8
9
[bare-current]
ok=false
exit=2
/bin/sh: can't fork: Function not implemented

[host-init-current]
ok=false
exit=2
pystudio-alpine-init: line 20: can't fork: Function not implemented

这说明普通 Termux prefix 终端可以作为主路径继续保留,Alpine/proot 要作为独立能力单独探测,不能把失败都归因到 UI 或下载。

真正的红线:app-domain SIGSYS

这张图是这次修复的关键:不是绕过 Android 的 seccomp,而是在 proot 捕获到 SIGSYS 后把 fork 分支改写到能继续走的 clone 路径。
后来构建了 fork trace 诊断版 proot,终于抓到关键日志:

1
2
3
4
SIGSYS pid=26687 code=1 syscall=57 arch=3221225534
handle_seccomp_event current=fork raw=57
handle_seccomp_event_common sys=fork
/bin/sh: can't fork: Function not implemented

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
2
3
proot 5.1.107.81-3
PyStudio-Proot-Variant: runtime-fork-to-clone-seccomp
PyStudio-Proot-Patch-Summary: rewrite x86_64 app-domain SIGSYS-trapped fork to clone(SIGCHLD)

也就是:当 App 域里 x86_64 fork 被 SIGSYS 拦住时,把它改写成:

1
clone(SIGCHLD, NULL, NULL, NULL, 0)

然后继续走 proot 已有的 child-event 路径。

同一批里也试过 vfork 方向,但结果是崩:

1
2
PTRACE_EVENT_VFORK ...
proot info: vpid 1: terminated with signal 11

所以不要把 vfork 当成运行时包路线。能过 App 域的,是 fork-to-clone。

r100 验收结果

在 x86_64 模拟器上,r100 过了真正的 App 内验收:

1
2
3
4
5
[host-init-current]
ok=true
exit=0
PYSTUDIO_ALPINE_PROBE_OK
x86_64

更新诊断命令后,其他行也都变绿:

1
2
3
4
5
6
7
8
9
10
11
[bare-current]
ok=true
exit=0
PYSTUDIO_ALPINE_BARE_PROBE_OK
x86_64

[env-no-seccomp]
ok=true
exit=0
PYSTUDIO_ALPINE_DIAG_ENV_NO_SECCOMP_OK
x86_64

这里还顺手修了一个诊断误报: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 域,后面才有资格谈修复。