From 4b230cbd0f7e5c5ac4358b3c011e0861b1186458 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:56:18 -0400 Subject: [PATCH 1/9] perf(proot): add opt-in glibc syscall patching --- docs/development/proroot-analysis.md | 311 +++++++++++++++++++++++++++ tools/proot/bench/svc-check.c | 121 +++++++++++ tools/proot/bench/svc-probe.c | 93 ++++++++ tools/proot/bench/svc-storage.c | 74 +++++++ tools/proot/bench/svc-workload.c | 91 ++++++++ tools/proot/fastpath/fastpath.c | 2 + tools/proot/fastpath/svc-patch.h | 250 +++++++++++++++++++++ tools/tests/test_fastpath_svc.py | 62 ++++++ 8 files changed, 1004 insertions(+) create mode 100644 docs/development/proroot-analysis.md create mode 100644 tools/proot/bench/svc-check.c create mode 100644 tools/proot/bench/svc-probe.c create mode 100644 tools/proot/bench/svc-storage.c create mode 100644 tools/proot/bench/svc-workload.c create mode 100644 tools/proot/fastpath/svc-patch.h create mode 100644 tools/tests/test_fastpath_svc.py diff --git a/docs/development/proroot-analysis.md b/docs/development/proroot-analysis.md new file mode 100644 index 000000000..81be73a11 --- /dev/null +++ b/docs/development/proroot-analysis.md @@ -0,0 +1,311 @@ +# proroot: what it does and what DroidDeck can take from it + +Static analysis of [coderredlab/proroot](https://github.com/coderredlab/proroot) v1.2.8 +(`c01039ab`, 2026-06-17). The repository holds only five arm64 binaries and a README. The licence is +proprietary, and redistributing modified binaries is not allowed, so only the ideas below are +usable, not the code. The binaries were read with the NDK's `llvm-readelf`, `llvm-nm`, +`llvm-objdump` and `llvm-strings`. `libproroot-stub-loader.so` and `libproroot-bridge.so` still carry +`.symtab`, and `libproroot-runtime.so` exports about 260 names, so the function names below are +proroot's own. + +## Architecture + +proroot does not use ptrace. Everything runs inside the guest process. + +| file | kind | role | +|---|---|---| +| `libproroot.so` | bionic PIE executable | Launcher. Parses `-r/-b/-w/-0/--link2symlink`, writes a config file (`.proroot-config-`, passed down as `PROROOT_CFG_FD`), and execs the guest through `libproroot-linker.so` or the stub loader. | +| `libproroot-linker.so` | freestanding PIE, run as an executable | A from-scratch glibc dynamic linker ("cleanroom linker") that replaces `ld-linux-aarch64.so.1`. It maps the program and its `DT_NEEDED` libraries, relocates them, sets up static TLS, and stubs `_dl_*` and `__tunable_get_val`. It finds `_rtld_global_ro` through a table of offsets per glibc version ("no offset table for glibc X, patches skipped"). It also installs a SIGSYS handler with an allow-list. | +| `libproroot-stub-loader.so` | freestanding static PIE | Userland `exec`. It maps the target and its `PT_INTERP` itself and builds the stack and auxv. It then **rewrites the `svc #0` sites** in the loaded images (`proroot_svc_scan`, `proroot_svc_patch_apply`, `select_trampoline_for_site`) to branch into per-syscall trampolines (`proroot_tramp_openat`, `_statx`, `_faccessat2`, `_clone3`, `_rt_sigaction`, `_shmget`, ...). Sites it cannot patch fall back to an in-process seccomp `RET_TRAP` filter with a SIGSYS handler ("trampoline fallback, activating trap-mode fallback"). | +| `libproroot-runtime.so` | glibc `LD_PRELOAD` | Hooks about 200 libc entry points: path translation, fake root (`fake_id0_*`), link2symlink, exec routing, SysV shm, netlink, `/proc` and `/sys` views, `brk`, a fake DRM/KMS and GBM device plus libseat for compositors, X11 and Unix-socket fixups. After load it **patches the svc sites in glibc itself** (`patch_libc_syscalls`). | +| `libproroot-bridge.so` | static `ET_EXEC` at `0x7000000000` | The "trampoline" for static guest binaries. It installs a path-trap seccomp filter plus a SIGSYS handler, maps the target, and serves a virtual `/proc` (`status`, `stat`, `cmdline`, `environ`, `mounts`, `mountinfo`, `fdinfo`, `auxv`). | + +The executable files all live in `nativeLibraryDir`, which is the only place an app may exec from. +That is why the dynamic linker and the static loader each have their own userland loader. proot +handles the same constraint with `libproot-loader.so`. + +## How the in-process syscall interception works + +1. **Patching svc sites in glibc** (`patch_phdr_callback`, called through `dl_iterate_phdr`): + - It scans only an allow-list of objects: `libc.so.6`/`libc-`, `ld-linux`, `libpthread`, + `librt`, `libdl`. Main executables get patched early only for listed programs (apt, dpkg, + curl, openssl, node, chromium/electron/playwright, passwd/useradd/...). Other programs are + patched later (`log_deferred_libc_patch`, `PROROOT_FORCE_EARLY_PATCH`). + - In each executable `PT_LOAD` it finds `svc #0` (`0xd4000001`). It checks the instructions + before it for a `mov x8, #nr` it recognises. One fixed glibc `openat` call sequence gets + special treatment: its `bl` is redirected to a relay (`emit_openat_call_relay_entry`). + - The site is replaced with a `b`/`bl` to a relay stub on a page allocated within ±128 MiB + (`alloc_relay_page`). Sites out of range are logged as `bl-out-of-range`; `large-image + multi-cover tramp` places extra relay pages for big images. + - **Write path:** it first tries `mprotect(PROT_READ|PROT_WRITE|PROT_EXEC)` on the segment. + If that is refused, it `pwrite`s the 4-byte instruction through `/proc/self/mem`. Either way + it then calls `__clear_cache`. The log line counts sites by kind: `patched %d seccomp + %d + path (%d relay) + %d brk`. +2. **In-process SIGSYS fallback**: + - A BPF filter allows syscalls whose instruction pointer is inside proroot's own code and + returns `SECCOMP_RET_TRAP` for the path, exec, clone and wait syscalls from anywhere else. + The IP check is the same idea as our patch 0014, with TRAP instead of TRACE. + - The handler emulates the call and writes the result into the signal frame. Guest SIGSYS + handlers are kept and chained (`proroot_record_user_sigsys`, `invoke_user_sigsys`). +3. **Keeping SIGSYS deliverable**, the objection in option 4 of + [proot-performance](proot-performance.md): + - The `sigprocmask` and `pthread_sigmask` hooks call `sigtrap_unblocked_mask`, which + `sigdelset`s `SIGSYS` (and `SIGTRAP` for some guests) from any `SIG_BLOCK`/`SIG_SETMASK` set. + - `rt_sigaction` is guarded so the guest cannot replace the handler (`proroot_tramp_rt_sigaction`, + the `sigaction_guard` trace points). + - The raw `rt_sigprocmask` that glibc makes internally, for example `pthread_create` blocking + every signal, goes through a patched svc site. The stub loader and the runtime both compare + against syscall 135. +4. **Android-blocked syscalls are answered in-process.** The stub loader dispatches + `set_robust_list`, `rseq`, `faccessat2`, `clone3`, `futex_waitv` and `seccomp`. The runtime + emulates SysV shm (`emu_shm*`), `set*id`/`get*id` under `-0`, and `brk` + (`emulate_guest_brk`, over a reserved mapping). + +## Compared with DroidDeck + +| | DroidDeck (proot + 0001–0018 + `libblfastpath.so`) | proroot | +|---|---|---| +| exec, `PT_INTERP`, `/proc/self/exe` | proot's loader under ptrace, 0.4–0.8 ms per exec | Its own loaders, with no tracer; also needs its own ld.so with a glibc offset table | +| path calls through libc wrappers | fast path, about 0.6–1.9 µs | runtime hooks | +| path calls from glibc internals (ld.so, fts, realpath, nss, `__open_nocancel`) and raw `svc` | **still go to proot (15–60 µs)** | **patched svc sites** | +| Android-blocked syscalls (`set_robust_list`, `faccessat2`, `clone3`, ...) | SIGSYS stop answered by proot (0016 etc.), one ptrace round trip and one signal round trip each | in-process | +| `brk` | proot emulation, a trap per call (mitigated by `top_pad`) | in-process emulation | +| correctness fallback | proot is always there | none: anything missed runs unconfined or is emulated wrongly | +| long Unix socket paths | fast path `/proc/self/fd/N/name` aliases | short socket aliases (`proroot_create_short_unix_socket_alias`) | +| rseq | `GLIBC_TUNABLES=glibc.pthread.rseq=0` | trampoline | + +proroot is not a model for replacing proot. It has no tracer to fall back on, so a missed call +site or an unexpected glibc build silently breaks isolation or correctness. Its own linker depends +on glibc internals for each version. Proton, Wine, FEX and Steam's self-updating ELFs are a much +wider surface than the Node, Python and Chromium workloads proroot was tested on. Its value for +DroidDeck is as a fast path *in front of* proot. + +## What is worth taking + +1. **Patch glibc's svc sites from `libblfastpath.so`'s constructor.** This is the largest gain + and the cheapest way to do option 6 in [proot-performance](proot-performance.md) ("a proot-aware + glibc") without rebuilding glibc. + - For `libc.so.6` (and `ld-linux-aarch64.so.1` for runtime `dlopen`), rewrite each `svc #0` + that follows a recognised `mov x8, #nr` for the path syscalls the fast path already handles. + The rewrite is a `b` to a relay that calls the existing translator. + - The relay issues the syscall from the `0xffff00000` trampoline page, which 0014 already lets + through. Anything the translator declines is re-issued from an svc in the relay page, outside + the trampoline, so proot traps it exactly as it does today. Correctness still rests on proot. + - This covers fts (`find` over 2.7k files, unchanged today), realpath, nss, `__open_nocancel`, + and `dlopen` after start-up. + - Library loading at start-up, before the constructor runs, still traps. +2. **Answer the Android-blocked syscalls through those relays.** + - `set_robust_list` and `get_robust_list`: keep the head in TLS, as 0016 already does in + proot. + - `faccessat2`: answer `ENOSYS`, or translate it when flags are 0, as the wrapper does. + - `clone3`: answer `ENOSYS` before Android 15, so glibc falls back to `clone`. + - `futex_waitv`: droiddeck-fsync already handles this. + - Each one removes a SIGSYS stop plus a ptrace round trip, about 30–50 µs. `set_robust_list` + alone is one per thread, which matters for Steam/CEF and Wine thread churn. +3. **The `/proc/self/mem` write as a fallback for patching.** If the app domain refuses + `mprotect(PROT_WRITE|PROT_EXEC)` on rootfs (`app_data_file`) text, i.e. an `execmod` denial, + a write through `/proc/self/mem` makes the page copy-on-write without an `mprotect`. That page + is never writable and executable at once. Check this on the Thor before relying on it. The + same question is still open for the trampoline page (`execmem`). +4. **Keep SIGSYS unblocked if in-process `SECCOMP_RET_TRAP` is ever used.** Strip `SIGSYS` in the + `sigprocmask`/`pthread_sigmask`/`rt_sigprocmask` paths, and keep the handler installed while + chaining the guest's. This only becomes safe once item 1 covers glibc's internal + `rt_sigprocmask`. It is a prerequisite, not a recommendation to switch. +5. **Cache patch sites by build-id.** proroot rescans each image on every exec and defers patching + for most programs to keep exec cheap. A scan of libc's roughly 1.5 MiB of text is cheap, but a + per-build-id site table, written once in `/tmp` by the fast path, would make it free. + +Not worth taking: the cleanroom ld.so and its glibc offset table; the userland exec loader (proot +already does this); the fake DRM/GBM/libseat device (DroidDeck has its own compositor path); +netlink and SysV shm (`netif.c` and `sysv.c` already cover these); and the long-socket aliasing +(the fast path covers it). + +## Open questions before prototyping item 1 + +- Does the app domain allow `mprotect(RWX)` on a private mapping of an `app_data_file`? Does a + `/proc/self/mem` write work under the app's SELinux context and proot's own `ptrace` attach? + `/proc/self/mem` writes need `PTRACE_MODE_ATTACH` on self, which is always allowed. +- How many `libc.so.6` path syscall sites fall outside a recognisable `mov x8, #nr` window? For + example, `syscall()` takes the number in a register, which needs the existing `syscall()` hook. +- Patched pages become private copies, about 4 KiB per patched page per process. Measure RSS + across the Steam/CEF tree. +- Equivalence: run `bench/equiv.py` plus a `find`/`realpath`/nss workload with `PROOT_FP_STATS`. + Compare the trap counts from patch 0018 before and after. + +## First implementation and Thor validation (2026-10-27) + +`tools/proot/fastpath/svc-patch.h`, included by `fastpath.c`, implements an opt-in first +slice of item 1. Set `PROOT_FP_PATCH_SVC=1` in a fresh process using the replacement +`libblfastpath.so`. The ordinary fast-path configuration and tracer-key check must also +succeed. It is disabled by default, skips processes that already have multiple threads, +and declines SVE/SME hardware because the relay preserves the base ARM64 register state. + +The scanner visits executable segments of the loaded `libc.so.6` and +`ld-linux-aarch64.so.1`. It only patches `openat` and `newfstatat` sites with an +unshifted `movz x8/w8, #nr` within the preceding eight instructions. Intervening +instructions must belong to a small allow-list of argument moves/sign-extension, +immediate moves, stack loads/stores and NOPs that leave x8 unchanged. Branches, +calls, x8 writes and unknown instructions stop recognition. This includes libc's +non-adjacent O_CREAT sequences. Register-number dispatchers and other syscall +numbers remain under PRoot. No glibc version offsets, proroot code, or modified +proroot binaries are used. + +Each replacement is a four-byte `b` into a nearby relay. Relay pages are filled while +writable, then changed to RX before publishing any branch. Text writes use +`/proc/self/mem`; failure leaves the site unpatched. The relay preserves x1–x30, all +32 SIMD registers, NZCV, FPCR/FPSR and the caller's `errno`, returning a raw kernel-style +result in x0. It calls the existing path translator, using the existing allowed +trampoline for translated calls. Declined and recursive calls execute an ordinary +`svc` in this library, where PRoot still intercepts them. No SIGSYS handler or new +seccomp filter is installed. `PROOT_FP_STATS=1` adds `fastpath-svc` exit counters. + +The connected AYN Thor was running runtime r9, glibc 2.43, with a 4 KiB page size in +`u:r:untrusted_app_27:s0`. `bench/svc-probe.c`, run through the app's guest bridge, +confirmed all of the following while PRoot was attached: + +- An RX private mapping of the rootfs libc accepted `mprotect(RWX)`. +- `/proc/self/mem` accepted a changed byte in that RX mapping; readback and restoration + succeeded, and the backing file stayed unchanged. +- An anonymous RW mapping could be changed to RX. +- libc contained 415 `svc #0` sites: 327 with an adjacent number load, 76 with a + number load only within the preceding eight instructions, and 12 with neither. + The linker contained 56 sites, 45 adjacent and 11 nearby-only. Nearby counts are + scan statistics, not proof that the number remains live at the syscall. + +The initial adjacent-only scanner patched nine sites; the final scanner patched +14, including file-creation paths, across libc and the linker. It used two relay +pages in standalone validation processes. `bench/svc-check.c` exercises the +actual emitted relay and checks raw success/error results, errno, x1–x30, q0–q31, +NZCV, FPCR/FPSR, four simultaneous threads, the multi-thread patching guard, and fork. +Null arguments and unsupported flags fall back to PRoot. The instruction decoder +and exact ±128 MiB branch limits also have a host regression test. + +Five alternating pairs of `bench/svc-workload.c` runs compared the same replacement +library with patching off/on, with fast-path stats enabled in both. The workload +gets `fstatat` and `__open64_nocancel` directly from libc, bypassing the preload's +public wrappers, and makes 5,000 of each call against a prepared regular file: + +| median | wrappers only | svc patching | +|---|---:|---:| +| internal `fstatat` | 38.6 µs | 3.0 µs | +| internal open + close | 39.3 µs | 5.8 µs | +| voluntary context switches over both loops | 10,000 | 0 | + +### SD allocation and writes + +`bench/svc-storage.c` separates the operations relevant to Steam downloads. Three +alternating pairs ran on internal `/tmp` and the selected `/mnt/droiddeck-sd` library, +with Steam idle and no simultaneous local Steam download. Each fresh directory held 64 long-named files +extended to 256 KiB, followed by 1,000 libc-internal stats and a 64 MiB sequential +write followed by `fdatasync`. Both modes used the same replacement library; only +`PROOT_FP_PATCH_SVC` changed. Medians on SD were: + +| phase | wrappers only | svc patching | +|---|---:|---:| +| create/extend/close 64 files | 1.500 s | 1.486 s | +| 1,000 internal stats | 61.2 ms | 5.4 ms | +| 64 MiB write calls | 43.9 ms | 40.9 ms | +| final `fdatasync` | 2.514 s | 2.539 s | + +Internal allocation fell from 7.1 ms to 1.9 ms; internal stats fell from 39.5 ms +to 2.5 ms. Internal `fdatasync` varied from 36 to 164 ms across individual runs. +The patched SD workload recorded 1,065 internal calls handled in-process, covering +64 file creations, 1,000 stats and the streaming-file creation. Per-phase medians +are independent and should not be added as a median whole-run duration. + +The SD metadata gain does not establish faster Steam downloads. File creation +improved by only a few percent, and write/flush speed did not improve in this probe. +The remaining SD cost is much larger than the saved syscall-interception time. +The earlier [Thor SD investigation](thor-sd-storage-investigation.md) likewise +identified native exFAT metadata waits; the current benchmark does not independently +attribute those waits to a particular mount flag. A real Steam install comparison +must separate preallocation, transfer/staging and final commit before claiming an +end-to-end improvement. + +### Real Steam downloads to SD + +Four fresh installs of owned, initially uninstalled **7 Billion Humans** (792100) +used library folder 1, `/mnt/droiddeck-sd`, and were uninstalled between trials. +Each downloaded 228,935,584 bytes and staged 237,717,350 bytes in 19 files, ending +at BuildID 8016557, depots 792101/5052600457178655633 and +792104/8245917369753676198. Every trial logged zero reused/stored/delta bytes at +the start and completed with `No Error`. Existing installed games were retained. + +The repeat off/on pair used the exact same replacement library, SHA-256 +`ab605c021aee271cd49fa1cb337d7ff0c762dbac198b711e9818639c0cd4b17d`. +The first off run used the earlier adjacent-only build, with patching disabled. +Only the opt-in environment flag changed for the final pair. Times below come +from Steam's one-second-resolution `content_log.txt`; total starts at Reconfiguring +and ends at scheduler completion, excluding session startup: + +| trial | patching | preallocate | download/stage | commit to completion | total | +|---|---|---:|---:|---:|---:| +| first baseline | off | 1 s | 123 s | 1 s | 135 s | +| first patched | on | <1 s | 47 s | 1 s | 50 s | +| repeat baseline | off | <1 s | 30 s | 1 s | 33 s | +| repeat patched | on | <1 s | 29 s | 2 s | 32 s | + +Reconfiguration accounts for the remaining total time. The first baseline +overlapped another Steam client's network download; that client's telemetry must +not be mistaken for the Thor's. Later samples were filtered to local client `0`. +The repeat pair reached similar network rates, roughly 7–8 MB/s while transferring. +All four attempts logged the same unsupported large-file preallocation retry. + +**This does not demonstrate a useful SD-download speedup.** The apparent initial +135→50 second gain disappeared when baseline was repeated. The final 33 versus +32 seconds is within this small experiment's timing/network variability. This +19-file title also does not settle performance for games with thousands of loose +files. Metadata calls clearly improve, while SD creation/writeback remain the +stronger next targets. No network settings, SD mount flags or root privileges +were changed for these tests. + +The internal-call equivalence workload, including fts, realpath, reentrant NSS, +runtime dlopen, missing paths and fallback errors, matched `PROOT_FP_OFF=1` at both +the default parent-cache TTL and TTL 0. The existing `equiv.py` also matched after +normalizing random/thermal read payloads to their lengths, volatile PID values in +the `/proc` directory listing, and tolerating Android's +denied `/dev` directory read. Its root fixtures were relocated into a temporary tree. + +`equiv2.py` used temporary root/SD fixtures, normalized `/proc/` output, +and tolerated the SD filesystem's unsupported symlink creation. Its patched output +matched the wrapper-only output at both TTLs. **It did not fully match PRoot:** +binding a Unix socket in the selected SD bind returned EPERM with the existing +wrappers, whereas PRoot succeeded. This mismatch reproduced with svc patching off; +it is a separate existing fast-path socket issue. The suite is not a clean full +equivalence pass. `equiv3.py` was not run because it rewrites live NSS configuration. + +A fresh Steam session using the override and `PROOT_FP_PATCH_SVC=1` rendered its +library UI. Sonic Mania (584400) gained focus and rendered its title screen. This +is startup smoke coverage, not gameplay or a Steam startup-speed measurement. +The installed PRoot pack emitted no profiling start/counter records despite the +queued profiling setting; the profiler report had `hasCounters: false`. That is a +measurement gap, not evidence of zero remaining traps. + +Before enabling this by default: validate signal/cancellation/unwind behavior, +additional glibc builds and workloads, startup cost and Steam/CEF tree RSS; obtain +matching patch-0018 tracer measurements; and resolve the existing SD socket mismatch. +Additional syscall families, Android-blocked syscall emulation and build-id caching +remain follow-ups. In particular this implementation does not remove startup loader +traps before the preload constructor, nor patch raw calls in arbitrary guest ELFs. + +Build the tools with the same ARM64 glibc toolchain as the fast path: + +```sh +aarch64-linux-gnu-gcc -O2 -Wall -Wextra -pthread \ + -o svc-check tools/proot/bench/svc-check.c -ldl +aarch64-linux-gnu-gcc -O2 -Wall -Wextra \ + -o svc-probe tools/proot/bench/svc-probe.c -ldl +aarch64-linux-gnu-gcc -O2 -Wall -Wextra \ + -o svc-workload tools/proot/bench/svc-workload.c -ldl +aarch64-linux-gnu-gcc -O2 -Wall -Wextra \ + -o svc-storage tools/proot/bench/svc-storage.c -ldl +``` + +Run `svc-probe` inside the app's guest environment, not from `adb shell`. +`svc-check` embeds the fast path and requires its normal enabled tracer environment. +For `svc-workload`, pass a prepared directory containing `file` and a `link` symlink +to it; add any second argument to enable timing. Use the documented library override +workflow in [proot-performance](proot-performance.md) for a fresh session, adding +`PROOT_FP_PATCH_SVC=1` through `droiddeckctl env set` before starting it. +`svc-storage` takes a new, unused directory name; it creates and removes its own test +files and directory. It writes 64 MiB plus 16 MiB of file extensions per run. diff --git a/tools/proot/bench/svc-check.c b/tools/proot/bench/svc-check.c new file mode 100644 index 000000000..fc76a6852 --- /dev/null +++ b/tools/proot/bench/svc-check.c @@ -0,0 +1,121 @@ +#include "../fastpath/fastpath.c" +#include +#include + +#if defined(__aarch64__) +struct snapshot { long x[32]; unsigned char q[32][16]; unsigned long nzcv, fpcr, fpsr; }; +extern uint32_t svc_check_site; +extern void svc_register_probe(struct snapshot *, const long *); + +__asm__( + ".text\n.p2align 4\n.type svc_register_probe,%function\nsvc_register_probe:\n" + "sub sp, sp, #1072\n" + "stp x19, x20, [sp]\nstp x21, x22, [sp, #16]\nstp x23, x24, [sp, #32]\n" + "stp x25, x26, [sp, #48]\nstp x27, x28, [sp, #64]\nstp x29, x30, [sp, #80]\n" + "stp q8, q9, [sp, #96]\nstp q10, q11, [sp, #128]\n" + "stp q12, q13, [sp, #160]\nstp q14, q15, [sp, #192]\n" + "stp x0, x1, [sp, #224]\n" + ".irp n,1,2,3,4,5,6,7,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30\n" + "mov x\\n, #\\n\n.endr\n" + ".irp n,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31\n" + "movi v\\n\\().16b, #0x5a\n.endr\n" + "mov x0, #0xa0000000\nmsr nzcv, x0\n" + "ldr x0, [sp, #232]\nldr x8, [x0, #48]\n" + "ldp x4, x5, [x0, #32]\nldp x2, x3, [x0, #16]\nldp x0, x1, [x0]\n" + ".global svc_check_site\nsvc_check_site:\nsvc #0\n" + ".irp n,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30\n" + "str x\\n, [sp, #(256 + \\n * 8)]\n.endr\n" + ".irp n,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31\n" + "str q\\n, [sp, #(512 + \\n * 16)]\n.endr\n" + "mrs x0, nzcv\nstr x0, [sp, #1024]\nmrs x0, fpcr\nstr x0, [sp, #1032]\n" + "mrs x0, fpsr\nstr x0, [sp, #1040]\n" + "ldr x0, [sp, #224]\nadd x1, sp, #256\nmov x2, #792\nbl memcpy\n" + "ldp q8, q9, [sp, #96]\nldp q10, q11, [sp, #128]\n" + "ldp q12, q13, [sp, #160]\nldp q14, q15, [sp, #192]\n" + "ldp x19, x20, [sp]\nldp x21, x22, [sp, #16]\nldp x23, x24, [sp, #32]\n" + "ldp x25, x26, [sp, #48]\nldp x27, x28, [sp, #64]\nldp x29, x30, [sp, #80]\n" + "add sp, sp, #1072\nret\n.size svc_register_probe, .-svc_register_probe\n" +); + +static void check(const long *args, long want) { + struct snapshot out = {0}; + unsigned long fpcr, fpsr; + __asm__ volatile("mrs %0, fpcr\nmrs %1, fpsr" : "=r"(fpcr), "=r"(fpsr)); + errno = 123; + svc_register_probe(&out, args); + assert(errno == 123); + assert(out.x[0] == want); + for (int i = 1; i <= 30; i++) { + long expected = i <= 5 ? args[i] : i == 8 ? args[6] : i; + if (out.x[i] != expected) { fprintf(stderr, "x%d: %lx != %lx\n", i, out.x[i], expected); abort(); } + } + for (int i = 0; i < 32; i++) + for (int j = 0; j < 16; j++) assert(out.q[i][j] == 0x5a); + assert(out.nzcv == 0xa0000000UL); + assert(out.fpcr == fpcr && out.fpsr == fpsr); +} + +static void *thread_check(void *unused) { + (void)unused; + struct stat st; + const long args[] = {AT_FDCWD, (long)"/usr/lib/libc.so.6", (long)&st, 0, 0, 0, SYS_newfstatat}; + for (int i = 0; i < 100; i++) check(args, 0); + return NULL; +} + +static pthread_barrier_t barrier; +static void *thread_probe(void *unused) { + pthread_barrier_wait(&barrier); + thread_check(unused); + pthread_barrier_wait(&barrier); + return NULL; +} + +int main(void) { + assert(fp_ready()); + assert(svc_single_thread()); + size_t page = sysconf(_SC_PAGESIZE); + uint32_t *relay = svc_relay_page((uintptr_t)&svc_check_site, page), branch; + assert(relay && svc_emit(relay, &svc_check_site, &branch)); + __builtin___clear_cache((char *)relay, (char *)relay + 32); + assert(mprotect(relay, page, PROT_READ | PROT_EXEC) == 0); + int mem = open("/proc/self/mem", O_RDWR | O_CLOEXEC); + assert(mem >= 0 && pwrite(mem, &branch, 4, (off_t)(uintptr_t)&svc_check_site) == 4); + close(mem); + __builtin___clear_cache((char *)&svc_check_site, (char *)(&svc_check_site + 1)); + long args[] = {0, 1, 2, 3, 4, 5, SYS_getpid}; + check(args, getpid()); + struct stat st; + args[0] = AT_FDCWD; args[1] = (long)"/usr/lib/libc.so.6"; args[2] = (long)&st; + args[3] = 0; args[6] = SYS_newfstatat; + check(args, 0); + assert(st.st_size > 0); + args[1] = (long)"/usr/lib/proot-svc-check-does-not-exist"; + check(args, -ENOENT); + args[1] = 0; + check(args, -EFAULT); + args[1] = (long)"/usr/lib/libc.so.6"; args[2] = 0; + check(args, -EFAULT); + args[1] = (long)"/proc/self/exe"; args[2] = (long)&st; + check(args, 0); + args[3] = 0x40000000; + check(args, -EINVAL); + pthread_t threads[4]; + assert(pthread_barrier_init(&barrier, NULL, 5) == 0); + for (int i = 0; i < 4; i++) assert(pthread_create(&threads[i], NULL, thread_probe, NULL) == 0); + pthread_barrier_wait(&barrier); + assert(!svc_single_thread()); + pthread_barrier_wait(&barrier); + for (int i = 0; i < 4; i++) assert(pthread_join(threads[i], NULL) == 0); + assert(pthread_barrier_destroy(&barrier) == 0); + pid_t pid = fork(); + assert(pid >= 0); + if (!pid) { thread_check(NULL); _exit(0); } + int status; + assert(waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0); + puts("PASS: raw results, errno, x1-x30, q0-q31, NZCV, FPCR/FPSR, 4 threads, fork"); + return 0; +} +#else +int main(void) { return 77; } +#endif diff --git a/tools/proot/bench/svc-probe.c b/tools/proot/bench/svc-probe.c new file mode 100644 index 000000000..43e123b42 --- /dev/null +++ b/tools/proot/bench/svc-probe.c @@ -0,0 +1,93 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +static int mov_sysnum(uint32_t insn) { + if ((insn & 0xffe0001fU) == 0xd2800008U || + (insn & 0xffe0001fU) == 0x52800008U) + return (insn >> 5) & 0xffff; + return -1; +} + +static int scan(struct dl_phdr_info *info, size_t size, void *unused) { + (void)size; + (void)unused; + const char *name = strrchr(info->dlpi_name, '/'); + name = name ? name + 1 : info->dlpi_name; + if (strcmp(name, "libc.so.6") && strcmp(name, "ld-linux-aarch64.so.1")) return 0; + unsigned total = 0, adjacent = 0, nearby = 0, counts[512] = {0}; + for (int i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *ph = &info->dlpi_phdr[i]; + if (ph->p_type != PT_LOAD || !(ph->p_flags & PF_X)) continue; + const uint32_t *text = (const uint32_t *)(info->dlpi_addr + ph->p_vaddr); + for (size_t j = 0; j < ph->p_memsz / 4; j++) { + if (text[j] != 0xd4000001U) continue; + total++; + int nr = j ? mov_sysnum(text[j - 1]) : -1; + if (nr >= 0) { + adjacent++; + if (nr < 512) counts[nr]++; + } else { + for (size_t k = 2; k <= 8 && k <= j; k++) + if (mov_sysnum(text[j - k]) >= 0) { nearby++; break; } + } + } + } + printf("{\"object\":\"%s\",\"svc\":%u,\"adjacent_mov_x8\":%u,\"nearby_only\":%u,\"adjacent_syscalls\":{", + name, total, adjacent, nearby); + int comma = 0; + for (unsigned nr = 0; nr < 512; nr++) + if (counts[nr]) { printf("%s\"%u\":%u", comma ? "," : "", nr, counts[nr]); comma = 1; } + puts("}}"); + return 0; +} + +int main(void) { + long page = sysconf(_SC_PAGESIZE); + int fd = open("/usr/lib/libc.so.6", O_RDONLY | O_CLOEXEC); + if (fd < 0) { perror("libc"); return 1; } + unsigned char *p = mmap(NULL, page, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0); + if (p == MAP_FAILED) { perror("file mmap RX"); close(fd); return 1; } + unsigned char before = p[0], changed = before ^ 1; + errno = 0; + int rwx = mprotect(p, page, PROT_READ | PROT_WRITE | PROT_EXEC), rwx_errno = errno; + if (rwx == 0) { + p[0] = changed; + p[0] = before; + if (mprotect(p, page, PROT_READ | PROT_EXEC)) { perror("restore RX"); return 1; } + } + int mem = open("/proc/self/mem", O_RDWR | O_CLOEXEC), mem_errno = mem < 0 ? errno : 0; + ssize_t written = -1; + int write_errno = 0, verified = 0, restored = 0; + if (mem >= 0) { + errno = 0; + written = pwrite(mem, &changed, 1, (off_t)(uintptr_t)p); + write_errno = errno; + verified = written == 1 && *(volatile unsigned char *)p == changed; + restored = pwrite(mem, &before, 1, (off_t)(uintptr_t)p) == 1 && + *(volatile unsigned char *)p == before; + close(mem); + } + unsigned char disk = 0; + int file_unchanged = pread(fd, &disk, 1, 0) == 1 && disk == before; + munmap(p, page); + close(fd); + void *anon = mmap(NULL, page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + errno = 0; + int execmem = anon == MAP_FAILED ? -1 : mprotect(anon, page, PROT_READ | PROT_EXEC); + int execmem_errno = errno; + if (anon != MAP_FAILED) munmap(anon, page); + printf("{\"page_size\":%ld,\"rwx\":%d,\"rwx_errno\":%d,\"mem_open_errno\":%d," + "\"mem_written\":%ld,\"mem_write_errno\":%d,\"mem_verified\":%d,\"mem_restored\":%d," + "\"file_unchanged\":%d,\"execmem\":%d,\"execmem_errno\":%d}\n", + page, rwx, rwx_errno, mem_errno, (long)written, write_errno, verified, restored, + file_unchanged, execmem, execmem_errno); + dl_iterate_phdr(scan, NULL); + return 0; +} diff --git a/tools/proot/bench/svc-storage.c b/tools/proot/bench/svc-storage.c new file mode 100644 index 000000000..2b9dd40ca --- /dev/null +++ b/tools/proot/bench/svc-storage.c @@ -0,0 +1,74 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static double now(void) { + struct timespec t; + assert(clock_gettime(CLOCK_MONOTONIC, &t) == 0); + return t.tv_sec + t.tv_nsec / 1e9; +} + +int main(int argc, char **argv) { + assert(argc == 2 && mkdir(argv[1], 0700) == 0); + void *libc = dlopen("libc.so.6", RTLD_NOW); + assert(libc); + int (*internal_open)(const char *, int, ...) = dlsym(libc, "__open64_nocancel"); + int (*internal_stat)(int, const char *, struct stat *, int) = dlsym(libc, "fstatat"); + assert(internal_open && internal_stat); + struct rusage before, after; + assert(getrusage(RUSAGE_SELF, &before) == 0); + char paths[64][4096], stream[4096]; + double creation = 0, extension = 0, closing = 0, total = now(); + for (int i = 0; i < 64; i++) { + snprintf(paths[i], sizeof paths[i], "%s/asset-with-a-longish-steam-depot-filename-%03d.dat", argv[1], i); + double start = now(); + int fd = internal_open(paths[i], O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600); + assert(fd >= 0); + creation += now() - start; + start = now(); + assert(ftruncate(fd, 262144) == 0); + extension += now() - start; + start = now(); + assert(close(fd) == 0); + closing += now() - start; + } + double allocation = now() - total; + struct stat st; + double start = now(); + for (int i = 0; i < 1000; i++) assert(internal_stat(AT_FDCWD, paths[i % 64], &st, 0) == 0); + double metadata = now() - start; + snprintf(stream, sizeof stream, "%s/stream.bin", argv[1]); + start = now(); + int fd = internal_open(stream, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600); + assert(fd >= 0); + double stream_create = now() - start; + static unsigned char block[1024 * 1024]; + memset(block, 0x5a, sizeof block); + start = now(); + for (int i = 0; i < 64; i++) assert(write(fd, block, sizeof block) == sizeof block); + double writing = now() - start; + start = now(); + assert(fdatasync(fd) == 0); + double syncing = now() - start; + assert(close(fd) == 0); + assert(getrusage(RUSAGE_SELF, &after) == 0); + printf("{\"files\":64,\"bytes_per_file\":262144,\"create_s\":%.6f,\"extend_s\":%.6f," + "\"close_s\":%.6f,\"allocation_s\":%.6f,\"stat1000_s\":%.6f," + "\"stream_bytes\":67108864,\"stream_create_s\":%.6f,\"write_s\":%.6f," + "\"fdatasync_s\":%.6f,\"voluntary_switches\":%ld}\n", + creation, extension, closing, allocation, metadata, stream_create, writing, + syncing, after.ru_nvcsw - before.ru_nvcsw); + fflush(stdout); + for (int i = 0; i < 64; i++) assert(unlink(paths[i]) == 0); + assert(unlink(stream) == 0 && rmdir(argv[1]) == 0); + assert(dlclose(libc) == 0); + return 0; +} diff --git a/tools/proot/bench/svc-workload.c b/tools/proot/bench/svc-workload.c new file mode 100644 index 000000000..350a7081c --- /dev/null +++ b/tools/proot/bench/svc-workload.c @@ -0,0 +1,91 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static double now(void) { + struct timespec t; + assert(clock_gettime(CLOCK_MONOTONIC, &t) == 0); + return t.tv_sec * 1e9 + t.tv_nsec; +} + +int main(int argc, char **argv) { + assert(argc >= 2); + void *libc = dlopen("libc.so.6", RTLD_NOW); + assert(libc); + int (*internal_stat)(int, const char *, struct stat *, int) = dlsym(libc, "fstatat"); + int (*internal_open)(const char *, int, ...) = dlsym(libc, "__open64_nocancel"); + assert(internal_stat && internal_open); + char path[4096], link[4096], missing[4096]; + snprintf(path, sizeof path, "%s/file", argv[1]); + snprintf(link, sizeof link, "%s/link", argv[1]); + snprintf(missing, sizeof missing, "%s/missing", argv[1]); + const char *paths[] = {path, link, missing, "/etc/os-release", "/lib/libc.so.6", "/proc/self/exe", "/dev/null", "", NULL}; + struct stat st; + for (size_t i = 0; i < sizeof paths / sizeof *paths; i++) { + errno = 0; + int r = internal_stat(AT_FDCWD, paths[i], &st, 0); + printf("stat %zu: %d %d", i, r, r ? errno : 0); + if (!r) printf(" %o %ld", st.st_mode & S_IFMT, S_ISREG(st.st_mode) && i != 5 ? st.st_size : 0L); + putchar('\n'); + errno = 0; + int fd = internal_open(paths[i], O_RDONLY | O_CLOEXEC); + int err = fd < 0 ? errno : 0; + printf("open %zu: %d %d\n", i, fd < 0 ? -1 : 0, err); + if (fd >= 0) close(fd); + } + errno = 0; + int r = internal_stat(AT_FDCWD, path, &st, 0x40000000); + printf("flags: %d %d\n", r, r ? errno : 0); + char *tree[] = {argv[1], NULL}; + FTS *fts = fts_open(tree, FTS_PHYSICAL | FTS_NOCHDIR, NULL); + assert(fts); + unsigned files = 0, dirs = 0, links = 0; + FTSENT *ent; + while ((ent = fts_read(fts))) { + if (ent->fts_info == FTS_F) files++; + if (ent->fts_info == FTS_D) dirs++; + if (ent->fts_info == FTS_SL) links++; + } + assert(fts_close(fts) == 0); + printf("fts: %u files %u dirs %u links\n", files, dirs, links); + char resolved[4096]; + assert(realpath(link, resolved) && !strcmp(resolved, path)); + struct passwd pw, *found; + char buf[16384]; + r = getpwnam_r("root", &pw, buf, sizeof buf, &found); + printf("nss: %d %d\n", r, found != NULL); + void *libm = dlopen("libm.so.6", RTLD_NOW | RTLD_LOCAL); + assert(libm && dlsym(libm, "cos")); + assert(dlclose(libm) == 0); + puts("realpath/dlopen: PASS"); + if (argc > 2) { + const int loops = 5000; + struct rusage before, after; + assert(getrusage(RUSAGE_SELF, &before) == 0); + double start = now(); + for (int i = 0; i < loops; i++) assert(internal_stat(AT_FDCWD, path, &st, 0) == 0); + double stat_ns = (now() - start) / loops; + start = now(); + for (int i = 0; i < loops; i++) { + int fd = internal_open(path, O_RDONLY | O_CLOEXEC); + assert(fd >= 0 && close(fd) == 0); + } + double open_ns = (now() - start) / loops; + assert(getrusage(RUSAGE_SELF, &after) == 0); + printf("bench: stat %.0f ns, open+close %.0f ns, voluntary switches %ld\n", + stat_ns, open_ns, after.ru_nvcsw - before.ru_nvcsw); + } + assert(dlclose(libc) == 0); + return 0; +} diff --git a/tools/proot/fastpath/fastpath.c b/tools/proot/fastpath/fastpath.c index bd3fc0d21..e03680865 100644 --- a/tools/proot/fastpath/fastpath.c +++ b/tools/proot/fastpath/fastpath.c @@ -1828,3 +1828,5 @@ struct passwd *getpwuid(uid_t uid) { missed(__func__, AT_FDCWD, "(passwd)"); return real_getpwuid(uid); } + +#include "svc-patch.h" diff --git a/tools/proot/fastpath/svc-patch.h b/tools/proot/fastpath/svc-patch.h new file mode 100644 index 000000000..f247f18d2 --- /dev/null +++ b/tools/proot/fastpath/svc-patch.h @@ -0,0 +1,250 @@ +static inline int svc_number(uint32_t insn) { + if ((insn & 0xffe0001fU) == 0xd2800008U || + (insn & 0xffe0001fU) == 0x52800008U) + return (insn >> 5) & 0xffff; + return -1; +} + +static inline int svc_argument_instruction(uint32_t insn) { + if (insn == 0xd503201fU) return 1; + if ((insn & 0xffc003e0U) == 0x290003e0U || + (insn & 0xffc003e0U) == 0xa90003e0U) return 1; + if ((insn & 31) == 8) return 0; + return (insn & 0xffe0ffe0U) == 0xaa0003e0U || + (insn & 0xffe0ffe0U) == 0x2a0003e0U || + (insn & 0xfffffc00U) == 0x93407c00U || + (insn & 0x7f800000U) == 0x52800000U || + (insn & 0x7f800000U) == 0x12800000U || + (insn & 0xffc003e0U) == 0xb94003e0U || + (insn & 0xffc003e0U) == 0xf94003e0U || + (insn & 0xffc003e0U) == 0xb98003e0U; +} + +static inline int svc_site_number(const uint32_t *text, size_t at) { + for (size_t back = 1; back <= 8 && back <= at; back++) { + uint32_t insn = text[at - back]; + int nr = svc_number(insn); + if (nr >= 0) return nr; + if (!svc_argument_instruction(insn)) break; + } + return -1; +} + +static inline int svc_branch(uintptr_t from, uintptr_t to, uint32_t *out) { + int64_t delta = (int64_t)to - (int64_t)from; + if ((delta & 3) || delta < -0x8000000LL || delta >= 0x8000000LL) return 0; + *out = 0x14000000U | ((uint32_t)(delta / 4) & 0x03ffffffU); + return 1; +} + +#if defined(__aarch64__) && !defined(FP_SVC_ENCODINGS_ONLY) +#include +#include + +static __thread int svc_busy __attribute__((tls_model("initial-exec"))); +static unsigned svc_sites, svc_pages; +static unsigned long svc_fast, svc_slow; + +static long svc_original(const long *r) { + register long x0 __asm__("x0") = r[0]; + register long x1 __asm__("x1") = r[1]; + register long x2 __asm__("x2") = r[2]; + register long x3 __asm__("x3") = r[3]; + register long x4 __asm__("x4") = r[4]; + register long x5 __asm__("x5") = r[5]; + register long x8 __asm__("x8") = r[8]; + __asm__ volatile("svc #0" : "+r"(x0) : "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5), "r"(x8) : "memory", "cc"); + return x0; +} + +__attribute__((used, visibility("hidden"))) +long fp_svc_dispatch(const long *r) { + int saved_errno = errno; + long result = FP_SLOW; + if (!svc_busy) { + svc_busy = 1; + if (r[8] == SYS_openat) + result = fp_open((int)r[0], (const char *)r[1], (int)r[2], (mode_t)r[3]); + else if (r[8] == SYS_newfstatat && r[2]) + result = fp_stat((int)r[0], (const char *)r[1], (int)r[3], (struct stat *)r[2]); + if (result != FP_SLOW) __atomic_fetch_add(&svc_fast, 1, __ATOMIC_RELAXED); + svc_busy = 0; + } + if (result == FP_SLOW) { + __atomic_fetch_add(&svc_slow, 1, __ATOMIC_RELAXED); + result = svc_original(r); + } + errno = saved_errno; + return result; +} + +void fp_svc_entry(void) __attribute__((visibility("hidden"))); +/* A replaced svc may have live values in every register, including NZCV and SIMD. */ +__asm__( + ".text\n.p2align 4\n.hidden fp_svc_entry\n.type fp_svc_entry,%function\nfp_svc_entry:\n" + "bti j\nsub sp, sp, #800\n" + "stp x0, x1, [sp, #0]\nstp x2, x3, [sp, #16]\n" + "stp x4, x5, [sp, #32]\nstp x6, x7, [sp, #48]\n" + "stp x8, x9, [sp, #64]\nstp x10, x11, [sp, #80]\n" + "stp x12, x13, [sp, #96]\nstp x14, x15, [sp, #112]\n" + "stp x18, x19, [sp, #128]\nstp x20, x21, [sp, #144]\n" + "stp x22, x23, [sp, #160]\nstp x24, x25, [sp, #176]\n" + "stp x26, x27, [sp, #192]\nstp x28, x29, [sp, #208]\n" + "stp x30, x17, [sp, #224]\nmrs x0, nzcv\nstr x0, [sp, #240]\n" + "mrs x0, fpcr\nstr x0, [sp, #768]\nmrs x0, fpsr\nstr x0, [sp, #776]\n" + "stp q0, q1, [sp, #256]\nstp q2, q3, [sp, #288]\n" + "stp q4, q5, [sp, #320]\nstp q6, q7, [sp, #352]\n" + "stp q8, q9, [sp, #384]\nstp q10, q11, [sp, #416]\n" + "stp q12, q13, [sp, #448]\nstp q14, q15, [sp, #480]\n" + "stp q16, q17, [sp, #512]\nstp q18, q19, [sp, #544]\n" + "stp q20, q21, [sp, #576]\nstp q22, q23, [sp, #608]\n" + "stp q24, q25, [sp, #640]\nstp q26, q27, [sp, #672]\n" + "stp q28, q29, [sp, #704]\nstp q30, q31, [sp, #736]\n" + "mov x0, sp\nbl fp_svc_dispatch\nstr x0, [sp]\n" + "ldp q0, q1, [sp, #256]\nldp q2, q3, [sp, #288]\n" + "ldp q4, q5, [sp, #320]\nldp q6, q7, [sp, #352]\n" + "ldp q8, q9, [sp, #384]\nldp q10, q11, [sp, #416]\n" + "ldp q12, q13, [sp, #448]\nldp q14, q15, [sp, #480]\n" + "ldp q16, q17, [sp, #512]\nldp q18, q19, [sp, #544]\n" + "ldp q20, q21, [sp, #576]\nldp q22, q23, [sp, #608]\n" + "ldp q24, q25, [sp, #640]\nldp q26, q27, [sp, #672]\n" + "ldp q28, q29, [sp, #704]\nldp q30, q31, [sp, #736]\n" + "ldr x0, [sp, #768]\nmsr fpcr, x0\nldr x0, [sp, #776]\nmsr fpsr, x0\n" + "ldr x0, [sp, #240]\nmsr nzcv, x0\n" + "ldp x0, x1, [sp, #0]\nldp x2, x3, [sp, #16]\n" + "ldp x4, x5, [sp, #32]\nldp x6, x7, [sp, #48]\n" + "ldp x8, x9, [sp, #64]\nldp x10, x11, [sp, #80]\n" + "ldp x12, x13, [sp, #96]\nldp x14, x15, [sp, #112]\n" + "ldp x18, x19, [sp, #128]\nldp x20, x21, [sp, #144]\n" + "ldp x22, x23, [sp, #160]\nldp x24, x25, [sp, #176]\n" + "ldp x26, x27, [sp, #192]\nldp x28, x29, [sp, #208]\n" + "ldr x30, [sp, #224]\nldr x16, [sp, #232]\n" + "add sp, sp, #800\nbr x16\n.size fp_svc_entry, .-fp_svc_entry\n" +); + +static int svc_emit(uint32_t *code, uint32_t *site, uint32_t *branch) { + uint32_t back; + if (!svc_branch((uintptr_t)site, (uintptr_t)code, branch) || + !svc_branch((uintptr_t)(code + 5), (uintptr_t)(site + 1), &back)) return 0; + code[0] = 0xa9bf47f0U; /* stp x16, x17, [sp, #-16]! */ + code[1] = 0x580000b0U; /* ldr x16, literal at +24 */ + code[2] = 0x10000051U; /* adr x17, +8 */ + code[3] = 0xd61f0200U; /* br x16 */ + code[4] = 0xa8c147f0U; /* ldp x16, x17, [sp], #16 */ + code[5] = back; + uintptr_t entry = (uintptr_t)fp_svc_entry; + memcpy(code + 6, &entry, sizeof entry); + return 1; +} + +struct svc_site { uint32_t *site; uint32_t branch; }; +static struct svc_site svc_plan[256]; +static size_t svc_planned; + +static int svc_object(struct dl_phdr_info *info, size_t size, void *unused) { + (void)size; + (void)unused; + const char *name = strrchr(info->dlpi_name, '/'); + name = name ? name + 1 : info->dlpi_name; + if (strcmp(name, "libc.so.6") && strcmp(name, "ld-linux-aarch64.so.1")) return 0; + for (int i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *ph = &info->dlpi_phdr[i]; + if (ph->p_type != PT_LOAD || !(ph->p_flags & PF_X)) continue; + uint32_t *text = (uint32_t *)(info->dlpi_addr + ph->p_vaddr); + for (size_t j = 1; j < ph->p_memsz / 4 && svc_planned < 256; j++) { + if (text[j] != 0xd4000001U) continue; + int nr = svc_site_number(text, j); + if (nr != SYS_openat && nr != SYS_newfstatat) continue; + svc_plan[svc_planned++].site = &text[j]; + } + } + return 0; +} + +static uint32_t *svc_relay_page(uintptr_t site, size_t page) { + uintptr_t base = site & ~(uintptr_t)(page - 1); + for (uintptr_t delta = page; delta < 0x7ff0000UL; delta += 0x10000UL) { + for (int direction = -1; direction <= 1; direction += 2) { + uintptr_t at = direction < 0 ? base - delta : base + delta; + if (at < page) continue; + void *p = mmap((void *)at, page, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (p == (void *)at) return p; + if (p != MAP_FAILED) munmap(p, page); + } + } + return NULL; +} + +static int svc_single_thread(void) { + struct entry { uint64_t ino; int64_t off; unsigned short len; unsigned char type; char name[]; }; + char buf[4096] __attribute__((aligned(8))); + long fd = sc(SYS_openat, AT_FDCWD, (long)"/proc/self/task", O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0, 0); + if (fd < 0) return 0; + int count = 0; + long n; + while ((n = sc(SYS_getdents64, fd, (long)buf, sizeof buf, 0, 0)) > 0) { + for (long at = 0; at < n;) { + const struct entry *e = (const struct entry *)(buf + at); + if (n - at < (long)offsetof(struct entry, name) + 1 || + e->len < offsetof(struct entry, name) + 1 || e->len > n - at) { count = -1; goto done; } + if (e->name[0] >= '0' && e->name[0] <= '9' && ++count > 1) goto done; + at += e->len; + } + } + if (n < 0) count = -1; +done: + sc(SYS_close, fd, 0, 0, 0, 0); + return count == 1; +} + +static void svc_stats(void) { + char line[160]; + int n = snprintf(line, sizeof line, "fastpath-svc[%d]: %u sites, %u relay pages, %lu fast, %lu to proot\n", + getpid(), svc_sites, svc_pages, svc_fast, svc_slow); + if (write(2, line, n) < 0) {} +} + +__attribute__((constructor)) static void svc_patch_init(void) { + const char *enabled = getenv("PROOT_FP_PATCH_SVC"); + if (!enabled || strcmp(enabled, "1") || !fp_ready()) return; + if (getauxval(AT_HWCAP) & HWCAP_SVE) return; +#ifdef HWCAP2_SME + if (getauxval(AT_HWCAP2) & HWCAP2_SME) return; +#endif + if (!svc_single_thread()) return; + size_t page = (size_t)sysconf(_SC_PAGESIZE); + if (page < 4096 || (page & (page - 1))) return; + int mem = (int)sc(SYS_openat, AT_FDCWD, (long)"/proc/self/mem", O_RDWR | O_CLOEXEC, 0, 0); + if (mem < 0) return; + dl_iterate_phdr(svc_object, NULL); + for (size_t first = 0; first < svc_planned;) { + uint32_t *relay = svc_relay_page((uintptr_t)svc_plan[first].site, page); + if (!relay) { first++; continue; } + size_t end = first; + for (; end < svc_planned && (end - first + 1) * 32 <= page; end++) { + uint32_t *code = relay + (end - first) * 8; + if (!svc_emit(code, svc_plan[end].site, &svc_plan[end].branch)) break; + } + __builtin___clear_cache((char *)relay, (char *)relay + page); + if (end == first || mprotect(relay, page, PROT_READ | PROT_EXEC)) { + munmap(relay, page); + first++; + continue; + } + unsigned patched = 0; + for (size_t i = first; i < end; i++) { + struct svc_site *s = &svc_plan[i]; + long written = sc(SYS_pwrite64, mem, (long)&s->branch, sizeof s->branch, (long)s->site, 0); + if (written != sizeof s->branch) continue; + __builtin___clear_cache((char *)s->site, (char *)(s->site + 1)); + patched++; + } + if (patched) { svc_sites += patched; svc_pages++; } + else munmap(relay, page); + first = end; + } + sc(SYS_close, mem, 0, 0, 0, 0); + if (getenv("PROOT_FP_STATS")) atexit(svc_stats); +} +#endif diff --git a/tools/tests/test_fastpath_svc.py b/tools/tests/test_fastpath_svc.py new file mode 100644 index 000000000..1f76a3465 --- /dev/null +++ b/tools/tests/test_fastpath_svc.py @@ -0,0 +1,62 @@ +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +HEADER = Path(__file__).resolve().parents[1] / "proot/fastpath/svc-patch.h" + + +class FastPathSvcEncodingTest(unittest.TestCase): + def test_instruction_decoding_and_branch_limits(self): + if shutil.which("cc") is None: + self.skipTest("cc is not available") + source = r''' +#include +#include +#include +#define FP_SVC_ENCODINGS_ONLY +#include "HEADER" +int main(void) { + assert(svc_number(0xd2800708) == 56); + assert(svc_number(0x528009e8) == 79); + assert(svc_number(0xd2800709) == -1); + assert(svc_number(0xd2a00708) == -1); + assert(svc_number(0xf2800708) == -1); + assert(svc_number(0xaa0003e8) == -1); + const uint32_t create[] = {0xd2800708, 0x29047fe0, 0x92800c60, 0xd4000001}; + const uint32_t create_at[] = {0xd2800708, 0x29047fe3, 0xb9404be3, 0xd4000001}; + const uint32_t xstat[] = {0xd28009e8, 0xaa0203e1, 0xaa0303e2, 0x93407c83, 0xd4000001}; + const uint32_t overwritten[] = {0xd2800708, 0xaa0003e8, 0xd4000001}; + const uint32_t loaded[] = {0xd2800708, 0xf94003e8, 0xd4000001}; + const uint32_t branch[] = {0xd2800708, 0x14000001, 0xd4000001}; + const uint32_t call[] = {0xd2800708, 0x94000001, 0xd4000001}; + const uint32_t shift[] = {0xd2800708, 0xd2a00708, 0xd4000001}; + assert(svc_site_number(create, 3) == 56); + assert(svc_site_number(create_at, 3) == 56); + assert(svc_site_number(xstat, 4) == 79); + assert(svc_site_number(overwritten, 2) == -1); + assert(svc_site_number(loaded, 2) == -1); + assert(svc_site_number(branch, 2) == -1); + assert(svc_site_number(call, 2) == -1); + assert(svc_site_number(shift, 2) == -1); + uint32_t out; + uintptr_t pc = 0x10000000; + assert(svc_branch(pc, pc + 4, &out) && out == 0x14000001); + assert(svc_branch(pc, pc - 4, &out) && out == 0x17ffffff); + assert(svc_branch(pc, pc + 0x7fffffc, &out) && out == 0x15ffffff); + assert(svc_branch(pc, pc - 0x8000000, &out) && out == 0x16000000); + assert(!svc_branch(pc, pc + 0x8000000, &out)); + assert(!svc_branch(pc, pc - 0x8000004, &out)); + assert(!svc_branch(pc, pc + 2, &out)); + return 0; +} +''' + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + program = work / "check.c" + program.write_text(source.replace("HEADER", HEADER.as_posix())) + output = work / "check" + subprocess.run(["cc", "-O2", "-Wall", "-Wextra", "-Werror", str(program), "-o", str(output)], check=True) + subprocess.run([str(output)], check=True, timeout=10) From 785d1bf1daeb59715ba904bb35f81b5261eb7ad4 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:29:58 -0400 Subject: [PATCH 2/9] perf(proot): enable syscall patching by default --- docs/development/proroot-analysis.md | 22 ++++++++++++++-------- tools/proot/fastpath/svc-patch.h | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/development/proroot-analysis.md b/docs/development/proroot-analysis.md index 81be73a11..a63e40ad3 100644 --- a/docs/development/proroot-analysis.md +++ b/docs/development/proroot-analysis.md @@ -134,10 +134,11 @@ netlink and SysV shm (`netif.c` and `sysv.c` already cover these); and the long- ## First implementation and Thor validation (2026-10-27) -`tools/proot/fastpath/svc-patch.h`, included by `fastpath.c`, implements an opt-in first -slice of item 1. Set `PROOT_FP_PATCH_SVC=1` in a fresh process using the replacement -`libblfastpath.so`. The ordinary fast-path configuration and tracer-key check must also -succeed. It is disabled by default, skips processes that already have multiple threads, +`tools/proot/fastpath/svc-patch.h`, included by `fastpath.c`, implements the first +slice of item 1. It runs automatically in fresh processes using the replacement +`libblfastpath.so` when the ordinary fast-path configuration and tracer-key check +succeed. Set `PROOT_FP_PATCH_SVC=0` to disable it for troubleshooting or comparisons; +`PROOT_FP_PATCH_SVC=1` also explicitly enables it. It skips processes that already have multiple threads, and declines SVE/SME hardware because the relay preserves the base ARM64 register state. The scanner visits executable segments of the loaded `libc.so.6` and @@ -180,6 +181,11 @@ NZCV, FPCR/FPSR, four simultaneous threads, the multi-thread patching guard, and Null arguments and unsupported flags fall back to PRoot. The instruction decoder and exact ±128 MiB branch limits also have a host regression test. +Default-policy validation on the Thor confirmed that an unset `PROOT_FP_PATCH_SVC` +and explicit `1` both patched 14 sites; explicit `0` left all 415 libc and 56 linker +syscall sites unchanged. The register/thread/fork check also passed with the +environment flag unset. + Five alternating pairs of `bench/svc-workload.c` runs compared the same replacement library with patching off/on, with fast-path stats enabled in both. The workload gets `fstatat` and `__open64_nocancel` directly from libc, bypassing the preload's @@ -234,7 +240,7 @@ the start and completed with `No Error`. Existing installed games were retained. The repeat off/on pair used the exact same replacement library, SHA-256 `ab605c021aee271cd49fa1cb337d7ff0c762dbac198b711e9818639c0cd4b17d`. The first off run used the earlier adjacent-only build, with patching disabled. -Only the opt-in environment flag changed for the final pair. Times below come +Only the patching environment flag changed for the final pair. Times below come from Steam's one-second-resolution `content_log.txt`; total starts at Reconfiguring and ends at scheduler completion, excluding session startup: @@ -281,7 +287,7 @@ The installed PRoot pack emitted no profiling start/counter records despite the queued profiling setting; the profiler report had `hasCounters: false`. That is a measurement gap, not evidence of zero remaining traps. -Before enabling this by default: validate signal/cancellation/unwind behavior, +Further validation includes signal/cancellation/unwind behavior, additional glibc builds and workloads, startup cost and Steam/CEF tree RSS; obtain matching patch-0018 tracer measurements; and resolve the existing SD socket mismatch. Additional syscall families, Android-blocked syscall emulation and build-id caching @@ -305,7 +311,7 @@ Run `svc-probe` inside the app's guest environment, not from `adb shell`. `svc-check` embeds the fast path and requires its normal enabled tracer environment. For `svc-workload`, pass a prepared directory containing `file` and a `link` symlink to it; add any second argument to enable timing. Use the documented library override -workflow in [proot-performance](proot-performance.md) for a fresh session, adding -`PROOT_FP_PATCH_SVC=1` through `droiddeckctl env set` before starting it. +workflow in [proot-performance](proot-performance.md) for a fresh session. No extra +environment setting is needed; use `PROOT_FP_PATCH_SVC=0` for the unpatched comparison. `svc-storage` takes a new, unused directory name; it creates and removes its own test files and directory. It writes 64 MiB plus 16 MiB of file extensions per run. diff --git a/tools/proot/fastpath/svc-patch.h b/tools/proot/fastpath/svc-patch.h index f247f18d2..fde1c2511 100644 --- a/tools/proot/fastpath/svc-patch.h +++ b/tools/proot/fastpath/svc-patch.h @@ -207,7 +207,7 @@ static void svc_stats(void) { __attribute__((constructor)) static void svc_patch_init(void) { const char *enabled = getenv("PROOT_FP_PATCH_SVC"); - if (!enabled || strcmp(enabled, "1") || !fp_ready()) return; + if ((enabled && strcmp(enabled, "1")) || !fp_ready()) return; if (getauxval(AT_HWCAP) & HWCAP_SVE) return; #ifdef HWCAP2_SME if (getauxval(AT_HWCAP2) & HWCAP2_SME) return; From c62b9b5d1bbdc4ebacebc368f65ecb422663c4ab Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:17:53 -0400 Subject: [PATCH 3/9] perf(proot): expand and cache syscall relays --- .../launcher/runtime/ProotFastPath.kt | 4 +- .../launcher/runtime/ProotFastPathTest.kt | 6 + docs/development/proroot-analysis.md | 64 +++++- tools/proot/bench/svc-check.c | 186 +++++++++++++++++- tools/proot/fastpath/fastpath.c | 144 +++++++++----- tools/proot/fastpath/svc-cache.h | 90 +++++++++ tools/proot/fastpath/svc-dispatch.h | 139 +++++++++++++ tools/proot/fastpath/svc-patch.h | 64 ++++-- tools/tests/test_fastpath_svc.py | 103 ++++++++++ 9 files changed, 719 insertions(+), 81 deletions(-) create mode 100644 tools/proot/fastpath/svc-cache.h create mode 100644 tools/proot/fastpath/svc-dispatch.h diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt b/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt index 485d7178a..974b6fe16 100644 --- a/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt +++ b/app/src/main/java/com/droiddeck/launcher/runtime/ProotFastPath.kt @@ -1,6 +1,7 @@ package com.droiddeck.launcher.runtime import android.content.Context +import android.os.Build import android.util.Log import com.droiddeck.launcher.session.SessionPrefs import java.io.File @@ -37,9 +38,10 @@ object ProotFastPath { fun hostEnv(key: String): Pair = "PROOT_FASTPATH" to key - fun guestEnv(root: File, binds: List, key: String): List = listOf( + fun guestEnv(root: File, binds: List, key: String, sdk: Int = Build.VERSION.SDK_INT): List = listOf( "PROOT_FP_ROOT=" + root.path, "PROOT_FP_BINDS=" + binds.joinToString("|"), "PROOT_FP_KEY=$key", + "PROOT_FP_ANDROID_API=$sdk", ) } diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt index 9b97bd418..12fb45bf9 100644 --- a/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/runtime/ProotFastPathTest.kt @@ -10,6 +10,12 @@ import java.io.File class ProotFastPathTest { private val root = File("/data/data/com.droiddeck.launcher/files/linuxfs") + @Test fun guestEnvironmentCarriesTheHostAndroidApi() { + for (sdk in listOf(33, 35, 36)) { + assertTrue(ProotFastPath.guestEnv(root, emptyList(), "test", sdk).contains("PROOT_FP_ANDROID_API=$sdk")) + } + } + @Test fun aSessionsUsualBindsGetAKey() { val binds = (1..87).map { "/host/$it:/guest/$it" } assertNotNull(ProotFastPath.key(root, binds)) diff --git a/docs/development/proroot-analysis.md b/docs/development/proroot-analysis.md index a63e40ad3..dd0220ef2 100644 --- a/docs/development/proroot-analysis.md +++ b/docs/development/proroot-analysis.md @@ -287,12 +287,64 @@ The installed PRoot pack emitted no profiling start/counter records despite the queued profiling setting; the profiler report had `hasCounters: false`. That is a measurement gap, not evidence of zero remaining traps. -Further validation includes signal/cancellation/unwind behavior, -additional glibc builds and workloads, startup cost and Steam/CEF tree RSS; obtain -matching patch-0018 tracer measurements; and resolve the existing SD socket mismatch. -Additional syscall families, Android-blocked syscall emulation and build-id caching -remain follow-ups. In particular this implementation does not remove startup loader -traps before the preload constructor, nor patch raw calls in arbitrary guest ELFs. +### Expanded relay coverage + +The subsequent revision keeps patching enabled by default and shares the existing +wrapper translators with 20 path syscall numbers: openat, newfstatat, statx, +readlinkat, faccessat, mkdirat, unlinkat, renameat, renameat2, symlinkat, fchmodat, +utimensat, truncate, statfs, getxattr, lgetxattr, listxattr, llistxattr, +inotify_add_watch and getcwd. This is an allowlist of safely recognized sites, not +a claim that every glibc implementation contains every site. Unsupported paths, +flags, instructions and inaccessible pointers still use the original PRoot route. +Input/output copies use process_vm_readv/writev rather than dereferencing arbitrary +guest pointers. The device matrix checks error priority, protected buffers and a +NUL-terminated path ending at a page boundary. + +The app forwards its Android SDK as `PROOT_FP_ANDROID_API`. With a known SDK, +set_robust_list retains the head in TLS and returns ENOSYS, matching patch 0016; +self get_robust_list uses that head. Uncaptured heads and other TIDs fall back to +PRoot. This does not implement kernel robust-mutex owner-death recovery. faccessat2 +returns ENOSYS for glibc's fallback. clone3 returns ENOSYS only before API 35; +it is never executed from the C relay, since successful clone3 would change the +child's stack. The [Android 14 allowlist](https://android.googlesource.com/platform/bionic/+/refs/heads/android14-release/libc/SECCOMP_ALLOWLIST_COMMON.TXT) +omits clone3 and the [Android 15 allowlist](https://android.googlesource.com/platform/bionic/+/refs/heads/android15-release/libc/SECCOMP_ALLOWLIST_COMMON.TXT) +includes it. Unknown platforms leave these sites untouched. Existing droiddeck-fsync +handling of futex_waitv is unchanged. + +Build-ID tables in `/tmp/.droiddeck-svc-v1--` cache relative site +offsets. Every hit revalidates executable-segment bounds, alignment, the current +SVC opcode and the recognized syscall number. Missing, malformed, truncated, +overly permissive or symlinked tables trigger a rescan; writes use a private +temporary file and atomic rename. Images without a readable GNU build-ID still +scan normally. `PROOT_FP_SVC_CACHE=0` disables caching for comparison. Changes to +the accepted instruction/syscall set must bump the cache version. + +On the Thor's glibc 2.43 / Android 13, 49 sites patch across libc and the loader. +Warm cached discovery took 60–63 microseconds in two samples versus roughly +0.6–0.7 milliseconds for the earlier uncached scan; patch publication took about +0.28 milliseconds. These are constructor phases, not whole Steam startup times. +Five alternating expanded-library pairs measured median internal stat at 39.5→4.9 +microseconds and open/close at 41.2→6.4 microseconds, with 10,000→0 voluntary +context switches over both loops. The earlier SD/download tables above describe +the initial 14-site revision and must not be attributed to this expanded build. + +The expanded ABI matrix passes raw return values, errno, all x1–x30 and q0–q31, +NZCV/FPCR/FPSR, four concurrent threads with distinct robust heads, fork, 100 +signals on an alternate stack, deferred cancellation and 32 thread create/join +cycles. Constructor patching still refuses a process with multiple live threads. +Host tests exercise corrupted cache entries, invalid offsets/opcodes, mismatched +API/version, unsafe file modes, symlinks and truncation. The Android unit test +checks API forwarding. The existing preload/agent selection passed 79 tests with +two skips before the cache regression test was added. + +Internal fts/realpath/NSS/dlopen and the normalized first equivalence suite still +match PRoot at both cache TTLs. The write suite still matches wrapper-only output +and reproduces the same existing SD Unix-socket mismatch described above. +No in-process RET_TRAP filter is installed, so the proposed SIGSYS mask/handler +machinery is not applicable. Startup loader traps before the constructor and +raw calls in arbitrary guest ELFs remain outside the patch scope. Additional +glibc builds, Android versions, unwind behavior, Steam/CEF tree RSS and matching +patch-0018 tracer measurements remain coverage gaps. Build the tools with the same ARM64 glibc toolchain as the fast path: diff --git a/tools/proot/bench/svc-check.c b/tools/proot/bench/svc-check.c index fc76a6852..80040b61b 100644 --- a/tools/proot/bench/svc-check.c +++ b/tools/proot/bench/svc-check.c @@ -1,6 +1,8 @@ #include "../fastpath/fastpath.c" #include #include +#include +#include #if defined(__aarch64__) struct snapshot { long x[32]; unsigned char q[32][16]; unsigned long nzcv, fpcr, fpsr; }; @@ -37,14 +39,13 @@ __asm__( "add sp, sp, #1072\nret\n.size svc_register_probe, .-svc_register_probe\n" ); -static void check(const long *args, long want) { +static long probe(const long *args) { struct snapshot out = {0}; unsigned long fpcr, fpsr; __asm__ volatile("mrs %0, fpcr\nmrs %1, fpsr" : "=r"(fpcr), "=r"(fpsr)); errno = 123; svc_register_probe(&out, args); assert(errno == 123); - assert(out.x[0] == want); for (int i = 1; i <= 30; i++) { long expected = i <= 5 ? args[i] : i == 8 ? args[6] : i; if (out.x[i] != expected) { fprintf(stderr, "x%d: %lx != %lx\n", i, out.x[i], expected); abort(); } @@ -53,11 +54,127 @@ static void check(const long *args, long want) { for (int j = 0; j < 16; j++) assert(out.q[i][j] == 0x5a); assert(out.nzcv == 0xa0000000UL); assert(out.fpcr == fpcr && out.fpsr == fpsr); + return out.x[0]; +} + +static void check(const long *args, long want) { + long got = probe(args); + if (got != want) { fprintf(stderr, "syscall %ld: got %ld, expected %ld\n", args[6], got, want); abort(); } +} + +static long raw(long nr, long a, long b, long c, long d, long e, long f) { + long args[] = {a, b, c, d, e, f, nr}; + return probe(args); +} + +static void fault(long nr, long a, long b, long c, long d, long e, long f) { + long args[] = {a, b, c, d, e, f, 0, 0, nr}; + long want = svc_original(args); + long call[] = {a, b, c, d, e, f, nr}; + check(call, want); +} + +static void path_checks(void) { + char work[] = "/tmp/proot-expanded-check-XXXXXX"; + assert(mkdtemp(work)); + int cwd = open(".", O_RDONLY | O_DIRECTORY); + assert(cwd >= 0 && chdir(work) == 0); + assert(raw(SYS_mkdirat, AT_FDCWD, (long)"dir", 0700, 0, 0, 0) == 0); + long fd = raw(SYS_openat, AT_FDCWD, (long)"dir/file", O_CREAT | O_RDWR | O_EXCL, 0600, 0, 0); + assert(fd >= 0 && write((int)fd, "hello", 5) == 5); + struct stat st; + struct statx sx; + assert(raw(SYS_newfstatat, AT_FDCWD, (long)"dir/file", (long)&st, 0, 0, 0) == 0 && st.st_size == 5); + assert(raw(SYS_statx, AT_FDCWD, (long)"dir/file", 0, STATX_BASIC_STATS, (long)&sx, 0) == 0 && sx.stx_size == 5); + assert(raw(SYS_faccessat, AT_FDCWD, (long)"dir/file", R_OK, 0, 0, 0) == 0); + assert(raw(SYS_symlinkat, (long)"file", AT_FDCWD, (long)"dir/link", 0, 0, 0) == 0); + char text[PATH_MAX]; + memset(text, '#', sizeof text); + assert(raw(SYS_readlinkat, AT_FDCWD, (long)"dir/link", (long)text, sizeof text, 0, 0) == 4); + assert(!memcmp(text, "file#", 5)); + assert(raw(SYS_fchmodat, AT_FDCWD, (long)"dir/file", 0640, 0, 0, 0) == 0); + struct timespec times[] = {{1000, 0}, {2000, 0}}; + assert(raw(SYS_truncate, (long)"dir/file", 11, 0, 0, 0, 0) == 0); + assert(raw(SYS_utimensat, AT_FDCWD, (long)"dir/file", (long)times, 0, 0, 0) == 0); + assert(raw(SYS_newfstatat, AT_FDCWD, (long)"dir/file", (long)&st, 0, 0, 0) == 0); + assert(st.st_size == 11 && (st.st_mode & 0777) == 0640 && st.st_mtime == 2000); + assert(raw(SYS_renameat, AT_FDCWD, (long)"dir/file", AT_FDCWD, (long)"dir/moved", 0, 0) == 0); + assert(raw(SYS_renameat2, AT_FDCWD, (long)"dir/moved", AT_FDCWD, (long)"dir/file", RENAME_NOREPLACE, 0) == 0); + struct statfs fs; + assert(raw(SYS_statfs, (long)"dir/file", (long)&fs, 0, 0, 0, 0) == 0); + long x = raw(SYS_getxattr, (long)"dir/file", (long)"user.proot-missing", (long)text, sizeof text, 0, 0); + assert(x == -ENODATA || x == -EOPNOTSUPP); + x = raw(SYS_lgetxattr, (long)"dir/link", (long)"user.proot-missing", (long)text, sizeof text, 0, 0); + assert(x == -ENODATA || x == -EOPNOTSUPP); + assert(raw(SYS_listxattr, (long)"dir/file", (long)text, sizeof text, 0, 0, 0) >= 0); + assert(raw(SYS_llistxattr, (long)"dir/link", (long)text, sizeof text, 0, 0, 0) >= 0); + int in = inotify_init1(IN_CLOEXEC); + assert(in >= 0 && raw(SYS_inotify_add_watch, in, (long)"dir", IN_CREATE, 0, 0, 0) >= 0); + close(in); + long n = raw(SYS_getcwd, (long)text, sizeof text, 0, 0, 0, 0); + assert(n == (long)strlen(work) + 1 && !strcmp(text, work)); + size_t page = sysconf(_SC_PAGESIZE); + char *guard = mmap(NULL, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + assert(guard != MAP_FAILED && mprotect(guard + page, page, PROT_NONE) == 0); + strcpy(guard + page - 2, "."); + assert(raw(SYS_newfstatat, AT_FDCWD, (long)(guard + page - 2), (long)&st, 0, 0, 0) == 0); + long bad = (long)(guard + page); + fault(SYS_openat, AT_FDCWD, bad, O_RDONLY, 0, 0, 0); + fault(SYS_newfstatat, AT_FDCWD, (long)"dir/file", bad, 0, 0, 0); + fault(SYS_statx, AT_FDCWD, (long)"dir/file", 0, STATX_BASIC_STATS, bad, 0); + fault(SYS_readlinkat, AT_FDCWD, (long)"dir/link", bad, 16, 0, 0); + fault(SYS_getcwd, bad, PATH_MAX, 0, 0, 0, 0); + fault(SYS_utimensat, AT_FDCWD, (long)"missing", bad, 0, 0, 0); + fault(SYS_inotify_add_watch, -1, (long)"missing", IN_CREATE, 0, 0, 0); + fault(SYS_renameat2, AT_FDCWD, (long)"dir/file", AT_FDCWD, (long)"dir/nope", 0x80000000, 0); + assert(munmap(guard, page * 2) == 0); + close((int)fd); + assert(raw(SYS_unlinkat, AT_FDCWD, (long)"dir/link", 0, 0, 0, 0) == 0); + assert(raw(SYS_unlinkat, AT_FDCWD, (long)"dir/file", 0, 0, 0, 0) == 0); + assert(raw(SYS_unlinkat, AT_FDCWD, (long)"dir", AT_REMOVEDIR, 0, 0, 0) == 0); + assert(fchdir(cwd) == 0 && close(cwd) == 0 && rmdir(work) == 0); + puts("PASS: path families, dirfds, flags, symlinks, protected pointers, page boundary"); +} + +static void robust_checks(void) { + if (!svc_android_api) return; + long original = 0; + size_t size = 0; + assert(raw(SYS_get_robust_list, 0, (long)&original, (long)&size, 0, 0, 0) == 0); + struct robust_list_head head = {0}; + assert(raw(SYS_set_robust_list, (long)&head, sizeof head, 0, 0, 0, 0) == -ENOSYS); + long got = 0; + assert(raw(SYS_get_robust_list, gettid(), (long)&got, (long)&size, 0, 0, 0) == 0); + assert(got == (long)&head && size == sizeof head); + assert(raw(SYS_get_robust_list, 0, 1, (long)&size, 0, 0, 0) == -EFAULT); + assert(raw(SYS_set_robust_list, original, sizeof head, 0, 0, 0, 0) == -ENOSYS); + assert(raw(SYS_faccessat2, AT_FDCWD, (long)"/usr/lib/libc.so.6", R_OK, 0, 0, 0) == -ENOSYS); + if (svc_android_api < 35) assert(raw(SYS_clone3, 0, 0, 0, 0, 0, 0) == -ENOSYS); + int api = svc_android_api; + svc_android_api = 35; + assert(!svc_supported(SYS_clone3)); + svc_android_api = 0; + assert(!svc_supported(SYS_clone3) && !svc_supported(SYS_set_robust_list)); + svc_android_api = api; + puts("PASS: robust-list TLS, errors, faccessat2, clone3 platform guard"); } static void *thread_check(void *unused) { (void)unused; struct stat st; + if (svc_android_api) { + long head; + size_t size; + assert(raw(SYS_get_robust_list, 0, (long)&head, (long)&size, 0, 0, 0) == 0 && head && size == 3 * sizeof(long)); + struct robust_list_head private = {0}; + assert(raw(SYS_set_robust_list, (long)&private, sizeof private, 0, 0, 0, 0) == -ENOSYS); + for (int i = 0; i < 10; i++) { + long got; + sched_yield(); + assert(raw(SYS_get_robust_list, 0, (long)&got, (long)&size, 0, 0, 0) == 0 && got == (long)&private); + } + assert(raw(SYS_set_robust_list, head, sizeof private, 0, 0, 0, 0) == -ENOSYS); + } const long args[] = {AT_FDCWD, (long)"/usr/lib/libc.so.6", (long)&st, 0, 0, 0, SYS_newfstatat}; for (int i = 0; i < 100; i++) check(args, 0); return NULL; @@ -71,8 +188,70 @@ static void *thread_probe(void *unused) { return NULL; } +static volatile sig_atomic_t signals_seen; +static void on_signal(int signal) { + (void)signal; + int saved = errno; + struct stat st; + assert(raw(SYS_newfstatat, AT_FDCWD, (long)"/usr/lib/libc.so.6", (long)&st, 0, 0, 0) == 0); + signals_seen++; + errno = saved; +} + +struct signal_sender { pthread_t target; int done; }; +static void *send_signals(void *arg) { + struct signal_sender *sender = arg; + const struct timespec delay = {0, 100000}; + for (int i = 0; i < 100; i++) { + assert(pthread_kill(sender->target, SIGUSR1) == 0); + nanosleep(&delay, NULL); + } + __atomic_store_n(&sender->done, 1, __ATOMIC_RELEASE); + return NULL; +} + +static void *cancel_worker(void *unused) { + (void)unused; + struct stat st; + for (;;) { + assert(raw(SYS_newfstatat, AT_FDCWD, (long)"/usr/lib/libc.so.6", (long)&st, 0, 0, 0) == 0); + pthread_testcancel(); + } +} + +static void signal_checks(void) { + stack_t old_stack, stack = {.ss_sp = malloc(128 * 1024), .ss_size = 128 * 1024}; + assert(stack.ss_sp && sigaltstack(&stack, &old_stack) == 0); + struct sigaction old, action = {.sa_handler = on_signal, .sa_flags = SA_ONSTACK}; + sigemptyset(&action.sa_mask); + assert(sigaction(SIGUSR1, &action, &old) == 0); + sigset_t deliver, old_mask; + sigemptyset(&deliver); + sigaddset(&deliver, SIGUSR1); + assert(pthread_sigmask(SIG_UNBLOCK, &deliver, &old_mask) == 0); + struct signal_sender sender = {.target = pthread_self()}; + pthread_t thread; + assert(pthread_create(&thread, NULL, send_signals, &sender) == 0); + struct stat st; + for (int i = 0; i < 2000 || !__atomic_load_n(&sender.done, __ATOMIC_ACQUIRE); i++) + assert(raw(SYS_newfstatat, AT_FDCWD, (long)"/usr/lib/libc.so.6", (long)&st, 0, 0, 0) == 0); + assert(pthread_join(thread, NULL) == 0 && signals_seen > 0); + assert(sigaction(SIGUSR1, &old, NULL) == 0 && sigaltstack(&old_stack, NULL) == 0); + assert(pthread_sigmask(SIG_SETMASK, &old_mask, NULL) == 0); + free(stack.ss_sp); + void *result; + assert(pthread_create(&thread, NULL, cancel_worker, NULL) == 0); + assert(pthread_cancel(thread) == 0 && pthread_join(thread, &result) == 0 && result == PTHREAD_CANCELED); + for (int i = 0; i < 32; i++) { + assert(pthread_create(&thread, NULL, thread_check, NULL) == 0); + assert(pthread_join(thread, NULL) == 0); + } + puts("PASS: signal reentry on alternate stack, deferred cancellation, thread churn"); +} + int main(void) { assert(fp_ready()); + printf("platform: android %d, patched %u sites\n", svc_android_api, svc_sites); assert(svc_single_thread()); size_t page = sysconf(_SC_PAGESIZE); uint32_t *relay = svc_relay_page((uintptr_t)&svc_check_site, page), branch; @@ -100,6 +279,8 @@ int main(void) { check(args, 0); args[3] = 0x40000000; check(args, -EINVAL); + path_checks(); + robust_checks(); pthread_t threads[4]; assert(pthread_barrier_init(&barrier, NULL, 5) == 0); for (int i = 0; i < 4; i++) assert(pthread_create(&threads[i], NULL, thread_probe, NULL) == 0); @@ -113,6 +294,7 @@ int main(void) { if (!pid) { thread_check(NULL); _exit(0); } int status; assert(waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0); + signal_checks(); puts("PASS: raw results, errno, x1-x30, q0-q31, NZCV, FPCR/FPSR, 4 threads, fork"); return 0; } diff --git a/tools/proot/fastpath/fastpath.c b/tools/proot/fastpath/fastpath.c index e03680865..ec1562bdf 100644 --- a/tools/proot/fastpath/fastpath.c +++ b/tools/proot/fastpath/fastpath.c @@ -931,29 +931,26 @@ static long statx_call(int dirfd, const char *path, int flags, unsigned mask, st return 0; } -int statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *out) { - REAL(statx, int (*)(int, const char *, int, unsigned, struct statx *)); +static long fp_statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *out) { struct res r; long rs; - if (mask & STATX__RESERVED) { errno = EINVAL; return -1; } -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wnonnull-compare" - if (!out) { errno = EFAULT; return -1; } - if (!path) { - if (!(flags & AT_EMPTY_PATH)) { errno = EFAULT; return -1; } - return real_statx ? real_statx(dirfd, path, flags, mask, out) : ret(sc(SYS_statx, dirfd, (long)path, flags, mask, (long)out)); - } -#pragma GCC diagnostic pop - if ((flags & AT_EMPTY_PATH) && !path[0] && fp_ready()) { - hits++; - return ret(statx_call(dirfd, "", flags, mask, out)); - } + if (mask & STATX__RESERVED) return -EINVAL; + if (!out) return -EFAULT; + if (!path) return flags & AT_EMPTY_PATH ? FP_SLOW : -EFAULT; + if ((flags & AT_EMPTY_PATH) && !path[0] && fp_ready()) + return statx_call(dirfd, "", flags, mask, out); rs = (flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT | AT_STATX_SYNC_TYPE)) ? FP_SLOW : resolve(dirfd, path, (flags & AT_SYMLINK_NOFOLLOW ? 0 : 1) | MAY_BE_UNSEEN, &r); if (rs == 1) rs = -ENOENT; if (rs == 0 || rs == UNSEEN) rs = statx_call(AT_FDCWD, r.host, flags | (rs == 0 && !r.magic ? AT_SYMLINK_NOFOLLOW : 0), mask, out); - ANSWER(rs, dirfd, path, real_statx(dirfd, path, flags, mask, out)); + return rs; +} + +int statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *out) { + REAL(statx, int (*)(int, const char *, int, unsigned, struct statx *)); + ANSWER(fp_statx(dirfd, path, flags, mask, out), dirfd, path, + real_statx ? real_statx(dirfd, path, flags, mask, out) : ret(sc(SYS_statx, dirfd, (long)path, flags, mask, (long)out))); } @@ -961,8 +958,7 @@ static int effective_is_real(void) { return !getauxval(AT_SECURE) && getuid() == geteuid() && getgid() == getegid(); } -static int access_common(int dirfd, const char *path, int mode, int flags) { - REAL(faccessat, int (*)(int, const char *, int, int)); +static long fp_access(int dirfd, const char *path, int mode, int flags) { struct res r; if ((flags & AT_EACCESS) && effective_is_real()) flags &= ~AT_EACCESS; long rs = (flags & ~AT_SYMLINK_NOFOLLOW) == 0 @@ -970,6 +966,12 @@ static int access_common(int dirfd, const char *path, int mode, int flags) { if (rs == 1) rs = -ENOENT; if (rs == 0 && S_ISLNK(r.st.st_mode)) rs = FP_SLOW; if (rs == 0 || rs == UNSEEN) rs = sc(SYS_faccessat, AT_FDCWD, (long)r.host, mode, 0, 0); + return rs; +} + +static int access_common(int dirfd, const char *path, int mode, int flags) { + REAL(faccessat, int (*)(int, const char *, int, int)); + long rs = fp_access(dirfd, path, mode, flags); if (rs != FP_SLOW) { hits++; return ret(rs); } missed(__func__, dirfd, path); if (flags == 0) return (int)syscall(SYS_faccessat, dirfd, path, mode); @@ -997,34 +999,36 @@ static long link_text(const struct res *r, char *text, size_t size, int *changed return (long)strlen(text); } -static ssize_t readlink_common(int dirfd, const char *path, char *buf, size_t size) { - REAL(readlinkat, ssize_t (*)(int, const char *, char *, size_t)); +static long fp_readlink(int dirfd, const char *path, char *buf, size_t size, size_t *written) { struct res r; char text[PATH_MAX]; int changed; long rs = size ? resolve(dirfd, path, 0, &r) : FP_SLOW; if (rs == 1) rs = -ENOENT; if (rs == 0) rs = link_text(&r, text, size, &changed); - if (rs == FP_SLOW) { - missed(__func__, dirfd, path); - return real_readlinkat(dirfd, path, buf, size); - } - hits++; - if (rs < 0) return ret(rs); + if (rs < 0) return rs; size_t n = rs; if (!changed) { n = n < size ? n : size; memcpy(buf, text, n); + if (written) *written = n; return n; } size_t max = size < PATH_MAX ? size : PATH_MAX; if (n + 1 < max) { memcpy(buf, text, n + 1); + if (written) *written = n + 1; return n; } memcpy(buf, text, max); + if (written) *written = max; return max; } + +static ssize_t readlink_common(int dirfd, const char *path, char *buf, size_t size) { + REAL(readlinkat, ssize_t (*)(int, const char *, char *, size_t)); + ANSWER(fp_readlink(dirfd, path, buf, size, NULL), dirfd, path, real_readlinkat(dirfd, path, buf, size)); +} ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t size) { return readlink_common(dirfd, path, buf, size); } ssize_t readlink(const char *path, char *buf, size_t size) { return readlink_common(AT_FDCWD, path, buf, size); } @@ -1086,12 +1090,15 @@ char *__getcwd_chk(char *buf, size_t size, size_t room) { } -int mkdirat(int dirfd, const char *path, mode_t mode) { - REAL(mkdirat, int (*)(int, const char *, mode_t)); +static long fp_mkdir(int dirfd, const char *path, mode_t mode) { struct res r; long rs = resolve(dirfd, path, MAY_BE_UNSEEN, &r); if (rs == 0 || rs == 1 || rs == UNSEEN) rs = sc(SYS_mkdirat, AT_FDCWD, (long)r.host, mode, 0, 0); - ANSWER(rs, dirfd, path, real_mkdirat(dirfd, path, mode)); + return rs; +} +int mkdirat(int dirfd, const char *path, mode_t mode) { + REAL(mkdirat, int (*)(int, const char *, mode_t)); + ANSWER(fp_mkdir(dirfd, path, mode), dirfd, path, real_mkdirat(dirfd, path, mode)); } int mkdir(const char *path, mode_t mode) { return mkdirat(AT_FDCWD, path, mode); } @@ -1137,8 +1144,7 @@ int unlinkat(int dirfd, const char *path, int flags) { return unlink_common(dirf int unlink(const char *path) { return unlink_common(AT_FDCWD, path, 0); } int rmdir(const char *path) { return unlink_common(AT_FDCWD, path, AT_REMOVEDIR); } -static int rename_common(int olddirfd, const char *old, int newdirfd, const char *new, unsigned flags) { - REAL(renameat2, int (*)(int, const char *, int, const char *, unsigned)); +static long fp_rename(int olddirfd, const char *old, int newdirfd, const char *new, unsigned flags) { struct res from, to; long rs = flags & ~RENAME_NOREPLACE || trailing_slash(old) || trailing_slash(new) ? FP_SLOW : resolve(olddirfd, old, 0, &from); if (rs == 1) rs = -ENOENT; @@ -1153,6 +1159,11 @@ static int rename_common(int olddirfd, const char *old, int newdirfd, const char if (rs == 0) forget(); } } + return rs; +} +static int rename_common(int olddirfd, const char *old, int newdirfd, const char *new, unsigned flags) { + REAL(renameat2, int (*)(int, const char *, int, const char *, unsigned)); + long rs = fp_rename(olddirfd, old, newdirfd, new, flags); if (rs != FP_SLOW) { hits++; return ret(rs); } missed(__func__, olddirfd, old); if (!flags) return (int)syscall(SYS_renameat, olddirfd, old, newdirfd, new); @@ -1164,30 +1175,35 @@ int renameat2(int olddirfd, const char *old, int newdirfd, const char *new, unsi int renameat(int olddirfd, const char *old, int newdirfd, const char *new) { return rename_common(olddirfd, old, newdirfd, new, 0); } int rename(const char *old, const char *new) { return rename_common(AT_FDCWD, old, AT_FDCWD, new, 0); } -int symlinkat(const char *target, int dirfd, const char *path) { - REAL(symlinkat, int (*)(const char *, int, const char *)); +static long fp_symlink(const char *target, int dirfd, const char *path) { struct res r; long rs = trailing_slash(path) ? FP_SLOW : resolve(dirfd, path, MAY_BE_UNSEEN, &r); if (rs == 0 || rs == 1 || rs == UNSEEN) { rs = sc(SYS_symlinkat, (long)target, AT_FDCWD, (long)r.host, 0, 0); if (rs == 0) forget(); } - ANSWER(rs, dirfd, path, real_symlinkat(target, dirfd, path)); + return rs; +} +int symlinkat(const char *target, int dirfd, const char *path) { + REAL(symlinkat, int (*)(const char *, int, const char *)); + ANSWER(fp_symlink(target, dirfd, path), dirfd, path, real_symlinkat(target, dirfd, path)); } int symlink(const char *target, const char *path) { return symlinkat(target, AT_FDCWD, path); } -int fchmodat(int dirfd, const char *path, mode_t mode, int flags) { - REAL(fchmodat, int (*)(int, const char *, mode_t, int)); +static long fp_chmod(int dirfd, const char *path, mode_t mode, int flags) { struct res r; long rs = flags ? FP_SLOW : resolve(dirfd, path, 1 | MAY_BE_UNSEEN, &r); if (rs == 1) rs = -ENOENT; if (rs == 0 || rs == UNSEEN) rs = sc(SYS_fchmodat, AT_FDCWD, (long)r.host, mode, 0, 0); - ANSWER(rs, dirfd, path, real_fchmodat(dirfd, path, mode, flags)); + return rs; +} +int fchmodat(int dirfd, const char *path, mode_t mode, int flags) { + REAL(fchmodat, int (*)(int, const char *, mode_t, int)); + ANSWER(fp_chmod(dirfd, path, mode, flags), dirfd, path, real_fchmodat(dirfd, path, mode, flags)); } int chmod(const char *path, mode_t mode) { return fchmodat(AT_FDCWD, path, mode, 0); } -static int utimens_common(int dirfd, const char *path, const struct timespec times[2], int flags) { - REAL(utimensat, int (*)(int, const char *, const struct timespec[2], int)); +static long fp_utimens(int dirfd, const char *path, const struct timespec times[2], int flags) { struct res r; long rs; if (!path && fp_ready()) { @@ -1197,7 +1213,11 @@ static int utimens_common(int dirfd, const char *path, const struct timespec tim if (rs == 1) rs = -ENOENT; if (rs == 0 || rs == UNSEEN) rs = sc(SYS_utimensat, AT_FDCWD, (long)r.host, (long)times, flags, 0); } - ANSWER(rs, dirfd, path, real_utimensat(dirfd, path, times, flags)); + return rs; +} +static int utimens_common(int dirfd, const char *path, const struct timespec times[2], int flags) { + REAL(utimensat, int (*)(int, const char *, const struct timespec[2], int)); + ANSWER(fp_utimens(dirfd, path, times, flags), dirfd, path, real_utimensat(dirfd, path, times, flags)); } int utimensat(int dirfd, const char *path, const struct timespec times[2], int flags) { return utimens_common(dirfd, path, times, flags); @@ -1224,23 +1244,29 @@ int utime(const char *path, const struct utimbuf *times) { return utimens_common(AT_FDCWD, path, ts, 0); } -int truncate(const char *path, off_t length) { - REAL(truncate, int (*)(const char *, off_t)); +static long fp_truncate(const char *path, off_t length) { struct res r; long rs = resolve(AT_FDCWD, path, 1 | MAY_BE_UNSEEN, &r); if (rs == 1) rs = -ENOENT; if (rs == 0 || rs == UNSEEN) rs = sc(SYS_truncate, (long)r.host, length, 0, 0, 0); - ANSWER(rs, AT_FDCWD, path, real_truncate(path, length)); + return rs; +} +int truncate(const char *path, off_t length) { + REAL(truncate, int (*)(const char *, off_t)); + ANSWER(fp_truncate(path, length), AT_FDCWD, path, real_truncate(path, length)); } int truncate64(const char *path, off_t length) __attribute__((alias("truncate"))); -static int statfs_common(const char *path, struct statfs *buf, int (*real)(const char *, struct statfs *)) { +static long fp_statfs(const char *path, struct statfs *buf) { struct res r; long rs = resolve(AT_FDCWD, path, 1, &r); if (rs == 1) rs = -ENOENT; if (rs == 0 && under(r.guest, "/dev/shm", 8)) rs = FP_SLOW; if (rs == 0) rs = sc(SYS_statfs, (long)r.host, (long)buf, 0, 0, 0); - ANSWER(rs, AT_FDCWD, path, real(path, buf)); + return rs; +} +static int statfs_common(const char *path, struct statfs *buf, int (*real)(const char *, struct statfs *)) { + ANSWER(fp_statfs(path, buf), AT_FDCWD, path, real(path, buf)); } int statfs(const char *path, struct statfs *buf) { REAL(statfs, int (*)(const char *, struct statfs *)); @@ -1288,12 +1314,16 @@ int statvfs64(const char *path, struct statvfs64 *buf) { return statvfs_common(path, (struct statvfs *)buf, real_statvfs64); } -static ssize_t getxattr_common(const char *path, const char *name, void *value, size_t size, int follow, - ssize_t (*real)(const char *, const char *, void *, size_t)) { +static long fp_getxattr(const char *path, const char *name, void *value, size_t size, int follow) { struct res r; long rs = resolve(AT_FDCWD, path, follow | MAY_BE_UNSEEN, &r); if (rs == 1) rs = -ENOENT; if (rs == 0 || rs == UNSEEN) rs = sc(follow ? SYS_getxattr : SYS_lgetxattr, (long)r.host, (long)name, (long)value, size, 0); + return rs; +} +static ssize_t getxattr_common(const char *path, const char *name, void *value, size_t size, int follow, + ssize_t (*real)(const char *, const char *, void *, size_t)) { + long rs = fp_getxattr(path, name, value, size, follow); if (rs != FP_SLOW) { hits++; return rs < 0 ? ret(rs) : rs; } missed(__func__, AT_FDCWD, path); return real(path, name, value, size); @@ -1307,12 +1337,16 @@ ssize_t lgetxattr(const char *path, const char *name, void *value, size_t size) return getxattr_common(path, name, value, size, 0, real_lgetxattr); } -static ssize_t listxattr_common(const char *path, char *list, size_t size, int follow, - ssize_t (*real)(const char *, char *, size_t)) { +static long fp_listxattr(const char *path, char *list, size_t size, int follow) { struct res r; long rs = resolve(AT_FDCWD, path, follow | MAY_BE_UNSEEN, &r); if (rs == 1) rs = -ENOENT; if (rs == 0 || rs == UNSEEN) rs = sc(follow ? SYS_listxattr : SYS_llistxattr, (long)r.host, (long)list, size, 0, 0); + return rs; +} +static ssize_t listxattr_common(const char *path, char *list, size_t size, int follow, + ssize_t (*real)(const char *, char *, size_t)) { + long rs = fp_listxattr(path, list, size, follow); if (rs != FP_SLOW) { hits++; return rs < 0 ? ret(rs) : rs; } missed(__func__, AT_FDCWD, path); return real(path, list, size); @@ -1326,13 +1360,15 @@ ssize_t llistxattr(const char *path, char *list, size_t size) { return listxattr_common(path, list, size, 0, real_llistxattr); } -int inotify_add_watch(int fd, const char *path, uint32_t mask) { - REAL(inotify_add_watch, int (*)(int, const char *, uint32_t)); +static long fp_inotify(int fd, const char *path, uint32_t mask) { struct res r; long rs = resolve(AT_FDCWD, path, (mask & IN_DONT_FOLLOW ? 0 : 1) | MAY_BE_UNSEEN, &r); - if (rs == 1) rs = -ENOENT; - if (rs == 0 || rs == UNSEEN) rs = sc(SYS_inotify_add_watch, fd, (long)r.host, mask, 0, 0); - ANSWER(rs, AT_FDCWD, path, real_inotify_add_watch(fd, path, mask)); + if (rs == 0 || rs == 1 || rs == UNSEEN) rs = sc(SYS_inotify_add_watch, fd, (long)r.host, mask, 0, 0); + return rs; +} +int inotify_add_watch(int fd, const char *path, uint32_t mask) { + REAL(inotify_add_watch, int (*)(int, const char *, uint32_t)); + ANSWER(fp_inotify(fd, path, mask), AT_FDCWD, path, real_inotify_add_watch(fd, path, mask)); } diff --git a/tools/proot/fastpath/svc-cache.h b/tools/proot/fastpath/svc-cache.h new file mode 100644 index 000000000..4a6f49b80 --- /dev/null +++ b/tools/proot/fastpath/svc-cache.h @@ -0,0 +1,90 @@ +struct svc_cached_sites { + uint64_t magic; + uint32_t api, count; + uint64_t offset[256]; +}; +static unsigned svc_cache_hits, svc_cache_misses; + +static int svc_segment(const struct dl_phdr_info *info, uint64_t at, size_t length, unsigned flags) { + for (int i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *p = &info->dlpi_phdr[i]; + if (p->p_type == PT_LOAD && (p->p_flags & flags) == flags && + at >= p->p_vaddr && at - p->p_vaddr <= p->p_memsz && + length <= p->p_memsz - (at - p->p_vaddr)) return i + 1; + } + return 0; +} + +static int svc_cache_name(const struct dl_phdr_info *info, char *path, size_t size) { + const char *enabled = getenv("PROOT_FP_SVC_CACHE"); + if (enabled && strcmp(enabled, "1")) return 0; + for (int i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *p = &info->dlpi_phdr[i]; + if (p->p_type != PT_NOTE || !svc_segment(info, p->p_vaddr, p->p_memsz, PF_R)) continue; + const unsigned char *note = (const unsigned char *)(info->dlpi_addr + p->p_vaddr); + for (size_t at = 0; p->p_memsz - at >= sizeof(ElfW(Nhdr));) { + ElfW(Nhdr) h; + memcpy(&h, note + at, sizeof h); + at += sizeof h; + size_t name = ((size_t)h.n_namesz + 3) & ~(size_t)3; + size_t desc = ((size_t)h.n_descsz + 3) & ~(size_t)3; + if (name > p->p_memsz - at || desc > p->p_memsz - at - name) break; + if (h.n_type == NT_GNU_BUILD_ID && h.n_namesz == 4 && !memcmp(note + at, "GNU", 4) && + h.n_descsz && h.n_descsz <= 64) { + char id[129]; + static const char hex[] = "0123456789abcdef"; + for (size_t j = 0; j < h.n_descsz; j++) { + unsigned char b = note[at + name + j]; + id[2 * j] = hex[b >> 4]; + id[2 * j + 1] = hex[b & 15]; + } + id[2 * h.n_descsz] = 0; + int n = snprintf(path, size, "/tmp/.droiddeck-svc-v1-%d-%s", svc_android_api, id); + return n > 0 && (size_t)n < size; + } + at += name + desc; + } + } + return 0; +} + +static int svc_cache_read(const struct dl_phdr_info *info, const char *path) { + long fd = fp_open(AT_FDCWD, path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK, 0); + if (fd < 0) return 0; + struct stat st; + struct svc_cached_sites table; + int valid = sc(SYS_fstat, fd, (long)&st, 0, 0, 0) == 0 && S_ISREG(st.st_mode) && + st.st_uid == (uid_t)sc(SYS_getuid, 0, 0, 0, 0, 0) && !(st.st_mode & 0077) && + st.st_size == sizeof table && sc(SYS_read, fd, (long)&table, sizeof table, 0, 0) == sizeof table; + sc(SYS_close, fd, 0, 0, 0, 0); + if (!valid || table.magic != 0x314356534444ULL || table.api != (unsigned)svc_android_api || + !table.count || table.count > 256 || table.count > 256 - svc_planned) return 0; + for (unsigned i = 0; i < table.count; i++) { + uint64_t offset = table.offset[i]; + int segment = svc_segment(info, offset, 4, PF_R | PF_X); + if ((offset & 3) || (i && offset <= table.offset[i - 1]) || !segment) return 0; + const ElfW(Phdr) *p = &info->dlpi_phdr[segment - 1]; + const uint32_t *text = (const uint32_t *)(info->dlpi_addr + p->p_vaddr); + size_t at = (offset - p->p_vaddr) / 4; + if (text[at] != 0xd4000001U || !svc_supported(svc_site_number(text, at))) return 0; + } + for (unsigned i = 0; i < table.count; i++) + svc_plan[svc_planned++].site = (uint32_t *)(info->dlpi_addr + table.offset[i]); + return 1; +} + +static void svc_cache_write(const struct dl_phdr_info *info, const char *path, size_t first) { + if (svc_planned == first || svc_planned == 256) return; + struct svc_cached_sites table = {.magic = 0x314356534444ULL, .api = svc_android_api, .count = svc_planned - first}; + for (unsigned i = 0; i < table.count; i++) + table.offset[i] = (uintptr_t)svc_plan[first + i].site - info->dlpi_addr; + char temporary[256]; + int n = snprintf(temporary, sizeof temporary, "%s.part-%d", path, getpid()); + if (n <= 0 || n >= (int)sizeof temporary) return; + long fd = fp_open(AT_FDCWD, temporary, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd < 0) return; + long written = sc(SYS_write, fd, (long)&table, sizeof table, 0, 0); + sc(SYS_close, fd, 0, 0, 0, 0); + if (written == sizeof table) fp_rename(AT_FDCWD, temporary, AT_FDCWD, path, 0); + fp_unlink(AT_FDCWD, temporary, 0); +} diff --git a/tools/proot/fastpath/svc-dispatch.h b/tools/proot/fastpath/svc-dispatch.h new file mode 100644 index 000000000..04afa1577 --- /dev/null +++ b/tools/proot/fastpath/svc-dispatch.h @@ -0,0 +1,139 @@ +static int svc_android_api; +static __thread struct { long head, tid; } svc_robust __attribute__((tls_model("initial-exec"))); + +static int svc_host_android_api(void) { + const char *value = getenv("PROOT_FP_ANDROID_API"); + if (!value || !*value) return 0; + char *end; + long api = strtol(value, &end, 10); + return !*end && api > 0 && api < 1000 ? (int)api : 0; +} + +static long svc_memory(void *local, long remote, size_t size, int write) { + if (!size) return 0; + struct iovec here = {local, size}, there = {(void *)remote, size}; + return sc(write ? SYS_process_vm_writev : SYS_process_vm_readv, getpid(), + (long)&here, 1, (long)&there, 1); +} + +static int svc_path(long pointer, char *path) { + long n = svc_memory(path, pointer, PATH_MAX, 0); + return n > 0 && memchr(path, 0, (size_t)n) != NULL; +} + +static long svc_output(long result, void *value, long out, size_t size) { + if (result < 0) return result; + return svc_memory(value, out, size, 1) == (long)size ? result : FP_SLOW; +} + +static int svc_supported(int nr) { + switch (nr) { + case SYS_openat: case SYS_newfstatat: case SYS_statx: case SYS_readlinkat: + case SYS_faccessat: case SYS_mkdirat: case SYS_unlinkat: case SYS_renameat: + case SYS_renameat2: case SYS_symlinkat: case SYS_fchmodat: case SYS_utimensat: + case SYS_truncate: case SYS_statfs: case SYS_getxattr: case SYS_lgetxattr: + case SYS_listxattr: case SYS_llistxattr: case SYS_inotify_add_watch: case SYS_getcwd: + return 1; + case SYS_set_robust_list: case SYS_get_robust_list: case SYS_faccessat2: + return svc_android_api > 0; + case SYS_clone3: + return svc_android_api > 0 && svc_android_api < 35; + default: return 0; + } +} + +static long svc_blocked(const long *r) { + if (!svc_android_api) return FP_SLOW; + if (r[8] == SYS_faccessat2 || (r[8] == SYS_clone3 && svc_android_api < 35)) return -ENOSYS; + if (r[8] == SYS_set_robust_list) { + svc_robust.head = r[0]; + svc_robust.tid = sc(SYS_gettid, 0, 0, 0, 0, 0); + return -ENOSYS; + } + if (r[8] == SYS_get_robust_list) { + long tid = sc(SYS_gettid, 0, 0, 0, 0, 0); + if ((r[0] && r[0] != tid) || svc_robust.tid != tid || !svc_robust.head) return FP_SLOW; + size_t size = 3 * sizeof(long); + if (svc_memory(&svc_robust.head, r[1], sizeof(long), 1) != sizeof(long) || + svc_memory(&size, r[2], sizeof size, 1) != sizeof size) return -EFAULT; + return 0; + } + return FP_SLOW; +} + +static long svc_path_call(const long *r) { + char path[PATH_MAX], other[PATH_MAX]; + struct timespec times[2]; + const struct timespec *when = (const struct timespec *)r[2]; + int first = 1, second = -1; + switch (r[8]) { + case SYS_getcwd: { + if (own_cwd(path) == FP_SLOW) return FP_SLOW; + size_t n = strlen(path) + 1; + if ((unsigned long)r[1] < n) return -ERANGE; + return svc_output(n, path, r[0], n); + } + case SYS_truncate: case SYS_statfs: case SYS_getxattr: case SYS_lgetxattr: + case SYS_listxattr: case SYS_llistxattr: first = 0; break; + case SYS_renameat: case SYS_renameat2: second = 3; break; + case SYS_symlinkat: first = 0; second = 2; break; + case SYS_utimensat: + if (when) { + if (svc_memory(times, r[2], sizeof times, 0) != sizeof times) return FP_SLOW; + for (int i = 0; i < 2; i++) + if (times[i].tv_nsec != UTIME_NOW && times[i].tv_nsec != UTIME_OMIT && + (times[i].tv_nsec < 0 || times[i].tv_nsec >= 1000000000L)) return FP_SLOW; + when = times; + } + if (!r[1]) return fp_utimens((int)r[0], NULL, when, (int)r[3]); + break; + case SYS_inotify_add_watch: { + char link[64], target[64]; + snprintf(link, sizeof link, "/proc/self/fd/%d", (int)r[0]); + long n = sc(SYS_readlinkat, AT_FDCWD, (long)link, (long)target, sizeof target - 1, 0); + if (n <= 0) return FP_SLOW; + target[n] = 0; + if (strcmp(target, "anon_inode:inotify")) return FP_SLOW; + break; + } + default: + if (!svc_supported((int)r[8])) return FP_SLOW; + } + if (!svc_path(r[first], path) || (second >= 0 && !svc_path(r[second], other))) return FP_SLOW; + switch (r[8]) { + case SYS_openat: return fp_open((int)r[0], path, (int)r[2], (mode_t)r[3]); + case SYS_newfstatat: { + struct stat st; + return svc_output(fp_stat((int)r[0], path, (int)r[3], &st), &st, r[2], sizeof st); + } + case SYS_statx: { + struct statx st; + return svc_output(fp_statx((int)r[0], path, (int)r[2], (unsigned)r[3], &st), &st, r[4], sizeof st); + } + case SYS_readlinkat: { + char text[PATH_MAX]; + size_t written = 0; + long result = fp_readlink((int)r[0], path, text, (size_t)r[3], &written); + return svc_output(result, text, r[2], written); + } + case SYS_faccessat: return fp_access((int)r[0], path, (int)r[2], 0); + case SYS_mkdirat: return fp_mkdir((int)r[0], path, (mode_t)r[2]); + case SYS_unlinkat: return fp_unlink((int)r[0], path, (int)r[2]); + case SYS_renameat: case SYS_renameat2: + return fp_rename((int)r[0], path, (int)r[2], other, r[8] == SYS_renameat2 ? (unsigned)r[4] : 0); + case SYS_symlinkat: return fp_symlink(path, (int)r[1], other); + case SYS_fchmodat: return fp_chmod((int)r[0], path, (mode_t)r[2], 0); + case SYS_utimensat: return fp_utimens((int)r[0], path, when, (int)r[3]); + case SYS_truncate: return fp_truncate(path, (off_t)r[1]); + case SYS_statfs: { + struct statfs st; + return svc_output(fp_statfs(path, &st), &st, r[1], sizeof st); + } + case SYS_getxattr: case SYS_lgetxattr: + return fp_getxattr(path, (const char *)r[1], (void *)r[2], (size_t)r[3], r[8] == SYS_getxattr); + case SYS_listxattr: case SYS_llistxattr: + return fp_listxattr(path, (char *)r[1], (size_t)r[2], r[8] == SYS_listxattr); + case SYS_inotify_add_watch: return fp_inotify((int)r[0], path, (uint32_t)r[2]); + default: return FP_SLOW; + } +} diff --git a/tools/proot/fastpath/svc-patch.h b/tools/proot/fastpath/svc-patch.h index fde1c2511..dbd4488f0 100644 --- a/tools/proot/fastpath/svc-patch.h +++ b/tools/proot/fastpath/svc-patch.h @@ -7,17 +7,23 @@ static inline int svc_number(uint32_t insn) { static inline int svc_argument_instruction(uint32_t insn) { if (insn == 0xd503201fU) return 1; - if ((insn & 0xffc003e0U) == 0x290003e0U || - (insn & 0xffc003e0U) == 0xa90003e0U) return 1; + if ((insn & 0xffc00000U) == 0x29000000U || + (insn & 0xffc00000U) == 0xa9000000U || + (insn & 0xffc00000U) == 0xb9000000U || + (insn & 0xffc00000U) == 0xf9000000U) return 1; if ((insn & 31) == 8) return 0; return (insn & 0xffe0ffe0U) == 0xaa0003e0U || (insn & 0xffe0ffe0U) == 0x2a0003e0U || (insn & 0xfffffc00U) == 0x93407c00U || (insn & 0x7f800000U) == 0x52800000U || (insn & 0x7f800000U) == 0x12800000U || - (insn & 0xffc003e0U) == 0xb94003e0U || - (insn & 0xffc003e0U) == 0xf94003e0U || - (insn & 0xffc003e0U) == 0xb98003e0U; + (insn & 0xffc00000U) == 0xb9400000U || + (insn & 0xffc00000U) == 0xf9400000U || + (insn & 0xffc00000U) == 0xb9800000U || + (insn & 0xff000000U) == 0x91000000U || + (insn & 0xff000000U) == 0xd1000000U || + (insn & 0xff000000U) == 0x11000000U || + (insn & 0xff000000U) == 0x51000000U; } static inline int svc_site_number(const uint32_t *text, size_t at) { @@ -40,10 +46,14 @@ static inline int svc_branch(uintptr_t from, uintptr_t to, uint32_t *out) { #if defined(__aarch64__) && !defined(FP_SVC_ENCODINGS_ONLY) #include #include +#include static __thread int svc_busy __attribute__((tls_model("initial-exec"))); static unsigned svc_sites, svc_pages; static unsigned long svc_fast, svc_slow; +static unsigned long long svc_scan_ns, svc_patch_ns; + +#include "svc-dispatch.h" static long svc_original(const long *r) { register long x0 __asm__("x0") = r[0]; @@ -60,20 +70,16 @@ static long svc_original(const long *r) { __attribute__((used, visibility("hidden"))) long fp_svc_dispatch(const long *r) { int saved_errno = errno; - long result = FP_SLOW; - if (!svc_busy) { + long result = svc_blocked(r); + if (result == FP_SLOW && !svc_busy) { svc_busy = 1; - if (r[8] == SYS_openat) - result = fp_open((int)r[0], (const char *)r[1], (int)r[2], (mode_t)r[3]); - else if (r[8] == SYS_newfstatat && r[2]) - result = fp_stat((int)r[0], (const char *)r[1], (int)r[3], (struct stat *)r[2]); - if (result != FP_SLOW) __atomic_fetch_add(&svc_fast, 1, __ATOMIC_RELAXED); + result = svc_path_call(r); svc_busy = 0; } if (result == FP_SLOW) { __atomic_fetch_add(&svc_slow, 1, __ATOMIC_RELAXED); result = svc_original(r); - } + } else __atomic_fetch_add(&svc_fast, 1, __ATOMIC_RELAXED); errno = saved_errno; return result; } @@ -141,12 +147,19 @@ struct svc_site { uint32_t *site; uint32_t branch; }; static struct svc_site svc_plan[256]; static size_t svc_planned; +#include "svc-cache.h" + static int svc_object(struct dl_phdr_info *info, size_t size, void *unused) { (void)size; (void)unused; const char *name = strrchr(info->dlpi_name, '/'); name = name ? name + 1 : info->dlpi_name; if (strcmp(name, "libc.so.6") && strcmp(name, "ld-linux-aarch64.so.1")) return 0; + char cache[224]; + int cached = svc_cache_name(info, cache, sizeof cache); + if (cached && svc_cache_read(info, cache)) { svc_cache_hits++; return 0; } + if (cached) svc_cache_misses++; + size_t first = svc_planned; for (int i = 0; i < info->dlpi_phnum; i++) { const ElfW(Phdr) *ph = &info->dlpi_phdr[i]; if (ph->p_type != PT_LOAD || !(ph->p_flags & PF_X)) continue; @@ -154,10 +167,11 @@ static int svc_object(struct dl_phdr_info *info, size_t size, void *unused) { for (size_t j = 1; j < ph->p_memsz / 4 && svc_planned < 256; j++) { if (text[j] != 0xd4000001U) continue; int nr = svc_site_number(text, j); - if (nr != SYS_openat && nr != SYS_newfstatat) continue; + if (!svc_supported(nr)) continue; svc_plan[svc_planned++].site = &text[j]; } } + if (cached) svc_cache_write(info, cache, first); return 0; } @@ -199,12 +213,20 @@ done: } static void svc_stats(void) { - char line[160]; - int n = snprintf(line, sizeof line, "fastpath-svc[%d]: %u sites, %u relay pages, %lu fast, %lu to proot\n", - getpid(), svc_sites, svc_pages, svc_fast, svc_slow); - if (write(2, line, n) < 0) {} + char line[256]; + int n = snprintf(line, sizeof line, "fastpath-svc[%d]: %u sites, %u relay pages, %lu fast, %lu to proot; scan %llu us, patch %llu us, android %d, cache %u hit %u miss\n", + getpid(), svc_sites, svc_pages, svc_fast, svc_slow, svc_scan_ns / 1000, svc_patch_ns / 1000, svc_android_api, svc_cache_hits, svc_cache_misses); + if (n > 0 && write(2, line, n < (int)sizeof line ? n : (int)sizeof line - 1) < 0) {} } +static unsigned long long svc_clock(void) { + struct timespec t; + if (sc(SYS_clock_gettime, CLOCK_MONOTONIC, (long)&t, 0, 0, 0) < 0) return 0; + return (unsigned long long)t.tv_sec * 1000000000ULL + t.tv_nsec; +} + +static void svc_child(void) { svc_busy = 0; } + __attribute__((constructor)) static void svc_patch_init(void) { const char *enabled = getenv("PROOT_FP_PATCH_SVC"); if ((enabled && strcmp(enabled, "1")) || !fp_ready()) return; @@ -213,11 +235,16 @@ __attribute__((constructor)) static void svc_patch_init(void) { if (getauxval(AT_HWCAP2) & HWCAP2_SME) return; #endif if (!svc_single_thread()) return; + if (pthread_atfork(NULL, NULL, svc_child)) return; + svc_android_api = svc_host_android_api(); size_t page = (size_t)sysconf(_SC_PAGESIZE); if (page < 4096 || (page & (page - 1))) return; int mem = (int)sc(SYS_openat, AT_FDCWD, (long)"/proc/self/mem", O_RDWR | O_CLOEXEC, 0, 0); if (mem < 0) return; + unsigned long long start = svc_clock(); dl_iterate_phdr(svc_object, NULL); + svc_scan_ns = svc_clock() - start; + start = svc_clock(); for (size_t first = 0; first < svc_planned;) { uint32_t *relay = svc_relay_page((uintptr_t)svc_plan[first].site, page); if (!relay) { first++; continue; } @@ -245,6 +272,7 @@ __attribute__((constructor)) static void svc_patch_init(void) { first = end; } sc(SYS_close, mem, 0, 0, 0, 0); + svc_patch_ns = svc_clock() - start; if (getenv("PROOT_FP_STATS")) atexit(svc_stats); } #endif diff --git a/tools/tests/test_fastpath_svc.py b/tools/tests/test_fastpath_svc.py index 1f76a3465..a0d84e924 100644 --- a/tools/tests/test_fastpath_svc.py +++ b/tools/tests/test_fastpath_svc.py @@ -33,6 +33,9 @@ int main(void) { const uint32_t branch[] = {0xd2800708, 0x14000001, 0xd4000001}; const uint32_t call[] = {0xd2800708, 0x94000001, 0xd4000001}; const uint32_t shift[] = {0xd2800708, 0xd2a00708, 0xd4000001}; + const uint32_t robust[] = {0xd2800c68, 0xf9000260, 0xa9000261, 0xf9400260, 0x9100e260, 0xd4000001}; + const uint32_t writeback[] = {0xd2800708, 0xa9bf07e0, 0xd4000001}; + const uint32_t added[] = {0xd2800708, 0x91000408, 0xd4000001}; assert(svc_site_number(create, 3) == 56); assert(svc_site_number(create_at, 3) == 56); assert(svc_site_number(xstat, 4) == 79); @@ -41,6 +44,9 @@ int main(void) { assert(svc_site_number(branch, 2) == -1); assert(svc_site_number(call, 2) == -1); assert(svc_site_number(shift, 2) == -1); + assert(svc_site_number(robust, 5) == 99); + assert(svc_site_number(writeback, 2) == -1); + assert(svc_site_number(added, 2) == -1); uint32_t out; uintptr_t pc = 0x10000000; assert(svc_branch(pc, pc + 4, &out) && out == 0x14000001); @@ -60,3 +66,100 @@ int main(void) { output = work / "check" subprocess.run(["cc", "-O2", "-Wall", "-Wextra", "-Werror", str(program), "-o", str(output)], check=True) subprocess.run([str(output)], check=True, timeout=10) + + def test_cache_revalidates_image_offsets_and_untrusted_files(self): + if shutil.which("cc") is None: + self.skipTest("cc is not available") + source = r''' +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define FP_SVC_ENCODINGS_ONLY +#include "HEADER" +static int svc_android_api = 33; +static struct { uint32_t *site; } svc_plan[256]; +static size_t svc_planned; +static int svc_supported(int nr) { return nr == 56 || nr == 79; } +#define sc(n,a,b,c,d,e) syscall(n,a,b,c,d,e) +static long fp_open(int d, const char *p, int f, mode_t m) { return openat(d,p,f,m); } +static long fp_rename(int d, const char *p, int e, const char *q, int f) { (void)f; return renameat(d,p,e,q); } +static long fp_unlink(int d, const char *p, int f) { return unlinkat(d,p,f); } +#include "CACHE" +static void store(const char *path, struct svc_cached_sites *table) { + int fd = open(path, O_WRONLY | O_TRUNC); + assert(fd >= 0 && write(fd, table, sizeof *table) == sizeof *table && close(fd) == 0); +} +int main(void) { + uint32_t image[32] = {0xd2800708, 0xd4000001, 0xd28009e8, 0xd4000001}; + ElfW(Phdr) ph[] = {{.p_type=PT_LOAD, .p_flags=PF_R|PF_X, .p_memsz=sizeof image}, + {.p_type=PT_NOTE, .p_vaddr=64, .p_memsz=24}}; + struct dl_phdr_info info = {.dlpi_addr=(uintptr_t)image, .dlpi_phdr=ph, .dlpi_phnum=2}; + ElfW(Nhdr) note = {.n_type=NT_GNU_BUILD_ID, .n_namesz=4, .n_descsz=8}; + memcpy((char *)image+64, ¬e, sizeof note); + memcpy((char *)image+76, "GNU", 4); + uint64_t id = getpid(); + memcpy((char *)image+80, &id, 8); + char path[256], other[256]; + assert(svc_cache_name(&info, path, sizeof path)); + svc_android_api = 35; + assert(svc_cache_name(&info, other, sizeof other) && strcmp(path, other)); + svc_android_api = 33; + setenv("PROOT_FP_SVC_CACHE", "0", 1); + assert(!svc_cache_name(&info, other, sizeof other)); + unsetenv("PROOT_FP_SVC_CACHE"); + ph[1].p_memsz = sizeof image; + assert(!svc_cache_name(&info, other, sizeof other)); + ph[1].p_memsz = 24; + svc_plan[0].site = image+1; svc_plan[1].site = image+3; svc_planned=2; + svc_cache_write(&info, path, 0); + svc_planned=0; + assert(svc_cache_read(&info, path) && svc_planned==2 && svc_plan[1].site==image+3); + struct svc_cached_sites valid, table; + int fd = open(path, O_RDONLY); + assert(fd >= 0 && read(fd, &valid, sizeof valid)==sizeof valid && close(fd)==0); +#define REJECT(change) do { table=valid; change; store(path,&table); svc_planned=0; \ + assert(!svc_cache_read(&info,path) && svc_planned==0); } while(0) + REJECT(table.magic++); + REJECT(table.api=35); + REJECT(table.count=0); + REJECT(table.count=257); + REJECT(table.offset[0]=3); + REJECT(table.offset[0]=sizeof image); + REJECT(table.offset[0]=UINT64_MAX-3); + REJECT(table.offset[1]=table.offset[0]); + REJECT(table.offset[0]=12; table.offset[1]=4); + store(path,&valid); + image[1]=0x14000001; + assert(!svc_cache_read(&info,path)); + image[1]=0xd4000001; image[0]=0xd2800008; + assert(!svc_cache_read(&info,path)); + image[0]=0xd2800708; + assert(chmod(path,0644)==0 && !svc_cache_read(&info,path)); + assert(chmod(path,0600)==0 && truncate(path,8)==0 && !svc_cache_read(&info,path)); + assert(unlink(path)==0 && symlink("/dev/zero",path)==0 && !svc_cache_read(&info,path)); + assert(unlink(path)==0 && mkdir(path,0700)==0 && !svc_cache_read(&info,path)); + assert(rmdir(path)==0); + svc_planned=0; + svc_cache_write(&info,path,0); + assert(access(path,F_OK)<0); + (void)svc_cache_hits; (void)svc_cache_misses; + return 0; +} +''' + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + program = work / "cache.c" + program.write_text(source.replace('"HEADER"', '"' + HEADER.as_posix() + '"') + .replace('"CACHE"', '"' + HEADER.with_name("svc-cache.h").as_posix() + '"')) + output = work / "cache" + subprocess.run(["cc", "-O2", "-Wall", "-Wextra", "-Werror", str(program), "-o", str(output)], check=True) + subprocess.run([str(output)], check=True, timeout=10) From 9a8f54dee7b847d3b2e8c843469274d5d79933e8 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:31:52 -0400 Subject: [PATCH 4/9] fix(proot): preserve cleared robust-list state --- tools/proot/bench/svc-check.c | 27 +++++++++++++++++ tools/proot/bench/svc-threads.c | 47 +++++++++++++++++++++++++++++ tools/proot/fastpath/svc-dispatch.h | 3 +- 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tools/proot/bench/svc-threads.c diff --git a/tools/proot/bench/svc-check.c b/tools/proot/bench/svc-check.c index 80040b61b..59f46ff86 100644 --- a/tools/proot/bench/svc-check.c +++ b/tools/proot/bench/svc-check.c @@ -147,6 +147,8 @@ static void robust_checks(void) { assert(raw(SYS_get_robust_list, gettid(), (long)&got, (long)&size, 0, 0, 0) == 0); assert(got == (long)&head && size == sizeof head); assert(raw(SYS_get_robust_list, 0, 1, (long)&size, 0, 0, 0) == -EFAULT); + assert(raw(SYS_set_robust_list, 0, sizeof head, 0, 0, 0, 0) == -ENOSYS); + assert(raw(SYS_get_robust_list, 0, (long)&got, (long)&size, 0, 0, 0) == -ENOSYS); assert(raw(SYS_set_robust_list, original, sizeof head, 0, 0, 0, 0) == -ENOSYS); assert(raw(SYS_faccessat2, AT_FDCWD, (long)"/usr/lib/libc.so.6", R_OK, 0, 0, 0) == -ENOSYS); if (svc_android_api < 35) assert(raw(SYS_clone3, 0, 0, 0, 0, 0, 0) == -ENOSYS); @@ -219,6 +221,18 @@ static void *cancel_worker(void *unused) { } } +struct cancel_open { const char *path; int (*open)(const char *, int, ...); int ready, cleaned; }; +static void cancel_cleanup(void *arg) { ((struct cancel_open *)arg)->cleaned = 1; } +static void *cancel_fifo(void *arg) { + struct cancel_open *test = arg; + pthread_cleanup_push(cancel_cleanup, test); + __atomic_store_n(&test->ready, 1, __ATOMIC_RELEASE); + int fd = test->open(test->path, O_RDONLY); + if (fd >= 0) close(fd); + pthread_cleanup_pop(1); + return NULL; +} + static void signal_checks(void) { stack_t old_stack, stack = {.ss_sp = malloc(128 * 1024), .ss_size = 128 * 1024}; assert(stack.ss_sp && sigaltstack(&stack, &old_stack) == 0); @@ -242,6 +256,19 @@ static void signal_checks(void) { void *result; assert(pthread_create(&thread, NULL, cancel_worker, NULL) == 0); assert(pthread_cancel(thread) == 0 && pthread_join(thread, &result) == 0 && result == PTHREAD_CANCELED); + char fifo[] = "/tmp/proot-expanded-fifo-XXXXXX"; + int fd = mkstemp(fifo); + assert(fd >= 0 && close(fd) == 0 && unlink(fifo) == 0 && mkfifo(fifo, 0600) == 0); + void *libc = dlopen("libc.so.6", RTLD_NOW); + struct cancel_open test = {.path = fifo, .open = dlsym(libc, "open")}; + assert(test.open && pthread_create(&thread, NULL, cancel_fifo, &test) == 0); + while (!__atomic_load_n(&test.ready, __ATOMIC_ACQUIRE)) sched_yield(); + struct timespec deadline = {0, 20000000}; + nanosleep(&deadline, NULL); + assert(pthread_cancel(thread) == 0 && clock_gettime(CLOCK_REALTIME, &deadline) == 0); + deadline.tv_sec += 2; + assert(pthread_timedjoin_np(thread, &result, &deadline) == 0 && result == PTHREAD_CANCELED && test.cleaned); + assert(unlink(fifo) == 0 && dlclose(libc) == 0); for (int i = 0; i < 32; i++) { assert(pthread_create(&thread, NULL, thread_check, NULL) == 0); assert(pthread_join(thread, NULL) == 0); diff --git a/tools/proot/bench/svc-threads.c b/tools/proot/bench/svc-threads.c new file mode 100644 index 000000000..88988bd2e --- /dev/null +++ b/tools/proot/bench/svc-threads.c @@ -0,0 +1,47 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +static double now(void) { + struct timespec t; + assert(clock_gettime(CLOCK_MONOTONIC, &t) == 0); + return t.tv_sec * 1e9 + t.tv_nsec; +} + +static void *thread(void *arg) { return arg; } + +int main(void) { + void *libc = dlopen("libc.so.6", RTLD_NOW); + assert(libc); + int (*access)(int, const char *, int, int) = dlsym(libc, "faccessat"); + char *(*cwd)(char *, size_t) = dlsym(libc, "getcwd"); + assert(access && cwd); + struct rusage before, after; + assert(getrusage(RUSAGE_SELF, &before) == 0); + const int threads = 128, calls = 2000; + double start = now(); + for (int i = 0; i < threads; i++) { + pthread_t id; + void *value; + assert(pthread_create(&id, NULL, thread, (void *)1) == 0); + assert(pthread_join(id, &value) == 0 && value == (void *)1); + } + double thread_us = (now() - start) / threads / 1000; + start = now(); + for (int i = 0; i < calls; i++) assert(access(AT_FDCWD, "/etc/os-release", R_OK, 0) == 0); + double access_ns = (now() - start) / calls; + char path[4096]; + start = now(); + for (int i = 0; i < calls; i++) assert(cwd(path, sizeof path)); + double cwd_ns = (now() - start) / calls; + assert(getrusage(RUSAGE_SELF, &after) == 0 && dlclose(libc) == 0); + printf("{\"thread_us\":%.0f,\"access_ns\":%.0f,\"cwd_ns\":%.0f,\"switches\":%ld}\n", + thread_us, access_ns, cwd_ns, after.ru_nvcsw - before.ru_nvcsw); + return 0; +} diff --git a/tools/proot/fastpath/svc-dispatch.h b/tools/proot/fastpath/svc-dispatch.h index 04afa1577..f8cdbbf76 100644 --- a/tools/proot/fastpath/svc-dispatch.h +++ b/tools/proot/fastpath/svc-dispatch.h @@ -52,7 +52,8 @@ static long svc_blocked(const long *r) { } if (r[8] == SYS_get_robust_list) { long tid = sc(SYS_gettid, 0, 0, 0, 0, 0); - if ((r[0] && r[0] != tid) || svc_robust.tid != tid || !svc_robust.head) return FP_SLOW; + if ((r[0] && r[0] != tid) || svc_robust.tid != tid) return FP_SLOW; + if (!svc_robust.head) return -ENOSYS; size_t size = 3 * sizeof(long); if (svc_memory(&svc_robust.head, r[1], sizeof(long), 1) != sizeof(long) || svc_memory(&size, r[2], sizeof size, 1) != sizeof size) return -EFAULT; From 40b7c480a3de320ab734b85c8a8e35a56f399160 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:44:59 -0400 Subject: [PATCH 5/9] fix(build): mirror pinned talloc downloads --- tools/proot/build.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tools/proot/build.sh b/tools/proot/build.sh index 5b76dd38f..087c23c81 100755 --- a/tools/proot/build.sh +++ b/tools/proot/build.sh @@ -23,13 +23,22 @@ fi WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT fetch() { - curl -fsSL --retry 3 -o "$WORK/$1" "$2" - echo "$3 $WORK/$1" | shasum -a 256 -c - - mkdir -p "$WORK/${1%%.*}" - tar -xzf "$WORK/$1" -C "$WORK/${1%%.*}" --strip-components=1 + local name=$1 primary=$2 sha=$3 url fetched=0 + shift 3 + for url in "$primary" "$@"; do + if curl -fsSL --connect-timeout 15 --max-time 90 --retry 3 --retry-max-time 120 -o "$WORK/$name" "$url" && + printf '%s %s\n' "$sha" "$WORK/$name" | shasum -a 256 -c -; then + fetched=1 + break + fi + done + [[ $fetched == 1 ]] || return 1 + mkdir -p "$WORK/${name%%.*}" + tar -xzf "$WORK/$name" -C "$WORK/${name%%.*}" --strip-components=1 } fetch proot.tar.gz "https://github.com/termux/proot/archive/${PROOT_COMMIT}.tar.gz" "$PROOT_SHA256" -fetch talloc.tar.gz "https://www.samba.org/ftp/talloc/talloc-${TALLOC_VERSION}.tar.gz" "$TALLOC_SHA256" +fetch talloc.tar.gz "https://www.samba.org/ftp/talloc/talloc-${TALLOC_VERSION}.tar.gz" "$TALLOC_SHA256" \ + "https://distfiles.macports.org/talloc/talloc-${TALLOC_VERSION}.tar.gz" cp "$HERE/statx_mnt_id.h" "$WORK/proot/src/tracee/statx_mnt_id.h" cp "$HERE/profile.h" "$WORK/proot/src/syscall/profile.h" for patch in "$HERE"/patches/*.patch; do From ddb8ed32cbbf74ebf23f8c9475f176c264d33e23 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:05:50 -0400 Subject: [PATCH 6/9] test(proot): record expanded relay validation --- docs/development/proroot-analysis.md | 86 +++++++++++++++++++++++++++- tools/proot/bench/svc-check.c | 15 ++++- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/docs/development/proroot-analysis.md b/docs/development/proroot-analysis.md index dd0220ef2..163620684 100644 --- a/docs/development/proroot-analysis.md +++ b/docs/development/proroot-analysis.md @@ -328,14 +328,90 @@ microseconds and open/close at 41.2→6.4 microseconds, with 10,000→0 voluntar context switches over both loops. The earlier SD/download tables above describe the initial 14-site revision and must not be attributed to this expanded build. +Five alternating pairs of `svc-threads` additionally measured thread create/join +at 138→113 microseconds, libc faccessat at 51.6→14.7 microseconds, and libc getcwd +at 46.8→4.1 microseconds. Each run creates 128 threads and executes 2,000 calls +to each libc entry point, bypassing the exported preload wrappers with dlsym. +The access call includes glibc's faccessat2 fallback. + +Repeated `/usr/bin/true` launches expose the startup tradeoff. Five batches of +40 children per mode measured medians of 3.834 ms with wrappers only, 4.882 ms +with uncached patching and 4.731 ms with cached patching, including subprocess +launch/wait and enabled exit statistics. Discovery across the 200 launches per +patched mode fell from a median 496 to 59 microseconds; publishing patches took +261/267 microseconds. The cache removes scanning work, but constructor setup and +patching still cost time for very short programs. No overall exec speedup is claimed. + +Three alternating storage pairs with the expanded library measured: + +| SD phase | wrappers only | expanded relays | +|---|---:|---:| +| create/extend/close 64 files | 1.556 s | 1.514 s | +| 1,000 internal stats | 73.2 ms | 12.5 ms | +| 64 MiB write calls | 54.2 ms | 55.1 ms | +| final fdatasync | 2.799 s | 2.730 s | + +These phase medians do not establish a general write-speed improvement. Internal +allocation was 7.1→3.6 ms and internal stats 49.8→7.7 ms; the SD filesystem's +allocation and flush waits remain much larger than the interception savings. + +### Expanded-build Steam SD comparison + +Owned, initially uninstalled Half-Life (70) supplies a larger file tree: 4,525 +installed files / 589,603,955 bytes, BuildID 15961492. Each fresh trial initially +scheduled 294,238,640 download bytes, 589,603,955 staged bytes and 4,488 preallocated +files on `/mnt/droiddeck-sd`. Steam reported successful installation in every run. +Local client `0` telemetry was selected. Times use one-second content-log timestamps; +total runs from the first main-app Reconfiguring event to scheduler completion. + +| trial | patching | initial preallocate | transfer/stage window | final commit | total | +|---|---|---:|---:|---:|---:| +| first baseline | off | 90 s | 52 s | 204 s | 350 s | +| expanded, default setting | on | 94 s | 48 s | 184 s | 326 s | +| repeat baseline | off | 97 s | 69 s | 175 s | 341 s | + +The patched and repeat-baseline trials use the same final library SHA-256 +`af4f66680dcfbd5002f641ffe206cdc7bd89400be169d3ef21c4e0ee7f979589`. +The first baseline used the preceding build with patching disabled; the only +subsequent production change corrected explicitly cleared robust-list TLS state. +All initial download records showed zero stored/reused/delta bytes. Shader updates +interrupted the main download in each trial; the transfer window includes those +pauses. On the repeat baseline, reconfiguration retained two small files already +downloaded during that same trial, so the final resumed job scheduled 294,236,672 +download / 589,599,865 stage bytes and committed 4,572 entries rather than 4,574. +This is not pre-existing payload reuse; the earlier fresh schedule is retained +in the measurement records. + +The observed whole-install difference does not establish a reliable SD speedup: +the repeat baseline committed faster than the patched run, while its transfer +window was slower. Allocation also did not improve. The 326 versus 341–350 second +totals combine network, shader scheduling and SD variability. The syscall gains +are demonstrated independently; these samples do not show that they fix Steam's +SD allocation/commit bottleneck. The test title was removed between trials; an +interrupted first uninstall left a repair queue, which was cleared by removing +only that task-created title while Steam was stopped before the subsequent fresh +trial. Existing installed games were retained. + The expanded ABI matrix passes raw return values, errno, all x1–x30 and q0–q31, -NZCV/FPCR/FPSR, four concurrent threads with distinct robust heads, fork, 100 -signals on an alternate stack, deferred cancellation and 32 thread create/join +NZCV and non-default FPCR/FPSR, four concurrent threads with distinct robust heads, fork, 100 +signals on an alternate stack, deferred cancellation, cancellation/cleanup of a +blocked libc FIFO open, clearing/restoring a robust head, and 32 thread create/join cycles. Constructor patching still refuses a process with multiple live threads. Host tests exercise corrupted cache entries, invalid offsets/opcodes, mismatched API/version, unsafe file modes, symlinks and truncation. The Android unit test checks API forwarding. The existing preload/agent selection passed 79 tests with two skips before the cache regression test was added. +API-selection probes patch 45 sites with an unknown SDK, 49 with API 33 and 48 +with simulated API 35; unset and explicit 1 agree, and explicit 0 leaves the +original sites intact. This does not substitute for testing on Android 15 hardware. + +CI at 40b7c480 passed 1,427 Python/native tests (49 skips), 2,630 Android tests and +162 Rust host tests, plus APK compilation. A local release APK built successfully, +passed runtime-payload/native-library checks and was signed with the installed +release certificate. The full local Windows JVM run reported 150 failures / 2,568 +tests (eight skips), including Unix-path, symlink and permission assumptions; +the four ProotFastPath tests passed in both local variants. The passing full +suite evidence is the Linux CI run, not a clean Windows full-suite result. Internal fts/realpath/NSS/dlopen and the normalized first equivalence suite still match PRoot at both cache TTLs. The write suite still matches wrapper-only output @@ -357,10 +433,16 @@ aarch64-linux-gnu-gcc -O2 -Wall -Wextra \ -o svc-workload tools/proot/bench/svc-workload.c -ldl aarch64-linux-gnu-gcc -O2 -Wall -Wextra \ -o svc-storage tools/proot/bench/svc-storage.c -ldl +aarch64-linux-gnu-gcc -O2 -Wall -Wextra -pthread \ + -o svc-threads tools/proot/bench/svc-threads.c -ldl ``` Run `svc-probe` inside the app's guest environment, not from `adb shell`. `svc-check` embeds the fast path and requires its normal enabled tracer environment. +For that embedded-library check, start with PROOT_FP_PATCH_SVC=0 when the session +also preloads a fast-path library (including through /etc/ld.so.preload). The check +then enables its own copy at main before creating threads, keeping another copy +from owning the glibc patch sites. The other tools use the normal preload. For `svc-workload`, pass a prepared directory containing `file` and a `link` symlink to it; add any second argument to enable timing. Use the documented library override workflow in [proot-performance](proot-performance.md) for a fresh session. No extra diff --git a/tools/proot/bench/svc-check.c b/tools/proot/bench/svc-check.c index 59f46ff86..36d439390 100644 --- a/tools/proot/bench/svc-check.c +++ b/tools/proot/bench/svc-check.c @@ -41,8 +41,10 @@ __asm__( static long probe(const long *args) { struct snapshot out = {0}; - unsigned long fpcr, fpsr; - __asm__ volatile("mrs %0, fpcr\nmrs %1, fpsr" : "=r"(fpcr), "=r"(fpsr)); + unsigned long saved_fpcr, saved_fpsr; + __asm__ volatile("mrs %0, fpcr\nmrs %1, fpsr" : "=r"(saved_fpcr), "=r"(saved_fpsr)); + unsigned long fpcr = saved_fpcr ^ (1UL << 22), fpsr = saved_fpsr | (1UL << 27); + __asm__ volatile("msr fpcr, %0\nmsr fpsr, %1" : : "r"(fpcr), "r"(fpsr)); errno = 123; svc_register_probe(&out, args); assert(errno == 123); @@ -54,6 +56,7 @@ static long probe(const long *args) { for (int j = 0; j < 16; j++) assert(out.q[i][j] == 0x5a); assert(out.nzcv == 0xa0000000UL); assert(out.fpcr == fpcr && out.fpsr == fpsr); + __asm__ volatile("msr fpcr, %0\nmsr fpsr, %1" : : "r"(saved_fpcr), "r"(saved_fpsr)); return out.x[0]; } @@ -273,12 +276,18 @@ static void signal_checks(void) { assert(pthread_create(&thread, NULL, thread_check, NULL) == 0); assert(pthread_join(thread, NULL) == 0); } - puts("PASS: signal reentry on alternate stack, deferred cancellation, thread churn"); + puts("PASS: signal reentry on alternate stack, deferred/FIFO cancellation with cleanup, thread churn"); } int main(void) { assert(fp_ready()); + const char *mode = getenv("PROOT_FP_PATCH_SVC"); + if (!svc_sites && mode && !strcmp(mode, "0")) { + assert(setenv("PROOT_FP_PATCH_SVC", "1", 1) == 0); + svc_patch_init(); + } printf("platform: android %d, patched %u sites\n", svc_android_api, svc_sites); + assert(svc_sites > 0); assert(svc_single_thread()); size_t page = sysconf(_SC_PAGESIZE); uint32_t *relay = svc_relay_page((uintptr_t)&svc_check_site, page), branch; From 4998b046366ecbba05641716ff3fa32d78d3f1a1 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:06:44 -0400 Subject: [PATCH 7/9] fix(proot): harden relay state and unwinding --- docs/development/proroot-analysis.md | 86 ++++++++++++++++- tools/proot/bench/svc-check.c | 132 +++++++++++++++++++++++++-- tools/proot/bench/svc-fail.c | 38 ++++++++ tools/proot/bench/svc-race.c | 49 ++++++++++ tools/proot/fastpath/svc-cache.h | 6 +- tools/proot/fastpath/svc-dispatch.h | 20 +--- tools/proot/fastpath/svc-patch.h | 61 ++++++------- tools/proot/fastpath/svc-publish.h | 28 ++++++ tools/proot/fastpath/svc-stack.h | 83 +++++++++++++++++ tools/tests/test_fastpath_svc.py | 114 +++++++++++++++++++++++ 10 files changed, 553 insertions(+), 64 deletions(-) create mode 100644 tools/proot/bench/svc-fail.c create mode 100644 tools/proot/bench/svc-race.c create mode 100644 tools/proot/fastpath/svc-publish.h create mode 100644 tools/proot/fastpath/svc-stack.h diff --git a/docs/development/proroot-analysis.md b/docs/development/proroot-analysis.md index 163620684..f3bdb0e39 100644 --- a/docs/development/proroot-analysis.md +++ b/docs/development/proroot-analysis.md @@ -287,7 +287,7 @@ The installed PRoot pack emitted no profiling start/counter records despite the queued profiling setting; the profiler report had `hasCounters: false`. That is a measurement gap, not evidence of zero remaining traps. -### Expanded relay coverage +### Expanded relay coverage (before the safety follow-up) The subsequent revision keeps patching enabled by default and shares the existing wrapper translators with 20 path syscall numbers: openat, newfstatat, statx, @@ -449,3 +449,87 @@ workflow in [proot-performance](proot-performance.md) for a fresh session. No ex environment setting is needed; use `PROOT_FP_PATCH_SVC=0` for the unpatched comparison. `svc-storage` takes a new, unused directory name; it creates and removes its own test files and directory. It writes 64 MiB plus 16 MiB of file extensions per run. + +### Safety follow-up (2026-10-28) + +Path patching remains enabled by default. Robust-list registration/query sites are +now deliberately excluded: a device regression test reproduced disagreement +between the relay's private TLS head and an unpatched raw get_robust_list call in +the preceding implementation. Both directions now use the existing PRoot/kernel +route, including cleared heads. This preserves the host's actual return policy: +the installed Thor runtime accepts set_robust_list with zero, whereas the removed +emulation returned ENOSYS. PRoot patch 0016 can instead return ENOSYS when Android +blocks registration; neither policy is replaced by private relay state. + +The build-ID cache is version 2 because the accepted syscall set changed. The +current glibc 2.43/API-33 build patches 47 sites rather than 49. faccessat2 and +pre-Android-15 clone3 fallback acceleration remain; droiddeck-fsync is unchanged. +The earlier 138-to-113-microsecond thread result belongs to the retired robust-list +implementation and must not be claimed for the current code. + +Relays now save the original post-SVC program counter and supply unwind metadata +for the saved caller registers. Device tests force an unwind from inside the +translator through a patched libc frame and verify caller cleanup. A signal handler +also exits with siglongjmp; a later call recovers the abandoned recursion guard +and resumes acceleration. These tests supplement deferred and blocked-FIFO +cancellation; they are not a claim of arbitrary asynchronous-cancellation safety. + +Compiler stack-usage output identified multiple nested PATH_MAX-sized frames. +Before entering them, a small dispatcher checks the active alternate signal stack +and cached writable stack mapping. Alternate-stack calls and calls with less than +96 KiB remaining use PRoot. Main-stack growth respects the current RLIMIT_STACK +and leaves space above the preceding mapping. The implementation adds only a few +TLS words, not a large private translation stack. Tests cover a restricted thread +stack, normal/alternate-stack signals, main-stack growth and a reduced/unavailable +stack limit. User-managed stacks and arbitrary mid-helper non-local exits remain +broader compatibility risks, not proven consequence-free cases. + +On Thor/Android 13, the embedded and shared-PIC matrices pass on glibc 2.43; +the shared-PIC matrix also passes with a separately loaded glibc 2.39. They cover register/FP/errno preservation, mixed raw +and relay robust calls, pointer and page boundaries, readlink buffers of target +length minus one through plus two, thread churn, fork, signal/non-local exits, +forced unwinding and limited stack space. Readlink preserves PRoot's existing +translated-link length behavior; bytes beyond the reported payload are unspecified. + +Test-process seccomp filters separately deny opening /proc/self/mem, nearby relay +mappings, executable mprotect and patch writes. All four device cases leave zero +patched sites and a working original libc syscall. Host injection additionally +checks that one failed site write permits other sites to install. Concurrent +atomic symlink replacement passes 3,000 internal stat/readlink iterations at both +zero and default cache TTLs. These are bounded tests; they do not prove every +possible filesystem race or reproduce a real SELinux execmod denial. + +Five new alternating pairs measured: + +| operation | wrappers only | hardened relays | +|---|---:|---:| +| libc-internal stat | 50.1 us | 5.9 us | +| libc-internal open/close | 51.9 us | 7.1 us | +| libc faccessat | 52.0 us | 20.2 us | +| libc getcwd | 51.7 us | 4.6 us | +| thread create/join | 129 us | 144 us | +| complete tiny exec/wait | 5.04 ms | 5.96 ms | + +The metadata gains survive the safety checks. This run shows no thread-creation +gain, and the roughly 0.9 ms tiny-exec cost remains. The earlier SD download and +flush results are historical measurements of the preceding build; no new claim +that SD media writes or whole Steam installs become reliably faster follows from +these syscall results. Android 15+ ARM64 and 16 KiB-page hardware remain untested. + +Three alternating pairs also measured session start through the Steam Big Picture +Home page becoming ready, using its DOM/Recent Games content rather than merely +the compositor's first frame. Off runs took 48.76/43.47/47.02 seconds; on runs took +43.43/45.25/50.40 seconds. The 47.02-to-45.25-second medians sit within overlapping +run-to-run variation and do not establish a reliable whole-client startup win. +These use the signed bundled-library build without overrides; the final stack-gap +guard only increases the reserved distance above other mappings. + +At Home-ready plus 20 seconds, three one-second-spaced process-memory samples per +run gave Steam/CEF-only median PSS of 1,223.1 MiB off and 1,233.9 MiB on. Including +their active Python/Wine descendants gave 1,243.7/1,254.6 MiB. The five Steam/CEF +processes were selected by executable name; descendants were included by parent +PID. Per-run Steam/CEF medians ranged 1,217.9–1,240.9 MiB off and +1,194.8–1,244.9 MiB on. The roughly 11 MiB median increase is smaller than this +variation, but it is not evidence of zero memory cost. PSS avoids counting shared +pages in full for each process. Startup and memory comparisons used one unchanged +APK; no downloads or games were initiated during them. diff --git a/tools/proot/bench/svc-check.c b/tools/proot/bench/svc-check.c index 36d439390..664181d60 100644 --- a/tools/proot/bench/svc-check.c +++ b/tools/proot/bench/svc-check.c @@ -1,10 +1,25 @@ +#if defined(__aarch64__) +static void interruption_hook(const long *); +#define FP_SVC_TEST_HOOK interruption_hook +#endif #include "../fastpath/fastpath.c" #include #include #include #include +#include #if defined(__aarch64__) +static void (*interrupt_path)(void); +static void interruption_hook(const long *r) { + (void)r; + if (interrupt_path) { + void (*hook)(void) = interrupt_path; + interrupt_path = NULL; + hook(); + } +} + struct snapshot { long x[32]; unsigned char q[32][16]; unsigned long nzcv, fpcr, fpsr; }; extern uint32_t svc_check_site; extern void svc_register_probe(struct snapshot *, const long *); @@ -77,6 +92,23 @@ static void fault(long nr, long a, long b, long c, long d, long e, long f) { check(call, want); } +static void readlink_boundaries(const char *path) { + char expected[PATH_MAX + 8], actual[PATH_MAX + 8]; + long args[] = {AT_FDCWD, (long)path, (long)expected, PATH_MAX, 0, 0, 0, 0, SYS_readlinkat}; + long length = svc_original(args); + assert(length > 0 && length < PATH_MAX - 2); + for (long size = length - 1; size <= length + 2; size++) { + memset(expected, 0x5a, sizeof expected); memset(actual, 0x5a, sizeof actual); + args[3] = size; + long want = svc_original(args); + long got = raw(SYS_readlinkat, AT_FDCWD, (long)path, (long)actual, size, 0, 0); + if (got != want || (want > 0 && memcmp(expected, actual, (size_t)want))) + fprintf(stderr, "readlink %s size %ld: relay %ld, proot %ld\n", path, size, got, want); + assert(got == want && (want <= 0 || !memcmp(expected, actual, (size_t)want))); + for (size_t i = (size_t)size; i < sizeof actual; i++) assert(actual[i] == 0x5a); + } +} + static void path_checks(void) { char work[] = "/tmp/proot-expanded-check-XXXXXX"; assert(mkdtemp(work)); @@ -91,6 +123,8 @@ static void path_checks(void) { assert(raw(SYS_statx, AT_FDCWD, (long)"dir/file", 0, STATX_BASIC_STATS, (long)&sx, 0) == 0 && sx.stx_size == 5); assert(raw(SYS_faccessat, AT_FDCWD, (long)"dir/file", R_OK, 0, 0, 0) == 0); assert(raw(SYS_symlinkat, (long)"file", AT_FDCWD, (long)"dir/link", 0, 0, 0) == 0); + readlink_boundaries("dir/link"); + readlink_boundaries("/proc/self/exe"); char text[PATH_MAX]; memset(text, '#', sizeof text); assert(raw(SYS_readlinkat, AT_FDCWD, (long)"dir/link", (long)text, sizeof text, 0, 0) == 4); @@ -145,14 +179,21 @@ static void robust_checks(void) { size_t size = 0; assert(raw(SYS_get_robust_list, 0, (long)&original, (long)&size, 0, 0, 0) == 0); struct robust_list_head head = {0}; - assert(raw(SYS_set_robust_list, (long)&head, sizeof head, 0, 0, 0, 0) == -ENOSYS); + long set_result = raw(SYS_set_robust_list, (long)&head, sizeof head, 0, 0, 0, 0); + assert(set_result == 0 || set_result == -ENOSYS); + long raw_head = 0; + long direct[] = {0, (long)&raw_head, (long)&size, 0, 0, 0, 0, 0, SYS_get_robust_list}; + assert(svc_original(direct) == 0 && raw_head == (long)&head); long got = 0; assert(raw(SYS_get_robust_list, gettid(), (long)&got, (long)&size, 0, 0, 0) == 0); assert(got == (long)&head && size == sizeof head); assert(raw(SYS_get_robust_list, 0, 1, (long)&size, 0, 0, 0) == -EFAULT); - assert(raw(SYS_set_robust_list, 0, sizeof head, 0, 0, 0, 0) == -ENOSYS); - assert(raw(SYS_get_robust_list, 0, (long)&got, (long)&size, 0, 0, 0) == -ENOSYS); - assert(raw(SYS_set_robust_list, original, sizeof head, 0, 0, 0, 0) == -ENOSYS); + assert(raw(SYS_set_robust_list, 0, sizeof head, 0, 0, 0, 0) == set_result); + long cleared = raw(SYS_get_robust_list, 0, (long)&got, (long)&size, 0, 0, 0); + assert(cleared == -ENOSYS || (cleared == 0 && got == 0)); + direct[0] = original; direct[1] = sizeof head; direct[8] = SYS_set_robust_list; + assert(svc_original(direct) == set_result); + assert(raw(SYS_get_robust_list, 0, (long)&got, (long)&size, 0, 0, 0) == 0 && original == got); assert(raw(SYS_faccessat2, AT_FDCWD, (long)"/usr/lib/libc.so.6", R_OK, 0, 0, 0) == -ENOSYS); if (svc_android_api < 35) assert(raw(SYS_clone3, 0, 0, 0, 0, 0, 0) == -ENOSYS); int api = svc_android_api; @@ -161,7 +202,8 @@ static void robust_checks(void) { svc_android_api = 0; assert(!svc_supported(SYS_clone3) && !svc_supported(SYS_set_robust_list)); svc_android_api = api; - puts("PASS: robust-list TLS, errors, faccessat2, clone3 platform guard"); + assert(!svc_supported(SYS_set_robust_list) && !svc_supported(SYS_get_robust_list)); + puts("PASS: mixed relay/raw robust-list callers, errors, faccessat2, clone3 platform guard"); } static void *thread_check(void *unused) { @@ -172,13 +214,14 @@ static void *thread_check(void *unused) { size_t size; assert(raw(SYS_get_robust_list, 0, (long)&head, (long)&size, 0, 0, 0) == 0 && head && size == 3 * sizeof(long)); struct robust_list_head private = {0}; - assert(raw(SYS_set_robust_list, (long)&private, sizeof private, 0, 0, 0, 0) == -ENOSYS); + long result = raw(SYS_set_robust_list, (long)&private, sizeof private, 0, 0, 0, 0); + assert(result == 0 || result == -ENOSYS); for (int i = 0; i < 10; i++) { long got; sched_yield(); assert(raw(SYS_get_robust_list, 0, (long)&got, (long)&size, 0, 0, 0) == 0 && got == (long)&private); } - assert(raw(SYS_set_robust_list, head, sizeof private, 0, 0, 0, 0) == -ENOSYS); + assert(raw(SYS_set_robust_list, head, sizeof private, 0, 0, 0, 0) == result); } const long args[] = {AT_FDCWD, (long)"/usr/lib/libc.so.6", (long)&st, 0, 0, 0, SYS_newfstatat}; for (int i = 0; i < 100; i++) check(args, 0); @@ -203,6 +246,75 @@ static void on_signal(int signal) { errno = saved; } +static sigjmp_buf interrupted; +static void jump_signal(int signal) { (void)signal; siglongjmp(interrupted, 1); } +static void jump_path(void) { assert(raise(SIGUSR2) == 0); abort(); } +static void exit_path(void) { pthread_exit(PTHREAD_CANCELED); } +static void mark_cleanup(void *arg) { *(int *)arg = 1; } +struct unwind_check { int cleaned; int (*stat)(int, const char *, struct stat *, int); }; +static void *unwind_worker(void *arg) { + struct unwind_check *test = arg; + struct stat st; + pthread_cleanup_push(mark_cleanup, &test->cleaned); + interrupt_path = exit_path; + test->stat(AT_FDCWD, "/usr/lib/libc.so.6", &st, 0); + pthread_cleanup_pop(1); + abort(); +} + +static void interruption_checks(void) { + void *libc = dlopen("libc.so.6", RTLD_NOW); + struct unwind_check test = {.stat = dlsym(libc, "fstatat")}; + assert(test.stat); + struct sigaction old, action = {.sa_handler = jump_signal}; + sigemptyset(&action.sa_mask); + assert(sigaction(SIGUSR2, &action, &old) == 0); + sigset_t deliver, old_mask; + sigemptyset(&deliver); sigaddset(&deliver, SIGUSR2); + assert(pthread_sigmask(SIG_UNBLOCK, &deliver, &old_mask) == 0); + if (!sigsetjmp(interrupted, 1)) { + struct stat st; + interrupt_path = jump_path; + test.stat(AT_FDCWD, "/usr/lib/libc.so.6", &st, 0); + abort(); + } + assert(svc_busy); + struct stat st; + unsigned long before = svc_fast; + assert(test.stat(AT_FDCWD, "/usr/lib/libc.so.6", &st, 0) == 0); + assert(!svc_busy && svc_fast > before); + assert(sigaction(SIGUSR2, &old, NULL) == 0 && pthread_sigmask(SIG_SETMASK, &old_mask, NULL) == 0); + pthread_t thread; + void *result; + assert(pthread_create(&thread, NULL, unwind_worker, &test) == 0); + assert(pthread_join(thread, &result) == 0 && result == PTHREAD_CANCELED && test.cleaned); + assert(dlclose(libc) == 0); + puts("PASS: signal siglongjmp recovery, forced unwind through patched libc with caller cleanup"); +} + +static void *small_stack_worker(void *unused) { + size_t consume = (size_t)(uintptr_t)unused - 48 * 1024; + volatile char *padding = __builtin_alloca(consume); + padding[0] = padding[consume - 1] = 1; + struct stat st; + unsigned long before = svc_fast; + for (int i = 0; i < 20; i++) + assert(raw(SYS_newfstatat, AT_FDCWD, (long)"/usr/lib/libc.so.6", (long)&st, 0, 0, 0) == 0); + assert(svc_fast == before && !svc_busy); + return NULL; +} + +static void small_stack_checks(void) { + pthread_attr_t attr; + pthread_t thread; + size_t size = PTHREAD_STACK_MIN; + if (size < 128 * 1024) size = 128 * 1024; + assert(pthread_attr_init(&attr) == 0 && pthread_attr_setstacksize(&attr, size) == 0); + assert(pthread_create(&thread, &attr, small_stack_worker, (void *)(uintptr_t)size) == 0); + assert(pthread_join(thread, NULL) == 0 && pthread_attr_destroy(&attr) == 0); + puts("PASS: limited remaining thread stack delegates path calls to PRoot"); +} + struct signal_sender { pthread_t target; int done; }; static void *send_signals(void *arg) { struct signal_sender *sender = arg; @@ -237,7 +349,7 @@ static void *cancel_fifo(void *arg) { } static void signal_checks(void) { - stack_t old_stack, stack = {.ss_sp = malloc(128 * 1024), .ss_size = 128 * 1024}; + stack_t old_stack, stack = {.ss_sp = malloc(SIGSTKSZ), .ss_size = SIGSTKSZ}; assert(stack.ss_sp && sigaltstack(&stack, &old_stack) == 0); struct sigaction old, action = {.sa_handler = on_signal, .sa_flags = SA_ONSTACK}; sigemptyset(&action.sa_mask); @@ -292,7 +404,7 @@ int main(void) { size_t page = sysconf(_SC_PAGESIZE); uint32_t *relay = svc_relay_page((uintptr_t)&svc_check_site, page), branch; assert(relay && svc_emit(relay, &svc_check_site, &branch)); - __builtin___clear_cache((char *)relay, (char *)relay + 32); + __builtin___clear_cache((char *)relay, (char *)relay + 40); assert(mprotect(relay, page, PROT_READ | PROT_EXEC) == 0); int mem = open("/proc/self/mem", O_RDWR | O_CLOEXEC); assert(mem >= 0 && pwrite(mem, &branch, 4, (off_t)(uintptr_t)&svc_check_site) == 4); @@ -331,6 +443,8 @@ int main(void) { int status; assert(waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0); signal_checks(); + interruption_checks(); + small_stack_checks(); puts("PASS: raw results, errno, x1-x30, q0-q31, NZCV, FPCR/FPSR, 4 threads, fork"); return 0; } diff --git a/tools/proot/bench/svc-fail.c b/tools/proot/bench/svc-fail.c new file mode 100644 index 000000000..94430bad4 --- /dev/null +++ b/tools/proot/bench/svc-fail.c @@ -0,0 +1,38 @@ +#include "../fastpath/fastpath.c" +#include +#include +#include +#include + +int main(int argc, char **argv) { +#if defined(__aarch64__) + assert(argc == 2 && fp_ready() && !svc_sites); + int nr = !strcmp(argv[1], "mem") ? SYS_openat : !strcmp(argv[1], "write") ? SYS_pwrite64 : + !strcmp(argv[1], "rx") ? SYS_mprotect : SYS_mmap; + unsigned offset = nr == SYS_mprotect ? offsetof(struct seccomp_data, args[2]) : offsetof(struct seccomp_data, args[3]); + unsigned mask = nr == SYS_mprotect ? PROT_EXEC : nr == SYS_mmap ? MAP_FIXED_NOREPLACE : ~0U; + struct sock_filter code[] = { + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 3), + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offset), + BPF_JUMP(BPF_JMP | BPF_JSET | BPF_K, mask, 0, 1), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EACCES), + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), + }; + if (nr == SYS_openat || nr == SYS_pwrite64) code[2] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_IMM, 1); + struct sock_fprog filter = {.len = sizeof code / sizeof *code, .filter = code}; + assert(setenv("PROOT_FP_PATCH_SVC", "1", 1) == 0); + if (sc(SYS_prctl, PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || + sc(SYS_prctl, PR_SET_SECCOMP, SECCOMP_MODE_FILTER, (long)&filter, 0, 0)) return 77; + svc_patch_init(); + assert(svc_sites == 0 && svc_pages == 0); + struct stat st; + long args[] = {AT_FDCWD, (long)"/etc/os-release", (long)&st, 0, 0, 0, 0, 0, SYS_newfstatat}; + assert(svc_original(args) == 0 && S_ISREG(st.st_mode)); + puts("PASS: denied patch installation leaves the original libc syscall usable"); + return 0; +#else + (void)argc; (void)argv; + return 77; +#endif +} diff --git a/tools/proot/bench/svc-race.c b/tools/proot/bench/svc-race.c new file mode 100644 index 000000000..4198f4e36 --- /dev/null +++ b/tools/proot/bench/svc-race.c @@ -0,0 +1,49 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + char work[] = "/tmp/proot-svc-race-XXXXXX"; + assert(mkdtemp(work)); + int cwd = open(".", O_RDONLY | O_DIRECTORY); + assert(cwd >= 0 && chdir(work) == 0); + for (int i = 0; i < 2; i++) { + int fd = open(i ? "b" : "a", O_CREAT | O_EXCL | O_WRONLY, 0600); + assert(fd >= 0 && ftruncate(fd, i + 1) == 0 && close(fd) == 0); + } + assert(symlink("a", "live") == 0); + void *libc = dlopen("libc.so.6", RTLD_NOW); + int (*stat)(int, const char *, struct stat *, int) = dlsym(libc, "fstatat"); + ssize_t (*link)(int, const char *, char *, size_t) = dlsym(libc, "readlinkat"); + assert(stat && link); + pid_t writer = fork(); + assert(writer >= 0); + if (!writer) { + for (int i = 0; i < 3000; i++) { + assert(symlink(i & 1 ? "b" : "a", "next") == 0); + assert(rename("next", "live") == 0); + } + _exit(0); + } + for (int i = 0; i < 3000; i++) { + struct stat st; + assert(stat(AT_FDCWD, "live", &st, 0) == 0 && S_ISREG(st.st_mode) && (st.st_size == 1 || st.st_size == 2)); + char text[8]; + assert(link(AT_FDCWD, "live", text, sizeof text) == 1 && (text[0] == 'a' || text[0] == 'b')); + } + int status; + assert(waitpid(writer, &status, 0) == writer && WIFEXITED(status) && WEXITSTATUS(status) == 0); + assert(unlink("live") == 0 && unlink("a") == 0 && unlink("b") == 0); + assert(dlclose(libc) == 0 && fchdir(cwd) == 0 && close(cwd) == 0 && rmdir(work) == 0); + puts("PASS: concurrent atomic symlink replacement, internal stat/readlink legal results"); + return 0; +} diff --git a/tools/proot/fastpath/svc-cache.h b/tools/proot/fastpath/svc-cache.h index 4a6f49b80..57c1be4ba 100644 --- a/tools/proot/fastpath/svc-cache.h +++ b/tools/proot/fastpath/svc-cache.h @@ -39,7 +39,7 @@ static int svc_cache_name(const struct dl_phdr_info *info, char *path, size_t si id[2 * j + 1] = hex[b & 15]; } id[2 * h.n_descsz] = 0; - int n = snprintf(path, size, "/tmp/.droiddeck-svc-v1-%d-%s", svc_android_api, id); + int n = snprintf(path, size, "/tmp/.droiddeck-svc-v2-%d-%s", svc_android_api, id); return n > 0 && (size_t)n < size; } at += name + desc; @@ -57,7 +57,7 @@ static int svc_cache_read(const struct dl_phdr_info *info, const char *path) { st.st_uid == (uid_t)sc(SYS_getuid, 0, 0, 0, 0, 0) && !(st.st_mode & 0077) && st.st_size == sizeof table && sc(SYS_read, fd, (long)&table, sizeof table, 0, 0) == sizeof table; sc(SYS_close, fd, 0, 0, 0, 0); - if (!valid || table.magic != 0x314356534444ULL || table.api != (unsigned)svc_android_api || + if (!valid || table.magic != 0x324356534444ULL || table.api != (unsigned)svc_android_api || !table.count || table.count > 256 || table.count > 256 - svc_planned) return 0; for (unsigned i = 0; i < table.count; i++) { uint64_t offset = table.offset[i]; @@ -75,7 +75,7 @@ static int svc_cache_read(const struct dl_phdr_info *info, const char *path) { static void svc_cache_write(const struct dl_phdr_info *info, const char *path, size_t first) { if (svc_planned == first || svc_planned == 256) return; - struct svc_cached_sites table = {.magic = 0x314356534444ULL, .api = svc_android_api, .count = svc_planned - first}; + struct svc_cached_sites table = {.magic = 0x324356534444ULL, .api = svc_android_api, .count = svc_planned - first}; for (unsigned i = 0; i < table.count; i++) table.offset[i] = (uintptr_t)svc_plan[first + i].site - info->dlpi_addr; char temporary[256]; diff --git a/tools/proot/fastpath/svc-dispatch.h b/tools/proot/fastpath/svc-dispatch.h index f8cdbbf76..b8e87475f 100644 --- a/tools/proot/fastpath/svc-dispatch.h +++ b/tools/proot/fastpath/svc-dispatch.h @@ -1,5 +1,4 @@ static int svc_android_api; -static __thread struct { long head, tid; } svc_robust __attribute__((tls_model("initial-exec"))); static int svc_host_android_api(void) { const char *value = getenv("PROOT_FP_ANDROID_API"); @@ -34,7 +33,7 @@ static int svc_supported(int nr) { case SYS_truncate: case SYS_statfs: case SYS_getxattr: case SYS_lgetxattr: case SYS_listxattr: case SYS_llistxattr: case SYS_inotify_add_watch: case SYS_getcwd: return 1; - case SYS_set_robust_list: case SYS_get_robust_list: case SYS_faccessat2: + case SYS_faccessat2: return svc_android_api > 0; case SYS_clone3: return svc_android_api > 0 && svc_android_api < 35; @@ -45,24 +44,13 @@ static int svc_supported(int nr) { static long svc_blocked(const long *r) { if (!svc_android_api) return FP_SLOW; if (r[8] == SYS_faccessat2 || (r[8] == SYS_clone3 && svc_android_api < 35)) return -ENOSYS; - if (r[8] == SYS_set_robust_list) { - svc_robust.head = r[0]; - svc_robust.tid = sc(SYS_gettid, 0, 0, 0, 0, 0); - return -ENOSYS; - } - if (r[8] == SYS_get_robust_list) { - long tid = sc(SYS_gettid, 0, 0, 0, 0, 0); - if ((r[0] && r[0] != tid) || svc_robust.tid != tid) return FP_SLOW; - if (!svc_robust.head) return -ENOSYS; - size_t size = 3 * sizeof(long); - if (svc_memory(&svc_robust.head, r[1], sizeof(long), 1) != sizeof(long) || - svc_memory(&size, r[2], sizeof size, 1) != sizeof size) return -EFAULT; - return 0; - } return FP_SLOW; } static long svc_path_call(const long *r) { +#ifdef FP_SVC_TEST_HOOK + FP_SVC_TEST_HOOK(r); +#endif char path[PATH_MAX], other[PATH_MAX]; struct timespec times[2]; const struct timespec *when = (const struct timespec *)r[2]; diff --git a/tools/proot/fastpath/svc-patch.h b/tools/proot/fastpath/svc-patch.h index dbd4488f0..300302f58 100644 --- a/tools/proot/fastpath/svc-patch.h +++ b/tools/proot/fastpath/svc-patch.h @@ -47,13 +47,15 @@ static inline int svc_branch(uintptr_t from, uintptr_t to, uint32_t *out) { #include #include #include +#include -static __thread int svc_busy __attribute__((tls_model("initial-exec"))); +static __thread uintptr_t svc_busy __attribute__((tls_model("initial-exec"))); static unsigned svc_sites, svc_pages; static unsigned long svc_fast, svc_slow; static unsigned long long svc_scan_ns, svc_patch_ns; #include "svc-dispatch.h" +#include "svc-stack.h" static long svc_original(const long *r) { register long x0 __asm__("x0") = r[0]; @@ -71,10 +73,12 @@ __attribute__((used, visibility("hidden"))) long fp_svc_dispatch(const long *r) { int saved_errno = errno; long result = svc_blocked(r); - if (result == FP_SLOW && !svc_busy) { - svc_busy = 1; - result = svc_path_call(r); - svc_busy = 0; + uintptr_t frame = (uintptr_t)r; + if (result == FP_SLOW && (!svc_busy || frame >= svc_busy)) { + uintptr_t previous = svc_busy; + svc_busy = frame; + if (svc_stack_room(frame)) { result = svc_path_call(r); svc_busy = 0; } + else svc_busy = previous; } if (result == FP_SLOW) { __atomic_fetch_add(&svc_slow, 1, __ATOMIC_RELAXED); @@ -88,7 +92,8 @@ void fp_svc_entry(void) __attribute__((visibility("hidden"))); /* A replaced svc may have live values in every register, including NZCV and SIMD. */ __asm__( ".text\n.p2align 4\n.hidden fp_svc_entry\n.type fp_svc_entry,%function\nfp_svc_entry:\n" - "bti j\nsub sp, sp, #800\n" + ".cfi_startproc\n.cfi_def_cfa sp, 16\n.cfi_return_column 16\n" + "bti j\nsub sp, sp, #800\n.cfi_def_cfa_offset 816\n" "stp x0, x1, [sp, #0]\nstp x2, x3, [sp, #16]\n" "stp x4, x5, [sp, #32]\nstp x6, x7, [sp, #48]\n" "stp x8, x9, [sp, #64]\nstp x10, x11, [sp, #80]\n" @@ -97,6 +102,13 @@ __asm__( "stp x22, x23, [sp, #160]\nstp x24, x25, [sp, #176]\n" "stp x26, x27, [sp, #192]\nstp x28, x29, [sp, #208]\n" "stp x30, x17, [sp, #224]\nmrs x0, nzcv\nstr x0, [sp, #240]\n" + "ldr x0, [x17, #8]\nstr x0, [sp, #784]\n" + ".cfi_offset 16, -32\n.cfi_offset 19, -680\n.cfi_offset 20, -672\n" + ".cfi_offset 21, -664\n.cfi_offset 22, -656\n.cfi_offset 23, -648\n" + ".cfi_offset 24, -640\n.cfi_offset 25, -632\n.cfi_offset 26, -624\n" + ".cfi_offset 27, -616\n.cfi_offset 28, -608\n.cfi_offset 29, -600\n.cfi_offset 30, -592\n" + ".cfi_offset 72, -432\n.cfi_offset 73, -416\n.cfi_offset 74, -400\n.cfi_offset 75, -384\n" + ".cfi_offset 76, -368\n.cfi_offset 77, -352\n.cfi_offset 78, -336\n.cfi_offset 79, -320\n" "mrs x0, fpcr\nstr x0, [sp, #768]\nmrs x0, fpsr\nstr x0, [sp, #776]\n" "stp q0, q1, [sp, #256]\nstp q2, q3, [sp, #288]\n" "stp q4, q5, [sp, #320]\nstp q6, q7, [sp, #352]\n" @@ -125,7 +137,7 @@ __asm__( "ldp x22, x23, [sp, #160]\nldp x24, x25, [sp, #176]\n" "ldp x26, x27, [sp, #192]\nldp x28, x29, [sp, #208]\n" "ldr x30, [sp, #224]\nldr x16, [sp, #232]\n" - "add sp, sp, #800\nbr x16\n.size fp_svc_entry, .-fp_svc_entry\n" + "add sp, sp, #800\n.cfi_def_cfa_offset 16\nbr x16\n.cfi_endproc\n.size fp_svc_entry, .-fp_svc_entry\n" ); static int svc_emit(uint32_t *code, uint32_t *site, uint32_t *branch) { @@ -133,13 +145,15 @@ static int svc_emit(uint32_t *code, uint32_t *site, uint32_t *branch) { if (!svc_branch((uintptr_t)site, (uintptr_t)code, branch) || !svc_branch((uintptr_t)(code + 5), (uintptr_t)(site + 1), &back)) return 0; code[0] = 0xa9bf47f0U; /* stp x16, x17, [sp, #-16]! */ - code[1] = 0x580000b0U; /* ldr x16, literal at +24 */ + code[1] = 0x580000f0U; /* ldr x16, literal at +32 */ code[2] = 0x10000051U; /* adr x17, +8 */ code[3] = 0xd61f0200U; /* br x16 */ code[4] = 0xa8c147f0U; /* ldp x16, x17, [sp], #16 */ code[5] = back; uintptr_t entry = (uintptr_t)fp_svc_entry; - memcpy(code + 6, &entry, sizeof entry); + uintptr_t resume = (uintptr_t)(site + 1); + memcpy(code + 6, &resume, sizeof resume); + memcpy(code + 8, &entry, sizeof entry); return 1; } @@ -190,6 +204,8 @@ static uint32_t *svc_relay_page(uintptr_t site, size_t page) { return NULL; } +#include "svc-publish.h" + static int svc_single_thread(void) { struct entry { uint64_t ino; int64_t off; unsigned short len; unsigned char type; char name[]; }; char buf[4096] __attribute__((aligned(8))); @@ -245,32 +261,7 @@ __attribute__((constructor)) static void svc_patch_init(void) { dl_iterate_phdr(svc_object, NULL); svc_scan_ns = svc_clock() - start; start = svc_clock(); - for (size_t first = 0; first < svc_planned;) { - uint32_t *relay = svc_relay_page((uintptr_t)svc_plan[first].site, page); - if (!relay) { first++; continue; } - size_t end = first; - for (; end < svc_planned && (end - first + 1) * 32 <= page; end++) { - uint32_t *code = relay + (end - first) * 8; - if (!svc_emit(code, svc_plan[end].site, &svc_plan[end].branch)) break; - } - __builtin___clear_cache((char *)relay, (char *)relay + page); - if (end == first || mprotect(relay, page, PROT_READ | PROT_EXEC)) { - munmap(relay, page); - first++; - continue; - } - unsigned patched = 0; - for (size_t i = first; i < end; i++) { - struct svc_site *s = &svc_plan[i]; - long written = sc(SYS_pwrite64, mem, (long)&s->branch, sizeof s->branch, (long)s->site, 0); - if (written != sizeof s->branch) continue; - __builtin___clear_cache((char *)s->site, (char *)(s->site + 1)); - patched++; - } - if (patched) { svc_sites += patched; svc_pages++; } - else munmap(relay, page); - first = end; - } + svc_publish(mem, page); sc(SYS_close, mem, 0, 0, 0, 0); svc_patch_ns = svc_clock() - start; if (getenv("PROOT_FP_STATS")) atexit(svc_stats); diff --git a/tools/proot/fastpath/svc-publish.h b/tools/proot/fastpath/svc-publish.h new file mode 100644 index 000000000..002e06c51 --- /dev/null +++ b/tools/proot/fastpath/svc-publish.h @@ -0,0 +1,28 @@ +static void svc_publish(int mem, size_t page) { + for (size_t first = 0; first < svc_planned;) { + uint32_t *relay = svc_relay_page((uintptr_t)svc_plan[first].site, page); + if (!relay) { first++; continue; } + size_t end = first; + for (; end < svc_planned && (end - first + 1) * 40 <= page; end++) { + uint32_t *code = relay + (end - first) * 10; + if (!svc_emit(code, svc_plan[end].site, &svc_plan[end].branch)) break; + } + __builtin___clear_cache((char *)relay, (char *)relay + page); + if (end == first || mprotect(relay, page, PROT_READ | PROT_EXEC)) { + munmap(relay, page); + first++; + continue; + } + unsigned patched = 0; + for (size_t i = first; i < end; i++) { + struct svc_site *s = &svc_plan[i]; + long written = sc(SYS_pwrite64, mem, (long)&s->branch, sizeof s->branch, (long)s->site, 0); + if (written != sizeof s->branch) continue; + __builtin___clear_cache((char *)s->site, (char *)(s->site + 1)); + patched++; + } + if (patched) { svc_sites += patched; svc_pages++; } + else munmap(relay, page); + first = end; + } +} diff --git a/tools/proot/fastpath/svc-stack.h b/tools/proot/fastpath/svc-stack.h new file mode 100644 index 000000000..d3a4ff528 --- /dev/null +++ b/tools/proot/fastpath/svc-stack.h @@ -0,0 +1,83 @@ +#include +#include + +static __thread uintptr_t svc_stack_low __attribute__((tls_model("initial-exec"))); +static __thread uintptr_t svc_stack_high __attribute__((tls_model("initial-exec"))); +static __thread uintptr_t svc_stack_gap __attribute__((tls_model("initial-exec"))); +static __thread int svc_stack_main __attribute__((tls_model("initial-exec"))); + +static int svc_hex(const char **text, uintptr_t *value) { + const char *p = *text; + uintptr_t n = 0; + int digits = 0; + for (;; p++) { + unsigned c = (unsigned char)*p; + unsigned d = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : 16; + if (d == 16) break; + if (++digits > (int)(2 * sizeof n)) return 0; + n = (n << 4) | d; + } + *text = p; + *value = n; + return digits > 0; +} + +static int svc_stack_line(const char *line, uintptr_t sp, uintptr_t *low, uintptr_t *high) { + uintptr_t a, b; + if (!svc_hex(&line, &a) || *line++ != '-' || !svc_hex(&line, &b) || + *line++ != ' ' || line[0] != 'r' || line[1] != 'w' || a >= b || sp < a || sp >= b) return 0; + *low = a; + *high = b; + return 1; +} + +static __attribute__((noinline)) int svc_stack_find(uintptr_t sp) { + char chunk[512], line[96]; + size_t at = 0; + long fd = sc(SYS_openat, AT_FDCWD, (long)"/proc/self/maps", O_RDONLY | O_CLOEXEC, 0, 0); + if (fd < 0) return 0; + long n; + uintptr_t low = 0, high = 0, previous = 0; + int found = 0; + while (!found && (n = sc(SYS_read, fd, (long)chunk, sizeof chunk, 0, 0)) > 0) { + for (long i = 0; i < n; i++) { + if (chunk[i] == '\n') { + line[at] = 0; + found = svc_stack_line(line, sp, &low, &high); + if (!found) { + const char *p = line; + uintptr_t a, b; + if (svc_hex(&p, &a) && *p++ == '-' && svc_hex(&p, &b) && b <= sp) previous = b; + } + at = 0; + if (found) break; + } else if (at < sizeof line - 1) line[at++] = chunk[i]; + } + } + sc(SYS_close, fd, 0, 0, 0, 0); + if (found) { + svc_stack_low = 0; + svc_stack_high = high; + svc_stack_main = strstr(line, "[stack]") != NULL; + svc_stack_gap = previous < low && low - previous > 16 * 1024 * 1024 ? previous + 16 * 1024 * 1024 : low; + svc_stack_low = low; + } + return found; +} + +static int svc_stack_room(uintptr_t sp) { + stack_t alt; + if (sc(SYS_sigaltstack, 0, (long)&alt, 0, 0, 0) < 0 || (alt.ss_flags & SS_ONSTACK)) return 0; + if (!svc_stack_low || sp < svc_stack_low || sp >= svc_stack_high) + if (!svc_stack_find(sp)) return 0; + uintptr_t low = svc_stack_low; + if (svc_stack_main) { + struct rlimit limit; + if (sc(SYS_prlimit64, 0, RLIMIT_STACK, 0, (long)&limit, 0) < 0) return 0; + uintptr_t grow = limit.rlim_cur >= svc_stack_high ? 0 : svc_stack_high - limit.rlim_cur; + if (grow < svc_stack_gap) grow = svc_stack_gap; + if (grow < low) low = grow; + } + /* Path translation nests several PATH_MAX-sized frames. */ + return sp - low >= 96 * 1024; +} diff --git a/tools/tests/test_fastpath_svc.py b/tools/tests/test_fastpath_svc.py index a0d84e924..41c1be275 100644 --- a/tools/tests/test_fastpath_svc.py +++ b/tools/tests/test_fastpath_svc.py @@ -9,6 +9,120 @@ HEADER = Path(__file__).resolve().parents[1] / "proot/fastpath/svc-patch.h" class FastPathSvcEncodingTest(unittest.TestCase): + def compile_check(self, source, header): + if shutil.which("cc") is None: + self.skipTest("cc is not available") + with tempfile.TemporaryDirectory() as directory: + program = Path(directory) / "check.c" + program.write_text(source.replace('"HEADER"', '"' + HEADER.with_name(header).as_posix() + '"')) + output = Path(directory) / "check" + subprocess.run(["cc", "-O2", "-Wall", "-Wextra", "-Werror", str(program), "-o", str(output)], check=True) + subprocess.run([str(output)], check=True, timeout=10) + + def test_stack_bounds_and_alternate_stack_fallback(self): + self.compile_check(r''' +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +static rlim_t stack_limit = 1024 * 1024; +static long sc(long n, long a, long b, long c, long d, long e) { + if (n == SYS_prlimit64) { + if (!stack_limit) return -1; + ((struct rlimit *)d)->rlim_cur = stack_limit; + return 0; + } + return syscall(n,a,b,c,d,e); +} +#include "HEADER" +int main(void) { + uintptr_t low, high; + assert(svc_stack_line("1000-9000 rw-p 00000000 00:00 0", 0x8000, &low, &high)); + assert(low == 0x1000 && high == 0x9000); + assert(!svc_stack_line("1000-9000 r--p", 0x8000, &low, &high)); + assert(!svc_stack_line("1000-9000 rw-p", 0x9000, &low, &high)); + assert(!svc_stack_line("9000-1000 rw-p", 0x8000, &low, &high)); + assert(!svc_stack_line("fffffffffffffffff-9000 rw-p", 0x8000, &low, &high)); + assert(!svc_stack_line("", 0x8000, &low, &high)); + char local; + assert(svc_stack_find((uintptr_t)&local)); + assert((uintptr_t)&local >= svc_stack_low && (uintptr_t)&local < svc_stack_high); + svc_stack_main = 0; svc_stack_low = 0x1000; svc_stack_high = 0x100000; + assert(!svc_stack_room(0x1000 + 96 * 1024 - 1)); + assert(svc_stack_room(0x1000 + 96 * 1024)); + svc_stack_main = 1; svc_stack_low = 0xf0000; svc_stack_gap = 0x1000; + assert(svc_stack_room(0xff000)); + stack_limit = 64 * 1024; + assert(!svc_stack_room(0xff000)); + stack_limit = 0; + assert(!svc_stack_room(0xff000)); + return 0; +} +''', "svc-stack.h") + + def test_patch_publication_failure_and_partial_success(self): + self.compile_check(r''' +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +static struct svc_site { uint32_t *site; uint32_t branch; } svc_plan[3]; +static size_t svc_planned = 3; +static unsigned svc_sites, svc_pages; +static int fail_alloc, fail_emit, fail_rx, fail_write, writes, live, rx; +static uint32_t *svc_relay_page(uintptr_t site, size_t page) { + (void)site; + if (fail_alloc) return NULL; + live++; rx = 0; + return calloc(1, page); +} +static int svc_emit(uint32_t *code, uint32_t *site, uint32_t *branch) { + (void)code; (void)site; + *branch = 0x14000001; + return !fail_emit; +} +static int mock_rx(void *p, size_t size, int prot) { + (void)p; (void)size; + assert(prot == (PROT_READ | PROT_EXEC)); + if (fail_rx) return -1; + rx = 1; + return 0; +} +static int mock_unmap(void *p, size_t size) { (void)size; free(p); live--; return 0; } +static long sc(long nr, long fd, long buf, long len, long at, long unused) { + (void)fd; (void)unused; + assert(nr == SYS_pwrite64 && len == 4 && rx); + if (++writes == fail_write) return -1; + memcpy((void *)at, (void *)buf, len); + return len; +} +#define mprotect mock_rx +#define munmap mock_unmap +#include "HEADER" +int main(void) { + static uint32_t sites[3]; + for (int mode = 0; mode < 5; mode++) { + for (int i=0; i<3; i++) { sites[i]=0xd4000001; svc_plan[i].site=&sites[i]; } + svc_sites=svc_pages=writes=live=rx=0; + fail_alloc=mode==0; fail_emit=mode==1; fail_rx=mode==2; fail_write=mode==3 ? 2 : 0; + svc_publish(7,4096); + if (mode<3) assert(!svc_sites && !svc_pages && !writes && !live); + else assert(svc_sites==(mode==3 ? 2U : 3U) && svc_pages==1 && live==1); + for (int i=0; i<3; i++) assert(sites[i]==(mode<3 || (mode==3 && i==1) ? 0xd4000001 : 0x14000001)); + } + return 0; +} +''', "svc-publish.h") + def test_instruction_decoding_and_branch_limits(self): if shutil.which("cc") is None: self.skipTest("cc is not available") From ac8d97e098924548360aff1a8f8984d20e624887 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:11:05 -0400 Subject: [PATCH 8/9] test(proot): target memory patch access denial --- tools/proot/bench/svc-fail.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/proot/bench/svc-fail.c b/tools/proot/bench/svc-fail.c index 94430bad4..3dcb8badf 100644 --- a/tools/proot/bench/svc-fail.c +++ b/tools/proot/bench/svc-fail.c @@ -6,11 +6,11 @@ int main(int argc, char **argv) { #if defined(__aarch64__) - assert(argc == 2 && fp_ready() && !svc_sites); + assert(argc == 2 && fp_ready() && !svc_sites && svc_single_thread()); int nr = !strcmp(argv[1], "mem") ? SYS_openat : !strcmp(argv[1], "write") ? SYS_pwrite64 : !strcmp(argv[1], "rx") ? SYS_mprotect : SYS_mmap; - unsigned offset = nr == SYS_mprotect ? offsetof(struct seccomp_data, args[2]) : offsetof(struct seccomp_data, args[3]); - unsigned mask = nr == SYS_mprotect ? PROT_EXEC : nr == SYS_mmap ? MAP_FIXED_NOREPLACE : ~0U; + unsigned offset = nr == SYS_mprotect || nr == SYS_openat ? offsetof(struct seccomp_data, args[2]) : offsetof(struct seccomp_data, args[3]); + unsigned mask = nr == SYS_mprotect ? PROT_EXEC : nr == SYS_mmap ? MAP_FIXED_NOREPLACE : nr == SYS_openat ? O_RDWR : ~0U; struct sock_filter code[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 3), @@ -19,11 +19,12 @@ int main(int argc, char **argv) { BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EACCES), BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), }; - if (nr == SYS_openat || nr == SYS_pwrite64) code[2] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_IMM, 1); + if (nr == SYS_pwrite64) code[2] = (struct sock_filter)BPF_STMT(BPF_LD | BPF_IMM, 1); struct sock_fprog filter = {.len = sizeof code / sizeof *code, .filter = code}; assert(setenv("PROOT_FP_PATCH_SVC", "1", 1) == 0); if (sc(SYS_prctl, PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || sc(SYS_prctl, PR_SET_SECCOMP, SECCOMP_MODE_FILTER, (long)&filter, 0, 0)) return 77; + assert(svc_single_thread()); svc_patch_init(); assert(svc_sites == 0 && svc_pages == 0); struct stat st; From 2c3c135b52c3dd949ef5a99a67d8c12eba24406c Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:19:29 -0400 Subject: [PATCH 9/9] test(proot): address benchmark review findings --- docs/development/proroot-analysis.md | 14 +++++++++++++- tools/proot/bench/checks.h | 10 ++++++++++ tools/proot/bench/svc-check.c | 2 +- tools/proot/bench/svc-fail.c | 2 +- tools/proot/bench/svc-race.c | 2 +- tools/proot/bench/svc-storage.c | 2 +- tools/proot/bench/svc-threads.c | 2 +- tools/proot/bench/svc-workload.c | 7 ++++--- tools/tests/test_fastpath_svc.py | 13 ++++++++++++- 9 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 tools/proot/bench/checks.h diff --git a/docs/development/proroot-analysis.md b/docs/development/proroot-analysis.md index f3bdb0e39..7d4139c35 100644 --- a/docs/development/proroot-analysis.md +++ b/docs/development/proroot-analysis.md @@ -70,7 +70,7 @@ handles the same constraint with `libproot-loader.so`. | path calls from glibc internals (ld.so, fts, realpath, nss, `__open_nocancel`) and raw `svc` | **still go to proot (15–60 µs)** | **patched svc sites** | | Android-blocked syscalls (`set_robust_list`, `faccessat2`, `clone3`, ...) | SIGSYS stop answered by proot (0016 etc.), one ptrace round trip and one signal round trip each | in-process | | `brk` | proot emulation, a trap per call (mitigated by `top_pad`) | in-process emulation | -| correctness fallback | proot is always there | none: anything missed runs unconfined or is emulated wrongly | +| correctness fallback | proot is always there | limited in-process seccomp/SIGSYS handling; no general tracer fallback, so calls outside the filter can run unconfined or be emulated incorrectly | | long Unix socket paths | fast path `/proc/self/fd/N/name` aliases | short socket aliases (`proroot_create_short_unix_socket_alias`) | | rseq | `GLIBC_TUNABLES=glibc.pthread.rseq=0` | trampoline | @@ -533,3 +533,15 @@ PID. Per-run Steam/CEF medians ranged 1,217.9–1,240.9 MiB off and variation, but it is not evidence of zero memory cost. PSS avoids counting shared pages in full for each process. Startup and memory comparisons used one unchanged APK; no downloads or games were initiated during them. + +CodeRabbit follow-up: the workload now compares canonical paths, so relative and +absolute fixture arguments agree. All SVC bench tools keep their checks active +under NDEBUG; their clock, syscall and thread operations cannot silently disappear. +Both cases were rebuilt with -DNDEBUG and checked on Thor. The memory-file-denial +test now blocks only O_RDWR opens and asserts the single-thread check still works, +so it reaches the intended /proc/self/mem failure. The initial proroot comparison +also distinguishes its limited in-process fallback from a general tracer. +The request to return to default-off was addressed through the interruption/unwind +fixes and tests while retaining the requested default. The proposed readlink count +change was not applied because exact boundary tests reproduce the current PRoot +behavior; changing that inherited policy belongs in a separate correctness change. diff --git a/tools/proot/bench/checks.h b/tools/proot/bench/checks.h new file mode 100644 index 000000000..2b0f9064a --- /dev/null +++ b/tools/proot/bench/checks.h @@ -0,0 +1,10 @@ +#ifndef DROIDDECK_BENCH_CHECKS_H +#define DROIDDECK_BENCH_CHECKS_H + +/* Benchmark assertions execute the operations being measured. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include + +#endif diff --git a/tools/proot/bench/svc-check.c b/tools/proot/bench/svc-check.c index 664181d60..bb40bdece 100644 --- a/tools/proot/bench/svc-check.c +++ b/tools/proot/bench/svc-check.c @@ -3,7 +3,7 @@ static void interruption_hook(const long *); #define FP_SVC_TEST_HOOK interruption_hook #endif #include "../fastpath/fastpath.c" -#include +#include "checks.h" #include #include #include diff --git a/tools/proot/bench/svc-fail.c b/tools/proot/bench/svc-fail.c index 3dcb8badf..c53bf8e18 100644 --- a/tools/proot/bench/svc-fail.c +++ b/tools/proot/bench/svc-fail.c @@ -1,5 +1,5 @@ #include "../fastpath/fastpath.c" -#include +#include "checks.h" #include #include #include diff --git a/tools/proot/bench/svc-race.c b/tools/proot/bench/svc-race.c index 4198f4e36..efd6a22fb 100644 --- a/tools/proot/bench/svc-race.c +++ b/tools/proot/bench/svc-race.c @@ -1,5 +1,5 @@ #define _GNU_SOURCE -#include +#include "checks.h" #include #include #include diff --git a/tools/proot/bench/svc-storage.c b/tools/proot/bench/svc-storage.c index 2b9dd40ca..ff829ecb9 100644 --- a/tools/proot/bench/svc-storage.c +++ b/tools/proot/bench/svc-storage.c @@ -1,5 +1,5 @@ #define _GNU_SOURCE -#include +#include "checks.h" #include #include #include diff --git a/tools/proot/bench/svc-threads.c b/tools/proot/bench/svc-threads.c index 88988bd2e..6eae555a8 100644 --- a/tools/proot/bench/svc-threads.c +++ b/tools/proot/bench/svc-threads.c @@ -1,5 +1,5 @@ #define _GNU_SOURCE -#include +#include "checks.h" #include #include #include diff --git a/tools/proot/bench/svc-workload.c b/tools/proot/bench/svc-workload.c index 350a7081c..89f0dc590 100644 --- a/tools/proot/bench/svc-workload.c +++ b/tools/proot/bench/svc-workload.c @@ -1,5 +1,5 @@ #define _GNU_SOURCE -#include +#include "checks.h" #include #include #include @@ -59,8 +59,9 @@ int main(int argc, char **argv) { } assert(fts_close(fts) == 0); printf("fts: %u files %u dirs %u links\n", files, dirs, links); - char resolved[4096]; - assert(realpath(link, resolved) && !strcmp(resolved, path)); + char resolved[4096], canonical[4096]; + assert(realpath(path, canonical)); + assert(realpath(link, resolved) && !strcmp(resolved, canonical)); struct passwd pw, *found; char buf[16384]; r = getpwnam_r("root", &pw, buf, sizeof buf, &found); diff --git a/tools/tests/test_fastpath_svc.py b/tools/tests/test_fastpath_svc.py index 41c1be275..7a74aac9a 100644 --- a/tools/tests/test_fastpath_svc.py +++ b/tools/tests/test_fastpath_svc.py @@ -14,11 +14,22 @@ class FastPathSvcEncodingTest(unittest.TestCase): self.skipTest("cc is not available") with tempfile.TemporaryDirectory() as directory: program = Path(directory) / "check.c" - program.write_text(source.replace('"HEADER"', '"' + HEADER.with_name(header).as_posix() + '"')) + program.write_text(source.replace('"HEADER"', '"' + (HEADER.parent / header).as_posix() + '"')) output = Path(directory) / "check" subprocess.run(["cc", "-O2", "-Wall", "-Wextra", "-Werror", str(program), "-o", str(output)], check=True) subprocess.run([str(output)], check=True, timeout=10) + def test_benchmark_checks_execute_with_ndebug(self): + self.compile_check(r''' +#define NDEBUG +#include "HEADER" +int main(void) { + int calls = 0; + assert(++calls == 1); + return calls != 1; +} +''', "../bench/checks.h") + def test_stack_bounds_and_alternate_stack_fallback(self): self.compile_check(r''' #define _GNU_SOURCE