diff --git a/tools/linuxfs/preload/opens.c b/tools/linuxfs/preload/opens.c index 52d52ec4c..4c8b6e165 100644 --- a/tools/linuxfs/preload/opens.c +++ b/tools/linuxfs/preload/opens.c @@ -34,6 +34,7 @@ #include int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden"))); +int bl_writable_retry(int dirfd, const char *path, int flags) __attribute__((visibility("hidden"))); static int retry_without_noatime(int fd, int flags) { return fd < 0 && errno == EPERM && (flags & O_NOATIME); @@ -67,6 +68,8 @@ int open(const char *path, int flags, ...) { } fd = real_open(path, flags, mode); if (retry_without_noatime(fd, flags)) fd = real_open(path, flags & ~O_NOATIME, mode); + /* A read-only file the Steam client rewrites (perms.c). */ + if (fd < 0 && bl_writable_retry(AT_FDCWD, path, flags)) fd = real_open(path, flags & ~O_NOATIME, mode); return fd; } @@ -91,6 +94,7 @@ int openat(int dirfd, const char *path, int flags, ...) { } fd = real_openat(dirfd, path, flags, mode); if (retry_without_noatime(fd, flags)) fd = real_openat(dirfd, path, flags & ~O_NOATIME, mode); + if (fd < 0 && bl_writable_retry(dirfd, path, flags)) fd = real_openat(dirfd, path, flags & ~O_NOATIME, mode); return fd; } diff --git a/tools/linuxfs/preload/perms.c b/tools/linuxfs/preload/perms.c new file mode 100644 index 000000000..0d41fa9ee --- /dev/null +++ b/tools/linuxfs/preload/perms.c @@ -0,0 +1,76 @@ +/* + * Root's way past a read-only file, for the Steam client. + * + * The client runs as root inside the session - proot's root, which the kernel never sees: to the + * kernel it is the app's own user. The client leaves many content files read-only (r-x------, + * ~290 files of Proton Experimental (ARM64) alone), and when an update later rewrites one of them + * it opens the file for writing expecting root's permission to override the mode. The kernel + * refuses (EACCES), the client reports "Missing file permissions" and cancels the update + * (2026-09-26: Proton Experimental (ARM64), "CFileWriter: errno: 13 ... wineopenxr.dll"). + * + * So for the client's own processes, a refused write-open of a file this user owns is answered the + * way root's would be: the owner's write bit is added - to the file, or to the directory a new + * file is being created in - and the open is tried once more. chmod only succeeds on what the app + * owns, so nothing it could not already change is touched. Games, Wine and everything else keep + * ordinary permissions: only the processes started from the client's own directories take this. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +/* The client and its helpers run from the Steam root's platform directories. */ +static int is_steam_client(void) { + static int cached = -1; + char exe[PATH_MAX]; + ssize_t n; + + if (cached >= 0) return cached; + n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + cached = 0; + if (n > 0) { + exe[n] = '\0'; + cached = strstr(exe, "/Steam/steamrtarm64/") != NULL || strstr(exe, "/Steam/ubuntu12_64/") != NULL + || strstr(exe, "/Steam/linuxarm64/") != NULL; + } + return cached; +} + +static int grant_owner_write(int dirfd, const char *path) { + struct stat st; + + if (fstatat(dirfd, path, &st, 0) != 0 || (st.st_mode & S_IWUSR)) return 0; + return fchmodat(dirfd, path, (st.st_mode & 07777) | S_IWUSR, 0) == 0; +} + +/* + * After an open refused with EACCES: returns 1 when a write bit was added and the open is worth + * one more try. errno is left as the open set it. + */ +__attribute__((visibility("hidden"))) +int bl_writable_retry(int dirfd, const char *path, int flags) { + int saved = errno; + int granted = 0; + char parent[PATH_MAX]; + const char *slash; + + if (saved != EACCES || path == NULL || (flags & O_ACCMODE) == O_RDONLY || geteuid() != 0 || !is_steam_client()) return 0; + if (faccessat(dirfd, path, F_OK, 0) == 0) { + granted = grant_owner_write(dirfd, path); + } else if (flags & O_CREAT) { + /* A new file in a read-only directory. */ + slash = strrchr(path, '/'); + if (slash == NULL) { + granted = grant_owner_write(dirfd, "."); + } else if ((size_t) (slash - path) < sizeof(parent)) { + memcpy(parent, path, slash - path); + parent[slash - path] = '\0'; + granted = grant_owner_write(dirfd, parent[0] ? parent : "/"); + } + } + errno = saved; + return granted; +}