Skip to content

Run the engine natively (no proot in the hot path): structural fix for the untrusted_app transport corruption #115

Description

@zo-sol

STATUS UPDATE (2026-07-12): the T0 probe (comment below) killed this issue's core premise — the Ubuntu glibc rootfs cannot be launched through the bionic linker (non-PIE rejected; glibc rtld aborts on PHDR under bionic execve; even PIE glibc binaries segfault). The linker-chain modules below (T1/T2 as written) are dead on arrival. The revised plan of record is in this comment: Phase 1 — instrument and patch proot's transport under untrusted_app (timeboxed); Phase 2 — if unfixable, adopt a bionic (termux-ecosystem) userland for the engine. The in-app probe (NativeProbeActivity) stays as the standing on-device gate. Original text kept below for the record.

Goal

Eliminate the #112 bug class at the architecture level: run the four engine binaries (node, claude, codex, git) as native Android processes, with no ptrace layer between them and the kernel. The dulwich clone shim from #114 is an interim patch for one subcommand of one tool; this issue is the structural fix, and it must hold on Snapdragon and MediaTek devices and in the Play Store release build (targetSdk 35).

Context: #112 (root cause + verification threads), #114 (interim shim, merged as 509dee6), fix ladder: #112 (comment)

Why the current architecture breaks

Every engine syscall crosses proot's ptrace translation. Under the untrusted_app SELinux domain (targetSdk 35, required for Play), that translation corrupts multi-process piped/socket data — measured as truncated pack streams and TLS decode failures. Anything pipe-heavy (git today, npm/cargo tomorrow) is exposed.

flowchart TD
    WV["WebView UI"] --> SM["ServerManager"]
    SM --> GE{{"GuestExec seam"}}
    GE --> DPE["DirectProotExec"]
    DPE --> PROOT["proot (ptrace tracer)"]
    PROOT -->|"every syscall translated"| NODE["node server"]
    NODE --> CLAUDE["claude CLI"]
    NODE --> CODEX["codex CLI"]
    NODE --> GIT["git + git-remote-https + index-pack"]
    PROOT -. "CORRUPTS multi-process<br/>pipe/socket data under<br/>untrusted_app (#112)" .-> GIT

    style PROOT fill:#ffe9e6,stroke:#ef4444
    style GIT fill:#ffe9e6,stroke:#ef4444
Loading

Target architecture

Engine processes launch through the system linker into the glibc runtime that already ships inside our rootfs; the rootfs becomes a plain directory prefix (no path emulation). proot stays only as the convenience layer (user shell, apt) where the bug cannot break the product.

flowchart TD
    SM["ServerManager"] --> GE{{"GuestExec seam"}}
    GE -->|"per-binary strategy map<br/>node/claude/codex/git: native or proot"| NLE["NativeLinkerExec (new)"]
    GE -->|"fallback + shell/apt"| DPE["DirectProotExec (kept)"]

    NLE --> L64["/system/bin/linker64<br/>(system_linker_exec: may exec app-storage PIE)"]
    L64 --> LDSO["glibc ld.so from OUR rootfs<br/>(PIE; launches our non-PIE node/claude)"]
    LDSO --> NODE["node server (native)"]
    NODE --> CLAUDE["claude (native, HOME=rootfs/root)"]
    NODE --> GIT["git + helpers (native)<br/>multi-process is safe without ptrace"]
    NODE -.-> CODEX["codex (static musl)<br/>route decided by T0 probe;<br/>may stay under proot"]

    SHIM["libagentnet-redirect.so (new, ours)<br/>LD_PRELOAD interpose: /etc/resolv.conf,<br/>/etc/hosts, nsswitch -> rootfs copies"] -.-> NODE
    SHIM -.-> CLAUDE
    SHIM -.-> GIT

    ROOTFS[("rootfs = plain directory<br/>same files both strategies:<br/>HOME, .claude/, .gitconfig, TMPDIR")] --- NODE
    ROOTFS --- DPE

    style NLE fill:#e6f0ff,stroke:#3b82f6
    style SHIM fill:#e6f0ff,stroke:#3b82f6
    style L64 fill:#e6ffe9,stroke:#22c55e
    style LDSO fill:#e6ffe9,stroke:#22c55e
Loading

Elements to build

# Element What it is Why it is needed
1 In-app probe screen (debug builds) Hidden screen/broadcast that runs each chain step AS the app and reports errno + avc: denials The failure is SELinux-domain-gated; adb run-as runs in runas_app and lies. All go/no-go data must come from untrusted_app
2 NativeLinkerExec : GuestExec New strategy behind the existing seam; builds linker64 -> ld.so -> binary argv + env (HOME, TMPDIR, PATH, preload) Engine launch without ptrace; DirectProotExec untouched as instant fallback
3 Per-binary strategy map {node, claude, codex, git} -> native | proot from BuildConfig + probe cache, logged at boot Rollout is per-binary, never all-or-nothing; codex (static musl) may legitimately stay proot'd
4 libagentnet-redirect.so ~100-line LD_PRELOAD interpose redirecting a fixed set of /etc/* opens into rootfs copies Without path emulation, glibc DNS reads a nonexistent /etc/resolv.conf; this is the one filesystem illusion the engine still needs
5 Env/state bridge HOME=<filesDir>/rootfs/root in both strategies; audit of absolute-path assumptions (.claude.json cwd entries, shebang spawns, /tmp) Sessions, auth, and configs must survive switching strategies in either direction
6 Benchmark harness (device, in-app) boot-to-200, spawn latency, git clone wall-clock, npm ci, TTFB, RSS — per strategy mix #112 shipped through an unmeasured gap; no default flips without a filled table

Work items

Device and release gates (hard requirements)

  • Verified on MediaTek (Seeker, MT6878, Android 16 — also carries the arm64 pointer-tagging constraint: the process_vm proot build must remain untouched for the convenience layer) and Snapdragon (any Android 13+), plus one Samsung One UI device before defaults flip
  • Works in the Play release build (targetSdk 35, release signing, Play Protect): the probe suite runs on the release AAB build, not only debug
  • Every stage behind a flag; a bad device report reverts to proot per binary without a new release

Non-goals / notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions