Skip to content

Commit

Permalink
joystick: Use inotify to detect joystick unplug if not using udev
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
smcv committed Nov 12, 2020
1 parent aae53d5 commit b0eba1c
Showing 1 changed file with 106 additions and 11 deletions.
117 changes: 106 additions & 11 deletions src/joystick/linux/SDL_sysjoystick.c
Expand Up @@ -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>
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -547,7 +613,10 @@ LINUX_JoystickDetect(void)
}
else
#endif
{
if (inotify_fd >= 0) {
LINUX_InotifyJoystickDetect();
}
else {
LINUX_FallbackJoystickDetect();
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit b0eba1c

Please sign in to comment.