Files
The412Banner ae94d9db54 feat: optional x86-64 Steam client (Valve's Linux client under FEX) beside the arm64 one (#152)
An opt-in second Steam client (Steam settings, Steam client: x86-64): Valve's
x86-64 Linux client, unchanged, runs under FEX in its own home with its own
game library and the same sign-in. Linux builds run directly, so titles such
as Team Fortress 2 and Half-Life use their Linux versions; Windows games use
DroidDeck-Proton as on arm64. The arm64 client stays the default.

- droiddeck-steam-x64: FEX and the x86-64 rootfs from DroidDeck-Components
  (sha256-pinned), client install and sync from Valve's CDN, sign-in shared
  with the arm64 client, its web helper on the GPU (ANGLE GL over FEX's GL
  thunk, zink, Turnip), Proton wrappers for the arm64 client's tools,
  DroidDeck direct for Linux builds, SD-card library (x86-64/ beside the
  arm64 one), repair.
- FEX 2610 built by build-fex.yml with thunks and three patches: an openat2
  fallback without openat2 (proot), newer-clang thunkgen, MAP_32BIT
  exhaustion returning ENOMEM. libblsession resolves RESOLVE_IN_ROOT paths
  inside the root.
- Session: x86-64 gating, shortcuts and compatibility tools per client,
  logs from the client that ran.
- App: client setting with the FEX version, an x86-64 chip on the Games
  header, the active client's library, art, storage, downloads, repair,
  Offline Mode, Decky, desktop mode, optional VAC diagnostics; per-client
  state for art, the content log, repair and leftovers.
- Strings in every locale.
- Review fixes: the library cache is saved only by the newest scan; an
  abandoned session's Steam logs come from the client it recorded; distinct
  VAC console names; FEXInterpreter in CPU pinning; clearer VAC hint in
  pl, ru, uk, cs and bg.
2026-10-17 23:57:52 -04:00

403 lines
13 KiB
C

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <spawn.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <linux/netlink.h>
#include <netinet/in.h>
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
static const char *net_dir(void) {
static char dir[256];
if (!dir[0]) {
const char *base = getenv("BL_SYSV_DIR");
snprintf(dir, sizeof(dir), "%s/net", base && *base ? base : "/dev/shm/wnsysv");
char parent[256];
snprintf(parent, sizeof(parent), "%s", dir);
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
mkdir(parent, 0777);
}
mkdir(dir, 0777);
}
return dir;
}
static int loopback_family(const struct sockaddr *address, socklen_t length) {
if (address && address->sa_family == AF_INET && length >= sizeof(struct sockaddr_in)) {
return ((const struct sockaddr_in *)address)->sin_addr.s_addr == htonl(INADDR_LOOPBACK) ? AF_INET : 0;
}
if (address && address->sa_family == AF_INET6 && length >= sizeof(struct sockaddr_in6)) {
const struct in6_addr *ip = &((const struct sockaddr_in6 *)address)->sin6_addr;
if (IN6_IS_ADDR_LOOPBACK(ip)) return AF_INET6;
if (IN6_IS_ADDR_V4MAPPED(ip) && ip->s6_addr[12] == 127 && ip->s6_addr[13] == 0 &&
ip->s6_addr[14] == 0 && ip->s6_addr[15] == 1) return AF_INET;
}
return 0;
}
static void record_port(int fd, unsigned peer) {
struct sockaddr_storage ss;
socklen_t len = sizeof(ss);
if (getsockname(fd, (struct sockaddr *)&ss, &len) != 0) {
return;
}
int family = loopback_family((struct sockaddr *)&ss, len);
if (!family) return;
unsigned port;
if (ss.ss_family == AF_INET) {
struct sockaddr_in *in = (struct sockaddr_in *)&ss;
port = ntohs(in->sin_port);
} else if (ss.ss_family == AF_INET6) {
port = ntohs(((struct sockaddr_in6 *)&ss)->sin6_port);
} else {
return;
}
if (port == 0) {
return;
}
struct stat socket_stat;
if (fstat(fd, &socket_stat) != 0) return;
char path[300];
snprintf(path, sizeof(path), "%s/p%u", net_dir(), port);
char staged[360];
snprintf(staged, sizeof(staged), "%s.tmp.%d.%d", path, (int)getpid(), fd);
char line[128];
int n = snprintf(line, sizeof(line), "%d %d %d %u %d %llu %d\n", (int)getpid(), (int)getppid(),
(int)getuid(), peer, fd, (unsigned long long)socket_stat.st_ino, family);
int out = open(staged, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600);
if (out >= 0) {
int complete = write(out, line, n) == n;
close(out);
if (complete) rename(staged, path);
unlink(staged);
}
}
static unsigned addr_port(const struct sockaddr *addr, socklen_t len) {
if (addr && addr->sa_family == AF_INET && len >= sizeof(struct sockaddr_in)) {
return ntohs(((const struct sockaddr_in *)addr)->sin_port);
}
if (addr && addr->sa_family == AF_INET6 && len >= sizeof(struct sockaddr_in6)) {
return ntohs(((const struct sockaddr_in6 *)addr)->sin6_port);
}
return 0;
}
static unsigned peer_port(int fd) {
struct sockaddr_storage ps;
socklen_t plen = sizeof(ps);
if (getpeername(fd, (struct sockaddr *)&ps, &plen) != 0) {
return 0;
}
return addr_port((struct sockaddr *)&ps, plen);
}
typedef int (*bind_fn)(int, const struct sockaddr *, socklen_t);
typedef int (*listen_fn)(int, int);
typedef int (*connect_fn)(int, const struct sockaddr *, socklen_t);
typedef int (*accept4_fn)(int, struct sockaddr *, socklen_t *, int);
static unsigned guarded_port(void);
__attribute__((visibility("hidden"))) int bl_udevmon_stand_in(int fd);
int bind(int fd, const struct sockaddr *addr, socklen_t len) {
static bind_fn real;
if (!real) real = (bind_fn)dlsym(RTLD_NEXT, "bind");
if (bl_udevmon_stand_in(fd)) {
if (addr == NULL || len < sizeof(struct sockaddr_nl) || addr->sa_family != AF_NETLINK) {
errno = EINVAL;
return -1;
}
return 0;
}
struct sockaddr_storage local;
unsigned guarded = guarded_port();
if (guarded && addr_port(addr, len) == guarded && len <= sizeof(local)) {
memcpy(&local, addr, len);
if (addr->sa_family == AF_INET) {
((struct sockaddr_in *)&local)->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr = (struct sockaddr *)&local;
} else if (addr->sa_family == AF_INET6) {
((struct sockaddr_in6 *)&local)->sin6_addr = in6addr_loopback;
addr = (struct sockaddr *)&local;
}
}
int ret = real(fd, addr, len);
if (ret == 0) record_port(fd, 0);
return ret;
}
int listen(int fd, int backlog) {
static listen_fn real;
if (!real) real = (listen_fn)dlsym(RTLD_NEXT, "listen");
int ret = real(fd, backlog);
if (ret == 0) record_port(fd, 0);
return ret;
}
static unsigned local_port(int fd) {
struct sockaddr_storage ss;
socklen_t len = sizeof(ss);
if (getsockname(fd, (struct sockaddr *)&ss, &len) != 0) {
return 0;
}
return addr_port((struct sockaddr *)&ss, len);
}
static void bind_before_loopback_connect(int fd, const struct sockaddr *addr, socklen_t len) {
if (!loopback_family(addr, len)) return;
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
if (getsockname(fd, (struct sockaddr *)&ss, &slen) != 0 || ss.ss_family != addr->sa_family ||
addr_port((struct sockaddr *)&ss, slen) != 0) {
return;
}
struct sockaddr_storage local;
if (len > sizeof(local)) return;
memcpy(&local, addr, len);
if (addr->sa_family == AF_INET) ((struct sockaddr_in *)&local)->sin_port = 0;
else ((struct sockaddr_in6 *)&local)->sin6_port = 0;
int saved = errno;
bind(fd, (struct sockaddr *)&local, len);
errno = saved;
}
static unsigned guarded_port(void) {
static int port = -1;
if (port < 0) {
const char *value = getenv("BL_CDP_GUARD");
port = value && *value ? atoi(value) : 0;
if (port < 0 || port > 65535) port = 0;
}
return (unsigned)port;
}
static int guest_peer(int fd) {
struct sockaddr_storage address;
socklen_t length = sizeof(address);
if (getpeername(fd, (struct sockaddr *)&address, &length) != 0) return 0;
int family = loopback_family((struct sockaddr *)&address, length);
if (!family) return 0;
unsigned peer = peer_port(fd);
if (peer == 0) {
return 0;
}
char path[300];
snprintf(path, sizeof(path), "%s/p%u", net_dir(), peer);
struct stat st;
if (stat(path, &st) != 0 || time(NULL) - st.st_mtime > 30) {
return 0;
}
int in = open(path, O_RDONLY);
if (in < 0) {
return 0;
}
char buf[128];
int n = read(in, buf, sizeof(buf) - 1);
close(in);
if (n <= 0) {
return 0;
}
buf[n] = '\0';
int pid, parent, uid, descriptor, recorded_family;
unsigned destination;
unsigned long long inode;
if (sscanf(buf, "%d %d %d %u %d %llu %d", &pid, &parent, &uid, &destination, &descriptor,
&inode, &recorded_family) != 7 || pid <= 0 || descriptor < 0 || destination != guarded_port() ||
recorded_family != family) return 0;
snprintf(path, sizeof(path), "/proc/%d/fd/%d", pid, descriptor);
return stat(path, &st) == 0 && S_ISSOCK(st.st_mode) && (unsigned long long)st.st_ino == inode;
}
int connect(int fd, const struct sockaddr *addr, socklen_t len) {
static connect_fn real;
if (!real) real = (connect_fn)dlsym(RTLD_NEXT, "connect");
bind_before_loopback_connect(fd, addr, len);
unsigned peer = loopback_family(addr, len) ? addr_port(addr, len) : 0;
if (peer) record_port(fd, peer);
int ret = real(fd, addr, len);
if (ret == 0 || errno == EINPROGRESS) {
int saved = errno;
record_port(fd, peer);
errno = saved;
}
return ret;
}
int accept4(int fd, struct sockaddr *addr, socklen_t *len, int flags) {
static accept4_fn real;
if (!real) real = (accept4_fn)dlsym(RTLD_NEXT, "accept4");
for (;;) {
int ret = real(fd, addr, len, flags);
if (ret < 0) return ret;
unsigned guarded = guarded_port();
if (guarded && local_port(ret) == guarded && !guest_peer(ret)) {
close(ret);
continue;
}
record_port(ret, peer_port(ret));
return ret;
}
}
int accept(int fd, struct sockaddr *addr, socklen_t *len) {
return accept4(fd, addr, len, 0);
}
extern char **environ;
typedef int (*spawn_fn)(pid_t *, const char *, const posix_spawn_file_actions_t *, const posix_spawnattr_t *,
char *const[], char *const[]);
__attribute__((visibility("hidden"))) void bl_binfmt_before(const char *path, char *const argv[], char *const envp[]);
__attribute__((visibility("hidden"))) int bl_binfmt_retry(const char *path, char *const argv[], char *const envp[], int error);
__attribute__((visibility("hidden"))) void bl_binfmt_before_search(const char *file, char *const argv[], char *const envp[]);
__attribute__((visibility("hidden"))) int bl_binfmt_after_search(const char *file, char *const argv[], char *const envp[], int error);
__attribute__((visibility("hidden"))) char *const *bl_webhelper_argv(const char *path, char *const argv[]);
__attribute__((visibility("hidden"))) int bl_binfmt_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *fa,
const posix_spawnattr_t *attr, char *const argv[], char *const envp[],
spawn_fn real);
static int maybe_answer_lsof(const char *path, char *const argv[]) {
const char *base = strrchr(path, '/');
base = base ? base + 1 : path;
if (strcmp(base, "lsof") != 0) {
return 0;
}
const char *port_str = NULL;
for (int i = 0; argv[i]; i++) {
const char *at = strstr(argv[i], "TCP@");
if (at) {
const char *colon = strrchr(at, ':');
if (colon) port_str = colon + 1;
}
}
if (!port_str) {
return 0;
}
char path_buf[300];
snprintf(path_buf, sizeof(path_buf), "%s/p%u", net_dir(), (unsigned)atoi(port_str));
int in = open(path_buf, O_RDONLY);
if (in < 0) {
_exit(1);
}
char buf[64];
int n = read(in, buf, sizeof(buf) - 1);
close(in);
if (n <= 0) {
_exit(1);
}
buf[n] = '\0';
int pid = 0, ppid = 0, uid = 0;
unsigned peer = 0;
sscanf(buf, "%d %d %d %u", &pid, &ppid, &uid, &peer);
char out[160];
int m = peer ? snprintf(out, sizeof(out), "p%d\nR%d\nu%d\nn127.0.0.1:%u->127.0.0.1:%u\n",
pid, ppid, uid, (unsigned)atoi(port_str), peer)
: snprintf(out, sizeof(out), "p%d\nR%d\nu%d\nn127.0.0.1:%u\n",
pid, ppid, uid, (unsigned)atoi(port_str));
if (write(1, out, m) != m) { }
_exit(0);
}
static char **web_helper_redirect(const char *path, char *const argv[], const char **launcher) {
const char *want = getenv("BL_X64_ARM_WEBHELPER");
if (!want || !*want || !path) return NULL;
const char *base = strrchr(path, '/');
if (strcmp(base ? base + 1 : path, "steamwebhelper") != 0) return NULL;
if (!strstr(path, "/steamrt64/")) {
char cwd[PATH_MAX];
size_t n;
if (path[0] == '/' || !getcwd(cwd, sizeof(cwd))) return NULL;
n = strlen(cwd);
if (n < 10 || strcmp(cwd + n - 10, "/steamrt64") != 0) return NULL;
}
int argc = 0;
while (argv && argv[argc]) argc++;
char **out = calloc((size_t)argc + 2, sizeof(char *));
if (!out) return NULL;
out[0] = (char *)want;
for (int i = 1; i < argc; i++) out[i] = argv[i];
*launcher = want;
return out;
}
int execve(const char *path, char *const argv_in[], char *const envp[]) {
char *const *argv = bl_webhelper_argv(path, argv_in);
static int (*real)(const char *, char *const[], char *const[]);
if (!real) real = (int (*)(const char *, char *const[], char *const[]))dlsym(RTLD_NEXT, "execve");
maybe_answer_lsof(path, argv);
const char *launcher = NULL;
char **redirected = web_helper_redirect(path, argv, &launcher);
if (redirected) {
real(launcher, redirected, envp);
free(redirected);
}
bl_binfmt_before(path, argv, envp);
real(path, argv, envp);
return bl_binfmt_retry(path, argv, envp, errno);
}
static int answer_lsof_via_spawn(const char *path, char *const argv[], pid_t *pid) {
pid_t child = fork();
if (child < 0) {
return errno;
}
if (child == 0) {
maybe_answer_lsof(path, argv);
_exit(127);
}
if (pid) {
*pid = child;
}
return 0;
}
int execvp(const char *file, char *const argv_in[]) {
char *const *argv = bl_webhelper_argv(file, argv_in);
static int (*real)(const char *, char *const[]);
if (!real) real = (int (*)(const char *, char *const[]))dlsym(RTLD_NEXT, "execvp");
maybe_answer_lsof(file, argv);
bl_binfmt_before_search(file, argv, environ);
real(file, argv);
return bl_binfmt_after_search(file, argv, environ, errno);
}
int execv(const char *path, char *const argv_in[]) {
char *const *argv = bl_webhelper_argv(path, argv_in);
static int (*real)(const char *, char *const[]);
if (!real) real = (int (*)(const char *, char *const[]))dlsym(RTLD_NEXT, "execv");
maybe_answer_lsof(path, argv);
const char *launcher = NULL;
char **redirected = web_helper_redirect(path, argv, &launcher);
if (redirected) {
real(launcher, redirected);
free(redirected);
}
bl_binfmt_before(path, argv, environ);
real(path, argv);
return bl_binfmt_retry(path, argv, environ, errno);
}
int posix_spawn(pid_t *pid, const char *path, const posix_spawn_file_actions_t *fa,
const posix_spawnattr_t *attr, char *const argv[], char *const envp[]) {
const char *base = strrchr(path, '/');
base = base ? base + 1 : path;
if (strcmp(base, "lsof") == 0 && !fa) {
return answer_lsof_via_spawn(path, argv, pid);
}
static spawn_fn real;
if (!real) real = (spawn_fn)dlsym(RTLD_NEXT, "posix_spawn");
return bl_binfmt_spawn(pid, path, fa, attr, argv, envp, real);
}