mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
164 lines
13 KiB
Markdown
164 lines
13 KiB
Markdown
# proot patches carried by the app
|
||
|
||
Built by `tools/proot/build.sh` with the NDK on top of termux/proot `4dba3afb` (Termux's
|
||
5.1.107-70 package, the proot the Linux runtime ships at `opt/android-host/proot`; every
|
||
`file.c:line` site in that binary matches this commit). talloc is linked in statically from
|
||
Samba's release tarball, so the apk carries one self-contained `libproot.so` plus
|
||
`libproot-loader.so`, and `LinuxRuntime` prefers them over the runtime's copy.
|
||
|
||
Ported from WinNative (`feature/proot-enhancements`, e7af0f24):
|
||
|
||
- `0001-tracee-lookup-by-pid.patch` - tracees are hashed by pid, so each ptrace stop finds its
|
||
tracee without walking one list entry per thread, and terminated tracees are swept only after a
|
||
termination.
|
||
- `0002-canon-resolve-parent-at-once.patch` - a clean absolute guest path at least three
|
||
directories deep under the rootfs binding alone has its parent opened once with `O_PATH` and
|
||
taken as canonical when `/proc/self/fd` names the path proot would build, replacing an `lstat`
|
||
per component. The final component of a call that does not follow it is no longer `lstat`ed.
|
||
Extensions still see the parent's host path, and the fast path stays off while the f2fs
|
||
workaround is active.
|
||
- `0003-clone3-flags-read-guard.patch` - a thread created with `clone3` keeps the flags it
|
||
inherited when `struct clone_args` cannot be read, instead of being tracked as a fork.
|
||
|
||
Ported from WinNative (`main`, 53836ca9, "Fix/performance and vac"):
|
||
|
||
- `0004-seccomp-filter-by-argument.patch` - the seccomp filter traces `prctl` only for
|
||
`PR_SET_DUMPABLE`, `setrlimit` only for `RLIMIT_STACK` and `prlimit64` only when it sets a new
|
||
`RLIMIT_STACK`, the only cases proot acts on; every other call runs without a stop. `uname` is
|
||
traced by the core only on x86_64, the one arch it rewrites; kompat still adds it for
|
||
`--kernel-release`.
|
||
- `0005-seccomp-skip-unneeded-sysexit.patch` - a seccomp stop fetches the registers once and
|
||
looks the filter flags up by syscall number, instead of a `PTRACE_GETEVENTMSG` and a second
|
||
register fetch. The flags table is built from proot's list merged with the enabled extensions'
|
||
(kompat and fake_id0 here), so it is exactly what the filter reports. `wait4`/`waitpid` left to
|
||
the kernel and `accept`/`accept4` without a sockaddr skip their exit stop, unless an extension
|
||
replaced the syscall (kompat turning `accept4` into `accept`).
|
||
- `0006-clone3-exit-signal.patch` - `clone3` flags and exit signal are read as the 64-bit fields
|
||
of `struct clone_args`, so a `clone3` fork is told apart from a thread when proot decides how the
|
||
child is traced.
|
||
- `0007-proc-self-thread-group.patch` - tracees track their thread group, `/proc/self` names it
|
||
instead of the calling thread, and `/proc/thread-self` resolves to `/proc/<tgid>/task/<tid>`.
|
||
- `0008-fchmodat2-openat2.patch` - `fchmodat2` paths are translated (honouring
|
||
`AT_SYMLINK_NOFOLLOW`), and syscall numbers past the end of a table are rejected instead of read.
|
||
Its `openat2` answer is replaced by `0019`.
|
||
- `0009-tracee-relatives-sweep.patch` - tracees count their children, so a terminating thread
|
||
with no children or ptracees no longer walks every tracee; the per-stop memory collector is
|
||
emptied instead of freed and reallocated.
|
||
|
||
Added for Flatpak:
|
||
|
||
- `0010-new-mount-api-enosys.patch` - `open_tree`, `move_mount`, `fspick` and `mount_setattr`
|
||
answer `ENOSYS`. They take paths proot never translated, so libglnx's `open_tree(AT_FDCWD, "/")`
|
||
handed Flatpak a descriptor for the host's root and it tried to create directories there
|
||
(`mkdirat(root): Operation not permitted`); unsupported, libglnx falls back to `openat`.
|
||
- `0012-android-hardlink-denial.patch` - Android's SELinux policy denies apps hard links, so
|
||
`linkat` fails with `EACCES` and Flatpak could not create its repo (`Creating repo: linkat:
|
||
Permission denied`). `O_TMPFILE` answers `EOPNOTSUPP`, so libglnx writes a named temporary file
|
||
and renames it, and a denied link answers `EPERM`, on which ostree's checkout copies instead.
|
||
- `0017-android-execstack-denial.patch` - the same policy denies apps executable stacks, so the
|
||
`mprotect(PROT_READ | PROT_WRITE | PROT_EXEC | PROT_GROWSDOWN)` with which `ld.so` makes the main
|
||
stack executable for an object whose `PT_GNU_STACK` asks for it fails with `EACCES`, and the
|
||
program stops before `main` ("cannot enable executable stack as shared object requires:
|
||
Permission denied", `libQt5Gui.so.5` in ONLYOFFICE's Flatpak). The filter traces `mprotect` only
|
||
for that exact request, and a denied one answers success: the stack stays non-executable, as
|
||
Android keeps it anyway, and objects that only carry the flag load.
|
||
Added by DroidDeck:
|
||
|
||
- `0011-kompat-utsname-only.patch` - `--kernel-release` (the guest's `DroidDeck` hostname) loads
|
||
kompat, whose filter traps `futex`, `fcntl`, `epoll_pwait`, `pselect6`, `pipe2`, `eventfd2`,
|
||
`socket` and more, and which strips `AT_SYSINFO_EHDR` on every `execve`, so glibc runs without
|
||
the vDSO. When the virtual release is not older than the real kernel and the hwcap is left
|
||
alone, every one of those handlers is a no-op: kompat now traces only `uname`, `sethostname`
|
||
and `setdomainname` and leaves the auxv as the kernel wrote it. On an SD 8 Gen 2 guest this
|
||
took a futex ping-pong from 467 to 97 us, `epoll_pwait` from 60 to 0.8 us and `fcntl` from
|
||
40-107 to 0.4 us, and `clock_gettime` back to the vDSO.
|
||
- `0012-fake_id0-identity-only.patch` - `-i uid:gid` (for Xwayland's setgid/setuid before it runs
|
||
xkbcomp) loads fake_id0, whose filter traps every `fstat`/`newfstatat`/`stat` (entry and exit),
|
||
every `sendmsg` (all Wayland, X11, Chromium and PulseAudio traffic), `socket`, `getsockopt`, the
|
||
`get*id` family and the chown/chmod family. When the ids given are the ones proot really has and
|
||
are not 0, every one of those handlers is a no-op; fake_id0 now traces only the `set*id` family
|
||
and the xattr permission fixups, and leaves set-user-ID bits alone on `execve` (Android mounts the
|
||
app's data `nosuid`, so the kernel would not honour them either), which keeps the ids fixed. On an
|
||
x86_64 host build under the same options: `fstat` 41.7 -> 1.3 us, `sendmsg` 18.0 -> 2.5 us.
|
||
- `0013-seccomp-ioctl-by-request-and-kernel-exit-stops.patch` - every `ioctl` stopped on entry and
|
||
exit, which is every GPU submit and wait. The filter now traces only the requests enter.c and
|
||
exit.c act on (`TCSETSF`, the four termios2 requests, `FICLONE`) and allows the rest at once, and
|
||
the ioctl block is emitted first (after WinNative 53836ca9, which traces only the termios2 ones).
|
||
The `faccessat2` exit stop (termux 5ba8b95, for glibc's ENOSYS fallback on kernels before 5.8) and
|
||
the `statx` one (emulation on kernels before 4.11) are dropped on kernels 5.8 and newer; patch
|
||
`0015` keeps it on 4.11–5.7 to return the mount ID added in 5.8.
|
||
Host build: `ioctl` 31.6 -> 0.5 us; `stat`/`statx`/`faccessat2` about 44 -> 28 us (one stop).
|
||
- `0015-statx-mnt-id-fdinfo.patch` - kernels before 5.8 can complete `statx` without the mount ID
|
||
that libglnx requires while chasing an open descriptor. PRoot keeps the exit stop on those
|
||
kernels and fills only a missing ordinary `STATX_MNT_ID` for `AT_EMPTY_PATH` after pinning the
|
||
tracee descriptor, matching its device and inode to the successful statx result, and reading
|
||
`mnt_id` from the pin's fdinfo. A later close/reuse of the tracee fd cannot redirect that read;
|
||
a concurrent reuse for the same inode through a different bind mount is indistinguishable at
|
||
the exit stop. It does not synthesize `STATX_MNT_ID_UNIQUE`.
|
||
- `0016-robust-list-from-seccomp.patch` - Android's app policy refuses `set_robust_list` and
|
||
`get_robust_list`. proot still answers the first with `ENOSYS` but keeps the head each thread
|
||
offered (cleared on `execve`, none in a new task), and answers the second for the calling thread
|
||
with it. SteamVR's `vrclient.so` asks for the head through libc's `syscall()`, which
|
||
`libblsession.so`'s robust.c never sees: `libopenvr_api.so` opens it `RTLD_DEEPBIND`. Steam
|
||
aborted on "futex robust_list not initialized by pthreads" as soon as SteamVR was registered.
|
||
|
||
Prototype (inert unless `PROOT_FASTPATH` is set; see `docs/development/proot-performance.md`):
|
||
|
||
- `0014-fastpath-trampoline.patch` - with `PROOT_FASTPATH`, every `SECCOMP_RET_TRACE` in the filter
|
||
is preceded by a check of the caller's address, and a syscall made from the fast path's trampoline
|
||
page (`fastpath/fastpath.c`, `0xffff00000`) runs without a stop: the tracee already translated it.
|
||
The check comes after the syscall-number dispatch, so untraced syscalls stay constant-ALLOW and keep
|
||
the kernel's seccomp action cache (checking first cost every syscall the full filter: +0.5 ms an
|
||
exec). `chdir`/`fchdir`, still emulated, also move the kernel's cwd to the host directory, and the
|
||
first tracee starts at `-w`'s, so relative lookups inside the tracee resolve where proot would.
|
||
SM8850 (adb shell): `stat` 25.3 -> 0.6 us, `open+close` 25.3 -> 0.9 us, ENOENT 22.9 -> 1.6 us,
|
||
8 threads `stat`ing 89k -> 3.6M/s; equivalence suite (`bench/equiv.py`) byte-identical to proot.
|
||
|
||
- `0019-openat2-emulation.patch` - `openat2` without `resolve` flags runs as a translated `openat`
|
||
with the same flag and mode checks as the kernel. Any `resolve` flag still answers `ENOSYS`,
|
||
because PRoot cannot confine symlinks or mounts, so callers keep their own confined fallback.
|
||
- `0022-fake-root-statx-owners.patch` - with `0013` dropping the `statx` exit stop, fake root never
|
||
rewrote the owner `statx` returns, so files read as owned by the app's uid while the process was
|
||
root. Rust's `metadata` and GNU `stat` use `statx`, so uutils' `test -w` (Ubuntu's `/usr/bin/test`)
|
||
said root could not write to its own directories and apt's AppStream hook never built Discover's
|
||
catalog. fake_id0 keeps that exit stop when it fakes ids; identity-only mode is unchanged.
|
||
- `0023-reused-pid-new-child.patch` - when a thread that is not its process's leader calls `execve`,
|
||
the kernel retires that thread's ID without reporting an exit, so PRoot keeps its entry. Once the
|
||
IDs wrap (32768 on many Android kernels) a new child can get that ID, and PRoot aborted on the
|
||
assertion that a new child has no state, ending the whole session. Chromium-based programs such as
|
||
Steam's client do this. PRoot now retires the stale entry and sets the new child up from scratch.
|
||
- `0024-execveat-absolute-path.patch` - `execveat` with an absolute path and neither `AT_EMPTY_PATH`
|
||
nor `AT_SYMLINK_NOFOLLOW` runs as `execve`. The kernel ignores the directory descriptor for such a
|
||
path, and PRoot refused the call unless the descriptor was `AT_FDCWD`. FEX starts every child of an
|
||
emulated program with `execveat(fd, "/proc/self/exe")`, so x86 programs in a Flatpak sandbox, which
|
||
does not carry the session's preloads, could not start other programs.
|
||
- `0025-fastpath-trampoline-sigsys-enosys.patch` - Android 10 and older refuse `statx` with
|
||
`SIGSYS`. A `statx` from the fast path's trampoline page already carries the host path, but
|
||
proot's `SIGSYS` emulation translated it again as a guest path (`<rootfs>/<rootfs>/...`) and
|
||
returned `ENOENT`, so every existing file looked missing to Rust's `metadata` and GNU `stat`:
|
||
uutils `mkdir -p` on an existing directory failed with "Already exists" and Steam setup stopped
|
||
(Pico 4, Android 10). A refused syscall from the trampoline now gets `ENOSYS` without
|
||
translation or extension handling, and the fast path falls back as it already does for
|
||
`statx` (`newfstatat`) and `memfd_create`. Syscalls outside the trampoline and sessions
|
||
without `PROOT_FASTPATH` are unchanged. `bench/statx-trap.c` traps `statx` and checks an
|
||
existing directory still reads as one.
|
||
|
||
Old kernel fixes:
|
||
|
||
- `0015-lost-fork-events.patch` - works around a problem reported on Xiaomi's msm 4.14 kernel:
|
||
the kernel sometimes reports a new process or thread but gives its ID as zero. Without that ID,
|
||
PRoot cannot finish setting it up, which can leave the session stuck. The patch searches `/proc`
|
||
for the child. If exactly one matches, PRoot uses its existing setup code. If several threads
|
||
match, it waits for the creating thread's `clone` call to return and uses the child ID returned
|
||
by the kernel instead. The original clone flags are preserved. When the kernel supplies a valid
|
||
ID in the notification, normal setup is unchanged. No timers or background polling are added.
|
||
Completely missing notifications and ambiguous matches for separate processes are not recovered.
|
||
|
||
Development diagnostics:
|
||
|
||
- `0018-syscall-handler-profile.patch` - `PROOT_PROFILE=1` enables per-syscall entry/exit handler
|
||
counts, wall/CPU time, periodic cumulative JSON totals, and bounded slow-handler samples on
|
||
stderr. `profile.h` is copied into the source tree by `build.sh`. It adds no ptrace stops;
|
||
handler timing excludes tracee kernel time and scheduling before handler entry. See
|
||
`docs/development/proot-performance.md` for the capture and library-override workflow.
|