mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
fix: let the Steam client find the Deck controller under an enforcing SELinux policy
This commit is contained in:
1 parent
2896e0a945
commit
5fe5fa45c7
3 files changed
+110
-3
No files matched your search
@@ -1488,8 +1488,18 @@ __attribute__((visibility("hidden"))) static int open_deck_hidraw(int flags) {
|
||||
// into /sys, so those paths are said to be sysfs; nothing else is touched.
|
||||
static constexpr long SYSFS_FS_MAGIC = 0x62656572;
|
||||
|
||||
// FAKE_DECK_SYSFS_LISTING: stand-ins for the listings of /sys, /sys/class and /sys/bus
|
||||
// (SteamDeckPad.listingDir), for a client that is not allowed to list the real ones.
|
||||
__attribute__((visibility("hidden"))) static const char *deck_listing_dir() {
|
||||
static const char *dir = getenv("FAKE_DECK_SYSFS_LISTING");
|
||||
return dir && *dir ? dir : nullptr;
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_deck_sysfs_path(const char *path) {
|
||||
return path && (!strncmp(path, "/sys/devices/droiddeck", 22) || !strncmp(path, "/sys/class/hidraw", 17));
|
||||
if (!path) return false;
|
||||
if (!strncmp(path, "/sys/devices/droiddeck", 22) || !strncmp(path, "/sys/class/hidraw", 17)) return true;
|
||||
const char *listing = deck_listing_dir();
|
||||
return listing && !strncmp(path, listing, strlen(listing));
|
||||
}
|
||||
|
||||
__attribute__((visibility("hidden"))) static bool is_deck_sysfs_fd(int fd) {
|
||||
@@ -1644,6 +1654,80 @@ ioctl_deck(DeckHidraw &deck, ioctl_request_t op, void *argp) {
|
||||
}
|
||||
}
|
||||
|
||||
// A device scan that fails on a phone and not on the Thor: libudev gives up its whole scan on any
|
||||
// sysfs directory it cannot read for a reason other than its absence, and a phone's SELinux policy
|
||||
// denies directories a permissive device lets through. The client's /sys failures that are not
|
||||
// ENOENT are logged, each path once, so pad.log names the directory that stopped the scan.
|
||||
__attribute__((visibility("hidden"))) static std::string note_sys_failure(int dirfd, const char *pathname) {
|
||||
int saved_errno = errno;
|
||||
std::string path;
|
||||
if (saved_errno == ENOENT || !pathname || !process_is_steam_client()) {
|
||||
errno = saved_errno;
|
||||
return path;
|
||||
}
|
||||
if (pathname[0] == '/' || dirfd == AT_FDCWD) {
|
||||
path = pathname;
|
||||
} else {
|
||||
char link[64], dir[PATH_MAX];
|
||||
snprintf(link, sizeof(link), "/proc/self/fd/%d", dirfd);
|
||||
ssize_t length = readlink(link, dir, sizeof(dir) - 1);
|
||||
if (length <= 0) {
|
||||
errno = saved_errno;
|
||||
return path;
|
||||
}
|
||||
path.assign(dir, length);
|
||||
path += '/';
|
||||
path += pathname;
|
||||
}
|
||||
if (path.compare(0, 5, "/sys/") != 0 && path != "/sys") {
|
||||
errno = saved_errno;
|
||||
return {};
|
||||
}
|
||||
static std::mutex mutex;
|
||||
static std::vector<std::string> *seen = new std::vector<std::string>();
|
||||
bool first = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
if (seen->size() < 64 && std::find(seen->begin(), seen->end(), path) == seen->end()) {
|
||||
seen->push_back(path);
|
||||
first = true;
|
||||
}
|
||||
}
|
||||
if (first) Logger::log("sysfs: %s not readable (%s)\n", path.c_str(), strerror(saved_errno));
|
||||
errno = saved_errno;
|
||||
return path;
|
||||
}
|
||||
|
||||
// An enforcing SELinux policy (every retail phone; the Thor only with it forced on) refuses an app
|
||||
// a listing of /sys, /sys/class and /sys/bus. libudev lists /sys/class to find the subsystems to
|
||||
// descend into and gives up its whole scan when it cannot, so the client never reached hidraw and
|
||||
// found no Deck. The client is listed the stand-ins instead - hidraw under class - and opens
|
||||
// /sys/class/hidraw itself, which is ours and readable. Only on that refusal, only for the client.
|
||||
__attribute__((visibility("hidden"))) static std::string deck_listing_for(const char *path) {
|
||||
const char *listing = deck_listing_dir();
|
||||
if (!path || !listing || errno != EACCES || !fake_deck_enabled() || !process_is_steam_client()) return {};
|
||||
std::string p(path);
|
||||
while (p.size() > 1 && p.back() == '/') p.pop_back();
|
||||
int which = p == "/sys" ? 0 : p == "/sys/class" ? 1 : p == "/sys/bus" ? 2 : -1;
|
||||
if (which < 0) return {};
|
||||
static const char *const names[] = {"sys", "class", "bus"};
|
||||
static std::atomic<int> logged{0};
|
||||
if (!(logged.fetch_or(1 << which) & (1 << which)))
|
||||
Logger::log("sysfs: %s listed from the Deck's stand-in\n", p.c_str());
|
||||
return std::string(listing) + "/" + names[which];
|
||||
}
|
||||
|
||||
EXPORT DIR *opendir(const char *name) {
|
||||
static auto real = reinterpret_cast<DIR *(*)(const char *)>(dlsym(RTLD_NEXT, "opendir"));
|
||||
DIR *dir = real(name);
|
||||
if (!dir) {
|
||||
note_sys_failure(AT_FDCWD, name);
|
||||
std::string listing = deck_listing_for(name);
|
||||
if (!listing.empty()) dir = real(listing.c_str());
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
EXPORT int open(const char *pathname, int flags, ...) {
|
||||
va_list va;
|
||||
mode_t mode;
|
||||
@@ -1707,6 +1791,10 @@ EXPORT int open(const char *pathname, int flags, ...) {
|
||||
fd = my_open(pathname, flags, mode);
|
||||
else
|
||||
fd = my_open(pathname, flags);
|
||||
if (fd < 0) {
|
||||
std::string listing = deck_listing_for(note_sys_failure(AT_FDCWD, pathname).c_str());
|
||||
if (!listing.empty()) fd = my_open(listing.c_str(), flags);
|
||||
}
|
||||
|
||||
if (fake_path)
|
||||
free(fake_path);
|
||||
@@ -1777,6 +1865,10 @@ EXPORT int openat(int dirfd, const char *pathname, int flags, ...) {
|
||||
fd = my_openat(dirfd, pathname, flags, mode);
|
||||
else
|
||||
fd = my_openat(dirfd, pathname, flags);
|
||||
if (fd < 0) {
|
||||
std::string listing = deck_listing_for(note_sys_failure(dirfd, pathname).c_str());
|
||||
if (!listing.empty()) fd = my_openat(AT_FDCWD, listing.c_str(), flags);
|
||||
}
|
||||
|
||||
if (fake_path)
|
||||
free(fake_path);
|
||||
|
||||
@@ -647,7 +647,10 @@ class SessionService : Service() {
|
||||
// /dev/uinput, stood in for by libfakeinput: the virtual pad Steam Input makes for a
|
||||
// game becomes a node the game reads, carrying the player's layout, as on a Deck.
|
||||
guest.add("FAKE_EVDEV_UINPUT=1")
|
||||
if (SessionState.deckPad) guest.add("FAKE_EVDEV_DECK=1")
|
||||
if (SessionState.deckPad) {
|
||||
guest.add("FAKE_EVDEV_DECK=1")
|
||||
guest.add("FAKE_DECK_SYSFS_LISTING=" + SteamDeckPad.listingDir(fakeInputDir.parentFile!!.parentFile!!).path)
|
||||
}
|
||||
} else if (SessionState.mode == MODE_STEAM) {
|
||||
// Without it, games see the pad itself wearing Steam Input's virtual-gamepad identity -
|
||||
// for games the client starts only. Everywhere else (the desktop, a program from the
|
||||
|
||||
@@ -23,7 +23,11 @@ import java.io.File
|
||||
* - `/sys/dev/char/240:16` - how the node's device number is looked up (in the directory the
|
||||
* runtime already binds there for the GPU);
|
||||
* - `/run/udev/data/c240:16` - the udev database entry that marks the device initialised, without
|
||||
* which libudev's enumeration leaves a device node out.
|
||||
* which libudev's enumeration leaves a device node out;
|
||||
* - [listingDir] - stand-ins for the listings of `/sys`, `/sys/class` and `/sys/bus`. An app under
|
||||
* an enforcing SELinux policy (every retail phone) may not list those directories, and libudev
|
||||
* abandons its whole scan when it cannot, so the client never reached `hidraw`; libfakeinput
|
||||
* lists these to the client instead when the real listing is refused (FAKE_DECK_SYSFS_LISTING).
|
||||
*
|
||||
* Symlinks are absolute guest paths; proot resolves them inside the session.
|
||||
*/
|
||||
@@ -47,6 +51,9 @@ object SteamDeckPad {
|
||||
0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0,
|
||||
).map { it.toByte() }.toByteArray()
|
||||
|
||||
/** Where [prepare] puts the stand-in listings; bound into the guest at the same path. */
|
||||
fun listingDir(sessionRoot: File) = File(sessionRoot, "sys/deck/listing")
|
||||
|
||||
/** Writes the tree and returns the `host:guest` binds for it, or none if it could not be made. */
|
||||
fun prepare(context: Context, sessionRoot: File): List<String> {
|
||||
val base = File(sessionRoot, "sys/deck")
|
||||
@@ -89,6 +96,11 @@ object SteamDeckPad {
|
||||
|
||||
hidrawClass.mkdirs()
|
||||
link(File(hidrawClass, NODE), HIDRAW)
|
||||
|
||||
// Only the names matter: libudev reads a listing for the subsystems to descend into and
|
||||
// then opens /sys/class/hidraw itself, which is the bind above.
|
||||
val listing = listingDir(sessionRoot)
|
||||
for (name in listOf("sys/bus", "sys/class", "sys/devices", "class/hidraw", "bus")) File(listing, name).mkdirs()
|
||||
// The runtime binds this directory over /sys/dev/char for the GPU (LinuxRuntime.bindGpuNode).
|
||||
link(File(context.cacheDir, "drm/sys/$MAJOR:$MINOR"), HIDRAW)
|
||||
|
||||
|
||||
Reference in new issue
Block a user