Compare commits

...
Author SHA1 Message Date
MaxsTechReview f39dcb4823 Merge branch 'main' into fix/steam-second-library 2026-10-05 07:35:33 -04:00
maxjivi05 5c6765233a Keep the second library and Games folders visible to the fast path
The fast path only kept the first 64 binds and silently dropped the
rest. A second library adds about 36 binds: the library itself and
private overrides for compatdata, shadercache and each Proton, runtime
and FEX depot, under both /mnt/droiddeck-sd and the legacy
/mnt/bannerlator-sd. With one chosen, a session has 96 binds.

Everything past the 64th was then mapped into the rootfs instead:
- the Games folders, bound last, showed their names but every stat and
  open failed, so owned games never matched. Their source stamps were
  null, the watcher restarted Steam every session to import them, and
  they stayed non-Steam shortcuts.
- most of the library's private Proton, runtime and downloading
  overrides were dropped. Opens and stats there reached the shared
  folder while proot's own calls reached the private copy.

The library now holds 256 binds of up to 1023 bytes per side. A view
that does not fit turns the fast path off instead of answering with a
partial table. The launcher checks the same limits before it hands the
binds over. If they do not fit, the session runs on proot alone and
the log says so.

Checked on device (SM8850, Android 16) with a second library and a
Games folder, in Deck mode and with Deck mode off:
- all 96 binds are active and the Games folder's files stat correctly
  with the fast path on
- both owned games route to their Steam entries; no shortcuts are left
  and Steam is not restarted again
- the library stays registered in libraryfolders.vdf
2026-10-04 15:54:49 -04:00
4 changed files with 70 additions and 30 deletions

No files matched your search

@@ -21,6 +21,8 @@ import java.security.MessageDigest
*/
object ProotFastPath {
const val LIBRARY = "/usr/local/lib/libblfastpath.so"
const val MAX_BINDS = 256
const val MAX_BIND_PATH = 1023
fun enabled(context: Context): Boolean =
SessionPrefs.prootFastPath(context) && !SessionPrefs.prootNoSeccomp(context)
@@ -28,6 +30,7 @@ object ProotFastPath {
/** The key for this rootfs and these binds, or null when the library cannot be told them. */
fun key(root: File, binds: List<String>): String? {
if (binds.any { it.contains('|') } || root.path.contains('|')) return null
if (binds.size > MAX_BINDS || binds.any { spec -> spec.split(':', limit = 2).any { it.length > MAX_BIND_PATH } }) return null
val digest = MessageDigest.getInstance("SHA-256")
.digest((root.path + "\n" + binds.joinToString("\n")).toByteArray())
return digest.take(12).joinToString("") { "%02x".format(it) }
@@ -402,7 +402,7 @@ class SessionService : Service() {
guest.addAll(fastPathAt, env)
shellGuest.addAll(fastPathAt, env)
Log.i(TAG, "proot: fast path on (${prootBinds.size} binds)")
}
} ?: null.also { Log.w(TAG, "proot: fast path off, it cannot be told these ${prootBinds.size} binds") }
} else null
val command = LinuxRuntime.command(
@@ -0,0 +1,28 @@
package com.droiddeck.launcher.runtime
import java.io.File
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
class ProotFastPathTest {
private val root = File("/data/user/0/com.droiddeck.launcher/files/linuxfs")
private fun binds(n: Int) = (0 until n).map { "/data/user/0/com.droiddeck.launcher/files/steam-libraries/x/steamapps/common/tool$it:/mnt/droiddeck-sd/steamapps/common/tool$it" }
@Test fun aSessionWithASecondLibraryAndGamesFoldersKeepsTheFastPath() {
assertNotNull(ProotFastPath.key(root, binds(96)))
assertNotNull(ProotFastPath.key(root, binds(ProotFastPath.MAX_BINDS)))
}
@Test fun moreBindsThanTheLibraryHoldsTurnTheFastPathOff() {
assertNull(ProotFastPath.key(root, binds(ProotFastPath.MAX_BINDS + 1)))
}
@Test fun aPathTooLongForTheLibraryTurnsTheFastPathOff() {
val long = "/storage/emulated/0/" + "d".repeat(ProotFastPath.MAX_BIND_PATH)
assertNull(ProotFastPath.key(root, binds(3) + "$long:/root/Games/Games"))
assertNull(ProotFastPath.key(root, binds(3) + "/storage/emulated/0/Games:/root/Games/$long"))
assertNotNull(ProotFastPath.key(root, binds(3) + "/storage/emulated/0/Games:/root/Games/Games"))
}
}
+38 -29
View File
@@ -48,7 +48,8 @@
#define FP_STUB_ADDR 0xffff00000UL /* must match FASTPATH_STUB_ADDR in proot's seccomp.c */
#define FP_STUB_SIZE 4096UL
#define MAX_BINDS 64
#define MAX_BINDS 256
#define BIND_PATH 1024
#define FP_SLOW (-100000L)
typedef long (*stub_fn)(long nr, long a, long b, long c, long d, long e, long f);
@@ -87,14 +88,18 @@ static void stats(void) {
}
/* The canonical form of a host path (bindings may name /sdcard, /data/user/0, ...). */
static void canonical(char *path, size_t size) {
static int canonical(char *path, size_t size) {
long fd = sc(SYS_openat, AT_FDCWD, (long)path, O_PATH | O_CLOEXEC, 0, 0);
if (fd < 0) return;
if (fd < 0) return 1;
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 && (size_t)n < size) { out[n] = 0; memcpy(path, out, n + 1); }
if (n <= 0) return 1;
if ((size_t)n >= size) return 0;
out[n] = 0;
memcpy(path, out, n + 1);
return 1;
}
static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER;
@@ -162,30 +167,31 @@ static void fp_init(void) {
snprintf(root, sizeof root, "%s", r);
canonical(root, sizeof root);
rootlen = strlen(root);
for (const char *s = b; s && *s && nbinds < MAX_BINDS;) {
static char hosts[MAX_BINDS][BIND_PATH], guests[MAX_BINDS][BIND_PATH];
for (const char *s = b; s && *s;) {
const char *end = strchr(s, '|');
size_t len = end ? (size_t)(end - s) : strlen(s);
char spec[2 * PATH_MAX];
if (len > 0 && len < sizeof spec) {
memcpy(spec, s, len);
spec[len] = 0;
char *colon = strchr(spec, ':');
if (colon) *colon = 0;
struct bind *bd = &binds[nbinds];
/* Canonicalised on first use: some (/sdcard) are FUSE, slow to look up at every exec.
* Static storage: malloc here would grow the heap with brk(2), which proot traps. */
static char hosts[MAX_BINDS][PATH_MAX], guests[MAX_BINDS][512];
const char *g = colon ? colon + 1 : spec;
if (strlen(g) >= sizeof guests[0]) { s = end ? end + 1 : NULL; continue; }
bd->host = hosts[nbinds];
bd->guest = guests[nbinds];
snprintf(bd->guest, sizeof guests[0], "%s", g);
snprintf(bd->host, PATH_MAX, "%.*s", PATH_MAX - 1, spec);
bd->hlen = strlen(bd->host);
bd->glen = strlen(bd->guest);
if (bd->guest[0] == '/' && bd->glen > 1 && bd->hlen > 0) nbinds++;
}
char spec[2 * BIND_PATH];
if (len >= sizeof spec) return;
memcpy(spec, s, len);
spec[len] = 0;
s = end ? end + 1 : NULL;
if (len == 0) continue;
if (nbinds == MAX_BINDS) return;
char *colon = strchr(spec, ':');
if (colon) *colon = 0;
const char *g = colon ? colon + 1 : spec;
if (strlen(spec) >= BIND_PATH || strlen(g) >= BIND_PATH) return;
/* Canonicalised on first use: some (/sdcard) are FUSE, slow to look up at every exec.
* Static storage: malloc here would grow the heap with brk(2), which proot traps. */
struct bind *bd = &binds[nbinds];
bd->host = hosts[nbinds];
bd->guest = guests[nbinds];
snprintf(bd->guest, BIND_PATH, "%s", g);
snprintf(bd->host, BIND_PATH, "%s", spec);
bd->hlen = strlen(bd->host);
bd->glen = strlen(bd->guest);
if (bd->guest[0] == '/' && bd->glen > 1 && bd->hlen > 0) nbinds++;
}
pthread_atfork(NULL, NULL, after_fork); /* registered once per exec */
fp_on = 1;
@@ -196,13 +202,16 @@ static pthread_mutex_t bind_lock = PTHREAD_MUTEX_INITIALIZER;
static const struct bind *canon(struct bind *bd) {
if (__atomic_load_n(&bd->canon, __ATOMIC_ACQUIRE)) return bd;
if (pthread_mutex_trylock(&bind_lock) != 0) return NULL;
int ok = 1;
if (!bd->canon) {
canonical(bd->host, PATH_MAX);
bd->hlen = strlen(bd->host);
__atomic_store_n(&bd->canon, 1, __ATOMIC_RELEASE);
ok = canonical(bd->host, BIND_PATH);
if (ok) {
bd->hlen = strlen(bd->host);
__atomic_store_n(&bd->canon, 1, __ATOMIC_RELEASE);
}
}
pthread_mutex_unlock(&bind_lock);
return bd;
return ok ? bd : NULL;
}
/* ---------------------------------------------------------------- verified parents */