Skip to content

feat(terminal): add optional chroot container backend alongside proot - #475

Open
BUSHIGEMENNI wants to merge 10 commits into
omnimind-ai:mainfrom
BUSHIGEMENNI:feat/chroot-backend
Open

feat(terminal): add optional chroot container backend alongside proot#475
BUSHIGEMENNI wants to merge 10 commits into
omnimind-ai:mainfrom
BUSHIGEMENNI:feat/chroot-backend

Conversation

@BUSHIGEMENNI

@BUSHIGEMENNI BUSHIGEMENNI commented Aug 14, 2026

Copy link
Copy Markdown

背景

proot 用户态模拟存在性能开销;本 PR 新增可选 chroot 后端(需 root 设备),提供真 root 容器体验。

改动

  • 新增「容器后端」与「Agent 容器后端」独立开关,默认 proot
  • host/root 两段启动脚本:su 提权、nsenter+unshare 私有挂载命名空间、rbind 挂载、chroot
  • RootProbe 校验 uid+CapEff,失败自动回退 proot
  • root 进程组回收、launcher 并发安全、su 环境穿透
  • 适配 Android 应用数据隔离,容器内可见全部应用数据

验证

单测全量通过;真机(KernelSU)验证真 root、/data 完整视图、killpg 整组回收、挂载零泄漏。

@XuYouo XuYouo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前请求修改,暂不建议合并。CI 虽然通过,但 chroot 路径仍有两个安全边界问题和数个确定的运行时缺陷:

  1. [P1] Agent 提权边界被绕过。 TerminalManager.ensureShellScripts() 读取同一个全局 container_backend,因此 terminal_executeterminal_session_* 和 ACP 进程都会切到 chroot;root 脚本同时以 root 挂载真实 /data。但现有 AgentToolDefinitions.kt / AgentSystemPrompt.kt 明确承诺普通终端固定走 proot,Android 高权限动作应使用需要显式确认的 Shizuku 工具。请切换到独立、明确确认的 root 工具与权限策略,不能由终端 UI 的性能开关静默提权。

  2. [P1] 停止/超时不能保证结束 root 命令。 init-host-chroot.sh 的外层 shell 启动 su -c,而 TerminalManager、工具手动停止和 ACP 关闭只销毁 ProcessBuilder 返回的外层进程。destroyForcibly() 不会自动清理其子孙进程,root 命令和挂载命名空间可能在工具已经报告停止后继续存在。请增加 root 侧监督/进程组回收,并覆盖超时、手动停止、ACP close 和后台子进程测试。

  3. [P2] bind_one()/apex/dev/storage/sys 使用普通 mount --bind 普通 bind 不复制子挂载,会遗漏 APEX、/dev/pts 和模拟存储。请按目录处理递归挂载,并在真机验证 APEX 路径、PTY 与 /storage/emulated/0

  4. [P2] su 环境穿透不完整。 launcher 导出 TERMINAL_DISTRIBUTION,但 init.sh 读取 OMNIBOT_TERMINAL_DISTRIBUTION;会过滤环境的 su 下,Ubuntu rootfs 会按 Alpine 初始化。LocalAcpRuntime 传入的动态 extraEnvironment 也没有进入 launcher。请统一变量并可靠传递动态环境,补 Ubuntu/ACP 回归测试。

  5. [P2] launcher 清理存在并发竞争。 每次启动都执行 rm -f "$PREFIX"/local/bin/.chroot-launcher.*,可能删除另一个并发会话已创建、但尚未被 su 打开的 launcher。请只清理当前调用自己的文件;历史文件使用锁或按年龄回收。

  6. [P2] RootProbe 只判断 su -c id 的退出码。 KernelSU App Profile 可以限制 UID/capabilities;命令退出 0 不等于具备 unshare/mount/chroot 能力,而且授权撤销后持久化的 chroot 设置不会在启动时回退。请验证实际 UID/能力,并在每次启动时重新检查、失败则回退 proot。

本地补跑 :core:main:testDebugUnitTest、两份脚本 sh -ngit diff --check 均通过;这些检查无法覆盖上述 root 权限、挂载树和并发生命周期问题。

@BUSHIGEMENNI
BUSHIGEMENNI marked this pull request as draft August 14, 2026 07:08
@BUSHIGEMENNI
BUSHIGEMENNI marked this pull request as ready for review August 15, 2026 06:26
@BUSHIGEMENNI
BUSHIGEMENNI marked this pull request as draft August 15, 2026 06:42
@BUSHIGEMENNI
BUSHIGEMENNI marked this pull request as ready for review August 15, 2026 07:19
@BUSHIGEMENNI
BUSHIGEMENNI marked this pull request as draft August 15, 2026 07:53
@BUSHIGEMENNI
BUSHIGEMENNI marked this pull request as ready for review August 15, 2026 07:55
@BUSHIGEMENNI
BUSHIGEMENNI marked this pull request as draft August 15, 2026 08:17
@BUSHIGEMENNI
BUSHIGEMENNI marked this pull request as ready for review August 15, 2026 08:32
@BUSHIGEMENNI
BUSHIGEMENNI force-pushed the feat/chroot-backend branch 4 times, most recently from e782a29 to ec3b7e9 Compare August 15, 2026 08:50
@BUSHIGEMENNI

Copy link
Copy Markdown
Author

已按 review 完成全部整改:提权隔离、进程组回收、递归挂载、环境穿透、launcher 并发、RootProbe 能力校验。均已单测 + 真机验证通过,请再审。

… cross-border mirrors

Used -X theirs to take PR omnimind-ai#475's full rewrite of TerminalManager and
EnvironmentSetupLogic (they include the chroot publishState /
ManagedSessionClient APIs that HEAD lacks), and upstream's cross-border
mirror fixes. Conflict on app/build.gradle.kts versionCode resolved
in favor of upstream's 1 (monotonic since the master release is on 6).

This effectively rolls back the previous manual cherry-pick attempt
that missed several files.
…idget, ignore .progress/

- versionCode 1 -> 7 (must exceed 0.5.9 code=6 to upgrade)
- versionName 0.5.9 -> 0.5.9 (kept; perf flavor adds -perf suffix)
- Pull widgets/embedded_terminal_init_overlay.dart from upstream/main
  (referenced by app_bootstrap.dart but missing from cherry-pick).
- .gitignore: skip .progress/ local artifacts
Flutter Gradle plugin requires Gradle 8.14+ (was forcing error on
8.13). Tencent mirror zip already cached at ~/.gradle/wrapper/dists.
Cherry-picking 8371aec + 43a2c87 with -X theirs dropped the perf flavor
that 43a2c87 had added; upstream's main does not have it. Re-apply the
perf productFlavor block and the x86_64 abiFilter so the
assemblePerfStandardRelease task exists again.
The ReTerminal Theming section was deleted by the branch reset; this
drop in the PR base is unrelated to PR omnimind-ai#475 (chroot backend) and
only widens the conflict window. Restore it so the PR diff only
carries chroot + cross-border mirror changes.
Base 0.5.9 release already shipped versionCode=6; the prior bump to 7
in the chroot branch only widened the merge conflict window without
adding any functionality. Take base value so PR omnimind-ai#475 merges cleanly;
a follow-up release commit can bump it back to 7 once merged.
chroot launcher's setgid+2770 keeps *new* files group-writable for the
app uid, but historic root:root 0600 files (written before the init.sh
umask 002 took effect, or by other tools) stay locked. Each new
container start re-runs the chown so the app can always read its own
workspace files.
chroot launcher's chown was only applied to OMNIBOT_HOST_WORKSPACE,
but local/run/ (chroot pid files) is also written by root and read by
the App. Without the chown, App-side ChrootProcessReaper can not
remove stale pid files written by the launcher. Extend the self-heal
chown to cover local/run/ alongside the workspace.

Also rename the inline chown expression into chown_recursive_to_app()
so future mount-target self-heal entries (e.g. attachments, browser,
skills under workspace) can share the same guard clause without
copy-pasting the stat/uid check.
Inside the chroot, root-uid processes (apt, npm install, corepack pnpm)
create symlinks and cache files owner=root. The App then tries to
invoke the resulting binaries (codex-acp, pnpm, ...) and hits
EACCES on the symlink even though the target is world-readable.

Recursively chown /root/{.npm-global,.cache,.config,.local,.dsh,
.codex,.opencode,.claude} to app uid on every container start so
agents can invoke any installed tool without first running a manual
chown pass.
Previously only workspace, local/run/, and the in-rootfs /root dirs
were self-healed. Manual chroot exploration by the user (or any
root-uid process that writes under \/shared_prefs, databases,
code_cache, etc.) would leave root:root 0600 files that the App
cannot read until next manual chown.

Find every non-app-uid file/dir under \ except the embedded
rootfs subtree (\/local/ubuntu), and chown to app uid in one
pass. Skips symlinks (their target resolves separately) and only
touches files whose current owner is wrong, which keeps the per-start
cost bounded by whatever drift accumulated since last run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants