You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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
#112 shipped through an unmeasured gap; no default flips without a filled table
Work items
T0 probe: chain viability per binary (node/claude/codex/git) + DNS with/without redirect shim, on Seeker (MediaTek) AND a Snapdragon device. Kill criterion: if linker64 -> ld.so cannot launch or mmap under untrusted_app, stop here and redirect effort to a proot transport patch
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
proot is not removed — demoted to the user-shell/apt layer
No licensing delta: the glibc runtime already ships in our rootfs (LGPL, arms-length as today); the redirect shim is our code; nothing is copied from GPL apps (prior art for the linker chain exists in the wild at targetSdk 37, e.g. termux-play-store)
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_appSELinux 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:#ef4444Target 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:#22c55eElements to build
avc:denialsadb run-asruns inrunas_appand lies. All go/no-go data must come fromuntrusted_appNativeLinkerExec : GuestExeclinker64 -> ld.so -> binaryargv + env (HOME,TMPDIR,PATH, preload)DirectProotExecuntouched as instant fallback{node, claude, codex, git} -> native | prootfrom BuildConfig + probe cache, logged at bootlibagentnet-redirect.so/etc/*opens into rootfs copies/etc/resolv.conf; this is the one filesystem illusion the engine still needsHOME=<filesDir>/rootfs/rootin both strategies; audit of absolute-path assumptions (.claude.jsoncwd entries, shebang spawns,/tmp)git clonewall-clock,npm ci, TTFB, RSS — per strategy mixWork items
linker64 -> ld.socannot launch or mmap underuntrusted_app, stop here and redirect effort to a proot transport patchNativeLinkerExec+ strategy map (dark on main, default = proot)Device and release gates (hard requirements)
Non-goals / notes