joystick: Use inotify to detect joystick unplug if not using udev This improves SDL's ability to detect joystick hotplug in a container environment. We cannot reliably receive events from udev in a container, because they are delivered as netlink events, which are authenticated by their uid being 0. However, in a user namespace created by an unprivileged user (for example bubblewrap, as used by Flatpak and Steam's pressure-vessel-wrap), the kernel does not allow us to map uid 0, and the netlink events appear to be from the kernel's overflowuid (typically 65534/nobody), meaning libudev cannot distinguish between genuine uevents from udevd and an attack by a malicious local user. Signed-off-by: Simon McVittie <smcv@collabora.com>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c
index fc145e9..1838cb3 100644
--- a/src/joystick/linux/SDL_sysjoystick.c
+++ b/src/joystick/linux/SDL_sysjoystick.c
@@ -32,6 +32,7 @@
#include <errno.h> /* errno, strerror */
#include <fcntl.h>
#include <limits.h> /* For the definition of PATH_MAX */
+#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <dirent.h>
@@ -40,6 +41,7 @@
#include "SDL_assert.h"
#include "SDL_hints.h"
#include "SDL_joystick.h"
+#include "SDL_log.h"
#include "SDL_endian.h"
#include "SDL_timer.h"
#include "../../events/SDL_events_c.h"
@@ -92,16 +94,10 @@ typedef enum
ENUMERATION_FALLBACK
} EnumerationMethod;
-#if SDL_USE_LIBUDEV
static EnumerationMethod enumeration_method = ENUMERATION_UNSET;
-#else
-const EnumerationMethod enumeration_method = ENUMERATION_FALLBACK;
-#endif
static int MaybeAddDevice(const char *path);
-#if SDL_USE_LIBUDEV
static int MaybeRemoveDevice(const char *path);
-#endif /* SDL_USE_LIBUDEV */
/* A linked list of available joysticks */
typedef struct SDL_joylist_item
@@ -121,6 +117,7 @@ typedef struct SDL_joylist_item
static SDL_joylist_item *SDL_joylist = NULL;
static SDL_joylist_item *SDL_joylist_tail = NULL;
static int numjoysticks = 0;
+static int inotify_fd = -1;
static Uint32 last_joy_detect_time;
static time_t last_input_dir_mtime;
@@ -188,7 +185,7 @@ IsJoystick(int fd, char **name_return, SDL_JoystickGUID *guid)
char product_string[128];
/* When udev is enabled we only get joystick devices here, so there's no need to test them */
- if (enumeration_method == ENUMERATION_FALLBACK && !GuessIsJoystick(fd)) {
+ if (enumeration_method != ENUMERATION_LIBUDEV && !GuessIsJoystick(fd)) {
return 0;
}
@@ -503,6 +500,75 @@ static void SteamControllerDisconnectedCallback(int device_instance)
}
}
+static int
+StrHasPrefix(const char *string, const char *prefix)
+{
+ return (SDL_strncmp(string, prefix, SDL_strlen(prefix)) == 0);
+}
+
+static int
+StrIsInteger(const char *string)
+{
+ const char *p;
+
+ if (*string == '\0') {
+ return 0;
+ }
+
+ for (p = string; *p != '\0'; p++) {
+ if (*p < '0' || *p > '9') {
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+static void
+LINUX_InotifyJoystickDetect(void)
+{
+ union
+ {
+ struct inotify_event event;
+ char storage[4096];
+ char enough_for_inotify[sizeof (struct inotify_event) + NAME_MAX + 1];
+ } buf;
+ ssize_t bytes;
+ size_t remain = 0;
+ size_t len;
+
+ bytes = read(inotify_fd, &buf, sizeof (buf));
+
+ if (bytes > 0) {
+ remain = (size_t) bytes;
+ }
+
+ while (remain > 0) {
+ if (buf.event.len > 0) {
+ if (StrHasPrefix(buf.event.name, "event") &&
+ StrIsInteger(buf.event.name + strlen ("event"))) {
+ char path[PATH_MAX];
+
+ SDL_snprintf(path, SDL_arraysize(path), "/dev/input/%s", buf.event.name);
+
+ if (buf.event.mask & (IN_CREATE | IN_MOVED_TO | IN_ATTRIB)) {
+ MaybeAddDevice(path);
+ }
+ else if (buf.event.mask & (IN_DELETE | IN_MOVED_FROM)) {
+ MaybeRemoveDevice(path);
+ }
+ }
+ }
+
+ len = sizeof (struct inotify_event) + buf.event.len;
+ remain -= len;
+
+ if (remain != 0) {
+ memmove (&buf.storage[0], &buf.storage[len], remain);
+ }
+ }
+}
+
static void
LINUX_FallbackJoystickDetect(void)
{
@@ -547,7 +613,10 @@ LINUX_JoystickDetect(void)
}
else
#endif
- {
+ if (inotify_fd >= 0) {
+ LINUX_InotifyJoystickDetect();
+ }
+ else {
LINUX_FallbackJoystickDetect();
}
@@ -589,6 +658,10 @@ LINUX_JoystickInit(void)
SDL_InitSteamControllers(SteamControllerConnectedCallback,
SteamControllerDisconnectedCallback);
+ /* Force immediate joystick detection if using fallback */
+ last_joy_detect_time = 0;
+ last_input_dir_mtime = 0;
+
#if SDL_USE_LIBUDEV
if (enumeration_method == ENUMERATION_LIBUDEV) {
if (SDL_UDEV_Init() < 0) {
@@ -607,9 +680,28 @@ LINUX_JoystickInit(void)
else
#endif
{
- /* Force immediate joystick detection */
- last_joy_detect_time = 0;
- last_input_dir_mtime = 0;
+ inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
+
+ if (inotify_fd < 0) {
+ SDL_LogWarn(SDL_LOG_CATEGORY_INPUT,
+ "Unable to initialize inotify, falling back to polling: %s",
+ strerror (errno));
+ }
+ else {
+ /* We need to watch for attribute changes in addition to
+ * creation, because when a device is first created, it has
+ * permissions that we can't read. When udev chmods it to
+ * something that we maybe *can* read, we'll get an
+ * IN_ATTRIB event to tell us. */
+ if (inotify_add_watch(inotify_fd, "/dev/input",
+ IN_CREATE | IN_DELETE | IN_MOVE | IN_ATTRIB) < 0) {
+ close(inotify_fd);
+ inotify_fd = -1;
+ SDL_LogWarn(SDL_LOG_CATEGORY_INPUT,
+ "Unable to add inotify watch, falling back to polling: %s",
+ strerror (errno));
+ }
+ }
/* Report all devices currently present */
LINUX_JoystickDetect();
@@ -1224,6 +1316,9 @@ LINUX_JoystickQuit(void)
SDL_joylist_item *item = NULL;
SDL_joylist_item *next = NULL;
+ close(inotify_fd);
+ inotify_fd = -1;
+
for (item = SDL_joylist; item; item = next) {
next = item->next;
SDL_free(item->path);