diff --git a/app/src/main/cpp/fakeinput_steam.cpp b/app/src/main/cpp/fakeinput_steam.cpp
index 9e68801fa..8598ffeb4 100644
--- a/app/src/main/cpp/fakeinput_steam.cpp
+++ b/app/src/main/cpp/fakeinput_steam.cpp
@@ -1304,11 +1304,14 @@ __attribute__((visibility("hidden"))) static bool process_is_steam_client() {
}
// What the client starts with, so a pad log says whether it was offered a Deck at all.
+__attribute__((visibility("hidden"))) static void probe_deck_discovery();
__attribute__((constructor)) static void log_client_start() {
config();
- if (process_is_steam_client())
+ if (process_is_steam_client()) {
Logger::log("loaded in the Steam client: deck %s, ring slot 0 %s\n", fake_deck_enabled() ? "on" : "off",
get_ring_path_for_slot(0).empty() ? "(none configured)" : get_ring_path_for_slot(0).c_str());
+ if (fake_deck_enabled() && Logger::log_enabled) probe_deck_discovery();
+ }
}
__attribute__((visibility("hidden"))) static bool is_deck_hidraw_path(const char *pathname) {
@@ -1517,6 +1520,74 @@ __attribute__((visibility("hidden"))) static std::string fd_path(int fd) {
return length > 0 ? std::string(path, length) : std::string();
}
+// A read-only view of the bound tree, independent of whether Steam ever tries to enumerate it.
+// Use the next libc functions so these lines cannot be mistaken for Steam's discovery calls.
+__attribute__((visibility("hidden"))) static void probe_deck_discovery() {
+ auto real_opendir = reinterpret_cast
(dlsym(RTLD_NEXT, "opendir"));
+ auto real_readlink = reinterpret_cast(dlsym(RTLD_NEXT, "readlink"));
+ auto real_realpath = reinterpret_cast(dlsym(RTLD_NEXT, "realpath"));
+ auto real_open = reinterpret_cast(dlsym(RTLD_NEXT, "open"));
+ auto real_fstatfs = reinterpret_cast(dlsym(RTLD_NEXT, "fstatfs"));
+ int saved_errno = errno;
+ DIR *dir = real_opendir("/sys/class/hidraw");
+ if (dir) {
+ unsigned count = 0;
+ bool deck = false;
+ unsigned deck_type = 0;
+ errno = 0;
+ while (auto *entry = readdir(dir)) {
+ if (entry->d_name[0] == '.') continue;
+ ++count;
+ if (!strcmp(entry->d_name, "hidraw16")) {
+ deck = true;
+ deck_type = entry->d_type;
+ }
+ }
+ int error = errno;
+ closedir(dir);
+ Logger::log("deck probe: /sys/class/hidraw entries=%u hidraw16=%s d_type=%u (%s)\n", count,
+ deck ? "present" : "absent", deck_type, error ? strerror(error) : "ok");
+ } else {
+ Logger::log("deck probe: /sys/class/hidraw failed (%s)\n", strerror(errno));
+ }
+ const char *node = "/sys/class/hidraw/hidraw16";
+ char resolved[PATH_MAX];
+ ssize_t length = real_readlink(node, resolved, sizeof(resolved) - 1);
+ if (length >= 0) {
+ resolved[length] = '\0';
+ Logger::log("deck probe: %s link=%s\n", node, resolved);
+ } else {
+ Logger::log("deck probe: %s readlink failed (%s)\n", node, strerror(errno));
+ }
+ if (real_realpath(node, resolved))
+ Logger::log("deck probe: %s resolved=%s\n", node, resolved);
+ else
+ Logger::log("deck probe: %s realpath failed (%s)\n", node, strerror(errno));
+ for (const char *path : {"/sys/class/hidraw/hidraw16/uevent", "/sys/class/hidraw/hidraw16/device/uevent",
+ "/sys/class/hidraw/hidraw16/dev", "/run/udev/data/c240:16"}) {
+ int fd = real_open(path, O_RDONLY | O_CLOEXEC);
+ if (fd >= 0 && !strcmp(path, "/sys/class/hidraw/hidraw16/uevent")) {
+ struct statfs fs;
+ int result = real_fstatfs(fd, &fs);
+ int error = errno;
+ Logger::log("deck probe: uevent fd path=%s raw_fs_type=0x%lx (%s)\n", fd_path(fd).c_str(),
+ result == 0 ? static_cast(fs.f_type) : 0, result == 0 ? "ok" : strerror(error));
+ }
+ char value[384];
+ ssize_t n = fd >= 0 ? syscall(SYS_read, fd, value, sizeof(value) - 1) : -1;
+ int error = errno;
+ if (fd >= 0) syscall(SYS_close, fd);
+ if (n >= 0) {
+ value[n] = '\0';
+ for (ssize_t i = 0; i < n; ++i) if (value[i] == '\n') value[i] = ';';
+ Logger::log("deck probe: %s value=%s\n", path, value);
+ } else {
+ Logger::log("deck probe: %s %s failed (%s)\n", path, fd < 0 ? "open" : "read", strerror(error));
+ }
+ }
+ errno = saved_errno;
+}
+
// Once per process: which guest or backing path sd-device checked and the filesystem it found.
__attribute__((visibility("hidden"))) static void note_deck_sysfs(const char *path, int result, long type) {
static std::atomic noted{false};
@@ -1710,9 +1781,34 @@ __attribute__((visibility("hidden"))) static std::string note_sys_failure(int di
return path;
}
-// A bind does not add its name to a real parent's listing. Always show Steam the merged class
-// listing, even on a readable kernel whose real /sys/class lacks hidraw. /sys and /sys/bus keep
-// their real listings unless SELinux denies them. Other processes and Xbox mode keep real sysfs.
+// Check the guest's native class names once. Readable listings containing hidraw stay native;
+// a missing name needs the merge because a bind alone does not add it to the parent's listing.
+__attribute__((visibility("hidden"))) static bool deck_class_missing_hidraw() {
+ static const bool missing = [] {
+ auto real = reinterpret_cast(dlsym(RTLD_NEXT, "opendir"));
+ int saved_errno = errno;
+ DIR *dir = real("/sys/class");
+ bool found = false;
+ bool readable = false;
+ int error = errno;
+ if (dir) {
+ errno = 0;
+ while (auto *entry = readdir(dir)) found |= !strcmp(entry->d_name, "hidraw");
+ error = errno;
+ readable = error == 0;
+ closedir(dir);
+ }
+ Logger::log("deck discovery: native /sys/class %s (%s)\n",
+ readable ? (found ? "contains hidraw; retained" : "missing hidraw; merge needed") : "not listable",
+ readable ? "ok" : strerror(error));
+ errno = saved_errno;
+ return readable && !found;
+ }();
+ return missing;
+}
+
+// /sys, /sys/bus and a native /sys/class containing hidraw use the old EACCES-only fallback.
+// Other processes and Xbox mode keep real sysfs.
__attribute__((visibility("hidden"))) static std::string deck_listing_for(const char *path, bool refused = false) {
const char *listing = deck_listing_dir();
if (!path || !listing || !fake_deck_enabled() || !process_is_steam_client()) return {};
@@ -1722,7 +1818,8 @@ __attribute__((visibility("hidden"))) static std::string deck_listing_for(const
if (p.compare(0, strlen(listing) + 5, std::string(listing) + "/sys/") == 0)
p = "/sys/" + p.substr(strlen(listing) + 5);
int which = p == "/sys" ? 0 : p == "/sys/class" ? 1 : p == "/sys/bus" ? 2 : -1;
- if (which < 0 || (which != 1 && (!refused || errno != EACCES))) return {};
+ bool denied = refused && errno == EACCES;
+ if (which < 0 || (!denied && (which != 1 || !deck_class_missing_hidraw()))) return {};
static const char *const names[] = {"sys", "class", "bus"};
static std::atomic logged{0};
if (!(logged.fetch_or(1 << which) & (1 << which)))
@@ -1747,13 +1844,141 @@ __attribute__((visibility("hidden"))) static std::string path_at(int dirfd, cons
return resolved;
}
+// Log only the discovery roots and our HID tree, including ENOENT. Ordinary sysfs failures keep
+// their existing log below; unrelated attribute traffic cannot consume this separate budget.
+__attribute__((visibility("hidden"))) static bool deck_discovery_path(const char *path) {
+ if (!path) return false;
+ auto under = [path](const std::string &root) {
+ return !root.empty() && !strncmp(path, root.c_str(), root.size()) &&
+ (path[root.size()] == '\0' || path[root.size()] == '/');
+ };
+ if (!strcmp(path, "/sys") || !strcmp(path, "/sys/") ||
+ !strcmp(path, "/sys/class") || !strcmp(path, "/sys/class/") ||
+ !strcmp(path, "/sys/bus") || !strcmp(path, "/sys/bus/") ||
+ !strcmp(path, "/sys/dev/char/240:16") || !strcmp(path, "/run/udev/data/c240:16") ||
+ under("/sys/class/hidraw") || under("/sys/devices/droiddeck")) return true;
+ const char *root = getenv("FAKE_DECK_SYSFS_ROOT");
+ if (root && (under(std::string(root) + "/devices") || under(std::string(root) + "/class-hidraw"))) return true;
+ const char *listing = deck_listing_dir();
+ return listing && (!strcmp(path, (std::string(listing) + "/class").c_str()) ||
+ under(std::string(listing) + "/class/hidraw"));
+}
+
+__attribute__((visibility("hidden"))) static bool deck_discovery_enabled() {
+ return Logger::log_enabled && fake_deck_enabled() && process_is_steam_client();
+}
+
+__attribute__((visibility("hidden"))) static void note_deck_discovery(
+ const char *operation, const char *path, long result, int flags = 0,
+ const char *detail = nullptr, bool descriptor = false) {
+ int saved_errno = errno;
+ if (Logger::log_enabled && fake_deck_enabled() && deck_discovery_path(path) && process_is_steam_client()) {
+ // Descriptor numbers change on each scan. Deduplicate by operation/path/flags/outcome instead.
+ int error = result < 0 ? saved_errno : 0;
+ std::string key = std::string(operation) + ":" + path + ":" + std::to_string(flags) + ":" + std::to_string(error);
+ static auto *seen = new std::vector();
+ bool first = false;
+ bool limit = false;
+ {
+ // This mutex is already reset in forked Steam children.
+ std::lock_guard guard(controller_mutex());
+ if (seen->size() < 96 && std::find(seen->begin(), seen->end(), key) == seen->end()) {
+ seen->push_back(key);
+ first = true;
+ limit = seen->size() == 96;
+ }
+ }
+ if (first) {
+ std::string extra = descriptor && result >= 0 ? fd_path(static_cast(result)) : std::string();
+ if (detail && *detail) extra += (extra.empty() ? "" : " ") + std::string(detail);
+ Logger::log("deck discovery: %s %s flags=0x%x result=%ld (%s)%s%s\n", operation, path, flags, result,
+ error ? strerror(error) : "ok", extra.empty() ? "" : " target=", extra.c_str());
+ if (limit) Logger::log("deck discovery: trace limit reached (96 unique operations)\n");
+ }
+ }
+ errno = saved_errno;
+}
+
+// These libc entry points can bypass the public open() hook. Keep their behavior unchanged and
+// expose the symlink chase, statx/fstatat validation and stdio attribute opens that sd-device uses.
+EXPORT ssize_t readlink(const char *path, char *buf, size_t size) {
+ static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "readlink"));
+ ssize_t result = real(path, buf, size);
+ int saved_errno = errno;
+ if (deck_discovery_path(path)) {
+ std::string target = result >= 0 ? std::string(buf, result) : std::string();
+ note_deck_discovery("readlink", path, result, 0, target.c_str());
+ }
+ errno = saved_errno;
+ return result;
+}
+
+EXPORT ssize_t readlinkat(int dirfd, const char *path, char *buf, size_t size) {
+ static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "readlinkat"));
+ ssize_t result = real(dirfd, path, buf, size);
+ int saved_errno = errno;
+ if (deck_discovery_enabled()) {
+ std::string resolved = path_at(dirfd, path);
+ std::string target = result >= 0 ? std::string(buf, result) : std::string();
+ note_deck_discovery("readlinkat", resolved.c_str(), result, 0, target.c_str());
+ }
+ errno = saved_errno;
+ return result;
+}
+
+EXPORT FILE *fopen(const char *path, const char *mode) {
+ static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "fopen"));
+ FILE *file = real(path, mode);
+ int saved_errno = errno;
+ note_deck_discovery("fopen", path, file ? fileno(file) : -1, 0, mode, true);
+ errno = saved_errno;
+ return file;
+}
+
+#ifdef __GLIBC__
+EXPORT FILE *fopen64(const char *path, const char *mode) {
+ return fopen(path, mode);
+}
+
+EXPORT int fstatat(int dirfd, const char *path, struct stat *buf, int flags) {
+ static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "fstatat"));
+ int result = real(dirfd, path, buf, flags);
+ int saved_errno = errno;
+ if (deck_discovery_enabled()) {
+ std::string resolved = path_at(dirfd, path);
+ char type[32];
+ snprintf(type, sizeof(type), "mode=0%o", result == 0 ? buf->st_mode : 0);
+ note_deck_discovery("fstatat", resolved.c_str(), result, flags, type);
+ }
+ errno = saved_errno;
+ return result;
+}
+
+EXPORT int fstatat64(int dirfd, const char *path, struct stat64 *buf, int flags) {
+ return fstatat(dirfd, path, reinterpret_cast(buf), flags);
+}
+
+EXPORT int statx(int dirfd, const char *path, int flags, unsigned mask, struct statx *buf) {
+ static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "statx"));
+ int result = real(dirfd, path, flags, mask, buf);
+ int saved_errno = errno;
+ if (deck_discovery_enabled()) {
+ std::string resolved = path_at(dirfd, path);
+ char type[48];
+ snprintf(type, sizeof(type), "mode=0%o mask=0x%x", result == 0 ? buf->stx_mode : 0, mask);
+ note_deck_discovery("statx", resolved.c_str(), result, flags, type);
+ }
+ errno = saved_errno;
+ return result;
+}
+#endif
+
// The listing supplies names only. A relative open on its fd must descend into real sysfs (or
// the bound hidraw class), not into an empty placeholder. In particular sd-device's chase uses
// openat(O_PATH) on each component; symlinks back to /sys/class would loop through this listing.
-__attribute__((visibility("hidden"))) static std::string deck_listing_child(int dirfd, const char *path) {
+__attribute__((visibility("hidden"))) static std::string deck_listing_child_path(const std::string &p) {
const char *listing = deck_listing_dir();
if (!listing || !fake_deck_enabled() || !process_is_steam_client()) return {};
- std::string p = path_at(dirfd, path);
for (const char *name : {"class", "sys"}) {
std::string prefix = std::string(listing) + "/" + name + "/";
if (p.compare(0, prefix.size(), prefix) == 0)
@@ -1762,18 +1987,23 @@ __attribute__((visibility("hidden"))) static std::string deck_listing_child(int
return {};
}
+__attribute__((visibility("hidden"))) static std::string deck_listing_child(int dirfd, const char *path) {
+ if (!deck_listing_dir() || !fake_deck_enabled() || !process_is_steam_client()) return {};
+ return deck_listing_child_path(path_at(dirfd, path));
+}
+
EXPORT DIR *opendir(const char *name) {
static auto real = reinterpret_cast(dlsym(RTLD_NEXT, "opendir"));
std::string child = deck_listing_child(AT_FDCWD, name);
if (!child.empty()) name = child.c_str();
std::string listing = deck_listing_for(name);
- if (!listing.empty()) return real(listing.c_str());
- DIR *dir = real(name);
+ DIR *dir = real(listing.empty() ? name : listing.c_str());
if (!dir) {
note_sys_failure(AT_FDCWD, name);
listing = deck_listing_for(name, true);
if (!listing.empty()) dir = real(listing.c_str());
}
+ note_deck_discovery("opendir", name, dir ? dirfd(dir) : -1, 0, nullptr, true);
return dir;
}
@@ -1838,6 +2068,7 @@ EXPORT int open(const char *pathname, int flags, ...) {
std::string listing;
std::string child;
+ const char *requested = pathname;
if ((flags & O_ACCMODE) == O_RDONLY && !(flags & (O_CREAT | O_TRUNC))) {
child = deck_listing_child(AT_FDCWD, pathname);
if (!child.empty()) pathname = child.c_str();
@@ -1853,6 +2084,8 @@ EXPORT int open(const char *pathname, int flags, ...) {
if (!fallback.empty()) fd = my_open(fallback.c_str(), flags);
}
+ note_deck_discovery("open", requested, fd, flags, nullptr, true);
+
if (fake_path)
free(fake_path);
@@ -1920,14 +2153,16 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
std::string listing;
std::string child;
+ std::string requested;
if (fake_deck_enabled() && process_is_steam_client() &&
(flags & O_ACCMODE) == O_RDONLY && !(flags & (O_CREAT | O_TRUNC))) {
- child = deck_listing_child(dirfd, pathname);
+ requested = path_at(dirfd, pathname);
+ child = deck_listing_child_path(requested);
if (!child.empty()) {
dirfd = AT_FDCWD;
pathname = child.c_str();
}
- listing = deck_listing_for(path_at(dirfd, pathname).c_str());
+ listing = deck_listing_for(child.empty() ? requested.c_str() : child.c_str());
}
if (!listing.empty()) {
dirfd = AT_FDCWD;
@@ -1943,6 +2178,8 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
if (!fallback.empty()) fd = my_openat(AT_FDCWD, fallback.c_str(), flags);
}
+ if (!requested.empty()) note_deck_discovery("openat", requested.c_str(), fd, flags, nullptr, true);
+
if (fake_path)
free(fake_path);
@@ -2109,13 +2346,13 @@ EXPORT int scandir(const char *dirp, struct dirent ***namelist,
std::string child = deck_listing_child(AT_FDCWD, dirp);
if (!child.empty()) dirp = child.c_str();
std::string listing = deck_listing_for(dirp);
- if (!listing.empty()) return my_scandir(listing.c_str(), namelist, filter, compar);
- int result = my_scandir(dirp, namelist, filter, compar);
+ int result = my_scandir(listing.empty() ? dirp : listing.c_str(), namelist, filter, compar);
if (result < 0) {
note_sys_failure(AT_FDCWD, dirp);
listing = deck_listing_for(dirp, true);
if (!listing.empty()) result = my_scandir(listing.c_str(), namelist, filter, compar);
}
+ note_deck_discovery("scandir", dirp, result);
return result;
}
diff --git a/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt b/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt
index d95b27170..4175b7944 100644
--- a/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt
+++ b/app/src/main/java/com/droiddeck/launcher/session/SteamDeckPad.kt
@@ -27,7 +27,8 @@ import java.io.File
* - [listingDir] - a merged `/sys/class` listing, with the host's class names plus `hidraw`.
* A proot bind is reachable by path but does not add its name to the parent's directory listing;
* a kernel without a native `hidraw` class therefore hid our bound class from libudev. The
- * libfakeinput routes relative opens back to the real guest paths. `/sys` and `/sys/bus` also have minimal
+ * libfakeinput uses this only when `hidraw` is missing or the native listing is denied, and routes
+ * relative opens back to the real guest paths. `/sys` and `/sys/bus` also have minimal
* stand-ins for a client whose SELinux policy refuses those listings.
*
* Symlinks are absolute guest paths; proot resolves them inside the session.
@@ -111,8 +112,8 @@ object SteamDeckPad {
File(listing, "class/$name").mkdirs()
}
Log.i(TAG, "deck pad: native /sys/class " + when (nativeClasses) {
- null -> "not listable; merged listing contains hidraw only"
- else -> "${nativeClasses.size} classes, hidraw ${if ("hidraw" in nativeClasses) "present" else "absent"}; merged listing includes hidraw"
+ null -> "not listable; hidraw listing fallback prepared"
+ else -> "${nativeClasses.size} classes, hidraw ${if ("hidraw" in nativeClasses) "present; native listing retained" else "absent; merged listing prepared"}"
})
// The runtime binds this directory over /sys/dev/char for the GPU (LinuxRuntime.bindGpuNode).
link(File(context.cacheDir, "drm/sys/$MAJOR:$MINOR"), HIDRAW)