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>
This commit is contained in:
Simon McVittie 2020-11-11 19:15:32 -08:00
parent aae53d5972
commit b0eba1c55d
1 changed files with 106 additions and 11 deletions

View File

@ -32,6 +32,7 @@
#include <errno.h> /* errno, strerror */ #include <errno.h> /* errno, strerror */
#include <fcntl.h> #include <fcntl.h>
#include <limits.h> /* For the definition of PATH_MAX */ #include <limits.h> /* For the definition of PATH_MAX */
#include <sys/inotify.h>
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <unistd.h> #include <unistd.h>
#include <dirent.h> #include <dirent.h>
@ -40,6 +41,7 @@
#include "SDL_assert.h" #include "SDL_assert.h"
#include "SDL_hints.h" #include "SDL_hints.h"
#include "SDL_joystick.h" #include "SDL_joystick.h"
#include "SDL_log.h"
#include "SDL_endian.h" #include "SDL_endian.h"
#include "SDL_timer.h" #include "SDL_timer.h"
#include "../../events/SDL_events_c.h" #include "../../events/SDL_events_c.h"
@ -92,16 +94,10 @@ typedef enum
ENUMERATION_FALLBACK ENUMERATION_FALLBACK
} EnumerationMethod; } EnumerationMethod;
#if SDL_USE_LIBUDEV
static EnumerationMethod enumeration_method = ENUMERATION_UNSET; static EnumerationMethod enumeration_method = ENUMERATION_UNSET;
#else
const EnumerationMethod enumeration_method = ENUMERATION_FALLBACK;
#endif
static int MaybeAddDevice(const char *path); static int MaybeAddDevice(const char *path);
#if SDL_USE_LIBUDEV
static int MaybeRemoveDevice(const char *path); static int MaybeRemoveDevice(const char *path);
#endif /* SDL_USE_LIBUDEV */
/* A linked list of available joysticks */ /* A linked list of available joysticks */
typedef struct SDL_joylist_item 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 = NULL;
static SDL_joylist_item *SDL_joylist_tail = NULL; static SDL_joylist_item *SDL_joylist_tail = NULL;
static int numjoysticks = 0; static int numjoysticks = 0;
static int inotify_fd = -1;
static Uint32 last_joy_detect_time; static Uint32 last_joy_detect_time;
static time_t last_input_dir_mtime; static time_t last_input_dir_mtime;
@ -188,7 +185,7 @@ IsJoystick(int fd, char **name_return, SDL_JoystickGUID *guid)
char product_string[128]; char product_string[128];
/* When udev is enabled we only get joystick devices here, so there's no need to test them */ /* 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; 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 static void
LINUX_FallbackJoystickDetect(void) LINUX_FallbackJoystickDetect(void)
{ {
@ -547,7 +613,10 @@ LINUX_JoystickDetect(void)
} }
else else
#endif #endif
{ if (inotify_fd >= 0) {
LINUX_InotifyJoystickDetect();
}
else {
LINUX_FallbackJoystickDetect(); LINUX_FallbackJoystickDetect();
} }
@ -589,6 +658,10 @@ LINUX_JoystickInit(void)
SDL_InitSteamControllers(SteamControllerConnectedCallback, SDL_InitSteamControllers(SteamControllerConnectedCallback,
SteamControllerDisconnectedCallback); SteamControllerDisconnectedCallback);
/* Force immediate joystick detection if using fallback */
last_joy_detect_time = 0;
last_input_dir_mtime = 0;
#if SDL_USE_LIBUDEV #if SDL_USE_LIBUDEV
if (enumeration_method == ENUMERATION_LIBUDEV) { if (enumeration_method == ENUMERATION_LIBUDEV) {
if (SDL_UDEV_Init() < 0) { if (SDL_UDEV_Init() < 0) {
@ -607,9 +680,28 @@ LINUX_JoystickInit(void)
else else
#endif #endif
{ {
/* Force immediate joystick detection */ inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
last_joy_detect_time = 0;
last_input_dir_mtime = 0; 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 */ /* Report all devices currently present */
LINUX_JoystickDetect(); LINUX_JoystickDetect();
@ -1224,6 +1316,9 @@ LINUX_JoystickQuit(void)
SDL_joylist_item *item = NULL; SDL_joylist_item *item = NULL;
SDL_joylist_item *next = NULL; SDL_joylist_item *next = NULL;
close(inotify_fd);
inotify_fd = -1;
for (item = SDL_joylist; item; item = next) { for (item = SDL_joylist; item; item = next) {
next = item->next; next = item->next;
SDL_free(item->path); SDL_free(item->path);