diff --git a/README.md b/README.md index a13a136..7c8ee44 100644 --- a/README.md +++ b/README.md @@ -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=` 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 diff --git a/contrib/helper.c b/contrib/helper.c index 912fd57..6fff27e 100644 --- a/contrib/helper.c +++ b/contrib/helper.c @@ -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 #include #include -#include -#include -#include +#include +#include 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; } diff --git a/contrib/helper.sh b/contrib/helper.sh deleted file mode 100644 index faa1d61..0000000 --- a/contrib/helper.sh +++ /dev/null @@ -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.$$" diff --git a/contrib/mdev.conf b/contrib/mdev.conf index 7bc3c09..18c9dc5 100644 --- a/contrib/mdev.conf +++ b/contrib/mdev.conf @@ -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 diff --git a/udev.h b/udev.h index 39e4043..057d4af 100644 --- a/udev.h +++ b/udev.h @@ -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 } diff --git a/udev_device.c b/udev_device.c index d9ca6c7..939ab46 100644 --- a/udev_device.c +++ b/udev_device.c @@ -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; diff --git a/udev_monitor.c b/udev_monitor.c index 1e79310..820d6a3 100644 --- a/udev_monitor.c +++ b/udev_monitor.c @@ -15,36 +15,26 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include -#include -#include #include #include #include -#include -#include -#include #include -#include -#include +#include #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; }