Repository navigation
Android (Termux): every confined file operation fails with EACCES — open_confined opens / with O_RDONLY|O_DIRECTORY, which SELinux denies to apps #6980
Description
Summary
Since v0.10.1, on Android/Termux every engine feature that goes through the confined file opener (WorkspaceFile::open_confined in crates/tui/src/fleet/files.rs) fails with Permission denied (os error 13):
workflow journal unreadable (~/.codewhale/workflow-runs.jsonl): Permission deniedon every session startcontext compaction failed ... error=Permission denied (os error 13)— and the turn is then settled as Failed, which is the most visible symptomspillover: could not save full tool output before cutting it for model context err=... PermissionDenied- skill loading via
fs_confined::read_to_stringfails (read cannot expose...style errors aside, direct SKILL.md loads return EACCES)
Everything that does not go through open_confined (plain std::fs paths such as save_checkpoint, shell tool output, bash subprocesses) works fine, which makes the failure look random.
Logs from v0.9.x sessions on the same device show zero occurrences of these warnings; they start exactly at the 0.10.1 install and now appear in every session.
Root cause
open_confined anchors its openat walk at the filesystem root:
let fd = unsafe {
libc::open(
c"/".as_ptr(),
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
)
};On Android, app processes run in the untrusted_app SELinux domain. That domain has search (traverse) permission on / and on mount-point directories such as /data and /data/data, but not read, so open("/", O_RDONLY|O_DIRECTORY) returns EACCES even though the DAC bits are drwxr-xr-x. Every WorkspaceFile operation therefore fails before it even reaches the workspace, and com.termux (uid u0_a435) is affected as untrusted_app (domain confirmed via /proc/self/attr/current = u:r:untrusted_app_27:s0:c179,c257,c512,c768).
Verified with strace on a live session — the only non-environment EACCES calls in the entire process lifetime are:
openat(AT_FDCWD, "/", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = -1 EACCES (Permission denied)
(52 occurrences in one session, interleaved with the failing journal/compaction/spillover warnings.)
DAC permissions, ownership, fcntl record locks (EAGAIN, not EACCES, when contended), disk/inode headroom, TMPDIR, and multi-process concurrent appends were all checked and are not factors. realpath/opendir on the same paths succeed — the failure is specific to opening / as a readable directory.
Minimal reproducer
Run inside Termux (python3 android-repro.py, attached):
workspace: /data/data/com.termux/files/home
open('/', O_RDONLY|O_DIRECTORY) [current open_confined anchor]: FAIL errno=13 (Permission denied)
open('/', O_PATH|O_DIRECTORY) [proposed fix anchor]: OK
confined walk (fix): OK
confined write/read through fix: OK
Proposed fix (patch attached, against main as of 2026-10-14)
Open the anchor and every intermediate component with O_PATH instead of O_RDONLY|O_DIRECTORY, and re-materialize the file's immediate parent as a real directory fd after the walk:
O_PATH|O_DIRECTORYstill fails withENOTDIRon non-directories, so the per-component type check and the symlink refusal are unchanged.O_PATHrequires no read permission, so SELinuxsearch-only domains (Android apps, hardened sandboxes) work.- The final parent is reopened with real flags (
openat(pinned_parent, ".")) because O_PATH fds cannot befsynced (EBADF) or read, andWorkspaceFileuses the directory fd forsync_all,renameat,unlinkatafterwards. This reopen cannot escape the walk: it resolves.inside the pinned parent. - Final file opens (
open_with_flags,atomic_write,mkdirat) are untouched.
Verified on-device that with the fix the full WorkspaceFile semantics work end-to-end: journal read, O_CREAT|O_EXCL leaf creation, mkdirat branch, atomic tmp+rename, fsync on the materialized parent, fstat/dup on the directory fd.
Environment
- codewhale 0.10.1 (fead51e), GitHub Releases aarch64 build
- Android 16 (Termux app,
untrusted_app_27SELinux domain, targetSdk profile c179/c257/c512/c768) - Workload: multiple parallel TUI sessions; failure rate 100% for compaction once context passes the auto-compact threshold
Happy to test a patched build on the device.
Activity
printf033 commentedon Oct 14, 2026
Attaching the minimal reproducer inline (single file, no deps; run with
python3 android-repro.pyinside Termux):Expected output on an affected device:
Hmbown commentedon Oct 15, 2026
🤖 This reply is from Claude Opus 5.5, an AI agent working on behalf of the Codewhale maintainers. It is posted through @Hmbown's account, but Hunter did not write it.
Thank you @printf033 — the strace line, the SELinux domain readback and the standalone reproducer make this an unusually clean diagnosis. I confirmed on current
mainthatWorkspaceFile::open_confinedstill anchors atopen("/", O_RDONLY|O_DIRECTORY|…)(crates/tui/src/fleet/files.rs:122) and opens every intermediate component with the same flags, so a search-only domain fails before the walk reaches the workspace.Your proposed shape (
O_PATHfor the anchor and intermediate components, then reopening the final parent as a real directory fd forfsync/renameat/unlinkat) is the direction we'd take. Would you open it as a PR againstmainso it lands under your name? Two things to keep in mind:O_PATHexists only on Linux/Android, so the change needs to becfg-gated with macOS/BSD keeping today's flags; and the reopen of the final parent should keepO_NOFOLLOW/O_CLOEXEC. Nothing is shipped yet. This is not in 0.10.2 until a PR lands and passes CI on all three OSes, and we can't verify on an Android device ourselves, so your on-device check of the PR build would be the acceptance evidence.— Written by Claude Opus 5.5 (AI agent) on behalf of the maintainers.
fix(tui): open confined paths with O_PATH anchors on Linux so Android…