一次 Android chroot 递归挂载事故
这台手机怎样安装 Ubuntu、配置 22 和 8022 两个 SSH 端口,写在上一篇。Ubuntu 24.04 运行在 Magisk 的 chroot-distro 中,没有 systemd,开机服务由 /data/adb/service.d/ 中的脚本启动。
2026 年 8 月 30 日,我在手机上使用了一次“清理/加速”。清理结束后,Android 上的 Tailscale、Termux SSH 和 Ubuntu SSH 都离线了,多个 App 一直停在启动画面,正常模式下很难继续操作。
这次故障实际包含三个问题:/data/user/0 权限被改、Ubuntu 递归挂载、Tailscale 被卸载。只有递归挂载找到了确认原因,其余两个只有现象,没有结论。
1. 恢复基本操作
我先重启进入安全模式。安全模式会停用第三方 App,此时系统设置中的页面可以正常打开。
随后正常重启,连接 Wi-Fi,并解锁一次手机。Android 的凭据加密存储要在首次解锁后才可用,Termux 也依赖这部分存储。完成解锁后,Termux 已经可以执行命令,但普通 App 仍然打不开。
故障前同时安装了 ACC、Scene、LSPosed 和 chroot 相关模块。为了减少同时启动的模块,我先保持 chroot 相关模块关闭,确认 Android 能正常启动并取得 root;之后只启用 ACC、BusyBox 和 chroot-distro,Scene 与 LSPosed 暂时没有启用。
2. Termux 恢复后的检查
Termux 可以执行命令后,我查看了 /data/user/0 的权限和 Android 当前的挂载。
当时 /data/user/0 是:
0700 system:system
这个目录是各个 App 私有数据目录的上级目录。正常权限是 0771 system:system,其他 App 的 UID 需要经过它,才能进入自己的数据目录。权限变成 0700 后,只有 system 用户可以进入。Termux 的日志中有 EACCES:创建 shared_prefs 目录时被拒绝,进程因此停在启动阶段。其他 App 的故障形式相同。
挂载数量也明显异常:Android 的总挂载数一度超过两千,Ubuntu 所在目录下有数百层重复挂载,其中还有递归绑定。
又经过一次安全模式和正常重启后,/data/user/0 的权限恢复为 0771 system:system,递归挂载消失,App 也能正常打开。/data/user/0 为什么变成 0700 没有查到,也没有留下权限被修改的日志。
3. Ubuntu 挂载为什么不断增加
事故前,/data/adb/service.d/ 中有三个脚本:一个启动 Ubuntu sshd,一个启动 fail2ban 和 rsyslog,一个启动 Beszel Agent。三个脚本各自带有等待和重试逻辑,并且都会调用:
chroot-distro command ubuntu "..."
chroot-distro command 执行命令前会准备 Ubuntu 所需的挂载,命令结束后不会自动卸载。三个脚本分别重试时,同一个 Ubuntu 根目录会一次次重新挂载。
当时 android_bind=1,Android 的 /data 也会绑定到 Ubuntu 中。Ubuntu 本身又放在 /data 下面,于是新一轮挂载会再次包含前一轮的内容,层数越来越多。
4. 插曲:pgrep 造成的误判
早期脚本用下面的命令检查 sshd:
pgrep -f /usr/sbin/sshd
pgrep -f 匹配完整命令行。执行检查的命令本身也带有 /usr/sbin/sshd,所以它可能匹配到自己,让脚本误以为 sshd 已经启动。后来我把检查方式改成查看 22 端口是否监听。
这个错误只影响 sshd 的启动检查,与递归挂载无关。
5. 把三个 Ubuntu 脚本换成一个
我把原来的三个 Ubuntu 脚本移出 /data/adb/service.d/ 并保留备份,再增加 94-ubuntu-services.sh。开机时先挂载一次 Ubuntu:
chroot-distro mount ubuntu
后续执行 Ubuntu 内的管理脚本时,禁止 command 再次准备挂载:
CHROOT_DISTRO_MOUNT=false \
chroot-distro command ubuntu "/usr/local/sbin/phone-server-services"
94-ubuntu-services.sh 只在首次成功时执行一次 mount。supervisor 退出后,脚本等待 15 秒再重新执行上面的 command,不再重复挂载;只有初次 mount 失败时才会等待 30 秒重试。这样即使 supervisor 反复退出,Ubuntu 的挂载层数也不会再增加。
Android 的 /data 绑定也关闭:
chroot-distro android-bind disable
这条命令修改 /data/local/chroot-distro/.config/android_bind。关闭后,Ubuntu 不再把 Android 的 /data 挂进来。android_bind 和 CHROOT_DISTRO_MOUNT 的说明可以查看 chroot-distro Magisk 模块文档。
Ubuntu 没有 systemd,/usr/local/sbin/phone-server-services 会创建 /run/sshd,再启动选中的服务。这个脚本默认只启动 sshd(下载链接见文末)。rsyslog 曾因为无法写入 /var/log/auth.log 报错,rsyslog 与 fail2ban 还没有确认能长期正常运行,因此默认关闭。
修改后,/data/adb/service.d/ 中与手机服务器有关的脚本如下:
| 脚本 | 作用 |
|---|---|
60-termux-wake-keeper.sh |
启动 Termux wake keeper,每 60 秒续锁 |
94-ubuntu-services.sh |
挂载一次 Ubuntu,并启动 Ubuntu 内的服务 |
98-termux-sshd.sh |
解锁后启动 Termux SSH(8022),最长重试约 30 分钟 |
99-phone-server.sh |
启动 wifi-keeper;Wi-Fi 与 IPv6 的检查和重连由 wifi-keeper 完成 |
可以从附件说明下载 94-ubuntu-services.sh 和 phone-server-services。最初安装 chroot 的方法来自 Semmering 的手机搭建服务器第五期。
6. 重新安装 Tailscale
Android 的软件包历史中有 Tailscale 的卸载记录,应用数据目录也已不存在,此外没有更详细的日志,旧节点无法继续使用。
Tailscale 只安装在 Android,Ubuntu 中没有另装一套。重新安装 App 后,我使用一次性 Headscale 密钥注册为 phone-server。确认新节点可以连接后,再删除旧节点。SSH、Kuma 和 Beszel 中需要修改的地址写在组网与监控篇。
7. 遗留的两个问题
手机上的 App、Termux SSH、Ubuntu SSH、Kuma 和 Beszel 都恢复了,但 Ubuntu 与 Android 显示的系统负载仍在 33 左右,目前还没有找到对应的进程、线程或内核态等待。
rsyslog 和 fail2ban 也没有完成长期运行检查。在这两个问题查明之前,这台手机还不能当作可以长期无人维护的服务器。