diff --git a/tools/linuxfs/preload/readdir_cache.c b/tools/linuxfs/preload/readdir_cache.c new file mode 100644 index 000000000..a5112fa0a --- /dev/null +++ b/tools/linuxfs/preload/readdir_cache.c @@ -0,0 +1,248 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define FUSE_SUPER_MAGIC 0x65735546 +#define CACHE_SLOTS 32 +#define MAX_ENTRIES 4096 +#define CACHE_TTL_NS 2000000000ULL + +struct listing { + dev_t dev; + ino_t ino; + struct timespec mtime; + struct timespec ctime; + uint64_t expires; + size_t count; + unsigned int readers; + struct dirent64 *entries; +}; + +struct reader { + DIR *dir; + struct listing *listing; + size_t pos; + struct reader *next; +}; + +static struct listing listings[CACHE_SLOTS]; +static struct reader *readers; +static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_once_t hooks_once = PTHREAD_ONCE_INIT; +static struct dirent64 *(*real_readdir64)(DIR *); +static int (*real_closedir)(DIR *); +static void (*real_rewinddir)(DIR *); +static long (*real_telldir)(DIR *); +static void (*real_seekdir)(DIR *, long); + +static void lock_for_fork(void) { pthread_mutex_lock(&cache_lock); } +static void unlock_after_fork(void) { pthread_mutex_unlock(&cache_lock); } + +static void init_hooks(void) { + real_readdir64 = dlsym(RTLD_NEXT, "readdir64"); + real_closedir = dlsym(RTLD_NEXT, "closedir"); + real_rewinddir = dlsym(RTLD_NEXT, "rewinddir"); + real_telldir = dlsym(RTLD_NEXT, "telldir"); + real_seekdir = dlsym(RTLD_NEXT, "seekdir"); + pthread_atfork(lock_for_fork, unlock_after_fork, unlock_after_fork); +} + +static int is_steam_runtime(void) { + return !strcmp(program_invocation_short_name, "steam") && + strstr(program_invocation_name, "/Steam/steamrtarm64/") != NULL; +} + +static uint64_t monotonic_ns(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (uint64_t)now.tv_sec * 1000000000ULL + now.tv_nsec; +} + +static int same_directory(const struct listing *listing, const struct stat *st, uint64_t now) { + return listing->entries && listing->expires > now && listing->dev == st->st_dev && + listing->ino == st->st_ino && listing->mtime.tv_sec == st->st_mtim.tv_sec && + listing->mtime.tv_nsec == st->st_mtim.tv_nsec && + listing->ctime.tv_sec == st->st_ctim.tv_sec && + listing->ctime.tv_nsec == st->st_ctim.tv_nsec; +} + +static struct reader *find_reader(DIR *dir) { + for (struct reader *reader = readers; reader; reader = reader->next) + if (reader->dir == dir) return reader; + return NULL; +} + +static void release_reader(DIR *dir) { + struct reader **link = &readers; + while (*link) { + if ((*link)->dir == dir) { + struct reader *reader = *link; + reader->listing->readers--; + *link = reader->next; + free(reader); + return; + } + link = &(*link)->next; + } +} + +static struct listing *available_listing(void) { + struct listing *oldest = NULL; + for (size_t i = 0; i < CACHE_SLOTS; i++) { + struct listing *listing = &listings[i]; + if (listing->readers == 0 && (!oldest || listing->expires < oldest->expires)) + oldest = listing; + } + return oldest; +} + +/* Capture a directory only for Steam's FUSE-backed library. The short lifetime is a fallback + * for filesystems that fail to update directory timestamps; normal create/remove operations + * invalidate the snapshot as soon as fstat observes their new mtime or ctime. */ +static struct listing *listing_for(DIR *dir) { + int fd = dirfd(dir); + struct statfs fs; + struct stat before; + if (!real_telldir || !real_rewinddir || real_telldir(dir) != 0 || fd < 0 || + fstatfs(fd, &fs) != 0 || fs.f_type != FUSE_SUPER_MAGIC || fstat(fd, &before) != 0) + return NULL; + + uint64_t now = monotonic_ns(); + for (size_t i = 0; i < CACHE_SLOTS; i++) + if (same_directory(&listings[i], &before, now)) return &listings[i]; + + struct listing *slot = available_listing(); + if (!slot) return NULL; + + struct dirent64 *entries = calloc(MAX_ENTRIES, sizeof(*entries)); + if (!entries) return NULL; + size_t count = 0; + int error = 0; + while (count < MAX_ENTRIES) { + errno = 0; + struct dirent64 *entry = real_readdir64(dir); + if (!entry) { + error = errno; + break; + } + size_t bytes = entry->d_reclen < sizeof(*entry) ? entry->d_reclen : sizeof(*entry); + memcpy(&entries[count++], entry, bytes); + } + real_rewinddir(dir); + + struct stat after; + if (count == MAX_ENTRIES || error || fstat(fd, &after) != 0 || + before.st_dev != after.st_dev || before.st_ino != after.st_ino || + before.st_mtim.tv_sec != after.st_mtim.tv_sec || + before.st_mtim.tv_nsec != after.st_mtim.tv_nsec || + before.st_ctim.tv_sec != after.st_ctim.tv_sec || + before.st_ctim.tv_nsec != after.st_ctim.tv_nsec) { + free(entries); + return NULL; + } + + struct dirent64 *small = realloc(entries, (count ? count : 1) * sizeof(*entries)); + if (small) entries = small; + free(slot->entries); + *slot = (struct listing){ + .dev = before.st_dev, + .ino = before.st_ino, + .mtime = before.st_mtim, + .ctime = before.st_ctim, + .expires = monotonic_ns() + CACHE_TTL_NS, + .count = count, + .entries = entries, + }; + return slot; +} + +struct dirent64 *readdir64(DIR *dir) { + pthread_once(&hooks_once, init_hooks); + if (!real_readdir64) { errno = ENOSYS; return NULL; } + if (!is_steam_runtime()) return real_readdir64(dir); + + int saved_errno = errno; + pthread_mutex_lock(&cache_lock); + struct reader *reader = find_reader(dir); + if (!reader) { + struct listing *listing = listing_for(dir); + if (listing && (reader = malloc(sizeof(*reader)))) { + *reader = (struct reader){dir, listing, 0, readers}; + readers = reader; + listing->readers++; + } + } + struct dirent64 *entry = NULL; + if (reader && reader->pos < reader->listing->count) + entry = &reader->listing->entries[reader->pos++]; + pthread_mutex_unlock(&cache_lock); + + if (reader) { + errno = saved_errno; + return entry; + } + return real_readdir64(dir); +} + +int closedir(DIR *dir) { + pthread_once(&hooks_once, init_hooks); + if (!real_closedir) { errno = ENOSYS; return -1; } + int saved_errno = errno; + pthread_mutex_lock(&cache_lock); + release_reader(dir); + pthread_mutex_unlock(&cache_lock); + errno = saved_errno; + return real_closedir(dir); +} + +void rewinddir(DIR *dir) { + pthread_once(&hooks_once, init_hooks); + int saved_errno = errno; + pthread_mutex_lock(&cache_lock); + release_reader(dir); + pthread_mutex_unlock(&cache_lock); + if (real_rewinddir) real_rewinddir(dir); + errno = saved_errno; +} + +long telldir(DIR *dir) { + pthread_once(&hooks_once, init_hooks); + int saved_errno = errno; + pthread_mutex_lock(&cache_lock); + struct reader *reader = find_reader(dir); + long offset = reader ? (reader->pos ? reader->listing->entries[reader->pos - 1].d_off : 0) : -1; + pthread_mutex_unlock(&cache_lock); + if (!reader && real_telldir) return real_telldir(dir); + if (!reader) { errno = ENOSYS; return -1; } + errno = saved_errno; + return offset; +} + +void seekdir(DIR *dir, long offset) { + pthread_once(&hooks_once, init_hooks); + int saved_errno = errno; + pthread_mutex_lock(&cache_lock); + struct reader *reader = find_reader(dir); + if (reader) { + size_t pos = 0; + if (offset) { + for (; pos < reader->listing->count; pos++) + if (reader->listing->entries[pos].d_off == offset) { pos++; break; } + if (!pos || reader->listing->entries[pos - 1].d_off != offset) + release_reader(dir), reader = NULL; + } + if (reader) reader->pos = pos; + } + pthread_mutex_unlock(&cache_lock); + if (real_seekdir) real_seekdir(dir, offset); + errno = saved_errno; +} diff --git a/tools/proot/fastpath/fastpath.c b/tools/proot/fastpath/fastpath.c index c7fb1f0b5..d1a6692e9 100644 --- a/tools/proot/fastpath/fastpath.c +++ b/tools/proot/fastpath/fastpath.c @@ -9,14 +9,15 @@ * * It decides nothing proot would decide differently. The guest path is mapped literally - the * longest binding whose guest path is a prefix, else the rootfs - and the parent directory is - * opened O_PATH and must come back from /proc/self/fd as exactly that host path, which proves no - * component on the way was a symlink: proot would then have walked the same directories. The - * last component is looked at without following it, and a symlink there goes to proot. So does - * "..", a trailing "/", anything under /proc, any flag not handled here and any failure to - * decide: the real libc call runs, proot traps it and answers as it always has. + * opened O_PATH and checked against /proc/self/fd. A case-only difference is accepted only on + * FUSE and only after each differently spelled component is verified as a directory rather than + * a symlink. The last component is looked at without following it; other symlinks go to proot. + * So do "..", a trailing "/", anything under /proc, any flag not handled here and any failure + * to decide: the real libc call runs, proot traps it and answers as it always has. * * Only syscalls Android's app seccomp policy allows are used (openat, newfstatat, statx, - * faccessat, readlinkat, close) - not openat2 or faccessat2, which it answers with SIGSYS. + * faccessat, fstatfs, readlinkat, close) - not openat2 or faccessat2, which it answers with + * SIGSYS. * * Verified parents are remembered for PROOT_FP_TTL_MS (default 2000; 0 = verify every call): a * parent directory replaced by a symlink within that window would be missed, the same trade the @@ -43,6 +44,8 @@ #include #include #include +#include +#include #include #include @@ -250,10 +253,27 @@ static long canonical_dir(const char *dir) { char link[40], out[PATH_MAX]; snprintf(link, sizeof link, "/proc/self/fd/%ld", fd); long n = sc(SYS_readlinkat, AT_FDCWD, (long)link, (long)out, sizeof out - 1, 0); - sc(SYS_close, fd, 0, 0, 0, 0); - if (n <= 0) return 0; + if (n <= 0) { sc(SYS_close, fd, 0, 0, 0, 0); return 0; } out[n] = 0; - if (strcmp(out, dir) != 0) return 0; + if (strcmp(out, dir) != 0) { + struct statfs fs; + int fuse = sc(SYS_fstatfs, fd, (long)&fs, 0, 0, 0) == 0 && fs.f_type == 0x65735546; + sc(SYS_close, fd, 0, 0, 0, 0); + if (!fuse || strcasecmp(out, dir) != 0) return 0; + /* FUSE case aliases return the stored spelling from /proc/self/fd. Prove the final + * component and each parent so a case-only symlink path still goes to proot. */ + struct stat st; + if (sc(SYS_newfstatat, AT_FDCWD, (long)dir, (long)&st, AT_SYMLINK_NOFOLLOW, 0) != 0 || + !S_ISDIR(st.st_mode)) return 0; + char parent[PATH_MAX]; + snprintf(parent, sizeof parent, "%s", dir); + char *slash = strrchr(parent, '/'); + if (!slash || slash == parent) return 0; + *slash = 0; + if (canonical_dir(parent) != 1) return 0; + } else { + sc(SYS_close, fd, 0, 0, 0, 0); + } remember(dir, h); return 1; } @@ -474,14 +494,16 @@ static int access_common(int dirfd, const char *path, int mode, int flags) { REAL(faccessat, int (*)(int, const char *, int, int)); char host[PATH_MAX]; struct stat st; - /* faccessat(2) has no flags: AT_EACCESS and AT_SYMLINK_NOFOLLOW stay with glibc and proot. */ - long rs = flags == 0 ? resolve(dirfd, path, host) : FP_SLOW; + /* F_OK with AT_SYMLINK_NOFOLLOW asks only whether the last directory entry exists. */ + int nofollow_exists = mode == F_OK && flags == AT_SYMLINK_NOFOLLOW; + long rs = flags == 0 || nofollow_exists ? resolve(dirfd, path, host) : FP_SLOW; if (rs == -ENOENT) { hits++; return ret(rs); } if (rs == 0) { long k = kind(host, &st); - if (k != 1) { + if (k != 1 || nofollow_exists) { hits++; if (k < 0) return ret(k); + if (nofollow_exists) return 0; return ret(sc(SYS_faccessat, AT_FDCWD, (long)host, mode, 0, 0)); } }