udev: serialize queued events on exit (#37047)

Previously, all queued events were discarded on exit, hence several
events might not be processed by udevd when it is restarted. Such
situation especially easily happens on switching root.

This makes queued events serialized on exit, and deserialized in the
next invocation. Hence, no events should be lost during restarting
udevd.

This is important with the several aspects. Basically
systemd-udev-trigger.service (re)triggers all devices anyway after
switching root, But the service may be disabled or modified by admin.
Moreover, the service produces only 'add' events, and thus the service
cannot cover events with other actions generated by the kernel during
switching root. Also, the userspace triggered events may not contain
some parameters compared with events triggered by the kernel.
This commit is contained in:
Daan De Meyer
2025-04-22 10:54:25 +02:00
committed by GitHub
2 changed files with 185 additions and 0 deletions
+110
View File
@@ -11,6 +11,7 @@
#include "fd-util.h"
#include "fs-util.h"
#include "hashmap.h"
#include "io-util.h"
#include "iovec-util.h"
#include "list.h"
#include "mkdir.h"
@@ -148,6 +149,7 @@ Manager* manager_free(Manager *manager) {
free(manager->worker_notify_socket_path);
sd_device_monitor_unref(manager->monitor);
udev_ctrl_unref(manager->ctrl);
sd_varlink_server_unref(manager->varlink_server);
@@ -863,6 +865,105 @@ static int event_queue_insert(Manager *manager, sd_device *dev) {
return 0;
}
static int manager_serialize_events(Manager *manager) {
int r;
assert(manager);
_cleanup_(sd_device_monitor_unrefp) sd_device_monitor *storage = NULL;
r = device_monitor_new_full(&storage, MONITOR_GROUP_NONE, -EBADF);
if (r < 0)
return log_warning_errno(r, "Failed to create new device monitor instance: %m");
union sockaddr_union a;
r = device_monitor_get_address(storage, &a);
if (r < 0)
return log_warning_errno(r, "Failed to get address of device monitor socket: %m");
uint64_t n = 0;
LIST_FOREACH(event, event, manager->events) {
if (event->state != EVENT_QUEUED)
continue;
r = device_monitor_send(storage, &a, event->dev);
if (r < 0) {
log_device_warning_errno(event->dev, r, "Failed to save event to socket storage, ignoring: %m");
continue;
}
n++;
}
if (n == 0)
return 0;
r = notify_push_fd(sd_device_monitor_get_fd(storage), "event-serialization");
if (r < 0)
return log_warning_errno(r, "Failed to push event serialization fd to service manager: %m");
log_debug("Serialized %"PRIu64" events.", n);
return 0;
}
static int manager_deserialize_events(Manager *manager, int *fd) {
int r;
assert(manager);
assert(fd);
assert(*fd >= 0);
/* This may take and invalidate passed file descriptor even on failure. */
/* At this stage, we have not receive any events from the kernel, hence should be empty. */
if (manager->events)
return log_warning_errno(SYNTHETIC_ERRNO(EALREADY), "Received multiple event storage socket (%i).", *fd);
r = sd_is_socket(*fd, AF_NETLINK, SOCK_RAW, /* listening = */ -1);
if (r < 0)
return log_warning_errno(r, "Failed to verify type of event storage socket (%i): %m", *fd);
if (r == 0)
return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Received invalid event storage socket (%i).", *fd);
_cleanup_(sd_device_monitor_unrefp) sd_device_monitor *storage = NULL;
r = device_monitor_new_full(&storage, MONITOR_GROUP_NONE, *fd);
if (r < 0)
return log_warning_errno(r, "Failed to initialize event storage: %m");
TAKE_FD(*fd);
r = device_monitor_allow_unicast_sender(storage, storage);
if (r < 0)
return log_warning_errno(r, "Failed to set trusted sender for event storage: %m");
uint64_t n = 0;
for (;;) {
r = fd_wait_for_event(sd_device_monitor_get_fd(storage), POLLIN, 0);
if (r == -EINTR)
continue;
if (r < 0)
return log_warning_errno(r, "Failed to wait for event from event storage: %m");
if (r == 0)
break;
_cleanup_(sd_device_unrefp) sd_device *dev = NULL;
r = sd_device_monitor_receive(storage, &dev);
if (r < 0) {
log_warning_errno(r, "Failed to receive device from event storage, ignoring: %m");
continue;
}
r = event_queue_insert(manager, dev);
if (r < 0) {
log_device_warning_errno(dev, r, "Failed to insert device into event queue, ignoring: %m");
continue;
}
n++;
}
log_debug("Deserialized %"PRIu64" events.", n);
return 0;
}
static int on_uevent(sd_device_monitor *monitor, sd_device *dev, void *userdata) {
Manager *manager = ASSERT_PTR(userdata);
int r;
@@ -1077,6 +1178,8 @@ static int on_post_exit(Manager *manager) {
if (!hashmap_isempty(manager->workers))
return 0; /* There still exist running workers. */
(void) manager_serialize_events(manager);
udev_watch_dump();
return sd_event_exit(manager->event, 0);
}
@@ -1181,6 +1284,8 @@ static int manager_listen_fds(Manager *manager) {
r = manager_init_inotify(manager, fd);
else if (streq(names[i], "config-serialization"))
r = manager_deserialize_config(manager, &fd);
else if (streq(names[i], "event-serialization"))
r = manager_deserialize_events(manager, &fd);
else
r = log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"Received unexpected fd (%s), ignoring.", names[i]);
@@ -1248,6 +1353,11 @@ int manager_main(Manager *manager) {
_unused_ _cleanup_(notify_on_cleanup) const char *notify_message =
notify_start(NOTIFY_READY, NOTIFY_STOPPING);
/* We will start processing events in the loop below. Before starting processing, let's remove the
* event serialization fd from the fdstore, to avoid retrieving the serialized events again in future
* invocations. Otherwise, the serialized events may be processed multiple times. */
(void) notify_remove_fd_warn("event-serialization");
r = sd_event_loop(manager->event);
if (r < 0)
return log_error_errno(r, "Event loop failed: %m");
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -ex
set -o pipefail
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
# Test for queued events serialization on stop and deserialization on start.
rules="/run/udev/rules.d/99-test-17.serialization.rules"
mkdir -p "${rules%/*}"
cat > "$rules" <<'EOF'
SUBSYSTEM!="mem", GOTO="end"
KERNEL!="null", GOTO="end"
ACTION=="remove", GOTO="end"
IMPORT{db}="INVOCATIONS"
IMPORT{program}="/bin/bash -c 'systemctl show --property=InvocationID systemd-udevd.service'"
ENV{INVOCATIONS}+="%E{ACTION}_%E{SEQNUM}_%E{InvocationID}"
ACTION=="add", RUN+="/bin/bash -c ':> /tmp/marker'", RUN+="/usr/bin/sleep 10"
LABEL="end"
EOF
udevadm control --reload --log-level=debug
udevadm settle --timeout 30
rm -f /tmp/marker
# Save the current invocation ID of udevd.
INVOCATION_BEFORE="$(systemctl show --property=InvocationID --value systemd-udevd.service)"
# Trigger several actions.
# The first 'add' event should be processed by the current invocation.
for action in remove add change change change change; do
udevadm trigger --action="$action" /dev/null
done
# While processing the first 'add' event, request restarting udevd.
# Hence, the queued subsequent 'change' events should be serialized,
# then deserialized, requeued, and processed in the next invocation.
timeout 10 bash -c 'until test -e /tmp/marker; do sleep .1; done'
systemctl restart systemd-udevd.service
udevadm settle --timeout 30
# Get the invocation ID of the restarted udevd.
INVOCATION_AFTER="$(systemctl show --property=InvocationID --value systemd-udevd.service)"
udevadm info --no-pager /dev/null
# Check the properties that records action, seqnum, and invocation ID.
previous_seqnum=0
expected_action=add
expected_invocation="$INVOCATION_BEFORE"
count=0
while read -r action seqnum invocation; do
test "$seqnum" -gt "$previous_seqnum"
assert_eq "$action" "$expected_action"
assert_eq "$invocation" "$expected_invocation"
previous_seqnum="$seqnum"
expected_action=change
expected_invocation="$INVOCATION_AFTER"
: $((++count))
done < <(udevadm info -q property --property=INVOCATIONS --value /dev/null | sed -e 's/ /\n/g; s/_/ /g')
# Check the total number of events processed by the udevd.
assert_eq "$count" "5"
rm -f "$rules" /tmp/marker
udevadm control --reload
exit 0