implement file-less hotplugging

This commit is contained in:
illiliti
2021-08-25 03:36:57 +03:00
parent 4510b27a9b
commit fc22990609
7 changed files with 143 additions and 260 deletions

View File

@@ -1,6 +1,6 @@
# libudev-zero
Drop-in replacement for `libudev` that intended to work with any device manager
Drop-in replacement for `libudev` intended to work with any device manager
## Why?
@@ -11,15 +11,11 @@ to rewrite[0] this crappy library because `libinput` hard-depends on `udev`.
Without `libinput` you can't use `wayland` and many other cool stuff.
Michael Forney (author of `cproc`, `samurai`, Oasis Linux, ...) decided to
fork[1] `libinput` and remove the hard dependency on `udev`. Is this a
solution? Yes. Is this a complete solution? No. This fork has a lot of
disadvantages like requiring patching applications to use `libinput_netlink`
fork[1] `libinput` and remove the hard dependency on `udev`. However, this
fork has a drawback that requires patching applications to use `libinput_netlink`
instead of the `libinput_udev` API in order to use the automatic detection of
input devices and hotplugging. Static configuration is also required for
anything other than input devices (e.g drm devices). Moreover hotplugging is
vulnerable to race conditions when `libinput` handles the `uevent` faster than
the device manager which can lead to file permission issues. `libudev-zero`
prevents these race conditions by design.
input devices and hotplugging. Static configuration is also required for anything
other than input devices (e.g drm devices).
Thankfully `udev` has stable API and hopefully no changes will be made to it
the future. On this basis I decided to create this clean-room implementation
@@ -45,7 +41,6 @@ of `libudev` which can be used with any or without a device manager.
* C99 compiler (build time)
* POSIX make (build time)
* POSIX & XSI libc
* inotify & eventfd
* Linux >= 2.6.39
## Installation
@@ -60,18 +55,21 @@ make PREFIX=/usr install
Note that hotplugging support is fully optional. You can skip
this step if you don't have a need for the hotplugging capability.
In order to use hotplugging, you need to configure device manager to send
`uevent` messages to `UDEV_MONITOR_DIR`. `UDEV_MONITOR_DIR` is arbitrary
shared directory used by `libudev-zero` to receive `uevent` messages. By
default, `UDEV_MONITOR_DIR` points to `/tmp/.libudev-zero`. You can change
that directory at compile time by passing `-DUDEV_MONITOR_DIR=<dir>` to
`CFLAGS` or at runtime by setting `UDEV_MONITOR_DIR` environment variable.
If you're using mdev-like device manager, refer to [mdev.conf](contrib/mdev.conf)
for config example.
Keep in mind that already processed `uevent` messages wouldn't be automatically
purged. You can set `UDEV_MONITOR_DIR` to directory on tmpfs to purge them on
reboot/shutdown.
If you're using other device manager, you need to configure it to rebroadcast
kernel uevents. You can do this by either patching(see below) device manager
or simply executing [helper.c](contrib/helper.c) for each uevent.
Refer to [contrib](contrib) for usage examples and configs.
If you're developing your own device manager, you need to rebroadcast kernel
uevents to `0x4` netlink group of `NETLINK_KOBJECT_UEVENT`. This is required
because libudev-zero can't simply listen to kernel uevents due to potential
race conditions. Refer(but don't copy blindly) to [helper.c](contrib/helper.c)
for example how it could be implemented in C.
Don't hesitate to ask me everything you don't understand. I'm usually hanging
around in #kisslinux at libera.chat, but you can also email me or open an issue here.
## Donate

View File

@@ -15,50 +15,27 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*
* NOTE: you don't need this if you have mdev/mdevd, refer to mdev.conf
* NOTE: you need this if you want to use bare-bones CONFIG_UEVENT_HELPER
*
* build:
* cc helper.c -o helper
*
* usage:
* echo /full/path/to/helper > /proc/sys/kernel/hotplug
* echo "/full/path/to/helper UDEV_MONITOR_DIR" > /proc/sys/kernel/hotplug
* Construct uevent message from environment and send it to 0x4 netlink group.
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <linux/netlink.h>
int main(int argc, char **argv)
{
struct sockaddr_nl sa = {0};
struct msghdr hdr = {0};
struct iovec iov = {0};
extern char **environ;
char path[PATH_MAX];
char *dir;
int fd, i;
char buf[8192];
size_t len;
int i, fd;
switch (argc) {
case 1:
dir = "/tmp/.libudev-zero";
break;
case 2:
dir = argv[1];
break;
default:
fprintf(stderr, "usage: %s [dir]\n", argv[0]);
return 2;
}
snprintf(path, sizeof(path), "%s/uevent.XXXXXX", dir);
fd = mkstemp(path);
if (fd == -1) {
perror("mkstemp");
return 1;
}
iov.iov_base = buf;
iov.iov_len = 0;
for (i = 0; environ[i]; i++) {
if (strncmp(environ[i], "PATH=", 5) == 0 ||
@@ -66,16 +43,44 @@ int main(int argc, char **argv)
continue;
}
if (write(fd, environ[i], strlen(environ[i])) == -1 ||
write(fd, "\n", 1) == -1) {
perror("write");
close(fd);
unlink(path);
len = strlen(environ[i]) + 1;
if (iov.iov_len + len > sizeof(buf)) {
fprintf(stderr, "%s: uevent exceeds buffer size", argv[0]);
return 1;
}
memcpy(buf + iov.iov_len, environ[i], len);
iov.iov_len += len;
}
sa.nl_family = AF_NETLINK;
sa.nl_groups = 0x4; // XXX
hdr.msg_name = &sa;
hdr.msg_namelen = sizeof(sa);
hdr.msg_iov = &iov;
hdr.msg_iovlen = 1;
fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
if (fd == -1) {
perror("socket");
return 1;
}
if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) == -1) {
perror("bind");
close(fd);
return 1;
}
if (sendmsg(fd, &hdr, 0) == -1) {
perror("sendmsg");
close(fd);
return 1;
}
fchmod(fd, 0444);
close(fd);
return 0;
}

View File

@@ -1,10 +0,0 @@
#!/bin/sh -f
#
# NOTE: you don't need this if you have mdev/mdevd, refer to mdev.conf
# NOTE: you need this if you want to use bare-bones CONFIG_UEVENT_HELPER
#
# usage:
# echo /full/path/to/helper.sh > /proc/sys/kernel/hotplug
# echo "/full/path/to/helper.sh UDEV_MONITOR_DIR" > /proc/sys/kernel/hotplug
exec env > "${1:-/tmp/.libudev-zero}/uevent.$$"

View File

@@ -1,12 +1,10 @@
#
# example rules for mdev.conf
#
# NOTE: you must change "/tmp/.libudev-zero" if you use non-default UDEV_MONITOR_DIR
# NOTE: you don't need helper.c or helper.sh, just add this to /etc/mdev.conf
# NOTE: replace /path/to/helper with path to compiled binary of helper.c
# handle all uevents(not recommended)
#-.* root:root 660 *env > /tmp/.libudev-zero/uevent.$$
#-.* root:root 660 */path/to/helper
# handle only drm and input uevents(recommended)
SUBSYSTEM=drm;.* root:video 660 *env > /tmp/.libudev-zero/uevent.$$
SUBSYSTEM=input;.* root:input 660 *env > /tmp/.libudev-zero/uevent.$$
SUBSYSTEM=drm;.* root:video 660 */path/to/helper
SUBSYSTEM=input;.* root:input 660 */path/to/helper

2
udev.h
View File

@@ -129,7 +129,7 @@ struct udev_hwdb *udev_hwdb_unref(struct udev_hwdb *hwdb);
struct udev_list_entry *udev_hwdb_get_properties_list_entry(struct udev_hwdb *hwdb, const char *modalias, unsigned int flags);
// this is "libudev-zero" extension. do not use if portability is concern
struct udev_device *udev_device_new_from_file(struct udev *udev, const char *path);
struct udev_device *udev_device_new_from_uevent(struct udev *udev, char *buf, size_t len);
#ifdef __cplusplus
}

View File

@@ -632,35 +632,20 @@ struct udev_device *udev_device_new_from_subsystem_sysname(struct udev *udev, co
return NULL;
}
struct udev_device *udev_device_new_from_file(struct udev *udev, const char *path)
struct udev_device *udev_device_new_from_uevent(struct udev *udev, char *buf, size_t len)
{
char line[LINE_MAX], syspath[PATH_MAX], devnode[PATH_MAX];
char syspath[PATH_MAX], devnode[PATH_MAX];
struct udev_device *udev_device;
struct stat st;
char *sysname;
FILE *file;
int i, cnt;
char *pos;
if (stat(path, &st) != 0 || st.st_size > 8192) {
return NULL;
}
file = fopen(path, "r");
if (!file) {
return NULL;
}
const char *sysname;
char *end, *pos;
int i, cnt = 0;
udev_device = calloc(1, sizeof(*udev_device));
if (!udev_device) {
fclose(file);
return NULL;
}
cnt = 0;
udev_device->udev = udev;
udev_device->refcount = 1;
udev_device->parent = NULL;
@@ -668,13 +653,11 @@ struct udev_device *udev_device_new_from_file(struct udev *udev, const char *pat
udev_list_entry_init(&udev_device->properties);
udev_list_entry_init(&udev_device->sysattrs);
while (fgets(line, sizeof(line), file)) {
line[strlen(line) - 1] = '\0';
if (strncmp(line, "DEVPATH=", 8) == 0) {
snprintf(syspath, sizeof(syspath), "/sys%s", line + 8);
for (end = buf + len; buf < end; buf += strlen(buf) + 1) {
if (strncmp(buf, "DEVPATH=", 8) == 0) {
snprintf(syspath, sizeof(syspath), "/sys%s", buf + 8);
udev_list_entry_add(&udev_device->properties, "SYSPATH", syspath, 0);
udev_list_entry_add(&udev_device->properties, "DEVPATH", line + 8, 0);
udev_list_entry_add(&udev_device->properties, "DEVPATH", buf + 8, 0);
sysname = strrchr(syspath, '/') + 1;
udev_list_entry_add(&udev_device->properties, "SYSNAME", sysname, 0);
@@ -688,35 +671,30 @@ struct udev_device *udev_device_new_from_file(struct udev *udev, const char *pat
cnt++;
}
else if (strncmp(line, "DEVNAME=", 8) == 0) {
snprintf(devnode, sizeof(devnode), "/dev/%s", line + 8);
else if (strncmp(buf, "DEVNAME=", 8) == 0) {
snprintf(devnode, sizeof(devnode), "/dev/%s", buf + 8);
udev_list_entry_add(&udev_device->properties, "DEVNAME", devnode, 0);
}
else {
pos = strchr(line, '=');
pos = strchr(buf, '=');
// file is malformed, abort here.
if (!pos) {
cnt = 0;
break;
continue;
}
*pos = '\0';
if (strncmp(line, "SUBSYSTEM", 9) == 0 ||
strncmp(line, "ACTION", 6) == 0 ||
strncmp(line, "SEQNUM", 6) == 0) {
if (strcmp(buf, "SUBSYSTEM") == 0 ||
strcmp(buf, "ACTION") == 0 ||
strcmp(buf, "SEQNUM") == 0) {
cnt++;
}
udev_list_entry_add(&udev_device->properties, line, pos + 1, 0);
udev_list_entry_add(&udev_device->properties, buf, pos + 1, 0);
*pos = '=';
}
}
fclose(file);
// https://freedesktop.org/software/systemd/man/udev_device_new_from_environment.html
// > The keys DEVPATH, SUBSYSTEM, ACTION, and SEQNUM are mandatory.
if (cnt != 4) {
udev_device_unref(udev_device);
return NULL;

View File

@@ -15,36 +15,26 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <poll.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <limits.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/eventfd.h>
#include <sys/inotify.h>
#include <linux/netlink.h>
#include "udev.h"
#include "udev_list.h"
#ifndef UDEV_MONITOR_DIR
#define UDEV_MONITOR_DIR "/tmp/.libudev-zero"
#ifndef UDEV_MONITOR_NLGRP
#define UDEV_MONITOR_NLGRP 0x4
#endif
struct udev_monitor {
struct udev_list_entry subsystem_match;
struct udev_list_entry devtype_match;
struct udev *udev;
pthread_t thread;
const char *dir;
int signal_fd;
int refcount;
int sfd[2];
int ifd;
int nlgrp;
int fd;
};
static int filter_devtype(struct udev_monitor *udev_monitor, struct udev_device *udev_device)
@@ -103,106 +93,75 @@ static int filter_subsystem(struct udev_monitor *udev_monitor, struct udev_devic
struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor)
{
char file[PATH_MAX], data[4096];
struct udev_device *udev_device;
if (recv(udev_monitor->sfd[0], data, sizeof(data), 0) == -1) {
return NULL;
}
// check truncation error to make gcc happy
if ((unsigned)snprintf(file, sizeof(file), "%s/%s", udev_monitor->dir, data) >= sizeof(file)) {
return NULL;
}
udev_device = udev_device_new_from_file(udev_monitor->udev, file);
if (!udev_device) {
return NULL;
}
if (!filter_subsystem(udev_monitor, udev_device) ||
!filter_devtype(udev_monitor, udev_device)) {
udev_device_unref(udev_device);
return NULL;
}
return udev_device;
}
static void *handle_event(void *ptr)
{
struct udev_monitor *udev_monitor = ptr;
struct inotify_event *event;
struct pollfd poll_fds[2];
char data[4096];
struct sockaddr_nl sa = {0};
struct msghdr hdr = {0};
struct iovec iov = {0};
char buf[8192];
ssize_t len;
int i;
poll_fds[0].fd = udev_monitor->ifd;
poll_fds[0].events = POLLIN;
iov.iov_base = buf;
iov.iov_len = sizeof(buf);
poll_fds[1].fd = udev_monitor->signal_fd;
poll_fds[1].events = POLLIN;
hdr.msg_name = &sa;
hdr.msg_namelen = sizeof(sa);
hdr.msg_iov = &iov;
hdr.msg_iovlen = 1;
while (1) {
if (poll(poll_fds, 2, -1) == -1) {
if (errno == EINTR) {
continue;
}
len = recvmsg(udev_monitor->fd, &hdr, 0);
return NULL;
if (len <= 0) {
break;
}
// exit on explicit signal
if (poll_fds[1].revents & POLLIN) {
return NULL;
if (hdr.msg_flags & MSG_TRUNC) {
continue;
}
// exit on poll error
if (!(poll_fds[0].revents & POLLIN)) {
return NULL;
if (sa.nl_groups == 0x0 || (sa.nl_groups == 0x1 && sa.nl_pid)) {
continue;
}
len = read(udev_monitor->ifd, data, sizeof(data));
udev_device = udev_device_new_from_uevent(udev_monitor->udev, buf, len);
if (len == -1) {
return NULL;
if (!udev_device) {
continue;
}
for (i = 0; i < len; i += sizeof(struct inotify_event) + event->len) {
event = (struct inotify_event *)&data[i];
// TODO directory is removed
if (event->mask & IN_IGNORED) {
break;
}
if (event->mask & IN_ISDIR) {
continue;
}
send(udev_monitor->sfd[1], event->name, event->len, 0);
if (!filter_subsystem(udev_monitor, udev_device) ||
!filter_devtype(udev_monitor, udev_device)) {
udev_device_unref(udev_device);
continue;
}
return udev_device;
}
// unreachable
return NULL;
}
int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor)
{
return udev_monitor ? (pthread_create(&udev_monitor->thread, NULL, handle_event, udev_monitor) == 0) - 1 : -1;
struct sockaddr_nl sa = {0};
if (!udev_monitor) {
return -1;
}
sa.nl_family = AF_NETLINK;
sa.nl_groups = udev_monitor->nlgrp;
return bind(udev_monitor->fd, (struct sockaddr *)&sa, sizeof(sa));
}
/* XXX NOT IMPLEMENTED */ int udev_monitor_set_receive_buffer_size(struct udev_monitor *udev_monitor, int size)
int udev_monitor_set_receive_buffer_size(struct udev_monitor *udev_monitor, int size)
{
return 0;
return udev_monitor ? setsockopt(udev_monitor->fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)) : -1;
}
int udev_monitor_get_fd(struct udev_monitor *udev_monitor)
{
return udev_monitor ? udev_monitor->sfd[0] : -1;
return udev_monitor ? udev_monitor->fd : -1;
}
struct udev *udev_monitor_get_udev(struct udev_monitor *udev_monitor)
@@ -254,60 +213,27 @@ struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char
return NULL;
}
udev_monitor->signal_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (udev_monitor->signal_fd == -1) {
goto free_monitor;
if (strcmp(name, "udev") == 0) {
udev_monitor->nlgrp = UDEV_MONITOR_NLGRP;
}
else if (strcmp(name, "kernel") == 0) {
udev_monitor->nlgrp = 0x1;
}
else {
free(udev_monitor);
return NULL;
}
udev_monitor->ifd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK);
udev_monitor->fd = socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_KOBJECT_UEVENT);
if (udev_monitor->ifd == -1) {
goto close_signal_fd;
}
// TODO docs
udev_monitor->dir = getenv("UDEV_MONITOR_DIR");
if (!udev_monitor->dir || udev_monitor->dir[0] == '\0') {
udev_monitor->dir = UDEV_MONITOR_DIR;
if (access(udev_monitor->dir, F_OK) == -1) {
if (errno != ENOENT) {
goto close_ifd;
}
if (mkdir(udev_monitor->dir, 0) == -1) {
goto close_ifd;
}
if (chmod(udev_monitor->dir, 0777) == -1) {
goto close_ifd;
}
}
}
if (inotify_add_watch(udev_monitor->ifd, udev_monitor->dir, IN_CLOSE_WRITE | IN_EXCL_UNLINK | IN_ONLYDIR) == -1) {
goto close_ifd;
}
if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, udev_monitor->sfd) == -1) {
goto close_ifd;
if (udev_monitor->fd == -1) {
free(udev_monitor);
return NULL;
}
udev_monitor->refcount = 1;
udev_monitor->udev = udev;
return udev_monitor;
close_ifd:
close(udev_monitor->ifd);
close_signal_fd:
close(udev_monitor->signal_fd);
free_monitor:
free(udev_monitor);
return NULL;
}
struct udev_monitor *udev_monitor_ref(struct udev_monitor *udev_monitor)
@@ -322,8 +248,6 @@ struct udev_monitor *udev_monitor_ref(struct udev_monitor *udev_monitor)
struct udev_monitor *udev_monitor_unref(struct udev_monitor *udev_monitor)
{
int i;
if (!udev_monitor) {
return NULL;
}
@@ -332,20 +256,10 @@ struct udev_monitor *udev_monitor_unref(struct udev_monitor *udev_monitor)
return NULL;
}
// Wake up the event thread
eventfd_write(udev_monitor->signal_fd, 1);
// waiting for event thread to end before freeing udev_monitor
pthread_join(udev_monitor->thread, NULL);
udev_list_entry_free_all(&udev_monitor->devtype_match);
udev_list_entry_free_all(&udev_monitor->subsystem_match);
for (i = 0; i < 2; i++) {
close(udev_monitor->sfd[i]);
}
close(udev_monitor->signal_fd);
close(udev_monitor->ifd);
close(udev_monitor->fd);
free(udev_monitor);
return NULL;
}