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)