mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
Fix Steam preallocation on external SD
This commit is contained in:
1 parent
b0d7fc01d8
commit
14031c6911
2 files changed
+282
-12
No files matched your search
@@ -0,0 +1,248 @@
|
||||
#define _GNU_SOURCE
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/statfs.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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 <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/vfs.h>
|
||||
#include <strings.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user