fix(android): #115 root cause — drop proot --link2symlink (fixes git, pnpm, all link() tools) - #116
Conversation
…ect=rename Teed up on top of Zo's root-cause hunt (link() false-success on .git/objects under untrusted_app). Verified against git v2.43.0 object-file.c: with core.createObject=rename git skips link() entirely and uses rename(), which survives proot (probe6). So the interim fix is one line of git config — no shim, no proot rebuild. - probes/probe10.sh: reproduces the loss in link mode, shows it gone in rename mode, then a real network clone with the fix. Run right after probe9 confirms. - shims/liblinkfix.c: broader LD_PRELOAD option (EXDEV on object-path link/ linkat → caller's rename fallback) for non-git tools that lack the config knob. - README: fix ladder — (1) core.createObject=rename config, (2) liblinkfix shim, (3) basement proot linkat patch. All gated on probe9 naming link as the culprit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… loss
proot's link() is a false success under untrusted_app: it returns 0 but the
target file never appears (proven on-device SM-A356E: `link ret=0
target_exists_after=0`, 0/50 loose objects survive). git's default finalize
(mkstemp -> link -> unlink) trusts the 0 and reports success, so clone/commit/
fetch silently lose objects ("remote did not send all necessary objects").
Write a system /etc/gitconfig at install with core.createObject=rename, which
makes git skip link() and use rename() (which proot handles correctly). Covers
every git — the node server's and the user's interactive shell — not just clone.
Verified end-to-end via the shipping path (system gitconfig, plain `git clone`,
no per-command flag): express.git clones exit 0, 53322 objects in-pack, clean
checkout, 0 fsck errors. Supersedes the clone-only dulwich shim (#114); the
all-tools basement fix (proot linkat patch) stays tracked in #115.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aunch configureGuest only runs on a fresh MARKER, so the #115 git fix (previous commit) would miss devices that installed before it: an APK update hits the isInstalled early return and never rewrites /etc. Extract the write into writeGuestGitConfig() and also call it on the already-installed path — idempotent 30-byte write, far cheaper than a MARKER bump + rootfs re-extract. Verified on-device (SM-A356E): app already installed ("already installed" log, no re-extract) → delete /etc/gitconfig, relaunch → recreated; fresh install writes it via configureGuest as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause, proven on-device (SM-A356E, untrusted_app): the kernel denies hardlinks in the untrusted_app domain (`ln a b` -> EACCES). --link2symlink catches that and FAKES success (pokes syscall result 0) via a symlink-refcount scheme whose links dangle in the guest namespace. The fake defeats callers' error handling: git's finalize_object_file falls back to rename, and pnpm's store-linker falls back to copy, ONLY when link FAILS — never when it lies with a 0. Hence silent git object loss and broken pnpm installs. Removing --link2symlink makes linkat fail honestly with EACCES, so every tool with a link fallback recovers on its own. Verified on-device with l2s off: - git (forced link mode): 50/50 loose objects survive (rename fallback) - pnpm (default hardlink): 589/589 files, require OK (copy fallback) - node server still boots: server ready (HTTP 200) — no regression This is the basement fix: one flag removed fixes git + pnpm + any link()-using tool, with no proot rebuild and no per-tool config. The core.createObject=rename gitconfig (prior commits) stays as belt-and-suspenders (skips the doomed link attempt for git). Supersedes the clone-only dulwich shim (#114). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update: found the real root cause — it's
|
PROOT_L2S_DIR pointed proot at a l2s db the extension no longer loads (--link2symlink was removed as the #115 root cause), and the launcher doc comment still described l2s as 'guest compatibility'. Drop the dead env var and correct the comment; note that pre-fix installs may keep orphaned .l2s.* files, which are harmless.
…enders) The copy-on-link work removed --backend=copyfile from the bun wrapper to rely solely on PRoot's new --copy-on-link. But that flag only takes effect once the source-built PRoot ships (DirectProotExec gates it on the binary containing the option string); with today's prebuilt binary the wrapper would run bun's default hardlink backend -> kernel EACCES -> "Failed to install N packages", empty node_modules (proven on-device). Restore --backend=copyfile: it is correct with OR without --copy-on-link (same result, bun just copies in userspace) and removes the regression window, mirroring how #116 keeps core.createObject=rename as belt-and-suspenders. node_modules pre-create is also unrelated to hardlinks, so --copy-on-link never covered it — kept. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/all-tools basement) The durable basement fix for the whole hardlink class. Under untrusted_app the kernel denies hardlinks (EACCES); tools with a copy/rename fallback recover once --link2symlink is gone (#116), but dpkg has NO fallback — link(status,status-old) -> EACCES leaves apt half-configured (proven on-device). Per-tool nudges don't scale to dpkg/coreutils, so fix it in PRoot. - surfaces/android/proot/: GPLv2 patch adding an opt-in --copy-on-link extension. PRoot attempts the real link()/linkat() first; ONLY on EACCES it byte-copies a regular source file to a fresh O_EXCL destination (snapshot, not inode-sharing) and reports success. Special files, symlink sources, AT_EMPTY_PATH, and other errno keep normal behavior. README pins the reproducible source inputs. - android-assets.yml: build PRoot from the pinned Termux recipe + this patch via the official package-builder, export PROOT_DEB to the asset step. - build-assets.sh: fetch_deb accepts the source-built .deb path (PROOT_DEB) as well as the runtime-lib URLs. - DirectProotExec: pass --copy-on-link, but ONLY if the shipped binary advertises it (grep the option string) — an older reused android-assets artifact never gets an unknown flag that would abort the guest boot. Not yet built/verified on-device — needs an android-assets run to produce the patched binary, then on-device: link EACCES copies, git clone, bun add, and apt install + dpkg --configure -a all clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The shim (guest/git-clone-shim.sh -> /usr/local/bin/git, routing `git clone` through dulwich) worked around #112 by using create+rename instead of git's link path. The actual root cause — proot's --link2symlink faking link() success under untrusted_app — is now fixed at the launch layer (#115/#116, DirectProotExec drops --link2symlink), so native git clone works with real git. Removes the shim install + python3-dulwich dep from build-assets.sh and deletes guest/git-clone-shim.sh + guest/agentnet-git-clone.py. Real /usr/bin/git is used for every command. GATE (before merge): verify real `git clone` end-to-end in the untrusted_app domain with l2s removed (hash-object 50/50 already confirms real git's object finalize works post-fix; clone adds index-pack over the same path). Takes effect on the next rootfs rebuild; existing installs keep the (now harmless) shim until a rootfs re-extract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Fix the #112/#115 git object-loss bug at its root: write a system
/etc/gitconfiginto the guest withcore.createObject = rename, so git stops using thelink()syscall that proot silently breaks.One line of effect (
Installer.kt):Why
Under the
untrusted_appSELinux domain (targetSdk 35, required for Play), proot'slink()is a false success — it returns0but the target file never appears. git's default loose-object finalize path (git 2.43object-file.c) ismkstemp → fsync → link(tmp,final) → unlink(tmp); git trusts the0fromlink, reports the object written, and the object is silently lost. This surfaces asfatal: remote did not send all necessary objects/bad objectongit clone, and would hitcommit/fetch/pullthe same way.core.createObject=renamesetsOBJECT_CREATION_USES_RENAMES, which makes git skiplink()entirely and userename()— which proot handles correctly.Proof (on-device, headless,
untrusted_app— SM-A356E / Android 16)Root cause caught at the syscall (probe9):
Fix proven (probe10):
Verified through the shipping path (system
/etc/gitconfig, plaingit clone, no per-command flag):Relation to other work
509dee6): that fixedcloneonly, by swapping real git for pure-Python dulwich (which writes objects with create+rename, dodginglink). This PR fixes all git commands with real git and one config line. Once merged, Run the engine natively (no proot in the hot path): structural fix for the untrusted_app transport corruption #115 T3 (remove the dulwich shim) can proceed.linkattranslation solink()stops lying for every tool (npm/cargo/…), in our own proot build (we own it from the Seeker process_vm fix; GPLv2 → publish the patch).core.createObject=renamefixes git, the only proven-affected tool today; the proot patch stays tracked in Run the engine natively (no proot in the hot path): structural fix for the untrusted_app transport corruption #115.Also in this branch (debug kit, not shipped code)
debug/issue-115/probes/probe10.sh— the fix test (reproduce in link mode → prove in rename mode → real clone).debug/issue-115/shims/liblinkfix.c— a broaderLD_PRELOADoption (returnsEXDEVon object-pathlink/linkat, forcing the caller's own rename fallback) for any non-git tool that hits the same false-success and has no config knob.debug/issue-115/README.md— the fix ladder (config → shim → proot patch).Test plan
configureGuest).git clone https://github.com/expressjs/express.git→ completes,git fsckclean.git config --show-origin core.createObject→file:/etc/gitconfig rename.