summaryrefslogtreecommitdiffstats
path: root/src/udev
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/udev/ata_id/ata_id.c635
-rw-r--r--src/udev/cdrom_id/cdrom_id.c1023
-rw-r--r--src/udev/dmi_memory_id/dmi_memory_id.c721
-rw-r--r--src/udev/fido_id/fido_id.c129
-rw-r--r--src/udev/fido_id/fido_id_desc.c92
-rw-r--r--src/udev/fido_id/fido_id_desc.h8
-rw-r--r--src/udev/fido_id/fuzz-fido-id-desc.c21
-rw-r--r--src/udev/fido_id/test-fido-id-desc.c80
-rw-r--r--src/udev/fuzz-udev-rule-parse-value.c32
-rw-r--r--src/udev/fuzz-udev-rules.c35
-rw-r--r--src/udev/fuzz-udev-rules.options2
-rwxr-xr-xsrc/udev/generate-keyboard-keys-gperf.sh20
-rwxr-xr-xsrc/udev/generate-keyboard-keys-list.sh9
-rw-r--r--src/udev/iocost/iocost.c321
-rw-r--r--src/udev/iocost/iocost.conf20
-rw-r--r--src/udev/meson.build273
-rw-r--r--src/udev/mtd_probe/mtd_probe.c90
-rw-r--r--src/udev/mtd_probe/mtd_probe.h52
-rw-r--r--src/udev/mtd_probe/probe_smartmedia.c97
-rw-r--r--src/udev/net/fuzz-link-parser.c27
-rw-r--r--src/udev/net/fuzz-link-parser.options2
-rw-r--r--src/udev/net/link-config-gperf.gperf118
-rw-r--r--src/udev/net/link-config.c1133
-rw-r--r--src/udev/net/link-config.h117
-rw-r--r--src/udev/net/test-link-config-tables.c13
-rw-r--r--src/udev/scsi_id/README4
-rw-r--r--src/udev/scsi_id/scsi.h100
-rw-r--r--src/udev/scsi_id/scsi_id.c515
-rw-r--r--src/udev/scsi_id/scsi_id.h63
-rw-r--r--src/udev/scsi_id/scsi_serial.c892
-rw-r--r--src/udev/test-udev-builtin.c20
-rw-r--r--src/udev/test-udev-format.c37
-rw-r--r--src/udev/test-udev-manager.c19
-rw-r--r--src/udev/test-udev-node.c50
-rw-r--r--src/udev/test-udev-rule-runner.c178
-rw-r--r--src/udev/test-udev-rules.c77
-rw-r--r--src/udev/test-udev-spawn.c113
-rw-r--r--src/udev/udev-builtin-blkid.c474
-rw-r--r--src/udev/udev-builtin-btrfs.c50
-rw-r--r--src/udev/udev-builtin-hwdb.c227
-rw-r--r--src/udev/udev-builtin-input_id.c433
-rw-r--r--src/udev/udev-builtin-keyboard.c252
-rw-r--r--src/udev/udev-builtin-kmod.c94
-rw-r--r--src/udev/udev-builtin-net_driver.c43
-rw-r--r--src/udev/udev-builtin-net_id.c1366
-rw-r--r--src/udev/udev-builtin-net_setup_link.c113
-rw-r--r--src/udev/udev-builtin-path_id.c896
-rw-r--r--src/udev/udev-builtin-uaccess.c81
-rw-r--r--src/udev/udev-builtin-usb_id.c489
-rw-r--r--src/udev/udev-builtin.c156
-rw-r--r--src/udev/udev-builtin.h88
-rw-r--r--src/udev/udev-ctrl.c353
-rw-r--r--src/udev/udev-ctrl.h78
-rw-r--r--src/udev/udev-event.c411
-rw-r--r--src/udev/udev-event.h60
-rw-r--r--src/udev/udev-format.c550
-rw-r--r--src/udev/udev-format.h21
-rw-r--r--src/udev/udev-manager.c1352
-rw-r--r--src/udev/udev-manager.h62
-rw-r--r--src/udev/udev-node.c790
-rw-r--r--src/udev/udev-node.h29
-rw-r--r--src/udev/udev-rules.c2965
-rw-r--r--src/udev/udev-rules.h49
-rw-r--r--src/udev/udev-spawn.c355
-rw-r--r--src/udev/udev-spawn.h28
-rw-r--r--src/udev/udev-trace.h35
-rw-r--r--src/udev/udev-watch.c260
-rw-r--r--src/udev/udev-watch.h13
-rw-r--r--src/udev/udev-worker.c352
-rw-r--r--src/udev/udev-worker.h52
-rw-r--r--src/udev/udev.conf11
-rw-r--r--src/udev/udev.pc.in15
-rw-r--r--src/udev/udevadm-control.c239
-rw-r--r--src/udev/udevadm-hwdb.c102
-rw-r--r--src/udev/udevadm-info.c1120
-rw-r--r--src/udev/udevadm-lock.c306
-rw-r--r--src/udev/udevadm-monitor.c246
-rw-r--r--src/udev/udevadm-settle.c252
-rw-r--r--src/udev/udevadm-test-builtin.c117
-rw-r--r--src/udev/udevadm-test.c152
-rw-r--r--src/udev/udevadm-trigger.c569
-rw-r--r--src/udev/udevadm-util.c124
-rw-r--r--src/udev/udevadm-util.h8
-rw-r--r--src/udev/udevadm-verify.c236
-rw-r--r--src/udev/udevadm-wait.c456
-rw-r--r--src/udev/udevadm.c140
-rw-r--r--src/udev/udevadm.h24
-rw-r--r--src/udev/udevd.c408
-rw-r--r--src/udev/udevd.h4
-rw-r--r--src/udev/v4l_id/v4l_id.c113
90 files changed, 24357 insertions, 0 deletions
diff --git a/src/udev/ata_id/ata_id.c b/src/udev/ata_id/ata_id.c
new file mode 100644
index 0000000..4dd7e54
--- /dev/null
+++ b/src/udev/ata_id/ata_id.c
@@ -0,0 +1,635 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * ata_id - reads product/serial number from ATA drives
+ *
+ * Copyright © 2009-2010 David Zeuthen <zeuthen@gmail.com>
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <linux/bsg.h>
+#include <linux/hdreg.h>
+#include <scsi/scsi.h>
+#include <scsi/scsi_ioctl.h>
+#include <scsi/sg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "build.h"
+#include "device-nodes.h"
+#include "fd-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "udev-util.h"
+#include "unaligned.h"
+
+#define COMMAND_TIMEOUT_MSEC (30 * 1000)
+
+static bool arg_export = false;
+static const char *arg_device = NULL;
+
+static int disk_scsi_inquiry_command(
+ int fd,
+ void *buf,
+ size_t buf_len) {
+
+ uint8_t cdb[6] = {
+ /* INQUIRY, see SPC-4 section 6.4 */
+ [0] = 0x12, /* OPERATION CODE: INQUIRY */
+ [3] = (buf_len >> 8), /* ALLOCATION LENGTH */
+ [4] = (buf_len & 0xff),
+ };
+ uint8_t sense[32] = {};
+ struct sg_io_v4 io_v4 = {
+ .guard = 'Q',
+ .protocol = BSG_PROTOCOL_SCSI,
+ .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD,
+ .request_len = sizeof(cdb),
+ .request = (uintptr_t) cdb,
+ .max_response_len = sizeof(sense),
+ .response = (uintptr_t) sense,
+ .din_xfer_len = buf_len,
+ .din_xferp = (uintptr_t) buf,
+ .timeout = COMMAND_TIMEOUT_MSEC,
+ };
+
+ if (ioctl(fd, SG_IO, &io_v4) != 0) {
+ if (errno != EINVAL)
+ return log_debug_errno(errno, "ioctl v4 failed: %m");
+
+ /* could be that the driver doesn't do version 4, try version 3 */
+ struct sg_io_hdr io_hdr = {
+ .interface_id = 'S',
+ .cmdp = (unsigned char*) cdb,
+ .cmd_len = sizeof (cdb),
+ .dxferp = buf,
+ .dxfer_len = buf_len,
+ .sbp = sense,
+ .mx_sb_len = sizeof(sense),
+ .dxfer_direction = SG_DXFER_FROM_DEV,
+ .timeout = COMMAND_TIMEOUT_MSEC,
+ };
+
+ if (ioctl(fd, SG_IO, &io_hdr) != 0)
+ return log_debug_errno(errno, "ioctl v3 failed: %m");
+
+ /* even if the ioctl succeeds, we need to check the return value */
+ if (io_hdr.status != 0 ||
+ io_hdr.host_status != 0 ||
+ io_hdr.driver_status != 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "ioctl v3 failed");
+
+ } else {
+ /* even if the ioctl succeeds, we need to check the return value */
+ if (io_v4.device_status != 0 ||
+ io_v4.transport_status != 0 ||
+ io_v4.driver_status != 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "ioctl v4 failed");
+ }
+
+ return 0;
+}
+
+static int disk_identify_command(
+ int fd,
+ void *buf,
+ size_t buf_len) {
+
+ uint8_t cdb[12] = {
+ /*
+ * ATA Pass-Through 12 byte command, as described in
+ *
+ * T10 04-262r8 ATA Command Pass-Through
+ *
+ * from http://www.t10.org/ftp/t10/document.04/04-262r8.pdf
+ */
+ [0] = 0xa1, /* OPERATION CODE: 12 byte pass through */
+ [1] = 4 << 1, /* PROTOCOL: PIO Data-in */
+ [2] = 0x2e, /* OFF_LINE=0, CK_COND=1, T_DIR=1, BYT_BLOK=1, T_LENGTH=2 */
+ [3] = 0, /* FEATURES */
+ [4] = 1, /* SECTORS */
+ [5] = 0, /* LBA LOW */
+ [6] = 0, /* LBA MID */
+ [7] = 0, /* LBA HIGH */
+ [8] = 0 & 0x4F, /* SELECT */
+ [9] = 0xEC, /* Command: ATA IDENTIFY DEVICE */
+ };
+ uint8_t sense[32] = {};
+ uint8_t *desc = sense + 8;
+ struct sg_io_v4 io_v4 = {
+ .guard = 'Q',
+ .protocol = BSG_PROTOCOL_SCSI,
+ .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD,
+ .request_len = sizeof(cdb),
+ .request = (uintptr_t) cdb,
+ .max_response_len = sizeof(sense),
+ .response = (uintptr_t) sense,
+ .din_xfer_len = buf_len,
+ .din_xferp = (uintptr_t) buf,
+ .timeout = COMMAND_TIMEOUT_MSEC,
+ };
+
+ if (ioctl(fd, SG_IO, &io_v4) != 0) {
+ if (errno != EINVAL)
+ return log_debug_errno(errno, "ioctl v4 failed: %m");
+
+ /* could be that the driver doesn't do version 4, try version 3 */
+ struct sg_io_hdr io_hdr = {
+ .interface_id = 'S',
+ .cmdp = (unsigned char*) cdb,
+ .cmd_len = sizeof (cdb),
+ .dxferp = buf,
+ .dxfer_len = buf_len,
+ .sbp = sense,
+ .mx_sb_len = sizeof (sense),
+ .dxfer_direction = SG_DXFER_FROM_DEV,
+ .timeout = COMMAND_TIMEOUT_MSEC,
+ };
+
+ if (ioctl(fd, SG_IO, &io_hdr) != 0)
+ return log_debug_errno(errno, "ioctl v3 failed: %m");
+ } else {
+ if (!((sense[0] & 0x7f) == 0x72 && desc[0] == 0x9 && desc[1] == 0x0c) &&
+ !((sense[0] & 0x7f) == 0x70 && sense[12] == 0x00 && sense[13] == 0x1d))
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "ioctl v4 failed: %m");
+ }
+
+ return 0;
+}
+
+static int disk_identify_packet_device_command(
+ int fd,
+ void *buf,
+ size_t buf_len) {
+
+ uint8_t cdb[16] = {
+ /*
+ * ATA Pass-Through 16 byte command, as described in
+ *
+ * T10 04-262r8 ATA Command Pass-Through
+ *
+ * from http://www.t10.org/ftp/t10/document.04/04-262r8.pdf
+ */
+ [0] = 0x85, /* OPERATION CODE: 16 byte pass through */
+ [1] = 4 << 1, /* PROTOCOL: PIO Data-in */
+ [2] = 0x2e, /* OFF_LINE=0, CK_COND=1, T_DIR=1, BYT_BLOK=1, T_LENGTH=2 */
+ [3] = 0, /* FEATURES */
+ [4] = 0, /* FEATURES */
+ [5] = 0, /* SECTORS */
+ [6] = 1, /* SECTORS */
+ [7] = 0, /* LBA LOW */
+ [8] = 0, /* LBA LOW */
+ [9] = 0, /* LBA MID */
+ [10] = 0, /* LBA MID */
+ [11] = 0, /* LBA HIGH */
+ [12] = 0, /* LBA HIGH */
+ [13] = 0, /* DEVICE */
+ [14] = 0xA1, /* Command: ATA IDENTIFY PACKET DEVICE */
+ [15] = 0, /* CONTROL */
+ };
+ uint8_t sense[32] = {};
+ uint8_t *desc = sense + 8;
+ struct sg_io_v4 io_v4 = {
+ .guard = 'Q',
+ .protocol = BSG_PROTOCOL_SCSI,
+ .subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD,
+ .request_len = sizeof (cdb),
+ .request = (uintptr_t) cdb,
+ .max_response_len = sizeof (sense),
+ .response = (uintptr_t) sense,
+ .din_xfer_len = buf_len,
+ .din_xferp = (uintptr_t) buf,
+ .timeout = COMMAND_TIMEOUT_MSEC,
+ };
+
+ if (ioctl(fd, SG_IO, &io_v4) != 0) {
+ if (errno != EINVAL)
+ return log_debug_errno(errno, "ioctl v4 failed: %m");
+
+ /* could be that the driver doesn't do version 4, try version 3 */
+ struct sg_io_hdr io_hdr = {
+ .interface_id = 'S',
+ .cmdp = (unsigned char*) cdb,
+ .cmd_len = sizeof (cdb),
+ .dxferp = buf,
+ .dxfer_len = buf_len,
+ .sbp = sense,
+ .mx_sb_len = sizeof (sense),
+ .dxfer_direction = SG_DXFER_FROM_DEV,
+ .timeout = COMMAND_TIMEOUT_MSEC,
+ };
+
+ if (ioctl(fd, SG_IO, &io_hdr) != 0)
+ return log_debug_errno(errno, "ioctl v3 failed: %m");
+ } else {
+ if ((sense[0] & 0x7f) != 0x72 || desc[0] != 0x9 || desc[1] != 0x0c)
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "ioctl v4 failed: %m");
+ }
+
+ return 0;
+}
+
+/**
+ * disk_identify_get_string:
+ * @identify: A block of IDENTIFY data
+ * @offset_words: Offset of the string to get, in words.
+ * @dest: Destination buffer for the string.
+ * @dest_len: Length of destination buffer, in bytes.
+ *
+ * Copies the ATA string from @identify located at @offset_words into @dest.
+ */
+static void disk_identify_get_string(
+ uint8_t identify[512],
+ unsigned offset_words,
+ char *dest,
+ size_t dest_len) {
+
+ unsigned c1;
+ unsigned c2;
+
+ while (dest_len > 0) {
+ c1 = identify[offset_words * 2 + 1];
+ c2 = identify[offset_words * 2];
+ *dest = c1;
+ dest++;
+ *dest = c2;
+ dest++;
+ offset_words++;
+ dest_len -= 2;
+ }
+}
+
+static void disk_identify_fixup_string(
+ uint8_t identify[512],
+ unsigned offset_words,
+ size_t len) {
+ assert(offset_words < 512/2);
+ disk_identify_get_string(identify, offset_words,
+ (char *) identify + offset_words * 2, len);
+}
+
+static void disk_identify_fixup_uint16(uint8_t identify[512], unsigned offset_words) {
+ assert(offset_words < 512/2);
+ unaligned_write_ne16(identify + offset_words * 2,
+ unaligned_read_le16(identify + offset_words * 2));
+}
+
+/**
+ * disk_identify:
+ * @fd: File descriptor for the block device.
+ * @out_identify: Return location for IDENTIFY data.
+ *
+ * Sends the IDENTIFY DEVICE or IDENTIFY PACKET DEVICE command to the
+ * device represented by @fd. If successful, then the result will be
+ * copied into @out_identify.
+ *
+ * This routine is based on code from libatasmart, LGPL v2.1.
+ *
+ * Returns: 0 if the data was successfully obtained, otherwise
+ * non-zero with errno set.
+ */
+static int disk_identify(int fd,
+ uint8_t out_identify[512],
+ int *ret_peripheral_device_type) {
+ uint8_t inquiry_buf[36];
+ int peripheral_device_type, r;
+
+ /* init results */
+ memzero(out_identify, 512);
+
+ /* If we were to use ATA PASS_THROUGH (12) on an ATAPI device
+ * we could accidentally blank media. This is because MMC's BLANK
+ * command has the same op-code (0x61).
+ *
+ * To prevent this from happening we bail out if the device
+ * isn't a Direct Access Block Device, e.g. SCSI type 0x00
+ * (CD/DVD devices are type 0x05). So we send a SCSI INQUIRY
+ * command first... libata is handling this via its SCSI
+ * emulation layer.
+ *
+ * This also ensures that we're actually dealing with a device
+ * that understands SCSI commands.
+ *
+ * (Yes, it is a bit perverse that we're tunneling the ATA
+ * command through SCSI and relying on the ATA driver
+ * emulating SCSI well-enough...)
+ *
+ * (See commit 160b069c25690bfb0c785994c7c3710289179107 for
+ * the original bug-fix and see http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=556635
+ * for the original bug-report.)
+ */
+ r = disk_scsi_inquiry_command(fd, inquiry_buf, sizeof inquiry_buf);
+ if (r < 0)
+ return r;
+
+ /* SPC-4, section 6.4.2: Standard INQUIRY data */
+ peripheral_device_type = inquiry_buf[0] & 0x1f;
+ if (peripheral_device_type == 0x05) {
+ r = disk_identify_packet_device_command(fd, out_identify, 512);
+ if (r < 0)
+ return r;
+
+ } else {
+ if (!IN_SET(peripheral_device_type, 0x00, 0x14))
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Unsupported device type.");
+
+ /* OK, now issue the IDENTIFY DEVICE command */
+ r = disk_identify_command(fd, out_identify, 512);
+ if (r < 0)
+ return r;
+ }
+
+ /* Check if IDENTIFY data is all NUL bytes - if so, bail */
+ bool all_nul_bytes = true;
+ for (size_t n = 0; n < 512; n++)
+ if (out_identify[n] != '\0') {
+ all_nul_bytes = false;
+ break;
+ }
+
+ if (all_nul_bytes)
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO), "IDENTIFY data is all zeroes.");
+
+ if (ret_peripheral_device_type)
+ *ret_peripheral_device_type = peripheral_device_type;
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "export", no_argument, NULL, 'x' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ {}
+ };
+ int c;
+
+ while ((c = getopt_long(argc, argv, "xh", options, NULL)) >= 0)
+ switch (c) {
+ case 'x':
+ arg_export = true;
+ break;
+ case 'h':
+ printf("%s [OPTIONS...] DEVICE\n\n"
+ " -x --export Print values as environment keys\n"
+ " -h --help Show this help text\n"
+ " --version Show package version\n",
+ program_invocation_short_name);
+ return 0;
+ case 'v':
+ return version();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ if (!argv[optind])
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "DEVICE argument missing.");
+
+ arg_device = argv[optind];
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ struct hd_driveid id;
+ union {
+ uint8_t byte[512];
+ uint16_t wyde[256];
+ } identify;
+ char model[41], model_enc[256], serial[21], revision[9];
+ _cleanup_close_ int fd = -EBADF;
+ uint16_t word;
+ int r, peripheral_device_type = -1;
+
+ log_set_target(LOG_TARGET_AUTO);
+ udev_parse_config();
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ fd = open(ASSERT_PTR(arg_device), O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0)
+ return log_error_errno(errno, "Cannot open %s: %m", arg_device);
+
+ if (disk_identify(fd, identify.byte, &peripheral_device_type) >= 0) {
+ /*
+ * fix up only the fields from the IDENTIFY data that we are going to
+ * use and copy it into the hd_driveid struct for convenience
+ */
+ disk_identify_fixup_string(identify.byte, 10, 20); /* serial */
+ disk_identify_fixup_string(identify.byte, 23, 8); /* fwrev */
+ disk_identify_fixup_string(identify.byte, 27, 40); /* model */
+ disk_identify_fixup_uint16(identify.byte, 0); /* configuration */
+ disk_identify_fixup_uint16(identify.byte, 75); /* queue depth */
+ disk_identify_fixup_uint16(identify.byte, 76); /* SATA capabilities */
+ disk_identify_fixup_uint16(identify.byte, 82); /* command set supported */
+ disk_identify_fixup_uint16(identify.byte, 83); /* command set supported */
+ disk_identify_fixup_uint16(identify.byte, 84); /* command set supported */
+ disk_identify_fixup_uint16(identify.byte, 85); /* command set supported */
+ disk_identify_fixup_uint16(identify.byte, 86); /* command set supported */
+ disk_identify_fixup_uint16(identify.byte, 87); /* command set supported */
+ disk_identify_fixup_uint16(identify.byte, 89); /* time required for SECURITY ERASE UNIT */
+ disk_identify_fixup_uint16(identify.byte, 90); /* time required for enhanced SECURITY ERASE UNIT */
+ disk_identify_fixup_uint16(identify.byte, 91); /* current APM values */
+ disk_identify_fixup_uint16(identify.byte, 94); /* current AAM value */
+ disk_identify_fixup_uint16(identify.byte, 108); /* WWN */
+ disk_identify_fixup_uint16(identify.byte, 109); /* WWN */
+ disk_identify_fixup_uint16(identify.byte, 110); /* WWN */
+ disk_identify_fixup_uint16(identify.byte, 111); /* WWN */
+ disk_identify_fixup_uint16(identify.byte, 128); /* device lock function */
+ disk_identify_fixup_uint16(identify.byte, 217); /* nominal media rotation rate */
+ memcpy(&id, identify.byte, sizeof id);
+ } else {
+ /* If this fails, then try HDIO_GET_IDENTITY */
+ if (ioctl(fd, HDIO_GET_IDENTITY, &id) != 0)
+ return log_debug_errno(errno, "%s: HDIO_GET_IDENTITY failed: %m", arg_device);
+ }
+
+ memcpy(model, id.model, 40);
+ model[40] = '\0';
+ encode_devnode_name(model, model_enc, sizeof(model_enc));
+ udev_replace_whitespace((char *) id.model, model, 40);
+ udev_replace_chars(model, NULL);
+ udev_replace_whitespace((char *) id.serial_no, serial, 20);
+ udev_replace_chars(serial, NULL);
+ udev_replace_whitespace((char *) id.fw_rev, revision, 8);
+ udev_replace_chars(revision, NULL);
+
+ if (arg_export) {
+ /* Set this to convey the disk speaks the ATA protocol */
+ printf("ID_ATA=1\n");
+
+ if ((id.config >> 8) & 0x80) {
+ /* This is an ATAPI device */
+ switch ((id.config >> 8) & 0x1f) {
+ case 0:
+ printf("ID_TYPE=cd\n");
+ break;
+ case 1:
+ printf("ID_TYPE=tape\n");
+ break;
+ case 5:
+ printf("ID_TYPE=cd\n");
+ break;
+ case 7:
+ printf("ID_TYPE=optical\n");
+ break;
+ default:
+ printf("ID_TYPE=generic\n");
+ break;
+ }
+ } else
+ printf("ID_TYPE=disk\n");
+ printf("ID_BUS=ata\n");
+ printf("ID_MODEL=%s\n", model);
+ printf("ID_MODEL_ENC=%s\n", model_enc);
+ printf("ID_REVISION=%s\n", revision);
+ if (serial[0] != '\0') {
+ printf("ID_SERIAL=%s_%s\n", model, serial);
+ printf("ID_SERIAL_SHORT=%s\n", serial);
+ } else
+ printf("ID_SERIAL=%s\n", model);
+
+ if (id.command_set_1 & (1<<5)) {
+ printf("ID_ATA_WRITE_CACHE=1\n");
+ printf("ID_ATA_WRITE_CACHE_ENABLED=%d\n", (id.cfs_enable_1 & (1<<5)) ? 1 : 0);
+ }
+ if (id.command_set_1 & (1<<10)) {
+ printf("ID_ATA_FEATURE_SET_HPA=1\n");
+ printf("ID_ATA_FEATURE_SET_HPA_ENABLED=%d\n", (id.cfs_enable_1 & (1<<10)) ? 1 : 0);
+
+ /*
+ * TODO: use the READ NATIVE MAX ADDRESS command to get the native max address
+ * so it is easy to check whether the protected area is in use.
+ */
+ }
+ if (id.command_set_1 & (1<<3)) {
+ printf("ID_ATA_FEATURE_SET_PM=1\n");
+ printf("ID_ATA_FEATURE_SET_PM_ENABLED=%d\n", (id.cfs_enable_1 & (1<<3)) ? 1 : 0);
+ }
+ if (id.command_set_1 & (1<<1)) {
+ printf("ID_ATA_FEATURE_SET_SECURITY=1\n");
+ printf("ID_ATA_FEATURE_SET_SECURITY_ENABLED=%d\n", (id.cfs_enable_1 & (1<<1)) ? 1 : 0);
+ printf("ID_ATA_FEATURE_SET_SECURITY_ERASE_UNIT_MIN=%d\n", id.trseuc * 2);
+ if ((id.cfs_enable_1 & (1<<1))) /* enabled */ {
+ if (id.dlf & (1<<8))
+ printf("ID_ATA_FEATURE_SET_SECURITY_LEVEL=maximum\n");
+ else
+ printf("ID_ATA_FEATURE_SET_SECURITY_LEVEL=high\n");
+ }
+ if (id.dlf & (1<<5))
+ printf("ID_ATA_FEATURE_SET_SECURITY_ENHANCED_ERASE_UNIT_MIN=%d\n", id.trsEuc * 2);
+ if (id.dlf & (1<<4))
+ printf("ID_ATA_FEATURE_SET_SECURITY_EXPIRE=1\n");
+ if (id.dlf & (1<<3))
+ printf("ID_ATA_FEATURE_SET_SECURITY_FROZEN=1\n");
+ if (id.dlf & (1<<2))
+ printf("ID_ATA_FEATURE_SET_SECURITY_LOCKED=1\n");
+ }
+ if (id.command_set_1 & (1<<0)) {
+ printf("ID_ATA_FEATURE_SET_SMART=1\n");
+ printf("ID_ATA_FEATURE_SET_SMART_ENABLED=%d\n", (id.cfs_enable_1 & (1<<0)) ? 1 : 0);
+ }
+ if (id.command_set_2 & (1<<9)) {
+ printf("ID_ATA_FEATURE_SET_AAM=1\n");
+ printf("ID_ATA_FEATURE_SET_AAM_ENABLED=%d\n", (id.cfs_enable_2 & (1<<9)) ? 1 : 0);
+ printf("ID_ATA_FEATURE_SET_AAM_VENDOR_RECOMMENDED_VALUE=%d\n", id.acoustic >> 8);
+ printf("ID_ATA_FEATURE_SET_AAM_CURRENT_VALUE=%d\n", id.acoustic & 0xff);
+ }
+ if (id.command_set_2 & (1<<5)) {
+ printf("ID_ATA_FEATURE_SET_PUIS=1\n");
+ printf("ID_ATA_FEATURE_SET_PUIS_ENABLED=%d\n", (id.cfs_enable_2 & (1<<5)) ? 1 : 0);
+ }
+ if (id.command_set_2 & (1<<3)) {
+ printf("ID_ATA_FEATURE_SET_APM=1\n");
+ printf("ID_ATA_FEATURE_SET_APM_ENABLED=%d\n", (id.cfs_enable_2 & (1<<3)) ? 1 : 0);
+ if ((id.cfs_enable_2 & (1<<3)))
+ printf("ID_ATA_FEATURE_SET_APM_CURRENT_VALUE=%d\n", id.CurAPMvalues & 0xff);
+ }
+ if (id.command_set_2 & (1<<0))
+ printf("ID_ATA_DOWNLOAD_MICROCODE=1\n");
+
+ /*
+ * Word 76 indicates the capabilities of a SATA device. A PATA device shall set
+ * word 76 to 0000h or FFFFh. If word 76 is set to 0000h or FFFFh, then
+ * the device does not claim compliance with the Serial ATA specification and words
+ * 76 through 79 are not valid and shall be ignored.
+ */
+
+ word = identify.wyde[76];
+ if (!IN_SET(word, 0x0000, 0xffff)) {
+ printf("ID_ATA_SATA=1\n");
+ /*
+ * If bit 2 of word 76 is set to one, then the device supports the Gen2
+ * signaling rate of 3.0 Gb/s (see SATA 2.6).
+ *
+ * If bit 1 of word 76 is set to one, then the device supports the Gen1
+ * signaling rate of 1.5 Gb/s (see SATA 2.6).
+ */
+ if (word & (1<<2))
+ printf("ID_ATA_SATA_SIGNAL_RATE_GEN2=1\n");
+ if (word & (1<<1))
+ printf("ID_ATA_SATA_SIGNAL_RATE_GEN1=1\n");
+ }
+
+ /* Word 217 indicates the nominal media rotation rate of the device */
+ word = identify.wyde[217];
+ if (word == 0x0001)
+ printf ("ID_ATA_ROTATION_RATE_RPM=0\n"); /* non-rotating e.g. SSD */
+ else if (word >= 0x0401 && word <= 0xfffe)
+ printf ("ID_ATA_ROTATION_RATE_RPM=%d\n", word);
+
+ /*
+ * Words 108-111 contain a mandatory World Wide Name (WWN) in the NAA IEEE Registered identifier
+ * format. Word 108 bits (15:12) shall contain 5h, indicating that the naming authority is IEEE.
+ * All other values are reserved.
+ */
+ word = identify.wyde[108];
+ if ((word & 0xf000) == 0x5000) {
+ uint64_t wwwn;
+
+ wwwn = identify.wyde[108];
+ wwwn <<= 16;
+ wwwn |= identify.wyde[109];
+ wwwn <<= 16;
+ wwwn |= identify.wyde[110];
+ wwwn <<= 16;
+ wwwn |= identify.wyde[111];
+ printf("ID_WWN=0x%1$" PRIx64 "\n"
+ "ID_WWN_WITH_EXTENSION=0x%1$" PRIx64 "\n",
+ wwwn);
+ }
+
+ /* from Linux's include/linux/ata.h */
+ if (IN_SET(identify.wyde[0], 0x848a, 0x844a) ||
+ (identify.wyde[83] & 0xc004) == 0x4004)
+ printf("ID_ATA_CFA=1\n");
+
+ if (peripheral_device_type >= 0)
+ printf("ID_ATA_PERIPHERAL_DEVICE_TYPE=%d\n", peripheral_device_type);
+ } else {
+ if (serial[0] != '\0')
+ printf("%s_%s\n", model, serial);
+ else
+ printf("%s\n", model);
+ }
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/cdrom_id/cdrom_id.c b/src/udev/cdrom_id/cdrom_id.c
new file mode 100644
index 0000000..9285dd8
--- /dev/null
+++ b/src/udev/cdrom_id/cdrom_id.c
@@ -0,0 +1,1023 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * cdrom_id - optical drive and media information prober
+ */
+
+#include <fcntl.h>
+#include <getopt.h>
+#include <linux/cdrom.h>
+#include <scsi/sg.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include "build.h"
+#include "fd-util.h"
+#include "main-func.h"
+#include "memory-util.h"
+#include "random-util.h"
+#include "sort-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "udev-util.h"
+#include "unaligned.h"
+
+static bool arg_eject = false;
+static bool arg_lock = false;
+static bool arg_unlock = false;
+static const char *arg_node = NULL;
+
+typedef enum Feature {
+ FEATURE_RW_NONREMOVABLE = 0x01,
+ FEATURE_RW_REMOVABLE = 0x02,
+
+ FEATURE_MO_SE = 0x03, /* sector erase */
+ FEATURE_MO_WO = 0x04, /* write once */
+ FEATURE_MO_AS = 0x05, /* advance storage */
+
+ FEATURE_CD_ROM = 0x08,
+ FEATURE_CD_R = 0x09,
+ FEATURE_CD_RW = 0x0a,
+
+ FEATURE_DVD_ROM = 0x10,
+ FEATURE_DVD_R = 0x11,
+ FEATURE_DVD_RAM = 0x12,
+ FEATURE_DVD_RW_RO = 0x13, /* restricted overwrite mode */
+ FEATURE_DVD_RW_SEQ = 0x14, /* sequential mode */
+ FEATURE_DVD_R_DL_SEQ = 0x15, /* sequential recording */
+ FEATURE_DVD_R_DL_JR = 0x16, /* jump recording */
+ FEATURE_DVD_RW_DL = 0x17,
+ FEATURE_DVD_R_DDR = 0x18, /* download disc recording - dvd for css managed recording */
+ FEATURE_DVD_PLUS_RW = 0x1a,
+ FEATURE_DVD_PLUS_R = 0x1b,
+
+ FEATURE_DDCD_ROM = 0x20,
+ FEATURE_DDCD_R = 0x21,
+ FEATURE_DDCD_RW = 0x22,
+
+ FEATURE_DVD_PLUS_RW_DL = 0x2a,
+ FEATURE_DVD_PLUS_R_DL = 0x2b,
+
+ FEATURE_BD = 0x40,
+ FEATURE_BD_R_SRM = 0x41, /* sequential recording mode */
+ FEATURE_BD_R_RRM = 0x42, /* random recording mode */
+ FEATURE_BD_RE = 0x43,
+
+ FEATURE_HDDVD = 0x50,
+ FEATURE_HDDVD_R = 0x51,
+ FEATURE_HDDVD_RAM = 0x52,
+ FEATURE_HDDVD_RW = 0x53,
+ FEATURE_HDDVD_R_DL = 0x58,
+ FEATURE_HDDVD_RW_DL = 0x5a,
+
+ FEATURE_MRW,
+ FEATURE_MRW_W,
+
+ _FEATURE_MAX,
+ _FEATURE_INVALID = -EINVAL,
+} Feature;
+
+typedef enum MediaState {
+ MEDIA_STATE_BLANK = 0,
+ MEDIA_STATE_APPENDABLE = 1,
+ MEDIA_STATE_COMPLETE = 2,
+ MEDIA_STATE_OTHER = 3,
+ _MEDIA_STATE_MAX,
+ _MEDIA_STATE_INVALID = -EINVAL,
+} MediaState;
+
+typedef struct Context {
+ int fd;
+
+ Feature *drive_features;
+ size_t n_drive_feature;
+
+ Feature media_feature;
+ bool has_media;
+
+ MediaState media_state;
+ unsigned media_session_next;
+ unsigned media_session_count;
+ unsigned media_track_count;
+ unsigned media_track_count_data;
+ unsigned media_track_count_audio;
+ uint64_t media_session_last_offset;
+} Context;
+
+#define CONTEXT_EMPTY { \
+ .fd = -EBADF, \
+ .media_feature = _FEATURE_INVALID, \
+ .media_state = _MEDIA_STATE_INVALID, \
+ }
+
+static void context_clear(Context *c) {
+ if (!c)
+ return;
+
+ safe_close(c->fd);
+ free(c->drive_features);
+}
+
+static bool drive_has_feature(const Context *c, Feature f) {
+ assert(c);
+
+ for (size_t i = 0; i < c->n_drive_feature; i++)
+ if (c->drive_features[i] == f)
+ return true;
+
+ return false;
+}
+
+static int set_drive_feature(Context *c, Feature f) {
+ assert(c);
+
+ if (drive_has_feature(c, f))
+ return 0;
+
+ if (!GREEDY_REALLOC(c->drive_features, c->n_drive_feature + 1))
+ return -ENOMEM;
+
+ c->drive_features[c->n_drive_feature++] = f;
+ return 1;
+}
+
+#define ERRCODE(s) ((((s)[2] & 0x0F) << 16) | ((s)[12] << 8) | ((s)[13]))
+#define SK(errcode) (((errcode) >> 16) & 0xFU)
+#define ASC(errcode) (((errcode) >> 8) & 0xFFU)
+#define ASCQ(errcode) ((errcode) & 0xFFU)
+#define CHECK_CONDITION 0x01
+
+static int log_scsi_debug_errno(int error, const char *msg) {
+ assert(error != 0);
+
+ /* error < 0 means errno-style error, error > 0 means SCSI error */
+
+ if (error < 0)
+ return log_debug_errno(error, "Failed to %s: %m", msg);
+
+ return log_debug_errno(SYNTHETIC_ERRNO(EIO),
+ "Failed to %s with SK=%X/ASC=%02X/ACQ=%02X",
+ msg, SK(error), ASC(error), ASCQ(error));
+}
+
+struct scsi_cmd {
+ struct cdrom_generic_command cgc;
+ union {
+ struct request_sense s;
+ unsigned char u[18];
+ } _sense;
+ struct sg_io_hdr sg_io;
+};
+
+static void scsi_cmd_init(struct scsi_cmd *cmd) {
+ memzero(cmd, sizeof(struct scsi_cmd));
+ cmd->cgc.quiet = 1;
+ cmd->cgc.sense = &cmd->_sense.s;
+ cmd->sg_io.interface_id = 'S';
+ cmd->sg_io.mx_sb_len = sizeof(cmd->_sense);
+ cmd->sg_io.cmdp = cmd->cgc.cmd;
+ cmd->sg_io.sbp = cmd->_sense.u;
+ cmd->sg_io.flags = SG_FLAG_LUN_INHIBIT | SG_FLAG_DIRECT_IO;
+}
+
+static void scsi_cmd_set(struct scsi_cmd *cmd, size_t i, unsigned char arg) {
+ cmd->sg_io.cmd_len = i + 1;
+ cmd->cgc.cmd[i] = arg;
+}
+
+static int scsi_cmd_run(struct scsi_cmd *cmd, int fd, unsigned char *buf, size_t bufsize) {
+ int r;
+
+ assert(cmd);
+ assert(fd >= 0);
+ assert(buf || bufsize == 0);
+
+ /* Return 0 on success. On failure, return negative errno or positive error code. */
+
+ if (bufsize > 0) {
+ cmd->sg_io.dxferp = buf;
+ cmd->sg_io.dxfer_len = bufsize;
+ cmd->sg_io.dxfer_direction = SG_DXFER_FROM_DEV;
+ } else
+ cmd->sg_io.dxfer_direction = SG_DXFER_NONE;
+
+ if (ioctl(fd, SG_IO, &cmd->sg_io) < 0)
+ return -errno;
+
+ if ((cmd->sg_io.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
+ if (cmd->sg_io.masked_status & CHECK_CONDITION) {
+ r = ERRCODE(cmd->_sense.u);
+ if (r != 0)
+ return r;
+ }
+ return -EIO;
+ }
+
+ return 0;
+}
+
+static int scsi_cmd_run_and_log(struct scsi_cmd *cmd, int fd, unsigned char *buf, size_t bufsize, const char *msg) {
+ int r;
+
+ assert(msg);
+
+ r = scsi_cmd_run(cmd, fd, buf, bufsize);
+ if (r != 0)
+ return log_scsi_debug_errno(r, msg);
+
+ return 0;
+}
+
+static int media_lock(int fd, bool lock) {
+ /* disable the kernel's lock logic */
+ if (ioctl(fd, CDROM_CLEAR_OPTIONS, CDO_LOCK) < 0)
+ log_debug_errno(errno, "Failed to issue ioctl(CDROM_CLEAR_OPTIONS, CDO_LOCK), ignoring: %m");
+
+ if (ioctl(fd, CDROM_LOCKDOOR, lock ? 1 : 0) < 0)
+ return log_debug_errno(errno, "Failed to issue ioctl(CDROM_LOCKDOOR): %m");
+
+ return 0;
+}
+
+static int media_eject(int fd) {
+ struct scsi_cmd sc;
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_START_STOP_UNIT);
+ scsi_cmd_set(&sc, 4, 0x02);
+ scsi_cmd_set(&sc, 5, 0);
+
+ return scsi_cmd_run_and_log(&sc, fd, NULL, 0, "start/stop unit");
+}
+
+static int cd_capability_compat(Context *c) {
+ int capability, r;
+
+ assert(c);
+
+ capability = ioctl(c->fd, CDROM_GET_CAPABILITY, NULL);
+ if (capability < 0)
+ return log_debug_errno(errno, "CDROM_GET_CAPABILITY failed");
+
+ if (capability & CDC_CD_R) {
+ r = set_drive_feature(c, FEATURE_CD_R);
+ if (r < 0)
+ return log_oom_debug();
+ }
+ if (capability & CDC_CD_RW) {
+ r = set_drive_feature(c, FEATURE_CD_RW);
+ if (r < 0)
+ return log_oom_debug();
+ }
+ if (capability & CDC_DVD) {
+ r = set_drive_feature(c, FEATURE_DVD_ROM);
+ if (r < 0)
+ return log_oom_debug();
+ }
+ if (capability & CDC_DVD_R) {
+ r = set_drive_feature(c, FEATURE_DVD_R);
+ if (r < 0)
+ return log_oom_debug();
+ }
+ if (capability & CDC_DVD_RAM) {
+ r = set_drive_feature(c, FEATURE_DVD_RAM);
+ if (r < 0)
+ return log_oom_debug();
+ }
+ if (capability & CDC_MRW) {
+ r = set_drive_feature(c, FEATURE_MRW);
+ if (r < 0)
+ return log_oom_debug();
+ }
+ if (capability & CDC_MRW_W) {
+ r = set_drive_feature(c, FEATURE_MRW_W);
+ if (r < 0)
+ return log_oom_debug();
+ }
+
+ return 0;
+}
+
+static int cd_media_compat(Context *c) {
+ int r;
+
+ assert(c);
+
+ r = ioctl(c->fd, CDROM_DRIVE_STATUS, CDSL_CURRENT);
+ if (r < 0)
+ return log_debug_errno(errno, "ioctl(CDROM_DRIVE_STATUS) failed: %m");
+ if (r != CDS_DISC_OK)
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM),
+ "ioctl(CDROM_DRIVE_STATUS) → %d (%s), ignoring.",
+ r,
+ r == CDS_NO_INFO ? "no info" :
+ r == CDS_NO_DISC ? "no disc" :
+ r == CDS_TRAY_OPEN ? "tray open" :
+ r == CDS_DRIVE_NOT_READY ? "drive not ready" :
+ "unknown status");
+
+ c->has_media = true;
+ return 0;
+}
+
+static int cd_inquiry(Context *c) {
+ struct scsi_cmd sc;
+ unsigned char inq[36];
+ int r;
+
+ assert(c);
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_INQUIRY);
+ scsi_cmd_set(&sc, 4, sizeof(inq));
+ scsi_cmd_set(&sc, 5, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, inq, sizeof(inq), "inquire");
+ if (r < 0)
+ return r;
+
+ if ((inq[0] & 0x1F) != 5)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Not an MMC unit.");
+
+ log_debug("INQUIRY: [%.8s][%.16s][%.4s]", inq + 8, inq + 16, inq + 32);
+ return 0;
+}
+
+static int feature_profiles(Context *c, const unsigned char *profiles, size_t size) {
+ int r;
+
+ assert(c);
+
+ for (size_t i = 0; i + 4 <= size; i += 4) {
+ r = set_drive_feature(c, (Feature) unaligned_read_be16(&profiles[i]));
+ if (r < 0)
+ return log_oom_debug();
+ }
+
+ return 1;
+}
+
+static int cd_profiles_old_mmc(Context *c) {
+ disc_information discinfo;
+ struct scsi_cmd sc;
+ size_t len;
+ int r;
+
+ assert(c);
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO);
+ scsi_cmd_set(&sc, 8, sizeof(discinfo.disc_information_length));
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, (unsigned char *)&discinfo.disc_information_length, sizeof(discinfo.disc_information_length), "read disc information");
+ if (r >= 0) {
+ /* Not all drives have the same disc_info length, so requeue
+ * packet with the length the drive tells us it can supply */
+ len = be16toh(discinfo.disc_information_length) + sizeof(discinfo.disc_information_length);
+ if (len > sizeof(discinfo))
+ len = sizeof(discinfo);
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO);
+ scsi_cmd_set(&sc, 8, len);
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, (unsigned char *)&discinfo, len, "read disc information");
+ }
+ if (r < 0) {
+ if (c->has_media) {
+ log_debug("No current profile, but disc is present; assuming CD-ROM.");
+ c->media_feature = FEATURE_CD_ROM;
+ c->media_track_count = 1;
+ c->media_track_count_data = 1;
+ return 1;
+ } else
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM),
+ "no current profile, assuming no media.");
+ };
+
+ c->has_media = true;
+
+ if (discinfo.erasable)
+ c->media_feature = FEATURE_CD_RW;
+ else if (discinfo.disc_status < 2 && drive_has_feature(c, FEATURE_CD_R))
+ c->media_feature = FEATURE_CD_R;
+ else
+ c->media_feature = FEATURE_CD_ROM;
+
+ return 0;
+}
+
+static int cd_profiles(Context *c) {
+ struct scsi_cmd sc;
+ unsigned char features[65530];
+ unsigned cur_profile;
+ size_t len;
+ int r;
+
+ assert(c);
+
+ /* First query the current profile */
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_GET_CONFIGURATION);
+ scsi_cmd_set(&sc, 8, 8);
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run(&sc, c->fd, features, 8);
+ if (r != 0) {
+ /* handle pre-MMC2 drives which do not support GET CONFIGURATION */
+ if (r > 0 && SK(r) == 0x5 && IN_SET(ASC(r), 0x20, 0x24)) {
+ log_debug("Drive is pre-MMC2 and does not support 46h get configuration command; "
+ "trying to work around the problem.");
+ return cd_profiles_old_mmc(c);
+ }
+
+ return log_scsi_debug_errno(r, "get configuration");
+ }
+
+ cur_profile = unaligned_read_be16(&features[6]);
+ if (cur_profile > 0) {
+ log_debug("current profile 0x%02x", cur_profile);
+ c->media_feature = (Feature) cur_profile;
+ c->has_media = true;
+ } else {
+ log_debug("no current profile, assuming no media");
+ c->has_media = false;
+ }
+
+ len = unaligned_read_be32(features);
+ log_debug("GET CONFIGURATION: size of features buffer %zu", len);
+
+ if (len > sizeof(features)) {
+ log_debug("Cannot get features in a single query, truncating.");
+ len = sizeof(features);
+ } else if (len <= 8)
+ len = sizeof(features);
+
+ /* Now get the full feature buffer */
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_GET_CONFIGURATION);
+ scsi_cmd_set(&sc, 7, (len >> 8) & 0xff);
+ scsi_cmd_set(&sc, 8, len & 0xff);
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, features, len, "get configuration");
+ if (r < 0)
+ return r;
+
+ /* parse the length once more, in case the drive decided to have other features suddenly :) */
+ len = unaligned_read_be32(features);
+ log_debug("GET CONFIGURATION: size of features buffer %zu", len);
+
+ if (len > sizeof(features)) {
+ log_debug("Cannot get features in a single query, truncating.");
+ len = sizeof(features);
+ }
+
+ /* device features */
+ for (size_t i = 8; i + 4 < len; i += 4 + features[i + 3]) {
+ unsigned feature;
+
+ feature = unaligned_read_be16(&features[i]);
+
+ switch (feature) {
+ case 0x00:
+ log_debug("GET CONFIGURATION: feature 'profiles', with %u entries", features[i + 3] / 4);
+ feature_profiles(c, features + i + 4, MIN(features[i + 3], len - i - 4));
+ break;
+ default:
+ log_debug("GET CONFIGURATION: feature 0x%04x <ignored>, with 0x%02x bytes", feature, features[i + 3]);
+ break;
+ }
+ }
+
+ return c->has_media;
+}
+
+static const char * const media_state_table[_MEDIA_STATE_MAX] = {
+ [MEDIA_STATE_BLANK] = "blank",
+ [MEDIA_STATE_APPENDABLE] = "appendable",
+ [MEDIA_STATE_COMPLETE] = "complete",
+ [MEDIA_STATE_OTHER] = "other",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(media_state, MediaState);
+
+static int dvd_ram_media_update_state(Context *c) {
+ struct scsi_cmd sc;
+ unsigned char dvdstruct[8];
+ unsigned char format[12];
+ unsigned char len;
+ int r;
+
+ assert(c);
+
+ /* Return 1 if media state is determined. */
+
+ if (c->media_feature != FEATURE_DVD_RAM)
+ return 0;
+
+ /* a write protected dvd-ram may report "complete" status */
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_DVD_STRUCTURE);
+ scsi_cmd_set(&sc, 7, 0xC0);
+ scsi_cmd_set(&sc, 9, sizeof(dvdstruct));
+ scsi_cmd_set(&sc, 11, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, dvdstruct, sizeof(dvdstruct), "read DVD structure");
+ if (r < 0)
+ return r;
+
+ if (dvdstruct[4] & 0x02) {
+ c->media_state = MEDIA_STATE_COMPLETE;
+ log_debug("Write-protected DVD-RAM media inserted");
+ return 1;
+ }
+
+ /* let's make sure we don't try to read unformatted media */
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_FORMAT_CAPACITIES);
+ scsi_cmd_set(&sc, 8, sizeof(format));
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, format, sizeof(format), "read DVD format capacities");
+ if (r < 0)
+ return r;
+
+ len = format[3];
+ if (len & 7 || len < 16)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid format capacities length.");
+
+ switch (format[8] & 3) {
+ case 1:
+ /* This means that last format was interrupted or failed, blank dvd-ram discs are
+ * factory formatted. Take no action here as it takes quite a while to reformat a
+ * dvd-ram and it's not automatically started. */
+ log_debug("Unformatted DVD-RAM media inserted.");
+ return 1;
+
+ case 2:
+ log_debug("Formatted DVD-RAM media inserted.");
+ return 0;
+
+ case 3:
+ c->has_media = false;
+ return log_debug_errno(SYNTHETIC_ERRNO(ENOMEDIUM),
+ "Format capacities returned no media.");
+ }
+
+ return 0;
+}
+
+static int dvd_media_update_state(Context *c) {
+ struct scsi_cmd sc;
+ unsigned char buffer[32 * 2048];
+ int r;
+
+ r = dvd_ram_media_update_state(c);
+ if (r != 0)
+ return r;
+
+ /* Take a closer look at formatted media (unformatted DVD+RW
+ * has "blank" status", DVD-RAM was examined earlier) and check
+ * for ISO and UDF PVDs or a fs superblock presence and do it
+ * in one ioctl (we need just sectors 0 and 16) */
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_10);
+ scsi_cmd_set(&sc, 5, 0);
+ scsi_cmd_set(&sc, 8, sizeof(buffer)/2048);
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, buffer, sizeof(buffer), "read first 32 blocks");
+ if (r < 0) {
+ c->has_media = false;
+ return r;
+ }
+
+ /* if any non-zero data is found in sector 16 (iso and udf) or
+ * eventually 0 (fat32 boot sector, ext2 superblock, etc), disc
+ * is assumed non-blank */
+
+ for (size_t offset = 32768; offset < 32768 + 2048; offset++)
+ if (buffer[offset] != 0) {
+ log_debug("Data in block 16, assuming complete.");
+ return 0;
+ }
+
+ for (size_t offset = 0; offset < 2048; offset++)
+ if (buffer[offset] != 0) {
+ log_debug("Data in block 0, assuming complete.");
+ return 0;
+ }
+
+ log_debug("No data in blocks 0 or 16, assuming blank.");
+ c->media_state = MEDIA_STATE_BLANK;
+ return 0;
+}
+
+static int cd_media_info(Context *c) {
+ struct scsi_cmd sc;
+ unsigned char header[32];
+ MediaState state;
+ int r;
+
+ assert(c);
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_DISC_INFO);
+ scsi_cmd_set(&sc, 8, sizeof(header));
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read disc information");
+ if (r < 0)
+ return r;
+
+ c->has_media = true;
+ log_debug("disk type %02x", header[8]);
+
+ state = (MediaState) (header[2] & 0x03);
+ log_debug("hardware reported media status: %s", strna(media_state_to_string(state)));
+
+ /* exclude plain CDROM, some fake cdroms return 0 for "blank" media here */
+ if (c->media_feature != FEATURE_CD_ROM)
+ c->media_state = state;
+
+ /* fresh DVD-RW in restricted overwrite mode reports itself as
+ * "appendable"; change it to "blank" to make it consistent with what
+ * gets reported after blanking, and what userspace expects. */
+ if (c->media_feature == FEATURE_DVD_RW_RO && state == MEDIA_STATE_APPENDABLE)
+ c->media_state = MEDIA_STATE_BLANK;
+
+ /* DVD+RW discs (and DVD-RW in restricted mode) once formatted are
+ * always "complete", DVD-RAM are "other" or "complete" if the disc is
+ * write protected; we need to check the contents if it is blank */
+ if (IN_SET(c->media_feature, FEATURE_DVD_RW_RO, FEATURE_DVD_PLUS_RW, FEATURE_DVD_PLUS_RW_DL, FEATURE_DVD_RAM) &&
+ IN_SET(state, MEDIA_STATE_COMPLETE, MEDIA_STATE_OTHER)) {
+ r = dvd_media_update_state(c);
+ if (r < 0)
+ return r;
+ }
+
+ /* "other" is e. g. DVD-RAM, can't append sessions there; DVDs in
+ * restricted overwrite mode can never append, only in sequential mode */
+ if (c->media_feature != FEATURE_DVD_RW_RO && IN_SET(state, MEDIA_STATE_BLANK, MEDIA_STATE_APPENDABLE))
+ c->media_session_next = header[10] << 8 | header[5];
+ c->media_session_count = header[9] << 8 | header[4];
+ c->media_track_count = header[11] << 8 | header[6];
+
+ return 0;
+}
+
+static int cd_media_toc(Context *c) {
+ struct scsi_cmd sc;
+ unsigned char header[12];
+ unsigned char toc[65536];
+ unsigned num_tracks;
+ size_t len;
+ int r;
+
+ assert(c);
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP);
+ scsi_cmd_set(&sc, 6, 1);
+ scsi_cmd_set(&sc, 8, sizeof(header));
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read TOC");
+ if (r < 0)
+ return r;
+
+ len = unaligned_read_be16(header) + 2;
+ log_debug("READ TOC: len: %zu, start track: %u, end track: %u", len, header[2], header[3]);
+
+ if (len > sizeof(toc))
+ return -1;
+ /* empty media has no tracks */
+ if (len < 8)
+ return 0;
+
+ /* 2: first track, 3: last track */
+ num_tracks = header[3] - header[2] + 1;
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP);
+ scsi_cmd_set(&sc, 6, header[2]); /* First Track/Session Number */
+ scsi_cmd_set(&sc, 7, (len >> 8) & 0xff);
+ scsi_cmd_set(&sc, 8, len & 0xff);
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, toc, len, "read TOC (tracks)");
+ if (r < 0)
+ return r;
+
+ /* Take care to not iterate beyond the last valid track as specified in
+ * the TOC, but also avoid going beyond the TOC length, just in case
+ * the last track number is invalidly large */
+ for (size_t i = 4; i + 8 <= len && num_tracks > 0; i += 8, --num_tracks) {
+ bool is_data_track;
+ uint32_t block;
+
+ is_data_track = (toc[i + 1] & 0x04) != 0;
+ block = unaligned_read_be32(&toc[i + 4]);
+
+ log_debug("track=%u info=0x%x(%s) start_block=%"PRIu32,
+ toc[i + 2], toc[i + 1] & 0x0FU, is_data_track ? "data":"audio", block);
+
+ if (is_data_track)
+ c->media_track_count_data++;
+ else
+ c->media_track_count_audio++;
+ }
+
+ scsi_cmd_init(&sc);
+ scsi_cmd_set(&sc, 0, GPCMD_READ_TOC_PMA_ATIP);
+ scsi_cmd_set(&sc, 2, 1); /* Session Info */
+ scsi_cmd_set(&sc, 8, sizeof(header));
+ scsi_cmd_set(&sc, 9, 0);
+ r = scsi_cmd_run_and_log(&sc, c->fd, header, sizeof(header), "read TOC (multi session)");
+ if (r < 0)
+ return r;
+
+ len = unaligned_read_be32(&header[8]);
+ log_debug("last track %u starts at block %zu", header[4+2], len);
+ c->media_session_last_offset = (uint64_t) len * 2048;
+
+ return 0;
+}
+
+static int open_drive(Context *c) {
+ int fd;
+
+ assert(c);
+ assert(c->fd < 0);
+
+ for (int cnt = 0;; cnt++) {
+ fd = open(arg_node, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY);
+ if (fd >= 0)
+ break;
+ if (++cnt >= 20 || errno != EBUSY)
+ return log_debug_errno(errno, "Unable to open '%s': %m", arg_node);
+
+ (void) usleep_safe(100 * USEC_PER_MSEC + random_u64_range(100 * USEC_PER_MSEC));
+ }
+
+ log_debug("probing: '%s'", arg_node);
+ c->fd = fd;
+ return 0;
+}
+
+typedef struct FeatureToString {
+ Feature feature;
+ const char *str;
+} FeatureToString;
+
+static const FeatureToString feature_to_string[] = {
+ { .feature = FEATURE_RW_NONREMOVABLE, .str = "RW_NONREMOVABLE", },
+ { .feature = FEATURE_RW_REMOVABLE, .str = "RW_REMOVABLE", },
+
+ { .feature = FEATURE_MO_SE, .str = "MO_SE", },
+ { .feature = FEATURE_MO_WO, .str = "MO_WO", },
+ { .feature = FEATURE_MO_AS, .str = "MO_AS", },
+
+ { .feature = FEATURE_CD_ROM, .str = "CD", },
+ { .feature = FEATURE_CD_R, .str = "CD_R", },
+ { .feature = FEATURE_CD_RW, .str = "CD_RW", },
+
+ { .feature = FEATURE_DVD_ROM, .str = "DVD", },
+ { .feature = FEATURE_DVD_R, .str = "DVD_R", },
+ { .feature = FEATURE_DVD_RAM, .str = "DVD_RAM", },
+ { .feature = FEATURE_DVD_RW_RO, .str = "DVD_RW_RO", },
+ { .feature = FEATURE_DVD_RW_SEQ, .str = "DVD_RW_SEQ", },
+ { .feature = FEATURE_DVD_R_DL_SEQ, .str = "DVD_R_DL_SEQ", },
+ { .feature = FEATURE_DVD_R_DL_JR, .str = "DVD_R_DL_JR", },
+ { .feature = FEATURE_DVD_RW_DL, .str = "DVD_RW_DL", },
+ { .feature = FEATURE_DVD_R_DDR, .str = "DVD_R_DDR", },
+ { .feature = FEATURE_DVD_PLUS_RW, .str = "DVD_PLUS_RW", },
+ { .feature = FEATURE_DVD_PLUS_R, .str = "DVD_PLUS_R", },
+
+ { .feature = FEATURE_DDCD_ROM, .str = "DDCD", },
+ { .feature = FEATURE_DDCD_R, .str = "DDCD_R", },
+ { .feature = FEATURE_DDCD_RW, .str = "DDCD_RW", },
+
+ { .feature = FEATURE_DVD_PLUS_RW_DL, .str = "DVD_PLUS_RW_DL", },
+ { .feature = FEATURE_DVD_PLUS_R_DL, .str = "DVD_PLUS_R_DL", },
+
+ { .feature = FEATURE_BD, .str = "BD", },
+ { .feature = FEATURE_BD_R_SRM, .str = "BD_R_SRM", },
+ { .feature = FEATURE_BD_R_RRM, .str = "BD_R_RRM", },
+ { .feature = FEATURE_BD_RE, .str = "BD_RE", },
+
+ { .feature = FEATURE_HDDVD, .str = "HDDVD", },
+ { .feature = FEATURE_HDDVD_R, .str = "HDDVD_R", },
+ { .feature = FEATURE_HDDVD_RAM, .str = "HDDVD_RAM", },
+ { .feature = FEATURE_HDDVD_RW, .str = "HDDVD_RW", },
+ { .feature = FEATURE_HDDVD_R_DL, .str = "HDDVD_R_DL", },
+ { .feature = FEATURE_HDDVD_RW_DL, .str = "HDDVD_RW_DL", },
+
+ { .feature = FEATURE_MRW, .str = "MRW", },
+ { .feature = FEATURE_MRW_W, .str = "MRW_W", },
+};
+
+static int feature_to_string_compare_func(const FeatureToString *a, const FeatureToString *b) {
+ assert(a);
+ assert(b);
+
+ return CMP(a->feature, b->feature);
+}
+
+static void print_feature(Feature feature, const char *prefix) {
+ const FeatureToString *found, in = {
+ .feature = feature,
+ };
+
+ assert(prefix);
+
+ found = typesafe_bsearch(&in, feature_to_string, ELEMENTSOF(feature_to_string), feature_to_string_compare_func);
+ if (!found)
+ return (void) log_debug("Unknown feature 0x%02x, ignoring.", (unsigned) feature);
+
+ printf("%s_%s=1\n", prefix, found->str);
+}
+
+static void print_properties(const Context *c) {
+ const char *state;
+
+ assert(c);
+
+ printf("ID_CDROM=1\n");
+ for (size_t i = 0; i < c->n_drive_feature; i++)
+ print_feature(c->drive_features[i], "ID_CDROM");
+
+ if (drive_has_feature(c, FEATURE_MO_SE) ||
+ drive_has_feature(c, FEATURE_MO_WO) ||
+ drive_has_feature(c, FEATURE_MO_AS))
+ printf("ID_CDROM_MO=1\n");
+
+ if (drive_has_feature(c, FEATURE_DVD_RW_RO) ||
+ drive_has_feature(c, FEATURE_DVD_RW_SEQ))
+ printf("ID_CDROM_DVD_RW=1\n");
+
+ if (drive_has_feature(c, FEATURE_DVD_R_DL_SEQ) ||
+ drive_has_feature(c, FEATURE_DVD_R_DL_JR))
+ printf("ID_CDROM_DVD_R_DL=1\n");
+
+ if (drive_has_feature(c, FEATURE_DVD_R_DDR))
+ printf("ID_CDROM_DVD_R=1\n");
+
+ if (drive_has_feature(c, FEATURE_BD_R_SRM) ||
+ drive_has_feature(c, FEATURE_BD_R_RRM))
+ printf("ID_CDROM_BD_R=1\n");
+
+ if (c->has_media) {
+ printf("ID_CDROM_MEDIA=1\n");
+ print_feature(c->media_feature, "ID_CDROM_MEDIA");
+
+ if (IN_SET(c->media_feature, FEATURE_MO_SE, FEATURE_MO_WO, FEATURE_MO_AS))
+ printf("ID_CDROM_MEDIA_MO=1\n");
+
+ if (IN_SET(c->media_feature, FEATURE_DVD_RW_RO, FEATURE_DVD_RW_SEQ))
+ printf("ID_CDROM_MEDIA_DVD_RW=1\n");
+
+ if (IN_SET(c->media_feature, FEATURE_DVD_R_DL_SEQ, FEATURE_DVD_R_DL_JR))
+ printf("ID_CDROM_MEDIA_DVD_R_DL=1\n");
+
+ if (c->media_feature == FEATURE_DVD_R_DDR)
+ printf("ID_CDROM_MEDIA_DVD_R=1\n");
+
+ if (IN_SET(c->media_feature, FEATURE_BD_R_SRM, FEATURE_BD_R_RRM))
+ printf("ID_CDROM_MEDIA_BD_R=1\n");
+ }
+
+ state = media_state_to_string(c->media_state);
+ if (state)
+ printf("ID_CDROM_MEDIA_STATE=%s\n", state);
+ if (c->media_session_next > 0)
+ printf("ID_CDROM_MEDIA_SESSION_NEXT=%u\n", c->media_session_next);
+ if (c->media_session_count > 0)
+ printf("ID_CDROM_MEDIA_SESSION_COUNT=%u\n", c->media_session_count);
+ if (c->media_session_count > 1 && c->media_session_last_offset > 0)
+ printf("ID_CDROM_MEDIA_SESSION_LAST_OFFSET=%" PRIu64 "\n", c->media_session_last_offset);
+ if (c->media_track_count > 0)
+ printf("ID_CDROM_MEDIA_TRACK_COUNT=%u\n", c->media_track_count);
+ if (c->media_track_count_audio > 0)
+ printf("ID_CDROM_MEDIA_TRACK_COUNT_AUDIO=%u\n", c->media_track_count_audio);
+ if (c->media_track_count_data > 0)
+ printf("ID_CDROM_MEDIA_TRACK_COUNT_DATA=%u\n", c->media_track_count_data);
+}
+
+static int help(void) {
+ printf("%s [OPTIONS...] DEVICE\n\n"
+ " -l --lock-media Lock the media (to enable eject request events)\n"
+ " -u --unlock-media Unlock the media\n"
+ " -e --eject-media Eject the media\n"
+ " -d --debug Print debug messages to stderr\n"
+ " -h --help Show this help text\n"
+ " --version Show package version\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "lock-media", no_argument, NULL, 'l' },
+ { "unlock-media", no_argument, NULL, 'u' },
+ { "eject-media", no_argument, NULL, 'e' },
+ { "debug", no_argument, NULL, 'd' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ {}
+ };
+ int c;
+
+ while ((c = getopt_long(argc, argv, "deluh", options, NULL)) >= 0)
+ switch (c) {
+ case 'l':
+ arg_lock = true;
+ break;
+ case 'u':
+ arg_unlock = true;
+ break;
+ case 'e':
+ arg_eject = true;
+ break;
+ case 'd':
+ log_set_target(LOG_TARGET_CONSOLE);
+ log_set_max_level(LOG_DEBUG);
+ log_open();
+ break;
+ case 'h':
+ return help();
+ case 'v':
+ return version();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ arg_node = argv[optind];
+ if (!arg_node)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No device specified.");
+
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(context_clear) Context c = CONTEXT_EMPTY;
+ int r;
+
+ log_set_target(LOG_TARGET_AUTO);
+ udev_parse_config();
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = open_drive(&c);
+ if (r < 0)
+ return r;
+
+ /* same data as original cdrom_id */
+ r = cd_capability_compat(&c);
+ if (r < 0)
+ return r;
+
+ /* check for media - don't bail if there's no media as we still need to
+ * to read profiles */
+ (void) cd_media_compat(&c);
+
+ /* check if drive talks MMC */
+ if (cd_inquiry(&c) < 0)
+ goto work;
+
+ r = cd_profiles(&c); /* read drive and possibly current profile */
+ if (r > 0) {
+ /* at this point we are guaranteed to have media in the drive - find out more about it */
+
+ /* get session/track info */
+ (void) cd_media_toc(&c);
+
+ /* get writable media state */
+ (void) cd_media_info(&c);
+ }
+
+work:
+ /* lock the media, so we enable eject button events */
+ if (arg_lock && c.has_media) {
+ log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (lock)");
+ (void) media_lock(c.fd, true);
+ }
+
+ if (arg_unlock && c.has_media) {
+ log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)");
+ (void) media_lock(c.fd, false);
+ }
+
+ if (arg_eject) {
+ log_debug("PREVENT_ALLOW_MEDIUM_REMOVAL (unlock)");
+ (void) media_lock(c.fd, false);
+ log_debug("START_STOP_UNIT (eject)");
+ (void) media_eject(c.fd);
+ }
+
+ print_properties(&c);
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/dmi_memory_id/dmi_memory_id.c b/src/udev/dmi_memory_id/dmi_memory_id.c
new file mode 100644
index 0000000..3f89cc7
--- /dev/null
+++ b/src/udev/dmi_memory_id/dmi_memory_id.c
@@ -0,0 +1,721 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * System Memory information
+ *
+ * Copyright (C) 2000-2002 Alan Cox <alan@redhat.com>
+ * Copyright (C) 2002-2020 Jean Delvare <jdelvare@suse.de>
+ * Copyright (C) 2020 Bastien Nocera <hadess@hadess.net>
+ *
+ * Unless specified otherwise, all references are aimed at the "System
+ * Management BIOS Reference Specification, Version 3.7.0" document,
+ * available from http://www.dmtf.org/standards/smbios.
+ *
+ * Note to contributors:
+ * Please reference every value you add or modify, especially if the
+ * information does not come from the above mentioned specification.
+ *
+ * Additional references:
+ * - Intel AP-485 revision 36
+ * "Intel Processor Identification and the CPUID Instruction"
+ * http://www.intel.com/support/processors/sb/cs-009861.htm
+ * - DMTF Common Information Model
+ * CIM Schema version 2.19.1
+ * http://www.dmtf.org/standards/cim/
+ * - IPMI 2.0 revision 1.0
+ * "Intelligent Platform Management Interface Specification"
+ * http://developer.intel.com/design/servers/ipmi/spec.htm
+ * - AMD publication #25481 revision 2.28
+ * "CPUID Specification"
+ * http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/25481.pdf
+ * - BIOS Integrity Services Application Programming Interface version 1.0
+ * http://www.intel.com/design/archives/wfm/downloads/bisspec.htm
+ * - DMTF DSP0239 version 1.1.0
+ * "Management Component Transport Protocol (MCTP) IDs and Codes"
+ * http://www.dmtf.org/standards/pmci
+ * - "TPM Main, Part 2 TPM Structures"
+ * Specification version 1.2, level 2, revision 116
+ * https://trustedcomputinggroup.org/tpm-main-specification/
+ * - "PC Client Platform TPM Profile (PTP) Specification"
+ * Family "2.0", Level 00, Revision 00.43, January 26, 2015
+ * https://trustedcomputinggroup.org/pc-client-platform-tpm-profile-ptp-specification/
+ * - "RedFish Host Interface Specification" (DMTF DSP0270)
+ * https://www.dmtf.org/sites/default/files/DSP0270_1.0.1.pdf
+ */
+
+#include <getopt.h>
+
+#include "alloc-util.h"
+#include "build.h"
+#include "fileio.h"
+#include "main-func.h"
+#include "string-util.h"
+#include "udev-util.h"
+#include "unaligned.h"
+
+#define SUPPORTED_SMBIOS_VER 0x030300
+
+#define OUT_OF_SPEC_STR "<OUT OF SPEC>"
+
+#define SYS_FIRMWARE_DIR "/sys/firmware/dmi/tables"
+#define SYS_ENTRY_FILE SYS_FIRMWARE_DIR "/smbios_entry_point"
+#define SYS_TABLE_FILE SYS_FIRMWARE_DIR "/DMI"
+
+/*
+ * Per SMBIOS v2.8.0 and later, all structures assume a little-endian
+ * ordering convention.
+ */
+#define WORD(x) (unaligned_read_le16(x))
+#define DWORD(x) (unaligned_read_le32(x))
+#define QWORD(x) (unaligned_read_le64(x))
+
+struct dmi_header {
+ uint8_t type;
+ uint8_t length;
+ uint16_t handle;
+ const uint8_t *data;
+};
+
+static const char *arg_source_file = NULL;
+
+static bool verify_checksum(const uint8_t *buf, size_t len) {
+ uint8_t sum = 0;
+
+ for (size_t a = 0; a < len; a++)
+ sum += buf[a];
+ return sum == 0;
+}
+
+/*
+ * Type-independent Stuff
+ */
+
+static const char *dmi_string(const struct dmi_header *dm, uint8_t s) {
+ const char *bp = (const char *) dm->data;
+
+ if (s == 0)
+ return "Not Specified";
+
+ bp += dm->length;
+ for (;s > 1 && !isempty(bp); s--)
+ bp += strlen(bp) + 1;
+
+ if (isempty(bp))
+ return "<BAD INDEX>";
+
+ return bp;
+}
+
+typedef enum {
+ MEMORY_SIZE_UNIT_BYTES,
+ MEMORY_SIZE_UNIT_KB
+} MemorySizeUnit;
+
+static void dmi_print_memory_size(
+ const char *attr_prefix, const char *attr_suffix,
+ int slot_num, uint64_t code, MemorySizeUnit unit) {
+ if (unit == MEMORY_SIZE_UNIT_KB)
+ code <<= 10;
+
+ if (slot_num >= 0)
+ printf("%s_%i_%s=%"PRIu64"\n", attr_prefix, slot_num, attr_suffix, code);
+ else
+ printf("%s_%s=%"PRIu64"\n", attr_prefix, attr_suffix, code);
+}
+
+/*
+ * 7.17 Physical Memory Array (Type 16)
+ */
+
+static void dmi_memory_array_location(uint8_t code) {
+ /* 7.17.1 */
+ static const char *location[] = {
+ [0x01] = "Other",
+ [0x02] = "Unknown",
+ [0x03] = "System Board Or Motherboard",
+ [0x04] = "ISA Add-on Card",
+ [0x05] = "EISA Add-on Card",
+ [0x06] = "PCI Add-on Card",
+ [0x07] = "MCA Add-on Card",
+ [0x08] = "PCMCIA Add-on Card",
+ [0x09] = "Proprietary Add-on Card",
+ [0x0A] = "NuBus",
+ };
+ static const char *location_0xA0[] = {
+ [0x00] = "PC-98/C20 Add-on Card", /* 0xA0 */
+ [0x01] = "PC-98/C24 Add-on Card", /* 0xA1 */
+ [0x02] = "PC-98/E Add-on Card", /* 0xA2 */
+ [0x03] = "PC-98/Local Bus Add-on Card", /* 0xA3 */
+ [0x04] = "CXL Add-on Card", /* 0xA4 */
+ };
+ const char *str = OUT_OF_SPEC_STR;
+
+ if (code < ELEMENTSOF(location) && location[code])
+ str = location[code];
+ else if (code >= 0xA0 && code < (ELEMENTSOF(location_0xA0) + 0xA0))
+ str = location_0xA0[code - 0xA0];
+
+ printf("MEMORY_ARRAY_LOCATION=%s\n", str);
+}
+
+static void dmi_memory_array_ec_type(uint8_t code) {
+ /* 7.17.3 */
+ static const char *type[] = {
+ [0x01] = "Other",
+ [0x02] = "Unknown",
+ [0x03] = "None",
+ [0x04] = "Parity",
+ [0x05] = "Single-bit ECC",
+ [0x06] = "Multi-bit ECC",
+ [0x07] = "CRC",
+ };
+
+ if (code != 0x03) /* Do not print "None". */
+ printf("MEMORY_ARRAY_EC_TYPE=%s\n",
+ code < ELEMENTSOF(type) && type[code] ? type[code] : OUT_OF_SPEC_STR);
+}
+
+/*
+ * 7.18 Memory Device (Type 17)
+ */
+
+static void dmi_memory_device_string(
+ const char *attr_suffix, unsigned slot_num,
+ const struct dmi_header *h, uint8_t s) {
+ char *str;
+
+ str = strdupa_safe(dmi_string(h, s));
+ str = strstrip(str);
+ if (!isempty(str))
+ printf("MEMORY_DEVICE_%u_%s=%s\n", slot_num, attr_suffix, str);
+}
+
+static void dmi_memory_device_width(
+ const char *attr_suffix,
+ unsigned slot_num, uint16_t code) {
+
+ /* If no memory module is present, width may be 0 */
+ if (!IN_SET(code, 0, 0xFFFF))
+ printf("MEMORY_DEVICE_%u_%s=%u\n", slot_num, attr_suffix, code);
+}
+
+static void dmi_memory_device_size(unsigned slot_num, uint16_t code) {
+ if (code == 0)
+ return (void) printf("MEMORY_DEVICE_%u_PRESENT=0\n", slot_num);
+ if (code == 0xFFFF)
+ return;
+
+ uint64_t s = code & 0x7FFF;
+ if (!(code & 0x8000))
+ s <<= 10;
+ dmi_print_memory_size("MEMORY_DEVICE", "SIZE", slot_num, s, MEMORY_SIZE_UNIT_KB);
+}
+
+static void dmi_memory_device_extended_size(unsigned slot_num, uint32_t code) {
+ uint64_t capacity = (uint64_t) code * 1024 * 1024;
+
+ printf("MEMORY_DEVICE_%u_SIZE=%"PRIu64"\n", slot_num, capacity);
+}
+
+static void dmi_memory_device_rank(unsigned slot_num, uint8_t code) {
+ code &= 0x0F;
+ if (code != 0)
+ printf("MEMORY_DEVICE_%u_RANK=%u\n", slot_num, code);
+}
+
+static void dmi_memory_device_voltage_value(
+ const char *attr_suffix,
+ unsigned slot_num, uint16_t code) {
+ if (code == 0)
+ return;
+ if (code % 100 != 0)
+ printf("MEMORY_DEVICE_%u_%s=%g\n", slot_num, attr_suffix, (double)code / 1000);
+ else
+ printf("MEMORY_DEVICE_%u_%s=%.1g\n", slot_num, attr_suffix, (double)code / 1000);
+}
+
+static void dmi_memory_device_form_factor(unsigned slot_num, uint8_t code) {
+ /* 7.18.1 */
+ static const char *form_factor[] = {
+ [0x01] = "Other",
+ [0x02] = "Unknown",
+ [0x03] = "SIMM",
+ [0x04] = "SIP",
+ [0x05] = "Chip",
+ [0x06] = "DIP",
+ [0x07] = "ZIP",
+ [0x08] = "Proprietary Card",
+ [0x09] = "DIMM",
+ [0x0A] = "TSOP",
+ [0x0B] = "Row Of Chips",
+ [0x0C] = "RIMM",
+ [0x0D] = "SODIMM",
+ [0x0E] = "SRIMM",
+ [0x0F] = "FB-DIMM",
+ [0x10] = "Die",
+ };
+
+ printf("MEMORY_DEVICE_%u_FORM_FACTOR=%s\n", slot_num,
+ code < ELEMENTSOF(form_factor) && form_factor[code] ? form_factor[code] : OUT_OF_SPEC_STR);
+}
+
+static void dmi_memory_device_set(unsigned slot_num, uint8_t code) {
+ if (code == 0xFF)
+ printf("MEMORY_DEVICE_%u_SET=%s\n", slot_num, "Unknown");
+ else if (code != 0)
+ printf("MEMORY_DEVICE_%u_SET=%"PRIu8"\n", slot_num, code);
+}
+
+static void dmi_memory_device_type(unsigned slot_num, uint8_t code) {
+ /* 7.18.2 */
+ static const char *type[] = {
+ [0x01] = "Other",
+ [0x02] = "Unknown",
+ [0x03] = "DRAM",
+ [0x04] = "EDRAM",
+ [0x05] = "VRAM",
+ [0x06] = "SRAM",
+ [0x07] = "RAM",
+ [0x08] = "ROM",
+ [0x09] = "Flash",
+ [0x0A] = "EEPROM",
+ [0x0B] = "FEPROM",
+ [0x0C] = "EPROM",
+ [0x0D] = "CDRAM",
+ [0x0E] = "3DRAM",
+ [0x0F] = "SDRAM",
+ [0x10] = "SGRAM",
+ [0x11] = "RDRAM",
+ [0x12] = "DDR",
+ [0x13] = "DDR2",
+ [0x14] = "DDR2 FB-DIMM",
+ [0x15] = "Reserved",
+ [0x16] = "Reserved",
+ [0x17] = "Reserved",
+ [0x18] = "DDR3",
+ [0x19] = "FBD2",
+ [0x1A] = "DDR4",
+ [0x1B] = "LPDDR",
+ [0x1C] = "LPDDR2",
+ [0x1D] = "LPDDR3",
+ [0x1E] = "LPDDR4",
+ [0x1F] = "Logical non-volatile device",
+ [0x20] = "HBM",
+ [0x21] = "HBM2",
+ [0x22] = "DDR5",
+ [0x23] = "LPDDR5",
+ [0x24] = "HBM3",
+ };
+
+ printf("MEMORY_DEVICE_%u_TYPE=%s\n", slot_num,
+ code < ELEMENTSOF(type) && type[code] ? type[code] : OUT_OF_SPEC_STR);
+}
+
+static void dmi_memory_device_type_detail(unsigned slot_num, uint16_t code) {
+ /* 7.18.3 */
+ static const char *detail[] = {
+ [1] = "Other",
+ [2] = "Unknown",
+ [3] = "Fast-paged",
+ [4] = "Static Column",
+ [5] = "Pseudo-static",
+ [6] = "RAMBUS",
+ [7] = "Synchronous",
+ [8] = "CMOS",
+ [9] = "EDO",
+ [10] = "Window DRAM",
+ [11] = "Cache DRAM",
+ [12] = "Non-Volatile",
+ [13] = "Registered (Buffered)",
+ [14] = "Unbuffered (Unregistered)",
+ [15] = "LRDIMM",
+ };
+
+ if ((code & 0xFFFE) == 0)
+ printf("MEMORY_DEVICE_%u_TYPE_DETAIL=%s\n", slot_num, "None");
+ else {
+ bool first_element = true;
+
+ printf("MEMORY_DEVICE_%u_TYPE_DETAIL=", slot_num);
+ for (size_t i = 1; i < ELEMENTSOF(detail); i++)
+ if (code & (1 << i)) {
+ printf("%s%s", first_element ? "" : " ", detail[i]);
+ first_element = false;
+ }
+ printf("\n");
+ }
+}
+
+static void dmi_memory_device_speed(
+ const char *attr_suffix,
+ unsigned slot_num, uint16_t code) {
+ if (code != 0)
+ printf("MEMORY_DEVICE_%u_%s=%u\n", slot_num, attr_suffix, code);
+}
+
+static void dmi_memory_device_technology(unsigned slot_num, uint8_t code) {
+ /* 7.18.6 */
+ static const char * const technology[] = {
+ [0x01] = "Other",
+ [0x02] = "Unknown",
+ [0x03] = "DRAM",
+ [0x04] = "NVDIMM-N",
+ [0x05] = "NVDIMM-F",
+ [0x06] = "NVDIMM-P",
+ [0x07] = "Intel Optane persistent memory",
+ };
+
+ printf("MEMORY_DEVICE_%u_MEMORY_TECHNOLOGY=%s\n", slot_num,
+ code < ELEMENTSOF(technology) && technology[code] ? technology[code] : OUT_OF_SPEC_STR);
+}
+
+static void dmi_memory_device_operating_mode_capability(unsigned slot_num, uint16_t code) {
+ /* 7.18.7 */
+ static const char * const mode[] = {
+ [1] = "Other",
+ [2] = "Unknown",
+ [3] = "Volatile memory",
+ [4] = "Byte-accessible persistent memory",
+ [5] = "Block-accessible persistent memory",
+ };
+
+ if ((code & 0xFFFE) != 0) {
+ bool first_element = true;
+
+ printf("MEMORY_DEVICE_%u_MEMORY_OPERATING_MODE_CAPABILITY=", slot_num);
+ for (size_t i = 1; i < ELEMENTSOF(mode); i++)
+ if (code & (1 << i)) {
+ printf("%s%s", first_element ? "" : " ", mode[i]);
+ first_element = false;
+ }
+ printf("\n");
+ }
+}
+
+static void dmi_memory_device_manufacturer_id(
+ const char *attr_suffix,
+ unsigned slot_num, uint16_t code) {
+ /* 7.18.8 */
+ /* 7.18.10 */
+ /* LSB is 7-bit Odd Parity number of continuation codes */
+ if (code != 0)
+ printf("MEMORY_DEVICE_%u_%s=Bank %d, Hex 0x%02X\n", slot_num, attr_suffix,
+ (code & 0x7F) + 1, code >> 8);
+}
+
+static void dmi_memory_device_product_id(
+ const char *attr_suffix,
+ unsigned slot_num, uint16_t code) {
+ /* 7.18.9 */
+ /* 7.18.11 */
+ if (code != 0)
+ printf("MEMORY_DEVICE_%u_%s=0x%04X\n", slot_num, attr_suffix, code);
+}
+
+static void dmi_memory_device_size_detail(
+ const char *attr_suffix,
+ unsigned slot_num, uint64_t code) {
+ /* 7.18.12 */
+ /* 7.18.13 */
+ if (!IN_SET(code, 0x0LU, 0xFFFFFFFFFFFFFFFFLU))
+ dmi_print_memory_size("MEMORY_DEVICE", attr_suffix, slot_num, code, MEMORY_SIZE_UNIT_BYTES);
+}
+
+static void dmi_decode(const struct dmi_header *h,
+ unsigned *next_slot_num) {
+ const uint8_t *data = h->data;
+ unsigned slot_num;
+
+ /*
+ * Note: DMI types 37 and 42 are untested
+ */
+ switch (h->type) {
+ case 16: /* 7.17 Physical Memory Array */
+ log_debug("Physical Memory Array");
+ if (h->length < 0x0F)
+ break;
+
+ if (data[0x05] != 0x03) /* 7.17.2, Use == "System Memory" */
+ break;
+
+ log_debug("Use: System Memory");
+ dmi_memory_array_location(data[0x04]);
+ dmi_memory_array_ec_type(data[0x06]);
+ if (DWORD(data + 0x07) != 0x80000000)
+ dmi_print_memory_size("MEMORY_ARRAY", "MAX_CAPACITY", -1, DWORD(data + 0x07), MEMORY_SIZE_UNIT_KB);
+ else if (h->length >= 0x17)
+ dmi_print_memory_size("MEMORY_ARRAY", "MAX_CAPACITY", -1, QWORD(data + 0x0F), MEMORY_SIZE_UNIT_BYTES);
+
+ break;
+
+ case 17: /* 7.18 Memory Device */
+ slot_num = *next_slot_num;
+ *next_slot_num = slot_num + 1;
+
+ log_debug("Memory Device: %u", slot_num);
+ if (h->length < 0x15)
+ break;
+
+ dmi_memory_device_width("TOTAL_WIDTH", slot_num, WORD(data + 0x08));
+ dmi_memory_device_width("DATA_WIDTH", slot_num, WORD(data + 0x0A));
+ if (h->length >= 0x20 && WORD(data + 0x0C) == 0x7FFF)
+ dmi_memory_device_extended_size(slot_num, DWORD(data + 0x1C));
+ else
+ dmi_memory_device_size(slot_num, WORD(data + 0x0C));
+ dmi_memory_device_form_factor(slot_num, data[0x0E]);
+ dmi_memory_device_set(slot_num, data[0x0F]);
+ dmi_memory_device_string("LOCATOR", slot_num, h, data[0x10]);
+ dmi_memory_device_string("BANK_LOCATOR", slot_num, h, data[0x11]);
+ dmi_memory_device_type(slot_num, data[0x12]);
+ dmi_memory_device_type_detail(slot_num, WORD(data + 0x13));
+ if (h->length < 0x17)
+ break;
+
+ dmi_memory_device_speed("SPEED_MTS", slot_num, WORD(data + 0x15));
+ if (h->length < 0x1B)
+ break;
+
+ dmi_memory_device_string("MANUFACTURER", slot_num, h, data[0x17]);
+ dmi_memory_device_string("SERIAL_NUMBER", slot_num, h, data[0x18]);
+ dmi_memory_device_string("ASSET_TAG", slot_num, h, data[0x19]);
+ dmi_memory_device_string("PART_NUMBER", slot_num, h, data[0x1A]);
+ if (h->length < 0x1C)
+ break;
+
+ dmi_memory_device_rank(slot_num, data[0x1B]);
+ if (h->length < 0x22)
+ break;
+
+ dmi_memory_device_speed("CONFIGURED_SPEED_MTS", slot_num, WORD(data + 0x20));
+ if (h->length < 0x28)
+ break;
+
+ dmi_memory_device_voltage_value("MINIMUM_VOLTAGE", slot_num, WORD(data + 0x22));
+ dmi_memory_device_voltage_value("MAXIMUM_VOLTAGE", slot_num, WORD(data + 0x24));
+ dmi_memory_device_voltage_value("CONFIGURED_VOLTAGE", slot_num, WORD(data + 0x26));
+ if (h->length < 0x34)
+ break;
+
+ dmi_memory_device_technology(slot_num, data[0x28]);
+ dmi_memory_device_operating_mode_capability(slot_num, WORD(data + 0x29));
+ dmi_memory_device_string("FIRMWARE_VERSION", slot_num, h, data[0x2B]);
+ dmi_memory_device_manufacturer_id("MODULE_MANUFACTURER_ID", slot_num, WORD(data + 0x2C));
+ dmi_memory_device_product_id("MODULE_PRODUCT_ID", slot_num, WORD(data + 0x2E));
+ dmi_memory_device_manufacturer_id("MEMORY_SUBSYSTEM_CONTROLLER_MANUFACTURER_ID",
+ slot_num, WORD(data + 0x30));
+ dmi_memory_device_product_id("MEMORY_SUBSYSTEM_CONTROLLER_PRODUCT_ID",
+ slot_num, WORD(data + 0x32));
+ if (h->length < 0x3C)
+ break;
+
+ dmi_memory_device_size_detail("NON_VOLATILE_SIZE", slot_num, QWORD(data + 0x34));
+ if (h->length < 0x44)
+ break;
+
+ dmi_memory_device_size_detail("VOLATILE_SIZE", slot_num, QWORD(data + 0x3C));
+ if (h->length < 0x4C)
+ break;
+
+ dmi_memory_device_size_detail("CACHE_SIZE", slot_num, QWORD(data + 0x44));
+ if (h->length < 0x54)
+ break;
+
+ dmi_memory_device_size_detail("LOGICAL_SIZE", slot_num, QWORD(data + 0x4C));
+
+ break;
+ }
+}
+
+static void dmi_table_decode(const uint8_t *buf, size_t len, uint16_t num) {
+ const uint8_t *data = buf;
+ unsigned next_slot_num = 0;
+
+ /* 4 is the length of an SMBIOS structure header */
+ for (uint16_t i = 0; (i < num || num == 0) && data + 4 <= buf + len; i++) {
+ struct dmi_header h = (struct dmi_header) {
+ .type = data[0],
+ .length = data[1],
+ .handle = WORD(data + 2),
+ .data = data,
+ };
+ bool display = !IN_SET(h.type, 126, 127);
+ const uint8_t *next;
+
+ /* If a short entry is found (less than 4 bytes), not only it
+ * is invalid, but we cannot reliably locate the next entry.
+ * Better stop at this point, and let the user know their
+ * table is broken. */
+ if (h.length < 4)
+ break;
+
+ /* In quiet mode, stop decoding at end of table marker */
+ if (h.type == 127)
+ break;
+
+ /* Look for the next handle */
+ next = data + h.length;
+ while ((size_t)(next - buf + 1) < len && (next[0] != 0 || next[1] != 0))
+ next++;
+ next += 2;
+
+ /* Make sure the whole structure fits in the table */
+ if ((size_t)(next - buf) > len)
+ break;
+
+ if (display)
+ dmi_decode(&h, &next_slot_num);
+
+ data = next;
+ }
+ if (next_slot_num > 0)
+ printf("MEMORY_ARRAY_NUM_DEVICES=%u\n", next_slot_num);
+}
+
+static int dmi_table(int64_t base, uint32_t len, uint16_t num, const char *devmem, bool no_file_offset) {
+ _cleanup_free_ uint8_t *buf = NULL;
+ size_t size;
+ int r;
+
+ /*
+ * When reading from sysfs or from a dump file, the file may be
+ * shorter than announced. For SMBIOS v3 this is expected, as we
+ * only know the maximum table size, not the actual table size.
+ * For older implementations (and for SMBIOS v3 too), this
+ * would be the result of the kernel truncating the table on
+ * parse error.
+ */
+ r = read_full_file_full(AT_FDCWD, devmem, no_file_offset ? 0 : base, len,
+ 0, NULL, (char **) &buf, &size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read table: %m");
+
+ dmi_table_decode(buf, size, num);
+
+ return 0;
+}
+
+/* Same thing for SMBIOS3 entry points */
+static int smbios3_decode(const uint8_t *buf, const char *devmem, bool no_file_offset) {
+ uint64_t offset;
+
+ /* Don't let checksum run beyond the buffer */
+ if (buf[0x06] > 0x20)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Entry point length too large (%"PRIu8" bytes, expected %u).",
+ buf[0x06], 0x18U);
+
+ if (!verify_checksum(buf, buf[0x06]))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to verify checksum.");
+
+ offset = QWORD(buf + 0x10);
+
+#if __SIZEOF_SIZE_T__ != 8
+ if (!no_file_offset && (offset >> 32) != 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "64-bit addresses not supported on 32-bit systems.");
+#endif
+
+ return dmi_table(offset, DWORD(buf + 0x0C), 0, devmem, no_file_offset);
+}
+
+static int smbios_decode(const uint8_t *buf, const char *devmem, bool no_file_offset) {
+ /* Don't let checksum run beyond the buffer */
+ if (buf[0x05] > 0x20)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Entry point length too large (%"PRIu8" bytes, expected %u).",
+ buf[0x05], 0x1FU);
+
+ if (!verify_checksum(buf, buf[0x05])
+ || memcmp(buf + 0x10, "_DMI_", 5) != 0
+ || !verify_checksum(buf + 0x10, 0x0F))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to verify checksum.");
+
+ return dmi_table(DWORD(buf + 0x18), WORD(buf + 0x16), WORD(buf + 0x1C),
+ devmem, no_file_offset);
+}
+
+static int legacy_decode(const uint8_t *buf, const char *devmem, bool no_file_offset) {
+ if (!verify_checksum(buf, 0x0F))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to verify checksum.");
+
+ return dmi_table(DWORD(buf + 0x08), WORD(buf + 0x06), WORD(buf + 0x0C),
+ devmem, no_file_offset);
+}
+
+static int help(void) {
+ printf("%s [OPTIONS...]\n\n"
+ " -F --from-dump FILE Read DMI information from a binary file\n"
+ " -h --help Show this help text\n"
+ " --version Show package version\n",
+ program_invocation_short_name);
+ return 0;
+}
+
+static int parse_argv(int argc, char * const *argv) {
+ static const struct option options[] = {
+ { "from-dump", required_argument, NULL, 'F' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ {}
+ };
+ int c;
+
+ while ((c = getopt_long(argc, argv, "F:hV", options, NULL)) >= 0)
+ switch (c) {
+ case 'F':
+ arg_source_file = optarg;
+ break;
+ case 'V':
+ return version();
+ case 'h':
+ return help();
+ case '?':
+ return -EINVAL;
+ case 'v':
+ return version();
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int run(int argc, char* const* argv) {
+ _cleanup_free_ uint8_t *buf = NULL;
+ bool no_file_offset = false;
+ size_t size;
+ int r;
+
+ log_set_target(LOG_TARGET_AUTO);
+ udev_parse_config();
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ /* Read from dump if so instructed */
+ r = read_full_file_full(AT_FDCWD,
+ arg_source_file ?: SYS_ENTRY_FILE,
+ 0, 0x20, 0, NULL, (char **) &buf, &size);
+ if (r < 0)
+ return log_full_errno(!arg_source_file && r == -ENOENT ? LOG_DEBUG : LOG_ERR,
+ r, "Reading \"%s\" failed: %m",
+ arg_source_file ?: SYS_ENTRY_FILE);
+
+ if (!arg_source_file) {
+ arg_source_file = SYS_TABLE_FILE;
+ no_file_offset = true;
+ }
+
+ if (size >= 24 && memory_startswith(buf, size, "_SM3_"))
+ return smbios3_decode(buf, arg_source_file, no_file_offset);
+ if (size >= 31 && memory_startswith(buf, size, "_SM_"))
+ return smbios_decode(buf, arg_source_file, no_file_offset);
+ if (size >= 15 && memory_startswith(buf, size, "_DMI_"))
+ return legacy_decode(buf, arg_source_file, no_file_offset);
+
+ return -EINVAL;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/fido_id/fido_id.c b/src/udev/fido_id/fido_id.c
new file mode 100644
index 0000000..e01f37d
--- /dev/null
+++ b/src/udev/fido_id/fido_id.c
@@ -0,0 +1,129 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Identifies FIDO CTAP1 ("U2F")/CTAP2 security tokens based on the usage declared in their report
+ * descriptor and outputs suitable environment variables.
+ *
+ * Inspired by Andrew Lutomirski's 'u2f-hidraw-policy.c'
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <linux/hid.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "build.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "fido_id_desc.h"
+#include "log.h"
+#include "macro.h"
+#include "main-func.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "udev-util.h"
+
+static const char *arg_device = NULL;
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ {}
+ };
+ int c;
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ switch (c) {
+ case 'h':
+ printf("%s [OPTIONS...] SYSFS_PATH\n\n"
+ " -h --help Show this help text\n"
+ " --version Show package version\n",
+ program_invocation_short_name);
+ return 0;
+ case 'v':
+ return version();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ if (argc > 2)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error: unexpected argument.");
+
+ arg_device = argv[optind];
+ return 1;
+}
+
+static int run(int argc, char **argv) {
+ _cleanup_(sd_device_unrefp) struct sd_device *device = NULL;
+ _cleanup_free_ char *desc_path = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ struct sd_device *hid_device;
+ const char *sys_path;
+ uint8_t desc[HID_MAX_DESCRIPTOR_SIZE];
+ ssize_t desc_len;
+ int r;
+
+ log_set_target(LOG_TARGET_AUTO);
+ udev_parse_config();
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (arg_device) {
+ r = sd_device_new_from_syspath(&device, arg_device);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get device from syspath %s: %m", arg_device);
+ } else {
+ r = device_new_from_strv(&device, environ);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get current device from environment: %m");
+ }
+
+ r = sd_device_get_parent(device, &hid_device);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to get parent HID device: %m");
+
+ r = sd_device_get_syspath(hid_device, &sys_path);
+ if (r < 0)
+ return log_device_error_errno(hid_device, r, "Failed to get syspath for HID device: %m");
+
+ desc_path = path_join(sys_path, "report_descriptor");
+ if (!desc_path)
+ return log_oom();
+
+ fd = open(desc_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NOCTTY);
+ if (fd < 0)
+ return log_device_error_errno(hid_device, errno,
+ "Failed to open report descriptor at '%s': %m", desc_path);
+
+ desc_len = read(fd, desc, sizeof(desc));
+ if (desc_len < 0)
+ return log_device_error_errno(hid_device, errno,
+ "Failed to read report descriptor at '%s': %m", desc_path);
+ if (desc_len == 0)
+ return log_device_debug_errno(hid_device, SYNTHETIC_ERRNO(EINVAL),
+ "Empty report descriptor at '%s'.", desc_path);
+
+ r = is_fido_security_token_desc(desc, desc_len);
+ if (r < 0)
+ return log_device_debug_errno(hid_device, r,
+ "Failed to parse report descriptor at '%s'.", desc_path);
+ if (r > 0) {
+ printf("ID_FIDO_TOKEN=1\n");
+ printf("ID_SECURITY_TOKEN=1\n");
+ }
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/fido_id/fido_id_desc.c b/src/udev/fido_id/fido_id_desc.c
new file mode 100644
index 0000000..2dfa759
--- /dev/null
+++ b/src/udev/fido_id/fido_id_desc.c
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* Inspired by Andrew Lutomirski's 'u2f-hidraw-policy.c' */
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "fido_id_desc.h"
+
+#define HID_RPTDESC_FIRST_BYTE_LONG_ITEM 0xfeu
+#define HID_RPTDESC_TYPE_GLOBAL 0x1u
+#define HID_RPTDESC_TYPE_LOCAL 0x2u
+#define HID_RPTDESC_TAG_USAGE_PAGE 0x0u
+#define HID_RPTDESC_TAG_USAGE 0x0u
+
+/*
+ * HID usage for FIDO CTAP1 ("U2F") and CTAP2 security tokens.
+ * https://fidoalliance.org/specs/fido-u2f-v1.0-ps-20141009/fido-u2f-u2f_hid.h-v1.0-ps-20141009.txt
+ * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#usb-discovery
+ * https://www.usb.org/sites/default/files/hutrr48.pdf
+ */
+#define FIDO_FULL_USAGE_CTAPHID 0xf1d00001u
+
+/*
+ * Parses a HID report descriptor and identifies FIDO CTAP1 ("U2F")/CTAP2 security tokens based on their
+ * declared usage.
+ * A positive return value indicates that the report descriptor belongs to a FIDO security token.
+ * https://www.usb.org/sites/default/files/documents/hid1_11.pdf (Section 6.2.2)
+ */
+int is_fido_security_token_desc(const uint8_t *desc, size_t desc_len) {
+ uint32_t usage = 0;
+
+ for (size_t pos = 0; pos < desc_len; ) {
+ uint8_t tag, type, size_code;
+ size_t size;
+ uint32_t value;
+
+ /* Report descriptors consists of short items (1-5 bytes) and long items (3-258 bytes). */
+ if (desc[pos] == HID_RPTDESC_FIRST_BYTE_LONG_ITEM) {
+ /* No long items are defined in the spec; skip them.
+ * The length of the data in a long item is contained in the byte after the long
+ * item tag. The header consists of three bytes: special long item tag, length,
+ * actual tag. */
+ if (pos + 1 >= desc_len)
+ return -EINVAL;
+ pos += desc[pos + 1] + 3;
+ continue;
+ }
+
+ /* The first byte of a short item encodes tag, type and size. */
+ tag = desc[pos] >> 4; /* Bits 7 to 4 */
+ type = (desc[pos] >> 2) & 0x3; /* Bits 3 and 2 */
+ size_code = desc[pos] & 0x3; /* Bits 1 and 0 */
+ /* Size is coded as follows:
+ * 0 -> 0 bytes, 1 -> 1 byte, 2 -> 2 bytes, 3 -> 4 bytes
+ */
+ size = size_code < 3 ? size_code : 4;
+ /* Consume header byte. */
+ pos++;
+
+ /* Extract the item value coded on size bytes. */
+ if (pos + size > desc_len)
+ return -EINVAL;
+ value = 0;
+ for (size_t i = 0; i < size; i++)
+ value |= (uint32_t) desc[pos + i] << (8 * i);
+ /* Consume value bytes. */
+ pos += size;
+
+ if (type == HID_RPTDESC_TYPE_GLOBAL && tag == HID_RPTDESC_TAG_USAGE_PAGE) {
+ /* A usage page is a 16 bit value coded on at most 16 bits. */
+ if (size > 2)
+ return -EINVAL;
+ /* A usage page sets the upper 16 bits of a following usage. */
+ usage = (value & 0x0000ffffu) << 16;
+ }
+
+ if (type == HID_RPTDESC_TYPE_LOCAL && tag == HID_RPTDESC_TAG_USAGE) {
+ /* A usage is a 32 bit value, but is prepended with the current usage page if
+ * coded on less than 4 bytes (that is, at most 2 bytes). */
+ if (size == 4)
+ usage = value;
+ else
+ usage = (usage & 0xffff0000u) | (value & 0x0000ffffu);
+ if (usage == FIDO_FULL_USAGE_CTAPHID)
+ return 1;
+ }
+ }
+
+ return 0;
+}
diff --git a/src/udev/fido_id/fido_id_desc.h b/src/udev/fido_id/fido_id_desc.h
new file mode 100644
index 0000000..57af57e
--- /dev/null
+++ b/src/udev/fido_id/fido_id_desc.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+
+int is_fido_security_token_desc(const uint8_t *desc, size_t desc_len);
diff --git a/src/udev/fido_id/fuzz-fido-id-desc.c b/src/udev/fido_id/fuzz-fido-id-desc.c
new file mode 100644
index 0000000..040d77a
--- /dev/null
+++ b/src/udev/fido_id/fuzz-fido-id-desc.c
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/hid.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "fido_id_desc.h"
+#include "fuzz.h"
+#include "log.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ fuzz_setup_logging();
+
+ if (outside_size_range(size, 0, HID_MAX_DESCRIPTOR_SIZE))
+ return 0;
+
+ (void) is_fido_security_token_desc(data, size);
+
+ return 0;
+}
diff --git a/src/udev/fido_id/test-fido-id-desc.c b/src/udev/fido_id/test-fido-id-desc.c
new file mode 100644
index 0000000..36c777a
--- /dev/null
+++ b/src/udev/fido_id/test-fido-id-desc.c
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "fido_id_desc.h"
+#include "macro.h"
+#include "tests.h"
+
+TEST(is_fido_security_token_desc__fido) {
+ static const uint8_t FIDO_HID_DESC_1[] = {
+ 0x06, 0xd0, 0xf1, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75,
+ 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95,
+ 0x40, 0x91, 0x02, 0xc0,
+ };
+ assert_se(is_fido_security_token_desc(FIDO_HID_DESC_1, sizeof(FIDO_HID_DESC_1)) > 0);
+
+ static const uint8_t FIDO_HID_DESC_2[] = {
+ 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25,
+ 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
+ 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91,
+ 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65,
+ 0x81, 0x00, 0x09, 0x03, 0x75, 0x08, 0x95, 0x08, 0xb1, 0x02, 0xc0,
+ 0x06, 0xd0, 0xf1, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75,
+ 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95,
+ 0x40, 0x91, 0x02, 0xc0,
+ };
+ assert_se(is_fido_security_token_desc(FIDO_HID_DESC_2, sizeof(FIDO_HID_DESC_2)) > 0);
+}
+
+TEST(is_fido_security_token_desc__non_fido) {
+ /* Wrong usage page */
+ static const uint8_t NON_FIDO_HID_DESC_1[] = {
+ 0x06, 0xd0, 0xf0, 0x09, 0x01, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75,
+ 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95,
+ 0x40, 0x91, 0x02, 0xc0,
+ };
+ assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_1, sizeof(NON_FIDO_HID_DESC_1)) == 0);
+
+ /* Wrong usage */
+ static const uint8_t NON_FIDO_HID_DESC_2[] = {
+ 0x06, 0xd0, 0xf1, 0x09, 0x02, 0xa1, 0x01, 0x09, 0x20, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75,
+ 0x08, 0x95, 0x40, 0x81, 0x02, 0x09, 0x21, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95,
+ 0x40, 0x91, 0x02, 0xc0,
+ };
+ assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_2, sizeof(NON_FIDO_HID_DESC_2)) == 0);
+
+ static const uint8_t NON_FIDO_HID_DESC_3[] = {
+ 0x05, 0x01, 0x09, 0x06, 0xa1, 0x01, 0x05, 0x07, 0x19, 0xe0, 0x29, 0xe7, 0x15, 0x00, 0x25,
+ 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05,
+ 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91,
+ 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, 0x00, 0x29, 0x65,
+ 0x81, 0x00, 0x09, 0x03, 0x75, 0x08, 0x95, 0x08, 0xb1, 0x02, 0xc0,
+ };
+ assert_se(is_fido_security_token_desc(NON_FIDO_HID_DESC_3, sizeof(NON_FIDO_HID_DESC_3)) == 0);
+}
+
+TEST(is_fido_security_token_desc__invalid) {
+ /* Size coded on 1 byte, but no byte given */
+ static const uint8_t INVALID_HID_DESC_1[] = { 0x01 };
+ assert_se(is_fido_security_token_desc(INVALID_HID_DESC_1, sizeof(INVALID_HID_DESC_1)) < 0);
+
+ /* Size coded on 2 bytes, but only 1 byte given */
+ static const uint8_t INVALID_HID_DESC_2[] = { 0x02, 0x01 };
+ assert_se(is_fido_security_token_desc(INVALID_HID_DESC_2, sizeof(INVALID_HID_DESC_2)) < 0);
+
+ /* Size coded on 4 bytes, but only 3 bytes given */
+ static const uint8_t INVALID_HID_DESC_3[] = { 0x03, 0x01, 0x02, 0x03 };
+ assert_se(is_fido_security_token_desc(INVALID_HID_DESC_3, sizeof(INVALID_HID_DESC_3)) < 0);
+
+ /* Long item without a size byte */
+ static const uint8_t INVALID_HID_DESC_4[] = { 0xfe };
+ assert_se(is_fido_security_token_desc(INVALID_HID_DESC_4, sizeof(INVALID_HID_DESC_4)) < 0);
+
+ /* Usage pages are coded on at most 2 bytes */
+ static const uint8_t INVALID_HID_DESC_5[] = { 0x07, 0x01, 0x02, 0x03, 0x04 };
+ assert_se(is_fido_security_token_desc(INVALID_HID_DESC_5, sizeof(INVALID_HID_DESC_5)) < 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/udev/fuzz-udev-rule-parse-value.c b/src/udev/fuzz-udev-rule-parse-value.c
new file mode 100644
index 0000000..1817c15
--- /dev/null
+++ b/src/udev/fuzz-udev-rule-parse-value.c
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <string.h>
+
+#include "alloc-util.h"
+#include "fuzz.h"
+#include "udev-rules.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ _cleanup_free_ char *str = NULL;
+ int r;
+ char *value = UINT_TO_PTR(0x12345678U);
+ char *endpos = UINT_TO_PTR(0x87654321U);
+
+ fuzz_setup_logging();
+
+ assert_se(str = malloc(size + 1));
+ memcpy(str, data, size);
+ str[size] = '\0';
+
+ r = udev_rule_parse_value(str, &value, &endpos);
+ if (r < 0) {
+ /* not modified on failure */
+ assert_se(value == UINT_TO_PTR(0x12345678U));
+ assert_se(endpos == UINT_TO_PTR(0x87654321U));
+ } else {
+ assert_se(endpos <= str + size);
+ assert_se(endpos > str + 1);
+ }
+
+ return 0;
+}
diff --git a/src/udev/fuzz-udev-rules.c b/src/udev/fuzz-udev-rules.c
new file mode 100644
index 0000000..0a1056d
--- /dev/null
+++ b/src/udev/fuzz-udev-rules.c
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "fd-util.h"
+#include "fs-util.h"
+#include "fuzz.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "udev-rules.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ _cleanup_(udev_rules_freep) UdevRules *rules = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(unlink_tempfilep) char filename[] = "/tmp/fuzz-udev-rules.XXXXXX";
+ int r;
+
+ if (outside_size_range(size, 0, 65536))
+ return 0;
+
+ fuzz_setup_logging();
+
+ assert_se(fmkostemp_safe(filename, "r+", &f) == 0);
+ if (size != 0)
+ assert_se(fwrite(data, size, 1, f) == 1);
+ fflush(f);
+
+ assert_se(rules = udev_rules_new(RESOLVE_NAME_EARLY));
+ r = udev_rules_parse_file(rules, filename, /* extra_checks = */ false, NULL);
+ log_info_errno(r, "Parsing %s: %m", filename);
+ assert_se(r >= 0 || /* OK */
+ r == -ENOBUFS); /* line length exceeded */
+
+ return 0;
+}
diff --git a/src/udev/fuzz-udev-rules.options b/src/udev/fuzz-udev-rules.options
new file mode 100644
index 0000000..678d526
--- /dev/null
+++ b/src/udev/fuzz-udev-rules.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65536
diff --git a/src/udev/generate-keyboard-keys-gperf.sh b/src/udev/generate-keyboard-keys-gperf.sh
new file mode 100755
index 0000000..9f4364c
--- /dev/null
+++ b/src/udev/generate-keyboard-keys-gperf.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eu
+
+# shellcheck disable=SC1004
+awk '
+ BEGIN {
+ print "%{\n\
+#if __GNUC__ >= 7\n\
+_Pragma(\"GCC diagnostic ignored \\\"-Wimplicit-fallthrough\\\"\")\n\
+#endif\n\
+%}"
+ print "struct key_name { const char* name; unsigned short id; };"
+ print "%null-strings"
+ print "%%"
+ }
+
+ /^KEY_/ { print tolower(substr($1 ,5)) ", " $1 }
+ { print tolower($1) ", " $1 }
+' <"${1:?}"
diff --git a/src/udev/generate-keyboard-keys-list.sh b/src/udev/generate-keyboard-keys-list.sh
new file mode 100755
index 0000000..ead3113
--- /dev/null
+++ b/src/udev/generate-keyboard-keys-list.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eu
+set -o pipefail
+
+${1:?} -dM -include linux/input.h - </dev/null | awk '
+ /\<(KEY_(MAX|MIN_INTERESTING))|(BTN_(MISC|MOUSE|JOYSTICK|GAMEPAD|DIGI|WHEEL|TRIGGER_HAPPY))\>/ { next }
+ /^#define[ \t]+(KEY|BTN)_[^ ]+[ \t]+[0-9BK]/ { print $2 }
+'
diff --git a/src/udev/iocost/iocost.c b/src/udev/iocost/iocost.c
new file mode 100644
index 0000000..2b2633e
--- /dev/null
+++ b/src/udev/iocost/iocost.c
@@ -0,0 +1,321 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "sd-device.h"
+
+#include "alloc-util.h"
+#include "build.h"
+#include "cgroup-util.h"
+#include "conf-parser.h"
+#include "device-util.h"
+#include "devnum-util.h"
+#include "main-func.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "udev-util.h"
+#include "verbs.h"
+
+static char *arg_target_solution = NULL;
+STATIC_DESTRUCTOR_REGISTER(arg_target_solution, freep);
+
+static int parse_config(void) {
+ static const ConfigTableItem items[] = {
+ { "IOCost", "TargetSolution", config_parse_string, 0, &arg_target_solution },
+ };
+ int r;
+
+ r = config_parse(
+ NULL,
+ "/etc/udev/iocost.conf",
+ NULL,
+ "IOCost\0",
+ config_item_table_lookup,
+ items,
+ CONFIG_PARSE_WARN,
+ NULL,
+ NULL);
+ if (r < 0)
+ return r;
+
+ if (!arg_target_solution) {
+ arg_target_solution = strdup("naive");
+ if (!arg_target_solution)
+ return log_oom();
+ }
+
+ log_debug("Target solution: %s", arg_target_solution);
+ return 0;
+}
+
+static int help(void) {
+ printf("%s [OPTIONS...]\n\n"
+ "Set up iocost model and qos solutions for block devices\n"
+ "\nCommands:\n"
+ " apply <path> [SOLUTION] Apply solution for the device if\n"
+ " found, do nothing otherwise\n"
+ " query <path> Query the known solution for\n"
+ " the device\n"
+ "\nOptions:\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_VERSION = 0x100,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ {}
+ };
+
+ int c;
+
+ assert(argc >= 1);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ return help();
+
+ case ARG_VERSION:
+ return version();
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+static int get_known_solutions(sd_device *device, int log_level, char ***ret_solutions, const char **ret_selected) {
+ _cleanup_free_ char **s = NULL;
+ const char *value, *found;
+ int r;
+
+ assert(ret_solutions);
+ assert(ret_selected);
+
+ r = sd_device_get_property_value(device, "IOCOST_SOLUTIONS", &value);
+ if (r == -ENOENT)
+ return log_device_full_errno(device, log_level, r, "No iocost solution found for device.");
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to query solutions from device: %m");
+
+ s = strv_split(value, WHITESPACE);
+ if (!s)
+ return log_oom();
+ if (strv_isempty(s))
+ return log_device_error_errno(device, SYNTHETIC_ERRNO(EINVAL),
+ "IOCOST_SOLUTIONS exists in hwdb but is empty.");
+
+ found = strv_find(s, arg_target_solution);
+ if (found) {
+ *ret_selected = found;
+ log_device_debug(device, "Selected solution based on target solution: %s", *ret_selected);
+ } else {
+ *ret_selected = s[0];
+ log_device_debug(device, "Selected first available solution: %s", *ret_selected);
+ }
+
+ *ret_solutions = TAKE_PTR(s);
+ return 0;
+}
+
+static int query_named_solution(
+ sd_device *device,
+ const char *name,
+ const char **ret_model,
+ const char **ret_qos) {
+
+ _cleanup_free_ char *upper_name = NULL, *qos_key = NULL, *model_key = NULL;
+ const char *qos, *model;
+ int r;
+
+ assert(name);
+ assert(ret_qos);
+ assert(ret_model);
+
+ upper_name = strdup(name);
+ if (!upper_name)
+ return log_oom();
+
+ ascii_strupper(upper_name);
+ string_replace_char(upper_name, '-', '_');
+
+ qos_key = strjoin("IOCOST_QOS_", upper_name);
+ if (!qos_key)
+ return log_oom();
+
+ model_key = strjoin("IOCOST_MODEL_", upper_name);
+ if (!model_key)
+ return log_oom();
+
+ r = sd_device_get_property_value(device, qos_key, &qos);
+ if (r == -ENOENT)
+ return log_device_debug_errno(device, r, "No value found for key %s, skipping iocost logic.", qos_key);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to obtain QoS for iocost solution from device: %m");
+
+ r = sd_device_get_property_value(device, model_key, &model);
+ if (r == -ENOENT)
+ return log_device_debug_errno(device, r, "No value found for key %s, skipping iocost logic.", model_key);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Failed to obtain model for iocost solution from device: %m");
+
+ *ret_qos = qos;
+ *ret_model = model;
+
+ return 0;
+}
+
+static int apply_solution_for_path(const char *path, const char *name) {
+ _cleanup_(sd_device_unrefp) sd_device *device = NULL;
+ _cleanup_strv_free_ char **solutions = NULL;
+ _cleanup_free_ char *qos = NULL, *model = NULL;
+ const char *qos_params, *model_params;
+ dev_t devnum;
+ int r;
+
+ r = sd_device_new_from_path(&device, path);
+ if (r < 0)
+ return log_error_errno(r, "Error looking up device: %m");
+
+ r = sd_device_get_devnum(device, &devnum);
+ if (r < 0)
+ return log_device_error_errno(device, r, "Error getting devnum: %m");
+
+ if (!name) {
+ r = get_known_solutions(device, LOG_DEBUG, &solutions, &name);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+ }
+
+ r = query_named_solution(device, name, &model_params, &qos_params);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ if (asprintf(&qos, DEVNUM_FORMAT_STR " enable=1 ctrl=user %s", DEVNUM_FORMAT_VAL(devnum), qos_params) < 0)
+ return log_oom();
+
+ if (asprintf(&model, DEVNUM_FORMAT_STR " model=linear ctrl=user %s", DEVNUM_FORMAT_VAL(devnum), model_params) < 0)
+ return log_oom();
+
+ log_debug("Applying iocost parameters to %s using solution '%s'\n"
+ "\tio.cost.qos: %s\n"
+ "\tio.cost.model: %s\n",
+ path, name, qos, model);
+
+ r = cg_set_attribute("io", NULL, "io.cost.qos", qos);
+ if (r < 0) {
+ log_device_full_errno(device, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to set io.cost.qos: %m");
+ return r == -ENOENT ? 0 : r;
+ }
+
+ r = cg_set_attribute("io", NULL, "io.cost.model", model);
+ if (r < 0) {
+ log_device_full_errno(device, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to set io.cost.model: %m");
+ return r == -ENOENT ? 0 : r;
+ }
+
+ return 0;
+}
+
+static int query_solutions_for_path(const char *path) {
+ _cleanup_(sd_device_unrefp) sd_device *device = NULL;
+ _cleanup_strv_free_ char **solutions = NULL;
+ const char *selected_solution, *model_name;
+ int r;
+
+ r = sd_device_new_from_path(&device, path);
+ if (r < 0)
+ return log_error_errno(r, "Error looking up device: %m");
+
+ r = device_get_model_string(device, &model_name);
+ if (r == -ENOENT) {
+ log_device_info(device, "Device model not found");
+ return 0;
+ }
+ if (r < 0)
+ return log_device_error_errno(device, r, "Model name for device %s is unknown", path);
+
+ r = get_known_solutions(device, LOG_INFO, &solutions, &selected_solution);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ log_info("Known solutions for %s model name: \"%s\"\n"
+ "Preferred solution: %s\n"
+ "Solution that would be applied: %s",
+ path, model_name,
+ arg_target_solution, selected_solution);
+
+ STRV_FOREACH(s, solutions) {
+ const char *model, *qos;
+
+ if (query_named_solution(device, *s, &model, &qos) < 0)
+ continue;
+
+ log_info("%s: io.cost.qos: %s\n"
+ "%s: io.cost.model: %s", *s, qos, *s, model);
+ }
+
+ return 0;
+}
+
+static int verb_query(int argc, char *argv[], void *userdata) {
+ return query_solutions_for_path(ASSERT_PTR(argv[1]));
+}
+
+static int verb_apply(int argc, char *argv[], void *userdata) {
+ return apply_solution_for_path(
+ ASSERT_PTR(argv[1]),
+ argc > 2 ? ASSERT_PTR(argv[2]) : NULL);
+}
+
+static int iocost_main(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "query", 2, 2, 0, verb_query },
+ { "apply", 2, 3, 0, verb_apply },
+ {},
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = parse_config();
+ if (r < 0)
+ return r;
+
+ return iocost_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/iocost/iocost.conf b/src/udev/iocost/iocost.conf
new file mode 100644
index 0000000..c0eeea3
--- /dev/null
+++ b/src/udev/iocost/iocost.conf
@@ -0,0 +1,20 @@
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# Entries in this file show the compile time defaults. Local configuration
+# should be created by either modifying this file (or a copy of it placed in
+# /etc/ if the original file is shipped in /usr/), or by creating "drop-ins" in
+# the /etc/udev/iocost.conf.d/ directory. The latter is generally recommended.
+# Defaults can be restored by simply deleting the main configuration file and
+# all drop-ins located in /etc/.
+#
+# Use 'systemd-analyze cat-config udev/iocost.conf' to display the full config.
+#
+# See iocost.conf(5) for details.
+
+[IOCost]
+#TargetSolution=naive
diff --git a/src/udev/meson.build b/src/udev/meson.build
new file mode 100644
index 0000000..824ec47
--- /dev/null
+++ b/src/udev/meson.build
@@ -0,0 +1,273 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+udevadm_sources = files(
+ 'udevadm-control.c',
+ 'udevadm-hwdb.c',
+ 'udevadm-info.c',
+ 'udevadm-lock.c',
+ 'udevadm-monitor.c',
+ 'udevadm-settle.c',
+ 'udevadm-test-builtin.c',
+ 'udevadm-test.c',
+ 'udevadm-trigger.c',
+ 'udevadm-util.c',
+ 'udevadm-verify.c',
+ 'udevadm-wait.c',
+ 'udevadm.c',
+ 'udevd.c',
+)
+
+libudevd_core_sources = files(
+ 'net/link-config.c',
+ 'udev-ctrl.c',
+ 'udev-event.c',
+ 'udev-format.c',
+ 'udev-manager.c',
+ 'udev-node.c',
+ 'udev-rules.c',
+ 'udev-spawn.c',
+ 'udev-watch.c',
+ 'udev-worker.c',
+ 'udev-builtin-btrfs.c',
+ 'udev-builtin-hwdb.c',
+ 'udev-builtin-input_id.c',
+ 'udev-builtin-keyboard.c',
+ 'udev-builtin-net_driver.c',
+ 'udev-builtin-net_id.c',
+ 'udev-builtin-net_setup_link.c',
+ 'udev-builtin-path_id.c',
+ 'udev-builtin-usb_id.c',
+ 'udev-builtin.c',
+)
+
+if conf.get('HAVE_KMOD') == 1
+ libudevd_core_sources += files('udev-builtin-kmod.c')
+endif
+
+if conf.get('HAVE_BLKID') == 1
+ libudevd_core_sources += files('udev-builtin-blkid.c')
+endif
+
+if conf.get('HAVE_ACL') == 1
+ libudevd_core_sources += files('udev-builtin-uaccess.c')
+endif
+
+############################################################
+
+generate_keyboard_keys_list = find_program('generate-keyboard-keys-list.sh')
+keyboard_keys_list_txt = custom_target(
+ 'keyboard-keys-list.txt',
+ output : 'keyboard-keys-list.txt',
+ command : [generate_keyboard_keys_list, cpp],
+ capture : true)
+
+generate_keyboard_keys_gperf = find_program('generate-keyboard-keys-gperf.sh')
+fname = 'keyboard-keys-from-name.gperf'
+gperf_file = custom_target(
+ fname,
+ input : keyboard_keys_list_txt,
+ output : fname,
+ command : [generate_keyboard_keys_gperf, '@INPUT@'],
+ capture : true)
+
+fname = 'keyboard-keys-from-name.h'
+keyboard_keys_from_name_h = custom_target(
+ fname,
+ input : gperf_file,
+ output : fname,
+ command : [gperf,
+ '-L', 'ANSI-C', '-t',
+ '-N', 'keyboard_lookup_key',
+ '-H', 'hash_key_name',
+ '-p', '-C',
+ '@INPUT@'],
+ capture : true)
+
+############################################################
+
+udev_link_gperf_gperf = files('net/link-config-gperf.gperf')
+
+link_config_gperf_c = custom_target(
+ 'link-config-gperf.c',
+ input : udev_link_gperf_gperf,
+ output : 'link-config-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+############################################################
+
+if get_option('link-udev-shared')
+ udev_link_with = [libshared]
+ udev_rpath = pkglibdir
+else
+ udev_link_with = [libshared_static,
+ libsystemd_static]
+ udev_rpath = ''
+endif
+
+############################################################
+
+libudevd_core = static_library(
+ 'udev-core',
+ libudevd_core_sources,
+ link_config_gperf_c,
+ keyboard_keys_from_name_h,
+ include_directories : includes + include_directories('net'),
+ link_with : udev_link_with,
+ dependencies : [libblkid,
+ libkmod,
+ userspace],
+ build_by_default : false)
+
+udev_dependencies = [
+ libacl,
+ libblkid,
+ libkmod,
+ threads,
+]
+
+udev_plugin_template = executable_template + {
+ 'public' : true,
+ 'link_with' : udev_link_with,
+ 'install_rpath' : udev_rpath,
+ 'install_dir' : udevlibexecdir,
+}
+
+udev_common_template = {
+ 'link_with' : [
+ libshared,
+ libudevd_core,
+ ],
+ 'dependencies' : [
+ libacl,
+ threads,
+ ],
+}
+udev_test_template = test_template + udev_common_template
+udev_fuzz_template = fuzz_template + udev_common_template
+
+executables += [
+ executable_template + {
+ 'name' : 'udevadm',
+ 'public' : true,
+ 'sources' : udevadm_sources,
+ 'link_with' : [libudevd_core],
+ 'dependencies' : udev_dependencies,
+ 'install_rpath' : udev_rpath,
+ },
+ udev_plugin_template + {
+ 'name' : 'ata_id',
+ 'sources' : files('ata_id/ata_id.c'),
+ },
+ udev_plugin_template + {
+ 'name' : 'cdrom_id',
+ 'sources' : files('cdrom_id/cdrom_id.c'),
+ },
+ udev_plugin_template + {
+ 'name' : 'dmi_memory_id',
+ 'conditions' : ['HAVE_DMI'],
+ 'sources' : files('dmi_memory_id/dmi_memory_id.c'),
+ },
+ udev_plugin_template + {
+ 'name' : 'fido_id',
+ 'sources' : files(
+ 'fido_id/fido_id.c',
+ 'fido_id/fido_id_desc.c',
+ ),
+ },
+ udev_plugin_template + {
+ 'name' : 'iocost',
+ 'sources' : files('iocost/iocost.c'),
+ },
+ udev_plugin_template + {
+ 'name' : 'mtd_probe',
+ 'sources' : files(
+ 'mtd_probe/mtd_probe.c',
+ 'mtd_probe/probe_smartmedia.c',
+ ),
+ },
+ udev_plugin_template + {
+ 'name' : 'scsi_id',
+ 'sources' : files(
+ 'scsi_id/scsi_id.c',
+ 'scsi_id/scsi_serial.c',
+ ),
+ },
+ udev_plugin_template + {
+ 'name' : 'v4l_id',
+ 'sources' : files('v4l_id/v4l_id.c'),
+ },
+ test_template + {
+ 'sources' : files(
+ 'fido_id/test-fido-id-desc.c',
+ 'fido_id/fido_id_desc.c',
+ ),
+ 'suite' : 'udev',
+ },
+ udev_test_template + {
+ 'sources' : files('net/test-link-config-tables.c'),
+ 'suite' : 'udev',
+ },
+ udev_test_template + {
+ 'sources' : files('test-udev-builtin.c'),
+ },
+ udev_test_template + {
+ 'sources' : files('test-udev-format.c'),
+ },
+ udev_test_template + {
+ 'sources' : files('test-udev-manager.c'),
+ },
+ udev_test_template + {
+ 'sources' : files('test-udev-node.c'),
+ },
+ udev_test_template + {
+ 'sources' : files('test-udev-rule-runner.c'),
+ 'dependencies' : udev_dependencies + [
+ libselinux,
+ ],
+ 'type' : 'manual',
+ },
+ udev_test_template + {
+ 'sources' : files('test-udev-rules.c'),
+ },
+ udev_test_template + {
+ 'sources' : files('test-udev-spawn.c'),
+ },
+ fuzz_template + {
+ 'sources' : files(
+ 'fido_id/fuzz-fido-id-desc.c',
+ 'fido_id/fido_id_desc.c',
+ ),
+ },
+ udev_fuzz_template + {
+ 'sources' : files('net/fuzz-link-parser.c'),
+ },
+ udev_fuzz_template + {
+ 'sources' : files('fuzz-udev-rule-parse-value.c'),
+ },
+ udev_fuzz_template + {
+ 'sources' : files('fuzz-udev-rules.c'),
+ },
+]
+
+meson.add_install_script(sh, '-c', ln_s.format(bindir / 'udevadm',
+ libexecdir / 'systemd-udevd'))
+
+if install_sysconfdir_samples
+ install_data('udev.conf',
+ install_dir : configfiledir / 'udev')
+ install_data('iocost/iocost.conf',
+ install_dir : configfiledir / 'udev')
+endif
+
+udev_pc = custom_target(
+ 'udev.pc',
+ input : 'udev.pc.in',
+ output : 'udev.pc',
+ command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
+ install : pkgconfigdatadir != 'no',
+ install_tag : 'devel',
+ install_dir : pkgconfigdatadir)
+
+if install_sysconfdir
+ install_emptydir(sysconfdir / 'udev/rules.d')
+endif
diff --git a/src/udev/mtd_probe/mtd_probe.c b/src/udev/mtd_probe/mtd_probe.c
new file mode 100644
index 0000000..1035320
--- /dev/null
+++ b/src/udev/mtd_probe/mtd_probe.c
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © 2010 - Maxim Levitsky
+ *
+ * mtd_probe is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mtd_probe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with mtd_probe; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301 USA
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <mtd/mtd-user.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "build.h"
+#include "fd-util.h"
+#include "main-func.h"
+#include "mtd_probe.h"
+
+static const char *arg_device = NULL;
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ {}
+ };
+ int c;
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ switch (c) {
+ case 'h':
+ printf("%s /dev/mtd[n]\n\n"
+ " -h --help Show this help text\n"
+ " --version Show package version\n",
+ program_invocation_short_name);
+ return 0;
+ case 'v':
+ return version();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ if (argc > 2)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error: unexpected argument.");
+
+ arg_device = argv[optind];
+ return 1;
+}
+
+static int run(int argc, char** argv) {
+ _cleanup_close_ int mtd_fd = -EBADF;
+ mtd_info_t mtd_info;
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ mtd_fd = open(argv[1], O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (mtd_fd < 0)
+ return log_error_errno(errno, "Failed to open: %m");
+
+ if (ioctl(mtd_fd, MEMGETINFO, &mtd_info) < 0)
+ return log_error_errno(errno, "MEMGETINFO ioctl failed: %m");
+
+ return probe_smart_media(mtd_fd, &mtd_info);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/mtd_probe/mtd_probe.h b/src/udev/mtd_probe/mtd_probe.h
new file mode 100644
index 0000000..ae03a7d
--- /dev/null
+++ b/src/udev/mtd_probe/mtd_probe.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+/*
+ * Copyright © 2010 - Maxim Levitsky
+ *
+ * mtd_probe is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mtd_probe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with mtd_probe; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301 USA
+ */
+
+#include <mtd/mtd-user.h>
+
+#include "macro.h"
+
+/* Full oob structure as written on the flash */
+struct sm_oob {
+ uint32_t reserved;
+ uint8_t data_status;
+ uint8_t block_status;
+ uint8_t lba_copy1[2];
+ uint8_t ecc2[3];
+ uint8_t lba_copy2[2];
+ uint8_t ecc1[3];
+} _packed_;
+
+/* one sector is always 512 bytes, but it can consist of two nand pages */
+#define SM_SECTOR_SIZE 512
+
+/* oob area is also 16 bytes, but might be from two pages */
+#define SM_OOB_SIZE 16
+
+/* This is maximum zone size, and all devices that have more that one zone
+ have this size */
+#define SM_MAX_ZONE_SIZE 1024
+
+/* support for small page nand */
+#define SM_SMALL_PAGE 256
+#define SM_SMALL_OOB_SIZE 8
+
+int probe_smart_media(int mtd_fd, mtd_info_t *info);
diff --git a/src/udev/mtd_probe/probe_smartmedia.c b/src/udev/mtd_probe/probe_smartmedia.c
new file mode 100644
index 0000000..368fab8
--- /dev/null
+++ b/src/udev/mtd_probe/probe_smartmedia.c
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © 2010 - Maxim Levitsky
+ *
+ * mtd_probe is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mtd_probe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with mtd_probe; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor,
+ * Boston, MA 02110-1301 USA
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <mtd/mtd-user.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "mtd_probe.h"
+
+static const uint8_t cis_signature[] = {
+ 0x01, 0x03, 0xD9, 0x01, 0xFF, 0x18, 0x02, 0xDF, 0x01, 0x20
+};
+
+int probe_smart_media(int mtd_fd, mtd_info_t* info) {
+ int sector_size;
+ int block_size;
+ int size_in_megs;
+ int spare_count;
+ _cleanup_free_ uint8_t *cis_buffer = NULL;
+ int offset;
+ int cis_found = 0;
+
+ cis_buffer = malloc(SM_SECTOR_SIZE);
+ if (!cis_buffer)
+ return log_oom();
+
+ if (info->type != MTD_NANDFLASH)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Not marked MTD_NANDFLASH.");
+
+ sector_size = info->writesize;
+ block_size = info->erasesize;
+ size_in_megs = info->size / (1024 * 1024);
+
+ if (!IN_SET(sector_size, SM_SECTOR_SIZE, SM_SMALL_PAGE))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unexpected sector size: %i", sector_size);
+
+ switch (size_in_megs) {
+ case 1:
+ case 2:
+ spare_count = 6;
+ break;
+ case 4:
+ spare_count = 12;
+ break;
+ default:
+ spare_count = 24;
+ break;
+ }
+
+ for (offset = 0; offset < block_size * spare_count; offset += sector_size) {
+ (void) lseek(mtd_fd, SEEK_SET, offset);
+
+ if (read(mtd_fd, cis_buffer, SM_SECTOR_SIZE) == SM_SECTOR_SIZE) {
+ cis_found = 1;
+ break;
+ }
+ }
+
+ if (!cis_found)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "CIS not found");
+
+ if (memcmp(cis_buffer, cis_signature, sizeof(cis_signature)) != 0 &&
+ memcmp(cis_buffer + SM_SMALL_PAGE, cis_signature, sizeof(cis_signature)) != 0)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "CIS signature didn't match");
+
+ printf("MTD_FTL=smartmedia\n");
+ return 0;
+}
diff --git a/src/udev/net/fuzz-link-parser.c b/src/udev/net/fuzz-link-parser.c
new file mode 100644
index 0000000..2833162
--- /dev/null
+++ b/src/udev/net/fuzz-link-parser.c
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fd-util.h"
+#include "fs-util.h"
+#include "fuzz.h"
+#include "link-config.h"
+#include "tmpfile-util.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ _cleanup_(link_config_ctx_freep) LinkConfigContext *ctx = NULL;
+ _cleanup_(unlink_tempfilep) char filename[] = "/tmp/fuzz-link-config.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+
+ if (outside_size_range(size, 0, 65536))
+ return 0;
+
+ fuzz_setup_logging();
+
+ assert_se(fmkostemp_safe(filename, "r+", &f) == 0);
+ if (size != 0)
+ assert_se(fwrite(data, size, 1, f) == 1);
+
+ fflush(f);
+ assert_se(link_config_ctx_new(&ctx) >= 0);
+ (void) link_load_one(ctx, filename);
+ return 0;
+}
diff --git a/src/udev/net/fuzz-link-parser.options b/src/udev/net/fuzz-link-parser.options
new file mode 100644
index 0000000..678d526
--- /dev/null
+++ b/src/udev/net/fuzz-link-parser.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65536
diff --git a/src/udev/net/link-config-gperf.gperf b/src/udev/net/link-config-gperf.gperf
new file mode 100644
index 0000000..240f16e
--- /dev/null
+++ b/src/udev/net/link-config-gperf.gperf
@@ -0,0 +1,118 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "conf-parser.h"
+#include "ethtool-util.h"
+#include "link-config.h"
+#include "net-condition.h"
+#include "netif-sriov.h"
+#include "socket-util.h"
+%}
+struct ConfigPerfItem;
+%null_strings
+%language=ANSI-C
+%define slot-name section_and_lvalue
+%define hash-function-name link_config_gperf_hash
+%define lookup-function-name link_config_gperf_lookup
+%readonly-tables
+%omit-struct-type
+%struct-type
+%includes
+%%
+Match.MACAddress, config_parse_hw_addrs, 0, offsetof(LinkConfig, match.hw_addr)
+Match.PermanentMACAddress, config_parse_hw_addrs, 0, offsetof(LinkConfig, match.permanent_hw_addr)
+Match.OriginalName, config_parse_match_ifnames, 0, offsetof(LinkConfig, match.ifname)
+Match.Path, config_parse_match_strv, 0, offsetof(LinkConfig, match.path)
+Match.Driver, config_parse_match_strv, 0, offsetof(LinkConfig, match.driver)
+Match.Type, config_parse_match_strv, 0, offsetof(LinkConfig, match.iftype)
+Match.Kind, config_parse_match_strv, 0, offsetof(LinkConfig, match.kind)
+Match.Property, config_parse_match_property, 0, offsetof(LinkConfig, match.property)
+Match.Host, config_parse_net_condition, CONDITION_HOST, offsetof(LinkConfig, conditions)
+Match.Virtualization, config_parse_net_condition, CONDITION_VIRTUALIZATION, offsetof(LinkConfig, conditions)
+Match.KernelCommandLine, config_parse_net_condition, CONDITION_KERNEL_COMMAND_LINE, offsetof(LinkConfig, conditions)
+Match.KernelVersion, config_parse_net_condition, CONDITION_KERNEL_VERSION, offsetof(LinkConfig, conditions)
+Match.Credential, config_parse_net_condition, CONDITION_CREDENTIAL, offsetof(LinkConfig, conditions)
+Match.Architecture, config_parse_net_condition, CONDITION_ARCHITECTURE, offsetof(LinkConfig, conditions)
+Match.Firmware, config_parse_net_condition, CONDITION_FIRMWARE, offsetof(LinkConfig, conditions)
+Link.Description, config_parse_string, 0, offsetof(LinkConfig, description)
+Link.MACAddressPolicy, config_parse_mac_address_policy, 0, offsetof(LinkConfig, mac_address_policy)
+Link.MACAddress, config_parse_hw_addr, 0, offsetof(LinkConfig, hw_addr)
+Link.NamePolicy, config_parse_name_policy, 0, offsetof(LinkConfig, name_policy)
+Link.Name, config_parse_ifname, 0, offsetof(LinkConfig, name)
+Link.AlternativeName, config_parse_ifnames, IFNAME_VALID_ALTERNATIVE, offsetof(LinkConfig, alternative_names)
+Link.AlternativeNamesPolicy, config_parse_alternative_names_policy, 0, offsetof(LinkConfig, alternative_names_policy)
+Link.Alias, config_parse_ifalias, 0, offsetof(LinkConfig, alias)
+Link.TransmitQueues, config_parse_rx_tx_queues, 0, offsetof(LinkConfig, txqueues)
+Link.ReceiveQueues, config_parse_rx_tx_queues, 0, offsetof(LinkConfig, rxqueues)
+Link.TransmitQueueLength, config_parse_txqueuelen, 0, offsetof(LinkConfig, txqueuelen)
+Link.MTUBytes, config_parse_mtu, AF_UNSPEC, offsetof(LinkConfig, mtu)
+Link.BitsPerSecond, config_parse_si_uint64, 0, offsetof(LinkConfig, speed)
+Link.Duplex, config_parse_duplex, 0, offsetof(LinkConfig, duplex)
+Link.AutoNegotiation, config_parse_tristate, 0, offsetof(LinkConfig, autonegotiation)
+Link.WakeOnLan, config_parse_wol, 0, offsetof(LinkConfig, wol)
+Link.WakeOnLanPassword, config_parse_wol_password, 0, 0
+Link.Port, config_parse_port, 0, offsetof(LinkConfig, port)
+Link.ReceiveChecksumOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_RXCSUM])
+Link.TransmitChecksumOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_TXCSUM])
+Link.GenericSegmentationOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_GSO])
+Link.TCPSegmentationOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_TSO])
+Link.TCP6SegmentationOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_TSO6])
+Link.UDPSegmentationOffload, config_parse_warn_compat, DISABLED_LEGACY, 0
+Link.GenericReceiveOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_GRO])
+Link.GenericReceiveOffloadHardware, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_GRO_HW])
+Link.LargeReceiveOffload, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_LRO])
+Link.ReceiveVLANCTAGHardwareAcceleration, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_HW_VLAN_CTAG_RX])
+Link.TransmitVLANCTAGHardwareAcceleration, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_HW_VLAN_CTAG_TX])
+Link.ReceiveVLANCTAGFilter, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_HW_VLAN_CTAG_FILTER])
+Link.TransmitVLANSTAGHardwareAcceleration, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_HW_VLAN_STAG_TX])
+Link.NTupleFilter, config_parse_tristate, 0, offsetof(LinkConfig, features[NET_DEV_FEAT_NTUPLE])
+Link.RxChannels, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, channels.rx)
+Link.TxChannels, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, channels.tx)
+Link.OtherChannels, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, channels.other)
+Link.CombinedChannels, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, channels.combined)
+Link.Advertise, config_parse_advertise, 0, offsetof(LinkConfig, advertise)
+Link.RxBufferSize, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, ring.rx)
+Link.RxMiniBufferSize, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, ring.rx_mini)
+Link.RxJumboBufferSize, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, ring.rx_jumbo)
+Link.TxBufferSize, config_parse_ring_buffer_or_channel, 0, offsetof(LinkConfig, ring.tx)
+Link.RxFlowControl, config_parse_tristate, 0, offsetof(LinkConfig, rx_flow_control)
+Link.TxFlowControl, config_parse_tristate, 0, offsetof(LinkConfig, tx_flow_control)
+Link.AutoNegotiationFlowControl, config_parse_tristate, 0, offsetof(LinkConfig, autoneg_flow_control)
+Link.GenericSegmentOffloadMaxBytes, config_parse_iec_size, 0, offsetof(LinkConfig, gso_max_size)
+Link.GenericSegmentOffloadMaxSegments, config_parse_uint32, 0, offsetof(LinkConfig, gso_max_segments)
+Link.RxCoalesceSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rx_coalesce_usecs)
+Link.RxMaxCoalescedFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.rx_max_coalesced_frames)
+Link.RxCoalesceIrqSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rx_coalesce_usecs_irq)
+Link.RxMaxCoalescedIrqFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.rx_max_coalesced_frames_irq)
+Link.TxCoalesceSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.tx_coalesce_usecs)
+Link.TxMaxCoalescedFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.tx_max_coalesced_frames)
+Link.TxCoalesceIrqSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.tx_coalesce_usecs_irq)
+Link.TxMaxCoalescedIrqFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.tx_max_coalesced_frames_irq)
+Link.StatisticsBlockCoalesceSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.stats_block_coalesce_usecs)
+Link.UseAdaptiveRxCoalesce, config_parse_tristate, 0, offsetof(LinkConfig, coalesce.use_adaptive_rx_coalesce)
+Link.UseAdaptiveTxCoalesce, config_parse_tristate, 0, offsetof(LinkConfig, coalesce.use_adaptive_tx_coalesce)
+Link.CoalescePacketRateLow, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.pkt_rate_low)
+Link.RxCoalesceLowSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rx_coalesce_usecs_low)
+Link.RxMaxCoalescedLowFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.rx_max_coalesced_frames_low)
+Link.TxCoalesceLowSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.tx_coalesce_usecs_low)
+Link.TxMaxCoalescedLowFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.tx_max_coalesced_frames_low)
+Link.CoalescePacketRateHigh, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.pkt_rate_high)
+Link.RxCoalesceHighSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rx_coalesce_usecs_high)
+Link.RxMaxCoalescedHighFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.rx_max_coalesced_frames_high)
+Link.TxCoalesceHighSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.tx_coalesce_usecs_high)
+Link.TxMaxCoalescedHighFrames, config_parse_coalesce_u32, 0, offsetof(LinkConfig, coalesce.tx_max_coalesced_frames_high)
+Link.CoalescePacketRateSampleIntervalSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rate_sample_interval)
+Link.MDI, config_parse_mdi, 0, offsetof(LinkConfig, mdi)
+Link.SR-IOVVirtualFunctions, config_parse_sr_iov_num_vfs, 0, offsetof(LinkConfig, sr_iov_num_vfs)
+SR-IOV.VirtualFunction, config_parse_sr_iov_uint32, 0, offsetof(LinkConfig, sr_iov_by_section)
+SR-IOV.VLANId, config_parse_sr_iov_uint32, 0, offsetof(LinkConfig, sr_iov_by_section)
+SR-IOV.QualityOfService, config_parse_sr_iov_uint32, 0, offsetof(LinkConfig, sr_iov_by_section)
+SR-IOV.VLANProtocol, config_parse_sr_iov_vlan_proto, 0, offsetof(LinkConfig, sr_iov_by_section)
+SR-IOV.MACSpoofCheck, config_parse_sr_iov_boolean, 0, offsetof(LinkConfig, sr_iov_by_section)
+SR-IOV.QueryReceiveSideScaling, config_parse_sr_iov_boolean, 0, offsetof(LinkConfig, sr_iov_by_section)
+SR-IOV.Trust, config_parse_sr_iov_boolean, 0, offsetof(LinkConfig, sr_iov_by_section)
+SR-IOV.LinkState, config_parse_sr_iov_link_state, 0, offsetof(LinkConfig, sr_iov_by_section)
+SR-IOV.MACAddress, config_parse_sr_iov_mac, 0, offsetof(LinkConfig, sr_iov_by_section)
diff --git a/src/udev/net/link-config.c b/src/udev/net/link-config.c
new file mode 100644
index 0000000..910ec27
--- /dev/null
+++ b/src/udev/net/link-config.c
@@ -0,0 +1,1133 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/netdevice.h>
+#include <netinet/ether.h>
+#include <unistd.h>
+
+#include "sd-device.h"
+#include "sd-netlink.h"
+
+#include "alloc-util.h"
+#include "arphrd-util.h"
+#include "conf-files.h"
+#include "conf-parser.h"
+#include "constants.h"
+#include "creds-util.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "ethtool-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "link-config.h"
+#include "log-link.h"
+#include "memory-util.h"
+#include "net-condition.h"
+#include "netif-sriov.h"
+#include "netif-util.h"
+#include "netlink-util.h"
+#include "parse-util.h"
+#include "path-lookup.h"
+#include "path-util.h"
+#include "proc-cmdline.h"
+#include "random-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "utf8.h"
+
+struct LinkConfigContext {
+ LIST_HEAD(LinkConfig, configs);
+ int ethtool_fd;
+ Hashmap *stats_by_path;
+};
+
+static LinkConfig* link_config_free(LinkConfig *config) {
+ if (!config)
+ return NULL;
+
+ free(config->filename);
+ strv_free(config->dropins);
+
+ net_match_clear(&config->match);
+ condition_free_list(config->conditions);
+
+ free(config->description);
+ free(config->name_policy);
+ free(config->name);
+ strv_free(config->alternative_names);
+ free(config->alternative_names_policy);
+ free(config->alias);
+ free(config->wol_password_file);
+ erase_and_free(config->wol_password);
+
+ ordered_hashmap_free_with_destructor(config->sr_iov_by_section, sr_iov_free);
+
+ return mfree(config);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(LinkConfig*, link_config_free);
+
+static void link_configs_free(LinkConfigContext *ctx) {
+ if (!ctx)
+ return;
+
+ ctx->stats_by_path = hashmap_free(ctx->stats_by_path);
+
+ LIST_FOREACH(configs, config, ctx->configs)
+ link_config_free(config);
+}
+
+LinkConfigContext *link_config_ctx_free(LinkConfigContext *ctx) {
+ if (!ctx)
+ return NULL;
+
+ safe_close(ctx->ethtool_fd);
+ link_configs_free(ctx);
+ return mfree(ctx);
+}
+
+int link_config_ctx_new(LinkConfigContext **ret) {
+ _cleanup_(link_config_ctx_freep) LinkConfigContext *ctx = NULL;
+
+ if (!ret)
+ return -EINVAL;
+
+ ctx = new(LinkConfigContext, 1);
+ if (!ctx)
+ return -ENOMEM;
+
+ *ctx = (LinkConfigContext) {
+ .ethtool_fd = -EBADF,
+ };
+
+ *ret = TAKE_PTR(ctx);
+
+ return 0;
+}
+
+static int link_parse_wol_password(LinkConfig *config, const char *str) {
+ _cleanup_(erase_and_freep) uint8_t *p = NULL;
+ int r;
+
+ assert(config);
+ assert(str);
+
+ assert_cc(sizeof(struct ether_addr) == SOPASS_MAX);
+
+ p = new(uint8_t, SOPASS_MAX);
+ if (!p)
+ return -ENOMEM;
+
+ /* Reuse parse_ether_addr(), as their formats are equivalent. */
+ r = parse_ether_addr(str, (struct ether_addr*) p);
+ if (r < 0)
+ return r;
+
+ erase_and_free(config->wol_password);
+ config->wol_password = TAKE_PTR(p);
+ return 0;
+}
+
+static int link_read_wol_password_from_file(LinkConfig *config) {
+ _cleanup_(erase_and_freep) char *password = NULL;
+ int r;
+
+ assert(config);
+
+ if (!config->wol_password_file)
+ return 0;
+
+ r = read_full_file_full(
+ AT_FDCWD, config->wol_password_file, UINT64_MAX, SIZE_MAX,
+ READ_FULL_FILE_SECURE | READ_FULL_FILE_WARN_WORLD_READABLE | READ_FULL_FILE_CONNECT_SOCKET,
+ NULL, &password, NULL);
+ if (r < 0)
+ return r;
+
+ return link_parse_wol_password(config, password);
+}
+
+static int link_read_wol_password_from_cred(LinkConfig *config) {
+ _cleanup_free_ char *base = NULL, *cred_name = NULL;
+ _cleanup_(erase_and_freep) char *password = NULL;
+ int r;
+
+ assert(config);
+ assert(config->filename);
+
+ if (config->wol == UINT32_MAX)
+ return 0; /* WakeOnLan= is not specified. */
+ if (!FLAGS_SET(config->wol, WAKE_MAGICSECURE))
+ return 0; /* secureon is not specified in WakeOnLan=. */
+ if (config->wol_password)
+ return 0; /* WakeOnLanPassword= is specified. */
+ if (config->wol_password_file)
+ return 0; /* a file name is specified in WakeOnLanPassword=, but failed to read it. */
+
+ r = path_extract_filename(config->filename, &base);
+ if (r < 0)
+ return r;
+
+ cred_name = strjoin(base, ".wol.password");
+ if (!cred_name)
+ return -ENOMEM;
+
+ r = read_credential(cred_name, (void**) &password, NULL);
+ if (r == -ENOENT)
+ r = read_credential("wol.password", (void**) &password, NULL);
+ if (r < 0)
+ return r;
+
+ return link_parse_wol_password(config, password);
+}
+
+static int link_adjust_wol_options(LinkConfig *config) {
+ int r;
+
+ assert(config);
+
+ r = link_read_wol_password_from_file(config);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ log_warning_errno(r, "Failed to read WakeOnLan password from %s, ignoring: %m", config->wol_password_file);
+
+ r = link_read_wol_password_from_cred(config);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ log_warning_errno(r, "Failed to read WakeOnLan password from credential, ignoring: %m");
+
+ if (config->wol != UINT32_MAX && config->wol_password)
+ /* Enable WAKE_MAGICSECURE flag when WakeOnLanPassword=. Note that when
+ * WakeOnLanPassword= is set without WakeOnLan=, then ethtool_set_wol() enables
+ * WAKE_MAGICSECURE flag and other flags are not changed. */
+ config->wol |= WAKE_MAGICSECURE;
+
+ return 0;
+}
+
+int link_load_one(LinkConfigContext *ctx, const char *filename) {
+ _cleanup_(link_config_freep) LinkConfig *config = NULL;
+ _cleanup_hashmap_free_ Hashmap *stats_by_path = NULL;
+ _cleanup_free_ char *name = NULL;
+ const char *dropin_dirname;
+ size_t i;
+ int r;
+
+ assert(ctx);
+ assert(filename);
+
+ r = null_or_empty_path(filename);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to check if \"%s\" is empty: %m", filename);
+ if (r > 0) {
+ log_debug("Skipping empty file: %s", filename);
+ return 0;
+ }
+
+ name = strdup(filename);
+ if (!name)
+ return log_oom();
+
+ config = new(LinkConfig, 1);
+ if (!config)
+ return log_oom();
+
+ *config = (LinkConfig) {
+ .filename = TAKE_PTR(name),
+ .mac_address_policy = MAC_ADDRESS_POLICY_NONE,
+ .wol = UINT32_MAX, /* UINT32_MAX means do not change WOL setting. */
+ .duplex = _DUP_INVALID,
+ .port = _NET_DEV_PORT_INVALID,
+ .autonegotiation = -1,
+ .rx_flow_control = -1,
+ .tx_flow_control = -1,
+ .autoneg_flow_control = -1,
+ .txqueuelen = UINT32_MAX,
+ .coalesce.use_adaptive_rx_coalesce = -1,
+ .coalesce.use_adaptive_tx_coalesce = -1,
+ .mdi = ETH_TP_MDI_INVALID,
+ .sr_iov_num_vfs = UINT32_MAX,
+ };
+
+ for (i = 0; i < ELEMENTSOF(config->features); i++)
+ config->features[i] = -1;
+
+ dropin_dirname = strjoina(basename(filename), ".d");
+ r = config_parse_many(
+ STRV_MAKE_CONST(filename),
+ NETWORK_DIRS,
+ dropin_dirname,
+ /* root = */ NULL,
+ "Match\0"
+ "Link\0"
+ "SR-IOV\0",
+ config_item_perf_lookup, link_config_gperf_lookup,
+ CONFIG_PARSE_WARN, config, &stats_by_path,
+ &config->dropins);
+ if (r < 0)
+ return r; /* config_parse_many() logs internally. */
+
+ if (ctx->stats_by_path) {
+ r = hashmap_move(ctx->stats_by_path, stats_by_path);
+ if (r < 0)
+ log_warning_errno(r, "Failed to save stats of '%s' and its drop-in configs, ignoring: %m", filename);
+ } else
+ ctx->stats_by_path = TAKE_PTR(stats_by_path);
+
+ if (net_match_is_empty(&config->match) && !config->conditions) {
+ log_warning("%s: No valid settings found in the [Match] section, ignoring file. "
+ "To match all interfaces, add OriginalName=* in the [Match] section.",
+ filename);
+ return 0;
+ }
+
+ if (!condition_test_list(config->conditions, environ, NULL, NULL, NULL)) {
+ log_debug("%s: Conditions do not match the system environment, skipping.", filename);
+ return 0;
+ }
+
+ if (IN_SET(config->mac_address_policy, MAC_ADDRESS_POLICY_PERSISTENT, MAC_ADDRESS_POLICY_RANDOM) &&
+ config->hw_addr.length > 0)
+ log_warning("%s: MACAddress= in [Link] section will be ignored when MACAddressPolicy= "
+ "is set to \"persistent\" or \"random\".",
+ filename);
+
+ r = link_adjust_wol_options(config);
+ if (r < 0)
+ return r; /* link_adjust_wol_options() logs internally. */
+
+ r = sr_iov_drop_invalid_sections(config->sr_iov_num_vfs, config->sr_iov_by_section);
+ if (r < 0)
+ return r; /* sr_iov_drop_invalid_sections() logs internally. */
+
+ log_debug("Parsed configuration file \"%s\"", filename);
+
+ LIST_PREPEND(configs, ctx->configs, TAKE_PTR(config));
+ return 0;
+}
+
+static int device_unsigned_attribute(sd_device *device, const char *attr, unsigned *type) {
+ const char *s;
+ int r;
+
+ r = sd_device_get_sysattr_value(device, attr, &s);
+ if (r < 0)
+ return log_device_debug_errno(device, r, "Failed to query %s: %m", attr);
+
+ r = safe_atou(s, type);
+ if (r < 0)
+ return log_device_warning_errno(device, r, "Failed to parse %s \"%s\": %m", attr, s);
+
+ log_device_debug(device, "Device has %s=%u", attr, *type);
+ return 0;
+}
+
+int link_config_load(LinkConfigContext *ctx) {
+ _cleanup_strv_free_ char **files = NULL;
+ int r;
+
+ assert(ctx);
+
+ link_configs_free(ctx);
+
+ r = conf_files_list_strv(&files, ".link", NULL, 0, NETWORK_DIRS);
+ if (r < 0)
+ return log_error_errno(r, "failed to enumerate link files: %m");
+
+ STRV_FOREACH_BACKWARDS(f, files)
+ (void) link_load_one(ctx, *f);
+
+ return 0;
+}
+
+bool link_config_should_reload(LinkConfigContext *ctx) {
+ _cleanup_hashmap_free_ Hashmap *stats_by_path = NULL;
+ int r;
+
+ assert(ctx);
+
+ r = config_get_stats_by_path(".link", NULL, 0, NETWORK_DIRS, /* check_dropins = */ true, &stats_by_path);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to get stats of .link files, ignoring: %m");
+ return true;
+ }
+
+ return !stats_by_path_equal(ctx->stats_by_path, stats_by_path);
+}
+
+Link *link_free(Link *link) {
+ if (!link)
+ return NULL;
+
+ sd_device_unref(link->device);
+ free(link->kind);
+ strv_free(link->altnames);
+ return mfree(link);
+}
+
+int link_new(LinkConfigContext *ctx, sd_netlink **rtnl, sd_device *device, Link **ret) {
+ _cleanup_(link_freep) Link *link = NULL;
+ int r;
+
+ assert(ctx);
+ assert(rtnl);
+ assert(device);
+ assert(ret);
+
+ link = new(Link, 1);
+ if (!link)
+ return -ENOMEM;
+
+ *link = (Link) {
+ .device = sd_device_ref(device),
+ };
+
+ r = sd_device_get_sysname(device, &link->ifname);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_ifindex(device, &link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_action(device, &link->action);
+ if (r < 0)
+ return r;
+
+ r = device_unsigned_attribute(device, "name_assign_type", &link->name_assign_type);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Failed to get \"name_assign_type\" attribute, ignoring: %m");
+
+ r = device_unsigned_attribute(device, "addr_assign_type", &link->addr_assign_type);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Failed to get \"addr_assign_type\" attribute, ignoring: %m");
+
+ r = rtnl_get_link_info(rtnl, link->ifindex, &link->iftype, &link->flags,
+ &link->kind, &link->hw_addr, &link->permanent_hw_addr);
+ if (r < 0)
+ return r;
+
+ if (link->hw_addr.length > 0 && link->permanent_hw_addr.length == 0) {
+ r = ethtool_get_permanent_hw_addr(&ctx->ethtool_fd, link->ifname, &link->permanent_hw_addr);
+ if (r < 0)
+ log_link_debug_errno(link, r, "Failed to get permanent hardware address, ignoring: %m");
+ }
+
+ r = sd_device_get_property_value(link->device, "ID_NET_DRIVER", &link->driver);
+ if (r < 0 && r != -ENOENT)
+ log_link_debug_errno(link, r, "Failed to get driver, ignoring: %m");
+
+ *ret = TAKE_PTR(link);
+ return 0;
+}
+
+int link_get_config(LinkConfigContext *ctx, Link *link) {
+ int r;
+
+ assert(ctx);
+ assert(link);
+
+ /* Do not configure loopback interfaces by .link files. */
+ if (link->flags & IFF_LOOPBACK)
+ return -ENOENT;
+
+ LIST_FOREACH(configs, config, ctx->configs) {
+ r = net_match_config(
+ &config->match,
+ link->device,
+ &link->hw_addr,
+ &link->permanent_hw_addr,
+ link->driver,
+ link->iftype,
+ link->kind,
+ link->ifname,
+ /* alternative_names = */ NULL,
+ /* wlan_iftype = */ 0,
+ /* ssid = */ NULL,
+ /* bssid = */ NULL);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ if (config->match.ifname && !strv_contains(config->match.ifname, "*") && link->name_assign_type == NET_NAME_ENUM)
+ log_link_warning(link, "Config file %s is applied to device based on potentially unpredictable interface name.",
+ config->filename);
+ else
+ log_link_debug(link, "Config file %s is applied", config->filename);
+
+ link->config = config;
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int link_apply_ethtool_settings(Link *link, int *ethtool_fd) {
+ LinkConfig *config;
+ const char *name;
+ int r;
+
+ assert(link);
+ assert(link->config);
+ assert(ethtool_fd);
+
+ config = link->config;
+ name = link->ifname;
+
+ r = ethtool_set_glinksettings(ethtool_fd, name,
+ config->autonegotiation, config->advertise,
+ config->speed, config->duplex, config->port, config->mdi);
+ if (r < 0) {
+ if (config->autonegotiation >= 0)
+ log_link_warning_errno(link, r, "Could not %s auto negotiation, ignoring: %m",
+ enable_disable(config->autonegotiation));
+
+ if (!eqzero(config->advertise))
+ log_link_warning_errno(link, r, "Could not set advertise mode, ignoring: %m");
+
+ if (config->speed > 0)
+ log_link_warning_errno(link, r, "Could not set speed to %"PRIu64"Mbps, ignoring: %m",
+ DIV_ROUND_UP(config->speed, 1000000));
+
+ if (config->duplex >= 0)
+ log_link_warning_errno(link, r, "Could not set duplex to %s, ignoring: %m",
+ duplex_to_string(config->duplex));
+
+ if (config->port >= 0)
+ log_link_warning_errno(link, r, "Could not set port to '%s', ignoring: %m",
+ port_to_string(config->port));
+
+ if (config->mdi != ETH_TP_MDI_INVALID)
+ log_link_warning_errno(link, r, "Could not set MDI-X to '%s', ignoring: %m",
+ mdi_to_string(config->mdi));
+ }
+
+ r = ethtool_set_wol(ethtool_fd, name, config->wol, config->wol_password);
+ if (r < 0) {
+ _cleanup_free_ char *str = NULL;
+
+ (void) wol_options_to_string_alloc(config->wol, &str);
+ log_link_warning_errno(link, r, "Could not set WakeOnLan%s%s, ignoring: %m",
+ isempty(str) ? "" : " to ", strempty(str));
+ }
+
+ r = ethtool_set_features(ethtool_fd, name, config->features);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not set offload features, ignoring: %m");
+
+ r = ethtool_set_channels(ethtool_fd, name, &config->channels);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not set channels, ignoring: %m");
+
+ r = ethtool_set_nic_buffer_size(ethtool_fd, name, &config->ring);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not set ring buffer, ignoring: %m");
+
+ r = ethtool_set_flow_control(ethtool_fd, name, config->rx_flow_control, config->tx_flow_control, config->autoneg_flow_control);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not set flow control, ignoring: %m");
+
+ r = ethtool_set_nic_coalesce_settings(ethtool_fd, name, &config->coalesce);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Could not set coalesce settings, ignoring: %m");
+
+ return 0;
+}
+
+static bool hw_addr_is_valid(Link *link, const struct hw_addr_data *hw_addr) {
+ assert(link);
+ assert(hw_addr);
+
+ switch (link->iftype) {
+ case ARPHRD_ETHER:
+ /* Refuse all zero and all 0xFF. */
+ assert(hw_addr->length == ETH_ALEN);
+ return !ether_addr_is_null(&hw_addr->ether) && !ether_addr_is_broadcast(&hw_addr->ether);
+
+ case ARPHRD_INFINIBAND:
+ /* The last 8 bytes cannot be zero. */
+ assert(hw_addr->length == INFINIBAND_ALEN);
+ return !memeqzero(hw_addr->bytes + INFINIBAND_ALEN - 8, 8);
+
+ default:
+ assert_not_reached();
+ }
+}
+
+static int link_generate_new_hw_addr(Link *link, struct hw_addr_data *ret) {
+ struct hw_addr_data hw_addr = HW_ADDR_NULL;
+ bool is_static = false;
+ uint8_t *p;
+ size_t len;
+ int r;
+
+ assert(link);
+ assert(link->config);
+ assert(link->device);
+ assert(ret);
+
+ if (link->hw_addr.length == 0)
+ goto finalize;
+
+ if (link->config->mac_address_policy == MAC_ADDRESS_POLICY_NONE) {
+ log_link_debug(link, "Using static MAC address.");
+ hw_addr = link->config->hw_addr;
+ is_static = true;
+ goto finalize;
+ }
+
+ if (!IN_SET(link->iftype, ARPHRD_ETHER, ARPHRD_INFINIBAND))
+ goto finalize;
+
+ switch (link->addr_assign_type) {
+ case NET_ADDR_SET:
+ log_link_debug(link, "MAC address on the device already set by userspace.");
+ goto finalize;
+ case NET_ADDR_STOLEN:
+ log_link_debug(link, "MAC address on the device already set based on another device.");
+ goto finalize;
+ case NET_ADDR_RANDOM:
+ case NET_ADDR_PERM:
+ break;
+ default:
+ log_link_warning(link, "Unknown addr_assign_type %u, ignoring", link->addr_assign_type);
+ goto finalize;
+ }
+
+ if ((link->config->mac_address_policy == MAC_ADDRESS_POLICY_RANDOM) == (link->addr_assign_type == NET_ADDR_RANDOM)) {
+ log_link_debug(link, "MAC address on the device already matches policy \"%s\".",
+ mac_address_policy_to_string(link->config->mac_address_policy));
+ goto finalize;
+ }
+
+ hw_addr = (struct hw_addr_data) {
+ .length = arphrd_to_hw_addr_len(link->iftype),
+ };
+
+ switch (link->iftype) {
+ case ARPHRD_ETHER:
+ p = hw_addr.bytes;
+ len = hw_addr.length;
+ break;
+ case ARPHRD_INFINIBAND:
+ p = hw_addr.bytes + INFINIBAND_ALEN - 8;
+ len = 8;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ if (link->config->mac_address_policy == MAC_ADDRESS_POLICY_RANDOM)
+ /* We require genuine randomness here, since we want to make sure we won't collide with other
+ * systems booting up at the very same time. */
+ for (;;) {
+ random_bytes(p, len);
+ if (hw_addr_is_valid(link, &hw_addr))
+ break;
+ }
+
+ else {
+ uint64_t result;
+
+ r = net_get_unique_predictable_data(link->device,
+ naming_scheme_has(NAMING_STABLE_VIRTUAL_MACS),
+ &result);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not generate persistent MAC address: %m");
+
+ assert(len <= sizeof(result));
+ memcpy(p, &result, len);
+ if (!hw_addr_is_valid(link, &hw_addr))
+ return log_link_warning_errno(link, SYNTHETIC_ERRNO(EINVAL),
+ "Could not generate valid persistent MAC address: %m");
+ }
+
+finalize:
+
+ r = net_verify_hardware_address(link->ifname, is_static, link->iftype, &link->hw_addr, &hw_addr);
+ if (r < 0)
+ return r;
+
+ if (hw_addr_equal(&link->hw_addr, &hw_addr)) {
+ *ret = HW_ADDR_NULL;
+ return 0;
+ }
+
+ if (hw_addr.length > 0)
+ log_link_debug(link, "Applying %s MAC address: %s",
+ link->config->mac_address_policy == MAC_ADDRESS_POLICY_NONE ? "static" :
+ mac_address_policy_to_string(link->config->mac_address_policy),
+ HW_ADDR_TO_STR(&hw_addr));
+
+ *ret = hw_addr;
+ return 0;
+}
+
+static int link_apply_rtnl_settings(Link *link, sd_netlink **rtnl) {
+ struct hw_addr_data hw_addr = {};
+ LinkConfig *config;
+ int r;
+
+ assert(link);
+ assert(link->config);
+ assert(rtnl);
+
+ config = link->config;
+
+ (void) link_generate_new_hw_addr(link, &hw_addr);
+
+ r = rtnl_set_link_properties(rtnl, link->ifindex, config->alias, &hw_addr,
+ config->txqueues, config->rxqueues, config->txqueuelen,
+ config->mtu, config->gso_max_size, config->gso_max_segments);
+ if (r < 0)
+ log_link_warning_errno(link, r,
+ "Could not set Alias=, MACAddress=/MACAddressPolicy=, "
+ "TransmitQueues=, ReceiveQueues=, TransmitQueueLength=, MTUBytes=, "
+ "GenericSegmentOffloadMaxBytes= or GenericSegmentOffloadMaxSegments=, "
+ "ignoring: %m");
+
+ return 0;
+}
+
+static bool enable_name_policy(void) {
+ static int cached = -1;
+ bool b;
+ int r;
+
+ if (cached >= 0)
+ return cached;
+
+ r = proc_cmdline_get_bool("net.ifnames", /* flags = */ 0, &b);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse net.ifnames= kernel command line option, ignoring: %m");
+ if (r <= 0)
+ return (cached = true);
+
+ if (!b)
+ log_info("Network interface NamePolicy= disabled on kernel command line.");
+
+ return (cached = b);
+}
+
+static int link_generate_new_name(Link *link) {
+ LinkConfig *config;
+ sd_device *device;
+
+ assert(link);
+ assert(link->config);
+ assert(link->device);
+
+ config = link->config;
+ device = link->device;
+
+ if (link->action != SD_DEVICE_ADD) {
+ log_link_debug(link, "Skipping to apply Name= and NamePolicy= on '%s' uevent.",
+ device_action_to_string(link->action));
+ goto no_rename;
+ }
+
+ if (IN_SET(link->name_assign_type, NET_NAME_USER, NET_NAME_RENAMED) &&
+ !naming_scheme_has(NAMING_ALLOW_RERENAMES)) {
+ log_link_debug(link, "Device already has a name given by userspace, not renaming.");
+ goto no_rename;
+ }
+
+ if (enable_name_policy() && config->name_policy)
+ for (NamePolicy *policy = config->name_policy; *policy != _NAMEPOLICY_INVALID; policy++) {
+ const char *new_name = NULL;
+
+ switch (*policy) {
+ case NAMEPOLICY_KERNEL:
+ if (link->name_assign_type != NET_NAME_PREDICTABLE)
+ continue;
+
+ /* The kernel claims to have given a predictable name, keep it. */
+ log_link_debug(link, "Policy *%s*: keeping predictable kernel name",
+ name_policy_to_string(*policy));
+ goto no_rename;
+ case NAMEPOLICY_KEEP:
+ if (!IN_SET(link->name_assign_type, NET_NAME_USER, NET_NAME_RENAMED))
+ continue;
+
+ log_link_debug(link, "Policy *%s*: keeping existing userspace name",
+ name_policy_to_string(*policy));
+ goto no_rename;
+ case NAMEPOLICY_DATABASE:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_FROM_DATABASE", &new_name);
+ break;
+ case NAMEPOLICY_ONBOARD:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_ONBOARD", &new_name);
+ break;
+ case NAMEPOLICY_SLOT:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_SLOT", &new_name);
+ break;
+ case NAMEPOLICY_PATH:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_PATH", &new_name);
+ break;
+ case NAMEPOLICY_MAC:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_MAC", &new_name);
+ break;
+ default:
+ assert_not_reached();
+ }
+ if (ifname_valid(new_name)) {
+ log_link_debug(link, "Policy *%s* yields \"%s\".", name_policy_to_string(*policy), new_name);
+ link->new_name = new_name;
+ return 0;
+ }
+ }
+
+ if (link->config->name) {
+ log_link_debug(link, "Policies didn't yield a name, using specified Name=%s.", link->config->name);
+ link->new_name = link->config->name;
+ return 0;
+ }
+
+ log_link_debug(link, "Policies didn't yield a name and Name= is not given, not renaming.");
+no_rename:
+ link->new_name = link->ifname;
+ return 0;
+}
+
+static int link_generate_alternative_names(Link *link) {
+ _cleanup_strv_free_ char **altnames = NULL;
+ LinkConfig *config;
+ sd_device *device;
+ int r;
+
+ assert(link);
+ config = ASSERT_PTR(link->config);
+ device = ASSERT_PTR(link->device);
+ assert(!link->altnames);
+
+ if (link->action != SD_DEVICE_ADD) {
+ log_link_debug(link, "Skipping to apply AlternativeNames= and AlternativeNamesPolicy= on '%s' uevent.",
+ device_action_to_string(link->action));
+ return 0;
+ }
+
+ if (config->alternative_names) {
+ altnames = strv_copy(config->alternative_names);
+ if (!altnames)
+ return log_oom();
+ }
+
+ if (config->alternative_names_policy)
+ for (NamePolicy *p = config->alternative_names_policy; *p != _NAMEPOLICY_INVALID; p++) {
+ const char *n = NULL;
+
+ switch (*p) {
+ case NAMEPOLICY_DATABASE:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_FROM_DATABASE", &n);
+ break;
+ case NAMEPOLICY_ONBOARD:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_ONBOARD", &n);
+ break;
+ case NAMEPOLICY_SLOT:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_SLOT", &n);
+ break;
+ case NAMEPOLICY_PATH:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_PATH", &n);
+ break;
+ case NAMEPOLICY_MAC:
+ (void) sd_device_get_property_value(device, "ID_NET_NAME_MAC", &n);
+ break;
+ default:
+ assert_not_reached();
+ }
+ if (ifname_valid_full(n, IFNAME_VALID_ALTERNATIVE)) {
+ r = strv_extend(&altnames, n);
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ link->altnames = TAKE_PTR(altnames);
+ return 0;
+}
+
+static int sr_iov_configure(Link *link, sd_netlink **rtnl, SRIOV *sr_iov) {
+ _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL;
+ int r;
+
+ assert(link);
+ assert(rtnl);
+ assert(link->ifindex > 0);
+
+ if (!*rtnl) {
+ r = sd_netlink_open(rtnl);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_rtnl_message_new_link(*rtnl, &req, RTM_SETLINK, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sr_iov_set_netlink_message(sr_iov, req);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(*rtnl, req, 0, NULL);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int link_apply_sr_iov_config(Link *link, sd_netlink **rtnl) {
+ SRIOV *sr_iov;
+ uint32_t n;
+ int r;
+
+ assert(link);
+ assert(link->config);
+ assert(link->device);
+
+ r = sr_iov_set_num_vfs(link->device, link->config->sr_iov_num_vfs, link->config->sr_iov_by_section);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to set the number of SR-IOV virtual functions, ignoring: %m");
+
+ if (ordered_hashmap_isempty(link->config->sr_iov_by_section))
+ return 0;
+
+ r = sr_iov_get_num_vfs(link->device, &n);
+ if (r < 0) {
+ log_link_warning_errno(link, r, "Failed to get the number of SR-IOV virtual functions, ignoring [SR-IOV] sections: %m");
+ return 0;
+ }
+ if (n == 0) {
+ log_link_warning(link, "No SR-IOV virtual function exists, ignoring [SR-IOV] sections: %m");
+ return 0;
+ }
+
+ ORDERED_HASHMAP_FOREACH(sr_iov, link->config->sr_iov_by_section) {
+ if (sr_iov->vf >= n) {
+ log_link_warning(link, "SR-IOV virtual function %"PRIu32" does not exist, ignoring.", sr_iov->vf);
+ continue;
+ }
+
+ r = sr_iov_configure(link, rtnl, sr_iov);
+ if (r < 0)
+ log_link_warning_errno(link, r,
+ "Failed to configure SR-IOV virtual function %"PRIu32", ignoring: %m",
+ sr_iov->vf);
+ }
+
+ return 0;
+}
+
+int link_apply_config(LinkConfigContext *ctx, sd_netlink **rtnl, Link *link) {
+ int r;
+
+ assert(ctx);
+ assert(rtnl);
+ assert(link);
+
+ if (!IN_SET(link->action, SD_DEVICE_ADD, SD_DEVICE_BIND, SD_DEVICE_MOVE)) {
+ log_link_debug(link, "Skipping to apply .link settings on '%s' uevent.",
+ device_action_to_string(link->action));
+
+ link->new_name = link->ifname;
+ return 0;
+ }
+
+ r = link_apply_ethtool_settings(link, &ctx->ethtool_fd);
+ if (r < 0)
+ return r;
+
+ r = link_apply_rtnl_settings(link, rtnl);
+ if (r < 0)
+ return r;
+
+ r = link_generate_new_name(link);
+ if (r < 0)
+ return r;
+
+ r = link_generate_alternative_names(link);
+ if (r < 0)
+ return r;
+
+ r = link_apply_sr_iov_config(link, rtnl);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int config_parse_ifalias(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ char **s = ASSERT_PTR(data);
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ *s = mfree(*s);
+ return 0;
+ }
+
+ if (!ascii_is_valid(rvalue)) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Interface alias is not ASCII clean, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ if (strlen(rvalue) >= IFALIASZ) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0,
+ "Interface alias is too long, ignoring assignment: %s", rvalue);
+ return 0;
+ }
+
+ return free_and_strdup_warn(s, rvalue);
+}
+
+int config_parse_rx_tx_queues(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint32_t k, *v = data;
+ int r;
+
+ if (isempty(rvalue)) {
+ *v = 0;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse %s=, ignoring assignment: %s.", lvalue, rvalue);
+ return 0;
+ }
+ if (k == 0 || k > 4096) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid %s=, ignoring assignment: %s.", lvalue, rvalue);
+ return 0;
+ }
+
+ *v = k;
+ return 0;
+}
+
+int config_parse_txqueuelen(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ uint32_t k, *v = data;
+ int r;
+
+ if (isempty(rvalue)) {
+ *v = UINT32_MAX;
+ return 0;
+ }
+
+ r = safe_atou32(rvalue, &k);
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse %s=, ignoring assignment: %s.", lvalue, rvalue);
+ return 0;
+ }
+ if (k == UINT32_MAX) {
+ log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid %s=, ignoring assignment: %s.", lvalue, rvalue);
+ return 0;
+ }
+
+ *v = k;
+ return 0;
+}
+
+int config_parse_wol_password(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) {
+
+ LinkConfig *config = ASSERT_PTR(userdata);
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+
+ if (isempty(rvalue)) {
+ config->wol_password = erase_and_free(config->wol_password);
+ config->wol_password_file = mfree(config->wol_password_file);
+ return 0;
+ }
+
+ if (path_is_absolute(rvalue) && path_is_safe(rvalue)) {
+ config->wol_password = erase_and_free(config->wol_password);
+ return free_and_strdup_warn(&config->wol_password_file, rvalue);
+ }
+
+ warn_file_is_world_accessible(filename, NULL, unit, line);
+
+ r = link_parse_wol_password(config, rvalue);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_syntax(unit, LOG_WARNING, filename, line, r,
+ "Failed to parse %s=, ignoring assignment: %s.", lvalue, rvalue);
+ return 0;
+ }
+
+ config->wol_password_file = mfree(config->wol_password_file);
+ return 0;
+}
+
+static const char* const mac_address_policy_table[_MAC_ADDRESS_POLICY_MAX] = {
+ [MAC_ADDRESS_POLICY_PERSISTENT] = "persistent",
+ [MAC_ADDRESS_POLICY_RANDOM] = "random",
+ [MAC_ADDRESS_POLICY_NONE] = "none",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(mac_address_policy, MACAddressPolicy);
+DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(
+ config_parse_mac_address_policy,
+ mac_address_policy,
+ MACAddressPolicy,
+ MAC_ADDRESS_POLICY_NONE,
+ "Failed to parse MAC address policy");
+
+DEFINE_CONFIG_PARSE_ENUMV(config_parse_name_policy, name_policy, NamePolicy,
+ _NAMEPOLICY_INVALID,
+ "Failed to parse interface name policy");
+
+DEFINE_CONFIG_PARSE_ENUMV(config_parse_alternative_names_policy, alternative_names_policy, NamePolicy,
+ _NAMEPOLICY_INVALID,
+ "Failed to parse alternative names policy");
diff --git a/src/udev/net/link-config.h b/src/udev/net/link-config.h
new file mode 100644
index 0000000..bab9d12
--- /dev/null
+++ b/src/udev/net/link-config.h
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-device.h"
+#include "sd-netlink.h"
+
+#include "condition.h"
+#include "conf-parser.h"
+#include "ethtool-util.h"
+#include "hashmap.h"
+#include "list.h"
+#include "net-condition.h"
+#include "netif-naming-scheme.h"
+
+typedef struct LinkConfigContext LinkConfigContext;
+typedef struct LinkConfig LinkConfig;
+
+typedef enum MACAddressPolicy {
+ MAC_ADDRESS_POLICY_PERSISTENT,
+ MAC_ADDRESS_POLICY_RANDOM,
+ MAC_ADDRESS_POLICY_NONE,
+ _MAC_ADDRESS_POLICY_MAX,
+ _MAC_ADDRESS_POLICY_INVALID = -EINVAL,
+} MACAddressPolicy;
+
+typedef struct Link {
+ int ifindex;
+ const char *ifname;
+ const char *new_name;
+ char **altnames;
+
+ LinkConfig *config;
+ sd_device *device;
+ sd_device_action_t action;
+
+ char *kind;
+ const char *driver;
+ uint16_t iftype;
+ uint32_t flags;
+ struct hw_addr_data hw_addr;
+ struct hw_addr_data permanent_hw_addr;
+ unsigned name_assign_type;
+ unsigned addr_assign_type;
+} Link;
+
+struct LinkConfig {
+ char *filename;
+ char **dropins;
+
+ NetMatch match;
+ LIST_HEAD(Condition, conditions);
+
+ char *description;
+ struct hw_addr_data hw_addr;
+ MACAddressPolicy mac_address_policy;
+ NamePolicy *name_policy;
+ NamePolicy *alternative_names_policy;
+ char *name;
+ char **alternative_names;
+ char *alias;
+ uint32_t txqueues;
+ uint32_t rxqueues;
+ uint32_t txqueuelen;
+ uint32_t mtu;
+ uint32_t gso_max_segments;
+ size_t gso_max_size;
+ uint64_t speed;
+ Duplex duplex;
+ int autonegotiation;
+ uint32_t advertise[N_ADVERTISE];
+ uint32_t wol;
+ char *wol_password_file;
+ uint8_t *wol_password;
+ NetDevPort port;
+ int features[_NET_DEV_FEAT_MAX];
+ netdev_channels channels;
+ netdev_ring_param ring;
+ int rx_flow_control;
+ int tx_flow_control;
+ int autoneg_flow_control;
+ netdev_coalesce_param coalesce;
+ uint8_t mdi;
+
+ uint32_t sr_iov_num_vfs;
+ OrderedHashmap *sr_iov_by_section;
+
+ LIST_FIELDS(LinkConfig, configs);
+};
+
+int link_config_ctx_new(LinkConfigContext **ret);
+LinkConfigContext* link_config_ctx_free(LinkConfigContext *ctx);
+DEFINE_TRIVIAL_CLEANUP_FUNC(LinkConfigContext*, link_config_ctx_free);
+
+int link_load_one(LinkConfigContext *ctx, const char *filename);
+int link_config_load(LinkConfigContext *ctx);
+bool link_config_should_reload(LinkConfigContext *ctx);
+
+int link_new(LinkConfigContext *ctx, sd_netlink **rtnl, sd_device *device, Link **ret);
+Link *link_free(Link *link);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free);
+
+int link_get_config(LinkConfigContext *ctx, Link *link);
+int link_apply_config(LinkConfigContext *ctx, sd_netlink **rtnl, Link *link);
+
+const char *mac_address_policy_to_string(MACAddressPolicy p) _const_;
+MACAddressPolicy mac_address_policy_from_string(const char *p) _pure_;
+
+/* gperf lookup function */
+const struct ConfigPerfItem* link_config_gperf_lookup(const char *key, GPERF_LEN_TYPE length);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_ifalias);
+CONFIG_PARSER_PROTOTYPE(config_parse_rx_tx_queues);
+CONFIG_PARSER_PROTOTYPE(config_parse_txqueuelen);
+CONFIG_PARSER_PROTOTYPE(config_parse_wol_password);
+CONFIG_PARSER_PROTOTYPE(config_parse_mac_address_policy);
+CONFIG_PARSER_PROTOTYPE(config_parse_name_policy);
+CONFIG_PARSER_PROTOTYPE(config_parse_alternative_names_policy);
diff --git a/src/udev/net/test-link-config-tables.c b/src/udev/net/test-link-config-tables.c
new file mode 100644
index 0000000..a433232
--- /dev/null
+++ b/src/udev/net/test-link-config-tables.c
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "link-config.h"
+#include "test-tables.h"
+#include "tests.h"
+
+int main(int argc, char **argv) {
+ test_setup_logging(LOG_DEBUG);
+
+ test_table(mac_address_policy, MAC_ADDRESS_POLICY);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/udev/scsi_id/README b/src/udev/scsi_id/README
new file mode 100644
index 0000000..9cfe739
--- /dev/null
+++ b/src/udev/scsi_id/README
@@ -0,0 +1,4 @@
+scsi_id - generate a SCSI unique identifier for a given SCSI device
+
+Please send questions, comments or patches to <patmans@us.ibm.com> or
+<linux-hotplug-devel@lists.sourceforge.net>.
diff --git a/src/udev/scsi_id/scsi.h b/src/udev/scsi_id/scsi.h
new file mode 100644
index 0000000..ee3e401
--- /dev/null
+++ b/src/udev/scsi_id/scsi.h
@@ -0,0 +1,100 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+/*
+ * scsi.h
+ *
+ * General scsi and linux scsi specific defines and structs.
+ *
+ * Copyright (C) IBM Corp. 2003
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation version 2 of the License.
+ */
+
+#include <scsi/scsi.h>
+
+struct scsi_ioctl_command {
+ unsigned inlen; /* excluding scsi command length */
+ unsigned outlen;
+ unsigned char data[1];
+ /* on input, scsi command starts here then opt. data */
+};
+
+/*
+ * Default 5 second timeout
+ */
+#define DEF_TIMEOUT 5000
+
+#define SENSE_BUFF_LEN 32
+
+/*
+ * The request buffer size passed to the SCSI INQUIRY commands, use 254,
+ * as this is a nice value for some devices, especially some of the usb
+ * mass storage devices.
+ */
+#define SCSI_INQ_BUFF_LEN 254
+
+/*
+ * SCSI INQUIRY vendor and model (really product) lengths.
+ */
+#define VENDOR_LENGTH 8
+#define MODEL_LENGTH 16
+
+#define INQUIRY_CMD 0x12
+#define INQUIRY_CMDLEN 6
+
+/*
+ * INQUIRY VPD page 0x83 identifier descriptor related values. Reference the
+ * SCSI Primary Commands specification for details.
+ */
+
+/*
+ * id type values of id descriptors. These are assumed to fit in 4 bits.
+ */
+#define SCSI_ID_VENDOR_SPECIFIC 0
+#define SCSI_ID_T10_VENDOR 1
+#define SCSI_ID_EUI_64 2
+#define SCSI_ID_NAA 3
+#define SCSI_ID_RELPORT 4
+#define SCSI_ID_TGTGROUP 5
+#define SCSI_ID_LUNGROUP 6
+#define SCSI_ID_MD5 7
+#define SCSI_ID_NAME 8
+
+/*
+ * Supported NAA values. These fit in 4 bits, so the "don't care" value
+ * cannot conflict with real values.
+ */
+#define SCSI_ID_NAA_DONT_CARE 0xff
+#define SCSI_ID_NAA_IEEE_REG 0x05
+#define SCSI_ID_NAA_IEEE_REG_EXTENDED 0x06
+
+/*
+ * Supported Code Set values.
+ */
+#define SCSI_ID_BINARY 1
+#define SCSI_ID_ASCII 2
+
+struct scsi_id_search_values {
+ u_char id_type;
+ u_char naa_type;
+ u_char code_set;
+};
+
+/*
+ * Following are the "true" SCSI status codes. Linux has traditionally
+ * used a 1 bit right and masked version of these. So now CHECK_CONDITION
+ * and friends (in <scsi/scsi.h>) are deprecated.
+ */
+#define SCSI_CHECK_CONDITION 0x02
+#define SCSI_CONDITION_MET 0x04
+#define SCSI_BUSY 0x08
+#define SCSI_IMMEDIATE 0x10
+#define SCSI_IMMEDIATE_CONDITION_MET 0x14
+#define SCSI_RESERVATION_CONFLICT 0x18
+#define SCSI_COMMAND_TERMINATED 0x22
+#define SCSI_TASK_SET_FULL 0x28
+#define SCSI_ACA_ACTIVE 0x30
+#define SCSI_TASK_ABORTED 0x40
diff --git a/src/udev/scsi_id/scsi_id.c b/src/udev/scsi_id/scsi_id.c
new file mode 100644
index 0000000..6308c52
--- /dev/null
+++ b/src/udev/scsi_id/scsi_id.c
@@ -0,0 +1,515 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © IBM Corp. 2003
+ * Copyright © SUSE Linux Products GmbH, 2006
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "build.h"
+#include "device-nodes.h"
+#include "extract-word.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "parse-util.h"
+#include "scsi_id.h"
+#include "string-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "udev-util.h"
+
+static const struct option options[] = {
+ { "device", required_argument, NULL, 'd' },
+ { "config", required_argument, NULL, 'f' },
+ { "page", required_argument, NULL, 'p' },
+ { "denylisted", no_argument, NULL, 'b' },
+ { "allowlisted", no_argument, NULL, 'g' },
+ { "blacklisted", no_argument, NULL, 'b' }, /* backward compat */
+ { "whitelisted", no_argument, NULL, 'g' }, /* backward compat */
+ { "replace-whitespace", no_argument, NULL, 'u' },
+ { "sg-version", required_argument, NULL, 's' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "version", no_argument, NULL, 'V' }, /* don't advertise -V */
+ { "export", no_argument, NULL, 'x' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+};
+
+static bool all_good = false;
+static bool dev_specified = false;
+static char config_file[MAX_PATH_LEN] = "/etc/scsi_id.config";
+static enum page_code default_page_code = PAGE_UNSPECIFIED;
+static int sg_version = 4;
+static bool reformat_serial = false;
+static bool export = false;
+static char vendor_str[64];
+static char model_str[64];
+static char vendor_enc_str[256];
+static char model_enc_str[256];
+static char revision_str[16];
+static char type_str[16];
+
+static void set_type(unsigned type_num, char *to, size_t len) {
+ const char *type;
+
+ switch (type_num) {
+ case 0:
+ type = "disk";
+ break;
+ case 1:
+ type = "tape";
+ break;
+ case 4:
+ type = "optical";
+ break;
+ case 5:
+ type = "cd";
+ break;
+ case 7:
+ type = "optical";
+ break;
+ case 0xe:
+ type = "disk";
+ break;
+ case 0xf:
+ type = "optical";
+ break;
+ default:
+ type = "generic";
+ break;
+ }
+ strscpy(to, len, type);
+}
+
+/*
+ * get_file_options:
+ *
+ * If vendor == NULL, find a line in the config file with only "OPTIONS=";
+ * if vendor and model are set find the first OPTIONS line in the config
+ * file that matches. Set argc and argv to match the OPTIONS string.
+ *
+ * vendor and model can end in '\n'.
+ */
+static int get_file_options(const char *vendor, const char *model,
+ int *argc, char ***newargv) {
+ _cleanup_free_ char *vendor_in = NULL, *model_in = NULL, *options_in = NULL; /* read in from file */
+ _cleanup_strv_free_ char **options_argv = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int lineno, r;
+
+ f = fopen(config_file, "re");
+ if (!f) {
+ if (errno == ENOENT)
+ return 1;
+ else {
+ log_error_errno(errno, "can't open %s: %m", config_file);
+ return -1;
+ }
+ }
+
+ *newargv = NULL;
+ lineno = 0;
+ for (;;) {
+ _cleanup_free_ char *buffer = NULL, *key = NULL, *value = NULL;
+ const char *buf;
+
+ vendor_in = model_in = options_in = NULL;
+
+ r = read_line(f, MAX_BUFFER_LEN, &buffer);
+ if (r < 0)
+ return log_error_errno(r, "read_line() on line %d of %s failed: %m", lineno, config_file);
+ if (r == 0)
+ break;
+ buf = buffer;
+ lineno++;
+
+ while (isspace(*buf))
+ buf++;
+
+ /* blank or all whitespace line */
+ if (*buf == '\0')
+ continue;
+
+ /* comment line */
+ if (*buf == '#')
+ continue;
+
+ r = extract_many_words(&buf, "=\",\n", 0, &key, &value, NULL);
+ if (r < 2)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error parsing config file line %d '%s'", lineno, buffer);
+
+ if (strcaseeq(key, "VENDOR")) {
+ vendor_in = TAKE_PTR(value);
+
+ key = mfree(key);
+ r = extract_many_words(&buf, "=\",\n", 0, &key, &value, NULL);
+ if (r < 2)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error parsing config file line %d '%s'", lineno, buffer);
+
+ if (strcaseeq(key, "MODEL")) {
+ model_in = TAKE_PTR(value);
+
+ key = mfree(key);
+ r = extract_many_words(&buf, "=\",\n", 0, &key, &value, NULL);
+ if (r < 2)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error parsing config file line %d '%s'", lineno, buffer);
+ }
+ }
+
+ if (strcaseeq(key, "OPTIONS"))
+ options_in = TAKE_PTR(value);
+
+ /*
+ * Only allow: [vendor=foo[,model=bar]]options=stuff
+ */
+ if (!options_in || (!vendor_in && model_in))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Error parsing config file line %d '%s'", lineno, buffer);
+ if (!vendor) {
+ if (!vendor_in)
+ break;
+ } else if (vendor_in &&
+ startswith(vendor, vendor_in) &&
+ (!model_in || startswith(model, model_in))) {
+ /*
+ * Matched vendor and optionally model.
+ *
+ * Note: a short vendor_in or model_in can
+ * give a partial match (that is FOO
+ * matches FOOBAR).
+ */
+ break;
+ }
+
+ vendor_in = mfree(vendor_in);
+ model_in = mfree(model_in);
+ options_in = mfree(options_in);
+
+ }
+
+ if (vendor_in == NULL && model_in == NULL && options_in == NULL)
+ return 1; /* No matches */
+
+ /*
+ * Something matched. Allocate newargv, and store
+ * values found in options_in.
+ */
+ options_argv = strv_split(options_in, " \t");
+ if (!options_argv)
+ return log_oom();
+ r = strv_prepend(&options_argv, ""); /* getopt skips over argv[0] */
+ if (r < 0)
+ return r;
+ *newargv = TAKE_PTR(options_argv);
+ *argc = strv_length(*newargv);
+
+ return 0;
+}
+
+static void help(void) {
+ printf("Usage: %s [OPTION...] DEVICE\n\n"
+ "SCSI device identification.\n\n"
+ " -h --help Print this message\n"
+ " --version Print version of the program\n\n"
+ " -d --device= Device node for SG_IO commands\n"
+ " -f --config= Location of config file\n"
+ " -p --page=0x80|0x83|pre-spc3-83 SCSI page (0x80, 0x83, pre-spc3-83)\n"
+ " -s --sg-version=3|4 Use SGv3 or SGv4\n"
+ " -b --denylisted Treat device as denylisted\n"
+ " -g --allowlisted Treat device as allowlisted\n"
+ " -u --replace-whitespace Replace all whitespace by underscores\n"
+ " -v --verbose Verbose logging\n"
+ " -x --export Print values as environment keys\n",
+ program_invocation_short_name);
+}
+
+static int set_options(int argc, char **argv,
+ char *maj_min_dev) {
+ int option;
+
+ /*
+ * optind is a global extern used by getopt. Since we can call
+ * set_options twice (once for command line, and once for config
+ * file) we have to reset this back to 1.
+ */
+ optind = 1;
+ while ((option = getopt_long(argc, argv, "d:f:gp:uvVxhbs:", options, NULL)) >= 0)
+ switch (option) {
+ case 'b':
+ all_good = false;
+ break;
+
+ case 'd':
+ dev_specified = true;
+ strscpy(maj_min_dev, MAX_PATH_LEN, optarg);
+ break;
+
+ case 'f':
+ strscpy(config_file, MAX_PATH_LEN, optarg);
+ break;
+
+ case 'g':
+ all_good = true;
+ break;
+
+ case 'h':
+ help();
+ exit(EXIT_SUCCESS);
+
+ case 'p':
+ if (streq(optarg, "0x80"))
+ default_page_code = PAGE_80;
+ else if (streq(optarg, "0x83"))
+ default_page_code = PAGE_83;
+ else if (streq(optarg, "pre-spc3-83"))
+ default_page_code = PAGE_83_PRE_SPC3;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown page code '%s'",
+ optarg);
+ break;
+
+ case 's':
+ sg_version = atoi(optarg);
+ if (sg_version < 3 || sg_version > 4)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown SG version '%s'",
+ optarg);
+ break;
+
+ case 'u':
+ reformat_serial = true;
+ break;
+
+ case 'v':
+ log_set_target(LOG_TARGET_CONSOLE);
+ log_set_max_level(LOG_DEBUG);
+ log_open();
+ break;
+
+ case 'V':
+ version();
+ exit(EXIT_SUCCESS);
+
+ case 'x':
+ export = true;
+ break;
+
+ case '?':
+ return -1;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (optind < argc && !dev_specified) {
+ dev_specified = true;
+ strscpy(maj_min_dev, MAX_PATH_LEN, argv[optind]);
+ }
+
+ return 0;
+}
+
+static int per_dev_options(struct scsi_id_device *dev_scsi, int *good_bad, int *page_code) {
+ _cleanup_strv_free_ char **newargv = NULL;
+ int retval;
+ int newargc;
+ int option;
+
+ *good_bad = all_good;
+ *page_code = default_page_code;
+
+ retval = get_file_options(vendor_str, model_str, &newargc, &newargv);
+
+ optind = 1; /* reset this global extern */
+ while (retval == 0) {
+ option = getopt_long(newargc, newargv, "bgp:", options, NULL);
+ if (option == -1)
+ break;
+
+ switch (option) {
+ case 'b':
+ *good_bad = 0;
+ break;
+
+ case 'g':
+ *good_bad = 1;
+ break;
+
+ case 'p':
+ if (streq(optarg, "0x80")) {
+ *page_code = PAGE_80;
+ } else if (streq(optarg, "0x83")) {
+ *page_code = PAGE_83;
+ } else if (streq(optarg, "pre-spc3-83")) {
+ *page_code = PAGE_83_PRE_SPC3;
+ } else {
+ log_error("Unknown page code '%s'", optarg);
+ retval = -1;
+ }
+ break;
+
+ default:
+ log_error("Unknown or bad option '%c' (0x%x)", option, (unsigned) option);
+ retval = -1;
+ break;
+ }
+ }
+
+ return retval;
+}
+
+static int set_inq_values(struct scsi_id_device *dev_scsi, const char *path) {
+ int retval;
+
+ dev_scsi->use_sg = sg_version;
+
+ retval = scsi_std_inquiry(dev_scsi, path);
+ if (retval)
+ return retval;
+
+ encode_devnode_name(dev_scsi->vendor, vendor_enc_str, sizeof(vendor_enc_str));
+ encode_devnode_name(dev_scsi->model, model_enc_str, sizeof(model_enc_str));
+
+ udev_replace_whitespace(dev_scsi->vendor, vendor_str, sizeof(vendor_str)-1);
+ udev_replace_chars(vendor_str, NULL);
+ udev_replace_whitespace(dev_scsi->model, model_str, sizeof(model_str)-1);
+ udev_replace_chars(model_str, NULL);
+ set_type(dev_scsi->type, type_str, sizeof(type_str));
+ udev_replace_whitespace(dev_scsi->revision, revision_str, sizeof(revision_str)-1);
+ udev_replace_chars(revision_str, NULL);
+ return 0;
+}
+
+/*
+ * scsi_id: try to get an id, if one is found, printf it to stdout.
+ * returns a value passed to exit() - 0 if printed an id, else 1.
+ */
+static int scsi_id(char *maj_min_dev) {
+ struct scsi_id_device dev_scsi = {};
+ int good_dev;
+ int page_code;
+ int retval = 0;
+
+ if (set_inq_values(&dev_scsi, maj_min_dev) < 0) {
+ retval = 1;
+ goto out;
+ }
+
+ /* get per device (vendor + model) options from the config file */
+ per_dev_options(&dev_scsi, &good_dev, &page_code);
+ if (!good_dev) {
+ retval = 1;
+ goto out;
+ }
+
+ /* read serial number from mode pages (no values for optical drives) */
+ scsi_get_serial(&dev_scsi, maj_min_dev, page_code, MAX_SERIAL_LEN);
+
+ if (export) {
+ char serial_str[MAX_SERIAL_LEN];
+
+ printf("ID_SCSI=1\n");
+ printf("ID_VENDOR=%s\n", vendor_str);
+ printf("ID_VENDOR_ENC=%s\n", vendor_enc_str);
+ printf("ID_MODEL=%s\n", model_str);
+ printf("ID_MODEL_ENC=%s\n", model_enc_str);
+ printf("ID_REVISION=%s\n", revision_str);
+ printf("ID_TYPE=%s\n", type_str);
+ if (dev_scsi.serial[0] != '\0') {
+ udev_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1);
+ udev_replace_chars(serial_str, NULL);
+ printf("ID_SERIAL=%s\n", serial_str);
+ udev_replace_whitespace(dev_scsi.serial_short, serial_str, sizeof(serial_str)-1);
+ udev_replace_chars(serial_str, NULL);
+ printf("ID_SERIAL_SHORT=%s\n", serial_str);
+ }
+ if (dev_scsi.wwn[0] != '\0') {
+ printf("ID_WWN=0x%s\n", dev_scsi.wwn);
+ if (dev_scsi.wwn_vendor_extension[0] != '\0') {
+ printf("ID_WWN_VENDOR_EXTENSION=0x%s\n", dev_scsi.wwn_vendor_extension);
+ printf("ID_WWN_WITH_EXTENSION=0x%s%s\n", dev_scsi.wwn, dev_scsi.wwn_vendor_extension);
+ } else
+ printf("ID_WWN_WITH_EXTENSION=0x%s\n", dev_scsi.wwn);
+ }
+ if (dev_scsi.tgpt_group[0] != '\0')
+ printf("ID_TARGET_PORT=%s\n", dev_scsi.tgpt_group);
+ if (dev_scsi.unit_serial_number[0] != '\0')
+ printf("ID_SCSI_SERIAL=%s\n", dev_scsi.unit_serial_number);
+ goto out;
+ }
+
+ if (dev_scsi.serial[0] == '\0') {
+ retval = 1;
+ goto out;
+ }
+
+ if (reformat_serial) {
+ char serial_str[MAX_SERIAL_LEN];
+
+ udev_replace_whitespace(dev_scsi.serial, serial_str, sizeof(serial_str)-1);
+ udev_replace_chars(serial_str, NULL);
+ printf("%s\n", serial_str);
+ goto out;
+ }
+
+ printf("%s\n", dev_scsi.serial);
+out:
+ return retval;
+}
+
+int main(int argc, char **argv) {
+ _cleanup_strv_free_ char **newargv = NULL;
+ int retval = 0;
+ char maj_min_dev[MAX_PATH_LEN];
+ int newargc;
+
+ log_set_target(LOG_TARGET_AUTO);
+ udev_parse_config();
+ log_parse_environment();
+ log_open();
+
+ /*
+ * Get config file options.
+ */
+ retval = get_file_options(NULL, NULL, &newargc, &newargv);
+ if (retval < 0) {
+ retval = 1;
+ goto exit;
+ }
+ if (retval == 0) {
+ assert(newargv);
+
+ if (set_options(newargc, newargv, maj_min_dev) < 0) {
+ retval = 2;
+ goto exit;
+ }
+ }
+
+ /*
+ * Get command line options (overriding any config file settings).
+ */
+ if (set_options(argc, argv, maj_min_dev) < 0)
+ exit(EXIT_FAILURE);
+
+ if (!dev_specified) {
+ log_error("No device specified.");
+ retval = 1;
+ goto exit;
+ }
+
+ retval = scsi_id(maj_min_dev);
+
+exit:
+ log_close();
+ return retval;
+}
diff --git a/src/udev/scsi_id/scsi_id.h b/src/udev/scsi_id/scsi_id.h
new file mode 100644
index 0000000..9ab3341
--- /dev/null
+++ b/src/udev/scsi_id/scsi_id.h
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+/*
+ * Copyright © IBM Corp. 2003
+ */
+
+#define MAX_PATH_LEN 512
+
+/*
+ * MAX_ATTR_LEN: maximum length of the result of reading a sysfs
+ * attribute.
+ */
+#define MAX_ATTR_LEN 256
+
+/*
+ * MAX_SERIAL_LEN: the maximum length of the serial number, including
+ * added prefixes such as vendor and product (model) strings.
+ */
+#define MAX_SERIAL_LEN 256
+
+/*
+ * MAX_BUFFER_LEN: maximum buffer size and line length used while reading
+ * the config file.
+ */
+#define MAX_BUFFER_LEN 256
+
+struct scsi_id_device {
+ char vendor[9];
+ char model[17];
+ char revision[5];
+ char kernel[64];
+ char serial[MAX_SERIAL_LEN];
+ char serial_short[MAX_SERIAL_LEN];
+ unsigned type;
+ int use_sg;
+
+ /* Always from page 0x80 e.g. 'B3G1P8500RWT' - may not be unique */
+ char unit_serial_number[MAX_SERIAL_LEN];
+
+ /* NULs if not set - otherwise hex encoding using lower-case e.g. '50014ee0016eb572' */
+ char wwn[17];
+
+ /* NULs if not set - otherwise hex encoding using lower-case e.g. '0xe00000d80000' */
+ char wwn_vendor_extension[17];
+
+ /* NULs if not set - otherwise decimal number */
+ char tgpt_group[8];
+};
+
+int scsi_std_inquiry(struct scsi_id_device *dev_scsi, const char *devname);
+int scsi_get_serial(struct scsi_id_device *dev_scsi, const char *devname,
+ int page_code, int len);
+
+/*
+ * Page code values.
+ */
+enum page_code {
+ PAGE_83_PRE_SPC3 = -0x83,
+ PAGE_UNSPECIFIED = 0x00,
+ PAGE_80 = 0x80,
+ PAGE_83 = 0x83,
+};
diff --git a/src/udev/scsi_id/scsi_serial.c b/src/udev/scsi_id/scsi_serial.c
new file mode 100644
index 0000000..aed6082
--- /dev/null
+++ b/src/udev/scsi_id/scsi_serial.c
@@ -0,0 +1,892 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © IBM Corp. 2003
+ *
+ * Author: Patrick Mansfield<patmans@us.ibm.com>
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/bsg.h>
+#include <linux/types.h>
+#include <scsi/scsi.h>
+#include <scsi/sg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "devnum-util.h"
+#include "memory-util.h"
+#include "random-util.h"
+#include "scsi.h"
+#include "scsi_id.h"
+#include "string-util.h"
+
+/*
+ * A priority based list of id, naa, and binary/ascii for the identifier
+ * descriptor in VPD page 0x83.
+ *
+ * Brute force search for a match starting with the first value in the
+ * following id_search_list. This is not a performance issue, since there
+ * is normally one or some small number of descriptors.
+ */
+static const struct scsi_id_search_values id_search_list[] = {
+ { SCSI_ID_TGTGROUP, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY },
+ { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG_EXTENDED, SCSI_ID_BINARY },
+ { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG_EXTENDED, SCSI_ID_ASCII },
+ { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG, SCSI_ID_BINARY },
+ { SCSI_ID_NAA, SCSI_ID_NAA_IEEE_REG, SCSI_ID_ASCII },
+ /*
+ * Devices already exist using NAA values that are now marked
+ * reserved. These should not conflict with other values, or it is
+ * a bug in the device. As long as we find the IEEE extended one
+ * first, we really don't care what other ones are used. Using
+ * don't care here means that a device that returns multiple
+ * non-IEEE descriptors in a random order will get different
+ * names.
+ */
+ { SCSI_ID_NAA, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY },
+ { SCSI_ID_NAA, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII },
+ { SCSI_ID_EUI_64, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY },
+ { SCSI_ID_EUI_64, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII },
+ { SCSI_ID_T10_VENDOR, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY },
+ { SCSI_ID_T10_VENDOR, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII },
+ { SCSI_ID_VENDOR_SPECIFIC, SCSI_ID_NAA_DONT_CARE, SCSI_ID_BINARY },
+ { SCSI_ID_VENDOR_SPECIFIC, SCSI_ID_NAA_DONT_CARE, SCSI_ID_ASCII },
+};
+
+static const char hex_str[]="0123456789abcdef";
+
+/*
+ * Values returned in the result/status, only the ones used by the code
+ * are used here.
+ */
+
+#define DID_NO_CONNECT 0x01 /* Unable to connect before timeout */
+#define DID_BUS_BUSY 0x02 /* Bus remain busy until timeout */
+#define DID_TIME_OUT 0x03 /* Timed out for some other reason */
+#define DID_TRANSPORT_DISRUPTED 0x0e /* Transport disrupted and should retry */
+#define DRIVER_TIMEOUT 0x06
+#define DRIVER_SENSE 0x08 /* Sense_buffer has been set */
+
+/* The following "category" function returns one of the following */
+#define SG_ERR_CAT_CLEAN 0 /* No errors or other information */
+#define SG_ERR_CAT_MEDIA_CHANGED 1 /* interpreted from sense buffer */
+#define SG_ERR_CAT_RESET 2 /* interpreted from sense buffer */
+#define SG_ERR_CAT_TIMEOUT 3
+#define SG_ERR_CAT_RECOVERED 4 /* Successful command after recovered err */
+#define SG_ERR_CAT_NOTSUPPORTED 5 /* Illegal / unsupported command */
+#define SG_ERR_CAT_RETRY 6 /* Command should be retried */
+#define SG_ERR_CAT_SENSE 98 /* Something else in the sense buffer */
+#define SG_ERR_CAT_OTHER 99 /* Some other error/warning */
+
+static int do_scsi_page80_inquiry(struct scsi_id_device *dev_scsi, int fd,
+ char *serial, char *serial_short, int max_len);
+
+static int sg_err_category_new(int scsi_status, int msg_status, int
+ host_status, int driver_status, const
+ unsigned char *sense_buffer, int sb_len) {
+ scsi_status &= 0x7e;
+
+ /*
+ * XXX change to return only two values - failed or OK.
+ */
+
+ if (!scsi_status && !host_status && !driver_status)
+ return SG_ERR_CAT_CLEAN;
+
+ if (IN_SET(scsi_status, SCSI_CHECK_CONDITION, SCSI_COMMAND_TERMINATED) ||
+ (driver_status & 0xf) == DRIVER_SENSE) {
+ if (sense_buffer && (sb_len > 2)) {
+ int sense_key;
+ unsigned char asc;
+
+ if (sense_buffer[0] & 0x2) {
+ sense_key = sense_buffer[1] & 0xf;
+ asc = sense_buffer[2];
+ } else {
+ sense_key = sense_buffer[2] & 0xf;
+ asc = (sb_len > 12) ? sense_buffer[12] : 0;
+ }
+
+ if (sense_key == RECOVERED_ERROR)
+ return SG_ERR_CAT_RECOVERED;
+ else if (sense_key == UNIT_ATTENTION) {
+ if (0x28 == asc)
+ return SG_ERR_CAT_MEDIA_CHANGED;
+ if (0x29 == asc)
+ return SG_ERR_CAT_RESET;
+ } else if (sense_key == ILLEGAL_REQUEST)
+ return SG_ERR_CAT_NOTSUPPORTED;
+ }
+ return SG_ERR_CAT_SENSE;
+ }
+ if (host_status) {
+ if (IN_SET(host_status, DID_NO_CONNECT, DID_BUS_BUSY, DID_TIME_OUT))
+ return SG_ERR_CAT_TIMEOUT;
+ if (host_status == DID_TRANSPORT_DISRUPTED)
+ return SG_ERR_CAT_RETRY;
+ }
+ if (driver_status) {
+ if (driver_status == DRIVER_TIMEOUT)
+ return SG_ERR_CAT_TIMEOUT;
+ }
+ return SG_ERR_CAT_OTHER;
+}
+
+static int sg_err_category3(struct sg_io_hdr *hp) {
+ return sg_err_category_new(hp->status, hp->msg_status,
+ hp->host_status, hp->driver_status,
+ hp->sbp, hp->sb_len_wr);
+}
+
+static int sg_err_category4(struct sg_io_v4 *hp) {
+ return sg_err_category_new(hp->device_status, 0,
+ hp->transport_status, hp->driver_status,
+ (unsigned char *)(uintptr_t)hp->response,
+ hp->response_len);
+}
+
+static int scsi_dump_sense(struct scsi_id_device *dev_scsi,
+ unsigned char *sense_buffer, int sb_len) {
+ int s;
+ unsigned code, sense_class, sense_key, asc, ascq;
+
+ /*
+ * Figure out and print the sense key, asc and ascq.
+ *
+ * If you want to suppress these for a particular drive model, add
+ * a deny list entry in the scsi_id config file.
+ *
+ * XXX We probably need to: lookup the sense/asc/ascq in a retry
+ * table, and if found return 1 (after dumping the sense, asc, and
+ * ascq). So, if/when we get something like a power on/reset,
+ * we'll retry the command.
+ */
+
+ if (sb_len < 1)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: sense buffer empty",
+ dev_scsi->kernel);
+
+ sense_class = (sense_buffer[0] >> 4) & 0x07;
+ code = sense_buffer[0] & 0xf;
+
+ if (sense_class == 7) {
+ /*
+ * extended sense data.
+ */
+ s = sense_buffer[7] + 8;
+ if (sb_len < s)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: sense buffer too small %d bytes, %d bytes too short",
+ dev_scsi->kernel, sb_len,
+ s - sb_len);
+
+ if (IN_SET(code, 0x0, 0x1)) {
+ sense_key = sense_buffer[2] & 0xf;
+ if (s < 14)
+ /*
+ * Possible?
+ */
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: sense result too small %d bytes",
+ dev_scsi->kernel, s);
+
+ asc = sense_buffer[12];
+ ascq = sense_buffer[13];
+ } else if (IN_SET(code, 0x2, 0x3)) {
+ sense_key = sense_buffer[1] & 0xf;
+ asc = sense_buffer[2];
+ ascq = sense_buffer[3];
+ } else
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: invalid sense code 0x%x",
+ dev_scsi->kernel, code);
+
+ log_debug("%s: sense key 0x%x ASC 0x%x ASCQ 0x%x",
+ dev_scsi->kernel, sense_key, asc, ascq);
+ } else {
+ if (sb_len < 4)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: sense buffer too small %d bytes, %d bytes too short",
+ dev_scsi->kernel, sb_len,
+ 4 - sb_len);
+
+ if (sense_buffer[0] < 15)
+ log_debug("%s: old sense key: 0x%x", dev_scsi->kernel, sense_buffer[0] & 0x0fu);
+ else
+ log_debug("%s: sense = %2x %2x",
+ dev_scsi->kernel, sense_buffer[0], sense_buffer[2]);
+ log_debug("%s: non-extended sense class %u code 0x%0x",
+ dev_scsi->kernel, sense_class, code);
+
+ }
+
+ return -1;
+}
+
+static int scsi_dump(struct scsi_id_device *dev_scsi, struct sg_io_hdr *io) {
+ if (!io->status && !io->host_status && !io->msg_status &&
+ !io->driver_status)
+ /*
+ * Impossible, should not be called.
+ */
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: called with no error",
+ __func__);
+
+ log_debug("%s: sg_io failed status 0x%x 0x%x 0x%x 0x%x",
+ dev_scsi->kernel, io->driver_status, io->host_status, io->msg_status, io->status);
+ if (io->status == SCSI_CHECK_CONDITION)
+ return scsi_dump_sense(dev_scsi, io->sbp, io->sb_len_wr);
+ else
+ return -1;
+}
+
+static int scsi_dump_v4(struct scsi_id_device *dev_scsi, struct sg_io_v4 *io) {
+ if (!io->device_status && !io->transport_status &&
+ !io->driver_status)
+ /*
+ * Impossible, should not be called.
+ */
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: called with no error",
+ __func__);
+
+ log_debug("%s: sg_io failed status 0x%x 0x%x 0x%x",
+ dev_scsi->kernel, io->driver_status, io->transport_status, io->device_status);
+ if (io->device_status == SCSI_CHECK_CONDITION)
+ return scsi_dump_sense(dev_scsi, (unsigned char *)(uintptr_t)io->response,
+ io->response_len);
+ else
+ return -1;
+}
+
+static int scsi_inquiry(struct scsi_id_device *dev_scsi, int fd,
+ unsigned char evpd, unsigned char page,
+ unsigned char *buf, unsigned buflen) {
+ unsigned char inq_cmd[INQUIRY_CMDLEN] =
+ { INQUIRY_CMD, evpd, page, 0, buflen, 0 };
+ unsigned char sense[SENSE_BUFF_LEN];
+ void *io_buf;
+ struct sg_io_v4 io_v4;
+ struct sg_io_hdr io_hdr;
+ int retry = 3; /* rather random */
+ int retval;
+
+ if (buflen > SCSI_INQ_BUFF_LEN)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "buflen %u too long", buflen);
+
+resend:
+ if (dev_scsi->use_sg == 4) {
+ memzero(&io_v4, sizeof(struct sg_io_v4));
+ io_v4.guard = 'Q';
+ io_v4.protocol = BSG_PROTOCOL_SCSI;
+ io_v4.subprotocol = BSG_SUB_PROTOCOL_SCSI_CMD;
+ io_v4.request_len = sizeof(inq_cmd);
+ io_v4.request = (uintptr_t)inq_cmd;
+ io_v4.max_response_len = sizeof(sense);
+ io_v4.response = (uintptr_t)sense;
+ io_v4.din_xfer_len = buflen;
+ io_v4.din_xferp = (uintptr_t)buf;
+ io_buf = (void *)&io_v4;
+ } else {
+ memzero(&io_hdr, sizeof(struct sg_io_hdr));
+ io_hdr.interface_id = 'S';
+ io_hdr.cmd_len = sizeof(inq_cmd);
+ io_hdr.mx_sb_len = sizeof(sense);
+ io_hdr.dxfer_direction = SG_DXFER_FROM_DEV;
+ io_hdr.dxfer_len = buflen;
+ io_hdr.dxferp = buf;
+ io_hdr.cmdp = inq_cmd;
+ io_hdr.sbp = sense;
+ io_hdr.timeout = DEF_TIMEOUT;
+ io_buf = (void *)&io_hdr;
+ }
+
+ retval = ioctl(fd, SG_IO, io_buf);
+ if (retval < 0) {
+ if (IN_SET(errno, EINVAL, ENOSYS) && dev_scsi->use_sg == 4) {
+ dev_scsi->use_sg = 3;
+ goto resend;
+ }
+ log_debug_errno(errno, "%s: ioctl failed: %m", dev_scsi->kernel);
+ goto error;
+ }
+
+ if (dev_scsi->use_sg == 4)
+ retval = sg_err_category4(io_buf);
+ else
+ retval = sg_err_category3(io_buf);
+
+ switch (retval) {
+ case SG_ERR_CAT_NOTSUPPORTED:
+ buf[1] = 0;
+ _fallthrough_;
+ case SG_ERR_CAT_CLEAN:
+ case SG_ERR_CAT_RECOVERED:
+ retval = 0;
+ break;
+ case SG_ERR_CAT_RETRY:
+ break;
+
+ default:
+ if (dev_scsi->use_sg == 4)
+ retval = scsi_dump_v4(dev_scsi, io_buf);
+ else
+ retval = scsi_dump(dev_scsi, io_buf);
+ }
+
+ if (!retval) {
+ retval = buflen;
+ } else if (retval > 0) {
+ if (--retry > 0)
+ goto resend;
+ retval = -1;
+ }
+
+error:
+ if (retval < 0)
+ log_debug("%s: Unable to get INQUIRY vpd %d page 0x%x.",
+ dev_scsi->kernel, evpd, page);
+
+ return retval;
+}
+
+/* Get list of supported EVPD pages */
+static int do_scsi_page0_inquiry(struct scsi_id_device *dev_scsi, int fd,
+ unsigned char *buffer, unsigned len) {
+ int retval;
+
+ memzero(buffer, len);
+ retval = scsi_inquiry(dev_scsi, fd, 1, 0x0, buffer, len);
+ if (retval < 0)
+ return 1;
+
+ if (buffer[1] != 0) {
+ log_debug("%s: page 0 not available.", dev_scsi->kernel);
+ return 1;
+ }
+ if (buffer[3] > len) {
+ log_debug("%s: page 0 buffer too long %d", dev_scsi->kernel, buffer[3]);
+ return 1;
+ }
+
+ /*
+ * Following check is based on code once included in the 2.5.x
+ * kernel.
+ *
+ * Some ill behaved devices return the standard inquiry here
+ * rather than the evpd data, snoop the data to verify.
+ */
+ if (buffer[3] > MODEL_LENGTH) {
+ /*
+ * If the vendor id appears in the page assume the page is
+ * invalid.
+ */
+ if (strneq((char*) buffer + VENDOR_LENGTH, dev_scsi->vendor, VENDOR_LENGTH)) {
+ log_debug("%s: invalid page0 data", dev_scsi->kernel);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int append_vendor_model(
+ const struct scsi_id_device *dev_scsi,
+ char buf[static VENDOR_LENGTH + MODEL_LENGTH]) {
+
+ assert(dev_scsi);
+ assert(buf);
+
+ if (strnlen(dev_scsi->vendor, VENDOR_LENGTH) != VENDOR_LENGTH)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: bad vendor string \"%s\"",
+ dev_scsi->kernel, dev_scsi->vendor);
+ if (strnlen(dev_scsi->model, MODEL_LENGTH) != MODEL_LENGTH)
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: bad model string \"%s\"",
+ dev_scsi->kernel, dev_scsi->model);
+ memcpy(buf, dev_scsi->vendor, VENDOR_LENGTH);
+ memcpy(buf + VENDOR_LENGTH, dev_scsi->model, MODEL_LENGTH);
+ return VENDOR_LENGTH + MODEL_LENGTH;
+}
+
+/*
+ * check_fill_0x83_id - check the page 0x83 id, if OK allocate and fill
+ * serial number.
+ */
+static int check_fill_0x83_id(struct scsi_id_device *dev_scsi,
+ unsigned char *page_83,
+ const struct scsi_id_search_values
+ *id_search, char *serial, char *serial_short,
+ int max_len, char *wwn,
+ char *wwn_vendor_extension, char *tgpt_group) {
+ int i, j, s, len;
+
+ /*
+ * ASSOCIATION must be with the device (value 0)
+ * or with the target port for SCSI_ID_TGTPORT
+ */
+ if ((page_83[1] & 0x30) == 0x10) {
+ if (id_search->id_type != SCSI_ID_TGTGROUP)
+ return 1;
+ } else if ((page_83[1] & 0x30) != 0)
+ return 1;
+
+ if ((page_83[1] & 0x0f) != id_search->id_type)
+ return 1;
+
+ /*
+ * Possibly check NAA sub-type.
+ */
+ if ((id_search->naa_type != SCSI_ID_NAA_DONT_CARE) &&
+ (id_search->naa_type != (page_83[4] & 0xf0) >> 4))
+ return 1;
+
+ /*
+ * Check for matching code set - ASCII or BINARY.
+ */
+ if ((page_83[0] & 0x0f) != id_search->code_set)
+ return 1;
+
+ /*
+ * page_83[3]: identifier length
+ */
+ len = page_83[3];
+ if ((page_83[0] & 0x0f) != SCSI_ID_ASCII)
+ /*
+ * If not ASCII, use two bytes for each binary value.
+ */
+ len *= 2;
+
+ /*
+ * Add one byte for the NUL termination, and one for the id_type.
+ */
+ len += 2;
+ if (id_search->id_type == SCSI_ID_VENDOR_SPECIFIC)
+ len += VENDOR_LENGTH + MODEL_LENGTH;
+
+ if (max_len < len) {
+ log_debug("%s: length %d too short - need %d",
+ dev_scsi->kernel, max_len, len);
+ return 1;
+ }
+
+ if (id_search->id_type == SCSI_ID_TGTGROUP && tgpt_group != NULL) {
+ unsigned group;
+
+ group = ((unsigned)page_83[6] << 8) | page_83[7];
+ sprintf(tgpt_group,"%x", group);
+ return 1;
+ }
+
+ serial[0] = hex_str[id_search->id_type];
+
+ /*
+ * For SCSI_ID_VENDOR_SPECIFIC prepend the vendor and model before
+ * the id since it is not unique across all vendors and models,
+ * this differs from SCSI_ID_T10_VENDOR, where the vendor is
+ * included in the identifier.
+ */
+ if (id_search->id_type == SCSI_ID_VENDOR_SPECIFIC)
+ if (append_vendor_model(dev_scsi, serial + 1) < 0)
+ return 1;
+
+ i = 4; /* offset to the start of the identifier */
+ s = j = strlen(serial);
+ if ((page_83[0] & 0x0f) == SCSI_ID_ASCII) {
+ /*
+ * ASCII descriptor.
+ */
+ while (i < (4 + page_83[3]))
+ serial[j++] = page_83[i++];
+ } else {
+ /*
+ * Binary descriptor, convert to ASCII, using two bytes of
+ * ASCII for each byte in the page_83.
+ */
+ while (i < (4 + page_83[3])) {
+ serial[j++] = hex_str[(page_83[i] & 0xf0) >> 4];
+ serial[j++] = hex_str[page_83[i] & 0x0f];
+ i++;
+ }
+ }
+
+ strcpy(serial_short, serial + s);
+
+ if (id_search->id_type == SCSI_ID_NAA && wwn != NULL) {
+ strncpy(wwn, serial + s, 16);
+ if (wwn_vendor_extension)
+ strncpy(wwn_vendor_extension, serial + s + 16, 16);
+ }
+
+ return 0;
+}
+
+/* Extract the raw binary from VPD 0x83 pre-SPC devices */
+static int check_fill_0x83_prespc3(struct scsi_id_device *dev_scsi,
+ unsigned char *page_83,
+ const struct scsi_id_search_values
+ *id_search, char *serial, char *serial_short, int max_len) {
+ int i, j;
+
+ serial[0] = hex_str[SCSI_ID_NAA];
+ /* serial has been memset to zero before */
+ j = strlen(serial); /* j = 1; */
+
+ for (i = 0; (i < page_83[3]) && (j < max_len-3); ++i) {
+ serial[j++] = hex_str[(page_83[4+i] & 0xf0) >> 4];
+ serial[j++] = hex_str[ page_83[4+i] & 0x0f];
+ }
+ serial[max_len-1] = 0;
+ strncpy(serial_short, serial, max_len-1);
+ return 0;
+}
+
+/* Get device identification VPD page */
+static int do_scsi_page83_inquiry(struct scsi_id_device *dev_scsi, int fd,
+ char *serial, char *serial_short, int len,
+ char *unit_serial_number, char *wwn,
+ char *wwn_vendor_extension, char *tgpt_group) {
+ int retval;
+ unsigned id_ind, j;
+ unsigned char page_83[SCSI_INQ_BUFF_LEN];
+
+ /* also pick up the page 80 serial number */
+ do_scsi_page80_inquiry(dev_scsi, fd, NULL, unit_serial_number, MAX_SERIAL_LEN);
+
+ memzero(page_83, SCSI_INQ_BUFF_LEN);
+ retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_83, page_83,
+ SCSI_INQ_BUFF_LEN);
+ if (retval < 0)
+ return 1;
+
+ if (page_83[1] != PAGE_83) {
+ log_debug("%s: Invalid page 0x83", dev_scsi->kernel);
+ return 1;
+ }
+
+ /*
+ * XXX Some devices (IBM 3542) return all spaces for an identifier if
+ * the LUN is not actually configured. This leads to identifiers of
+ * the form: "1 ".
+ */
+
+ /*
+ * Model 4, 5, and (some) model 6 EMC Symmetrix devices return
+ * a page 83 reply according to SCSI-2 format instead of SPC-2/3.
+ *
+ * The SCSI-2 page 83 format returns an IEEE WWN in binary
+ * encoded hexi-decimal in the 16 bytes following the initial
+ * 4-byte page 83 reply header.
+ *
+ * Both the SPC-2 and SPC-3 formats return an IEEE WWN as part
+ * of an Identification descriptor. The 3rd byte of the first
+ * Identification descriptor is a reserved (BSZ) byte field.
+ *
+ * Reference the 7th byte of the page 83 reply to determine
+ * whether the reply is compliant with SCSI-2 or SPC-2/3
+ * specifications. A zero value in the 7th byte indicates
+ * an SPC-2/3 conformant reply, (i.e., the reserved field of the
+ * first Identification descriptor). This byte will be non-zero
+ * for a SCSI-2 conformant page 83 reply from these EMC
+ * Symmetrix models since the 7th byte of the reply corresponds
+ * to the 4th and 5th nibbles of the 6-byte OUI for EMC, that is,
+ * 0x006048.
+ */
+
+ if (page_83[6] != 0)
+ return check_fill_0x83_prespc3(dev_scsi, page_83, id_search_list,
+ serial, serial_short, len);
+
+ /*
+ * Search for a match in the prioritized id_search_list - since WWN ids
+ * come first we can pick up the WWN in check_fill_0x83_id().
+ */
+ for (id_ind = 0;
+ id_ind < sizeof(id_search_list)/sizeof(id_search_list[0]);
+ id_ind++) {
+ /*
+ * Examine each descriptor returned. There is normally only
+ * one or a small number of descriptors.
+ */
+ for (j = 4; j <= ((unsigned)page_83[2] << 8) + (unsigned)page_83[3] + 3; j += page_83[j + 3] + 4) {
+ retval = check_fill_0x83_id(dev_scsi, page_83 + j,
+ id_search_list + id_ind,
+ serial, serial_short, len,
+ wwn, wwn_vendor_extension,
+ tgpt_group);
+ if (!retval)
+ return retval;
+ else if (retval < 0)
+ return retval;
+ }
+ }
+ return 1;
+}
+
+/*
+ * Get device identification VPD page for older SCSI-2 device which is not
+ * compliant with either SPC-2 or SPC-3 format.
+ *
+ * Return the hard coded error code value 2 if the page 83 reply is not
+ * conformant to the SCSI-2 format.
+ */
+static int do_scsi_page83_prespc3_inquiry(struct scsi_id_device *dev_scsi, int fd,
+ char *serial, char *serial_short, int len) {
+ int retval;
+ int i, j;
+ unsigned char page_83[SCSI_INQ_BUFF_LEN];
+
+ memzero(page_83, SCSI_INQ_BUFF_LEN);
+ retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_83, page_83, SCSI_INQ_BUFF_LEN);
+ if (retval < 0)
+ return 1;
+
+ if (page_83[1] != PAGE_83) {
+ log_debug("%s: Invalid page 0x83", dev_scsi->kernel);
+ return 1;
+ }
+ /*
+ * Model 4, 5, and (some) model 6 EMC Symmetrix devices return
+ * a page 83 reply according to SCSI-2 format instead of SPC-2/3.
+ *
+ * The SCSI-2 page 83 format returns an IEEE WWN in binary
+ * encoded hexi-decimal in the 16 bytes following the initial
+ * 4-byte page 83 reply header.
+ *
+ * Both the SPC-2 and SPC-3 formats return an IEEE WWN as part
+ * of an Identification descriptor. The 3rd byte of the first
+ * Identification descriptor is a reserved (BSZ) byte field.
+ *
+ * Reference the 7th byte of the page 83 reply to determine
+ * whether the reply is compliant with SCSI-2 or SPC-2/3
+ * specifications. A zero value in the 7th byte indicates
+ * an SPC-2/3 conformant reply, (i.e., the reserved field of the
+ * first Identification descriptor). This byte will be non-zero
+ * for a SCSI-2 conformant page 83 reply from these EMC
+ * Symmetrix models since the 7th byte of the reply corresponds
+ * to the 4th and 5th nibbles of the 6-byte OUI for EMC, that is,
+ * 0x006048.
+ */
+ if (page_83[6] == 0)
+ return 2;
+
+ serial[0] = hex_str[SCSI_ID_NAA];
+ /*
+ * The first four bytes contain data, not a descriptor.
+ */
+ i = 4;
+ j = strlen(serial);
+ /*
+ * Binary descriptor, convert to ASCII,
+ * using two bytes of ASCII for each byte
+ * in the page_83.
+ */
+ while (i < (page_83[3]+4)) {
+ serial[j++] = hex_str[(page_83[i] & 0xf0) >> 4];
+ serial[j++] = hex_str[page_83[i] & 0x0f];
+ i++;
+ }
+ return 0;
+}
+
+/* Get unit serial number VPD page */
+static int do_scsi_page80_inquiry(struct scsi_id_device *dev_scsi, int fd,
+ char *serial, char *serial_short, int max_len) {
+ int retval;
+ int ser_ind;
+ int i;
+ int len;
+ unsigned char buf[SCSI_INQ_BUFF_LEN];
+
+ memzero(buf, SCSI_INQ_BUFF_LEN);
+ retval = scsi_inquiry(dev_scsi, fd, 1, PAGE_80, buf, SCSI_INQ_BUFF_LEN);
+ if (retval < 0)
+ return retval;
+
+ if (buf[1] != PAGE_80) {
+ log_debug("%s: Invalid page 0x80", dev_scsi->kernel);
+ return 1;
+ }
+
+ len = 1 + VENDOR_LENGTH + MODEL_LENGTH + buf[3];
+ if (max_len < len) {
+ log_debug("%s: length %d too short - need %d",
+ dev_scsi->kernel, max_len, len);
+ return 1;
+ }
+ /*
+ * Prepend 'S' to avoid unlikely collision with page 0x83 vendor
+ * specific type where we prepend '0' + vendor + model.
+ */
+ len = buf[3];
+ if (serial) {
+ serial[0] = 'S';
+ ser_ind = append_vendor_model(dev_scsi, serial + 1);
+ if (ser_ind < 0)
+ return 1;
+ ser_ind++; /* for the leading 'S' */
+ for (i = 4; i < len + 4; i++, ser_ind++)
+ serial[ser_ind] = buf[i];
+ }
+ if (serial_short) {
+ memcpy(serial_short, buf + 4, len);
+ serial_short[len] = '\0';
+ }
+ return 0;
+}
+
+int scsi_std_inquiry(struct scsi_id_device *dev_scsi, const char *devname) {
+ int fd;
+ unsigned char buf[SCSI_INQ_BUFF_LEN];
+ struct stat statbuf;
+ int err = 0;
+
+ fd = open(devname, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
+ if (fd < 0) {
+ log_debug_errno(errno, "scsi_id: cannot open %s: %m", devname);
+ return 1;
+ }
+
+ if (fstat(fd, &statbuf) < 0) {
+ log_debug_errno(errno, "scsi_id: cannot stat %s: %m", devname);
+ err = 2;
+ goto out;
+ }
+ format_devnum(statbuf.st_rdev, dev_scsi->kernel);
+
+ memzero(buf, SCSI_INQ_BUFF_LEN);
+ err = scsi_inquiry(dev_scsi, fd, 0, 0, buf, SCSI_INQ_BUFF_LEN);
+ if (err < 0)
+ goto out;
+
+ err = 0;
+ memcpy(dev_scsi->vendor, buf + 8, 8);
+ dev_scsi->vendor[8] = '\0';
+ memcpy(dev_scsi->model, buf + 16, 16);
+ dev_scsi->model[16] = '\0';
+ memcpy(dev_scsi->revision, buf + 32, 4);
+ dev_scsi->revision[4] = '\0';
+ dev_scsi->type = buf[0] & 0x1f;
+
+out:
+ close(fd);
+ return err;
+}
+
+int scsi_get_serial(struct scsi_id_device *dev_scsi, const char *devname,
+ int page_code, int len) {
+ unsigned char page0[SCSI_INQ_BUFF_LEN];
+ int fd = -EBADF;
+ int cnt;
+ int ind;
+ int retval;
+
+ memzero(dev_scsi->serial, len);
+ for (cnt = 20; cnt > 0; cnt--) {
+ fd = open(devname, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
+ if (fd >= 0 || errno != EBUSY)
+ break;
+
+ usleep_safe(200U*USEC_PER_MSEC + random_u64_range(100U*USEC_PER_MSEC));
+ }
+ if (fd < 0)
+ return 1;
+
+ if (page_code == PAGE_80) {
+ if (do_scsi_page80_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len)) {
+ retval = 1;
+ goto completed;
+ } else {
+ retval = 0;
+ goto completed;
+ }
+ } else if (page_code == PAGE_83) {
+ if (do_scsi_page83_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) {
+ retval = 1;
+ goto completed;
+ } else {
+ retval = 0;
+ goto completed;
+ }
+ } else if (page_code == PAGE_83_PRE_SPC3) {
+ retval = do_scsi_page83_prespc3_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len);
+ if (retval) {
+ /*
+ * Fallback to servicing a SPC-2/3 compliant page 83
+ * inquiry if the page 83 reply format does not
+ * conform to pre-SPC3 expectations.
+ */
+ if (retval == 2) {
+ if (do_scsi_page83_inquiry(dev_scsi, fd, dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) {
+ retval = 1;
+ goto completed;
+ } else {
+ retval = 0;
+ goto completed;
+ }
+ }
+ else {
+ retval = 1;
+ goto completed;
+ }
+ } else {
+ retval = 0;
+ goto completed;
+ }
+ } else if (page_code != 0x00) {
+ log_debug("%s: unsupported page code 0x%d", dev_scsi->kernel, page_code);
+ retval = 1;
+ goto completed;
+ }
+
+ /*
+ * Get page 0, the page of the pages. By default, try from best to
+ * worst of supported pages: 0x83 then 0x80.
+ */
+ if (do_scsi_page0_inquiry(dev_scsi, fd, page0, SCSI_INQ_BUFF_LEN)) {
+ /*
+ * Don't try anything else. Black list if a specific page
+ * should be used for this vendor+model, or maybe have an
+ * optional fall-back to page 0x80 or page 0x83.
+ */
+ retval = 1;
+ goto completed;
+ }
+
+ for (ind = 4; ind <= page0[3] + 3; ind++)
+ if (page0[ind] == PAGE_83)
+ if (!do_scsi_page83_inquiry(dev_scsi, fd,
+ dev_scsi->serial, dev_scsi->serial_short, len, dev_scsi->unit_serial_number, dev_scsi->wwn, dev_scsi->wwn_vendor_extension, dev_scsi->tgpt_group)) {
+ /*
+ * Success
+ */
+ retval = 0;
+ goto completed;
+ }
+
+ for (ind = 4; ind <= page0[3] + 3; ind++)
+ if (page0[ind] == PAGE_80)
+ if (!do_scsi_page80_inquiry(dev_scsi, fd,
+ dev_scsi->serial, dev_scsi->serial_short, len)) {
+ /*
+ * Success
+ */
+ retval = 0;
+ goto completed;
+ }
+ retval = 1;
+
+completed:
+ close(fd);
+ return retval;
+}
diff --git a/src/udev/test-udev-builtin.c b/src/udev/test-udev-builtin.c
new file mode 100644
index 0000000..2ce7f19
--- /dev/null
+++ b/src/udev/test-udev-builtin.c
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "tests.h"
+#include "udev-builtin.h"
+
+TEST(udev_builtin_cmd_to_ptr) {
+ /* Those could have been static asserts, but ({}) is not allowed there. */
+#if HAVE_BLKID
+ assert_se(UDEV_BUILTIN_CMD_TO_PTR(UDEV_BUILTIN_BLKID));
+ assert_se(PTR_TO_UDEV_BUILTIN_CMD(UDEV_BUILTIN_CMD_TO_PTR(UDEV_BUILTIN_BLKID)) == UDEV_BUILTIN_BLKID);
+#endif
+ assert_se(UDEV_BUILTIN_CMD_TO_PTR(UDEV_BUILTIN_BTRFS));
+ assert_se(PTR_TO_UDEV_BUILTIN_CMD(UDEV_BUILTIN_CMD_TO_PTR(UDEV_BUILTIN_BTRFS)) == UDEV_BUILTIN_BTRFS);
+ assert_se(PTR_TO_UDEV_BUILTIN_CMD(UDEV_BUILTIN_CMD_TO_PTR(_UDEV_BUILTIN_INVALID)) == _UDEV_BUILTIN_INVALID);
+
+ assert_se(PTR_TO_UDEV_BUILTIN_CMD(NULL) == _UDEV_BUILTIN_INVALID);
+ assert_se(PTR_TO_UDEV_BUILTIN_CMD((void*) 10000) == _UDEV_BUILTIN_INVALID);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/udev/test-udev-format.c b/src/udev/test-udev-format.c
new file mode 100644
index 0000000..d8e3808
--- /dev/null
+++ b/src/udev/test-udev-format.c
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "string-util.h"
+#include "tests.h"
+#include "udev-format.h"
+
+static void test_udev_resolve_subsys_kernel_one(const char *str, bool read_value, int retval, const char *expected) {
+ char result[PATH_MAX] = "";
+ int r;
+
+ r = udev_resolve_subsys_kernel(str, result, sizeof(result), read_value);
+ log_info("\"%s\" → expect: \"%s\", %d, actual: \"%s\", %d", str, strnull(expected), retval, result, r);
+ assert_se(r == retval);
+ if (r >= 0)
+ assert_se(streq(result, expected));
+}
+
+TEST(udev_resolve_subsys_kernel) {
+ test_udev_resolve_subsys_kernel_one("hoge", false, -EINVAL, NULL);
+ test_udev_resolve_subsys_kernel_one("[hoge", false, -EINVAL, NULL);
+ test_udev_resolve_subsys_kernel_one("[hoge/foo", false, -EINVAL, NULL);
+ test_udev_resolve_subsys_kernel_one("[hoge/]", false, -EINVAL, NULL);
+
+ test_udev_resolve_subsys_kernel_one("[net/lo]", false, 0, "/sys/devices/virtual/net/lo");
+ test_udev_resolve_subsys_kernel_one("[net/lo]/", false, 0, "/sys/devices/virtual/net/lo");
+ test_udev_resolve_subsys_kernel_one("[net/lo]hoge", false, 0, "/sys/devices/virtual/net/lo/hoge");
+ test_udev_resolve_subsys_kernel_one("[net/lo]/hoge", false, 0, "/sys/devices/virtual/net/lo/hoge");
+
+ test_udev_resolve_subsys_kernel_one("[net/lo]", true, -EINVAL, NULL);
+ test_udev_resolve_subsys_kernel_one("[net/lo]/", true, -EINVAL, NULL);
+ test_udev_resolve_subsys_kernel_one("[net/lo]hoge", true, 0, "");
+ test_udev_resolve_subsys_kernel_one("[net/lo]/hoge", true, 0, "");
+ test_udev_resolve_subsys_kernel_one("[net/lo]address", true, 0, "00:00:00:00:00:00");
+ test_udev_resolve_subsys_kernel_one("[net/lo]/address", true, 0, "00:00:00:00:00:00");
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/udev/test-udev-manager.c b/src/udev/test-udev-manager.c
new file mode 100644
index 0000000..d444b0b
--- /dev/null
+++ b/src/udev/test-udev-manager.c
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "tests.h"
+#include "udev-manager.h"
+
+TEST(devpath_conflict) {
+ assert_se(!devpath_conflict(NULL, NULL));
+ assert_se(!devpath_conflict(NULL, "/devices/pci0000:00/0000:00:1c.4"));
+ assert_se(!devpath_conflict("/devices/pci0000:00/0000:00:1c.4", NULL));
+ assert_se(!devpath_conflict("/devices/pci0000:00/0000:00:1c.4", "/devices/pci0000:00/0000:00:00.0"));
+ assert_se(!devpath_conflict("/devices/virtual/net/veth99", "/devices/virtual/net/veth999"));
+
+ assert_se(devpath_conflict("/devices/pci0000:00/0000:00:1c.4", "/devices/pci0000:00/0000:00:1c.4"));
+ assert_se(devpath_conflict("/devices/pci0000:00/0000:00:1c.4", "/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0"));
+ assert_se(devpath_conflict("/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0/nvme/nvme0/nvme0n1",
+ "/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0/nvme/nvme0/nvme0n1/nvme0n1p1"));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/udev/test-udev-node.c b/src/udev/test-udev-node.c
new file mode 100644
index 0000000..b5eaa0d
--- /dev/null
+++ b/src/udev/test-udev-node.c
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "tests.h"
+#include "udev-node.h"
+
+static void test_udev_node_escape_path_one(const char *path, const char *expected) {
+ char buf[NAME_MAX+1];
+ size_t r;
+
+ r = udev_node_escape_path(path, buf, sizeof buf);
+ log_debug("udev_node_escape_path(%s) -> %s (expected: %s)", path, buf, expected);
+ assert_se(r == strlen(expected));
+ assert_se(streq(buf, expected));
+}
+
+TEST(udev_node_escape_path) {
+ char a[NAME_MAX+1], b[NAME_MAX+1];
+
+ test_udev_node_escape_path_one("/disk/by-id/nvme-eui.1922908022470001001b448b44ccb9d6", "\\x2fdisk\\x2fby-id\\x2fnvme-eui.1922908022470001001b448b44ccb9d6");
+ test_udev_node_escape_path_one("/disk/by-id/nvme-eui.1922908022470001001b448b44ccb9d6-part1", "\\x2fdisk\\x2fby-id\\x2fnvme-eui.1922908022470001001b448b44ccb9d6-part1");
+ test_udev_node_escape_path_one("/disk/by-id/nvme-eui.1922908022470001001b448b44ccb9d6-part2", "\\x2fdisk\\x2fby-id\\x2fnvme-eui.1922908022470001001b448b44ccb9d6-part2");
+ test_udev_node_escape_path_one("/disk/by-id/nvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247", "\\x2fdisk\\x2fby-id\\x2fnvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247");
+ test_udev_node_escape_path_one("/disk/by-id/nvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247-part1", "\\x2fdisk\\x2fby-id\\x2fnvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247-part1");
+ test_udev_node_escape_path_one("/disk/by-id/nvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247-part2", "\\x2fdisk\\x2fby-id\\x2fnvme-WDC_PC_SN720_SDAQNTW-512G-1001_192290802247-part2");
+ test_udev_node_escape_path_one("/disk/by-id/usb-Generic-_SD_MMC_20120501030900000-0:0", "\\x2fdisk\\x2fby-id\\x2fusb-Generic-_SD_MMC_20120501030900000-0:0");
+
+ memset(a, 'a', sizeof(a) - 1);
+ memcpy(a, "/disk/by-id/", strlen("/disk/by-id/"));
+ char_array_0(a);
+
+ memset(b, 'a', sizeof(b) - 1);
+ memcpy(b, "\\x2fdisk\\x2fby-id\\x2f", strlen("\\x2fdisk\\x2fby-id\\x2f"));
+ strcpy(b + sizeof(b) - 12, "N3YhcCqFeID");
+
+ test_udev_node_escape_path_one(a, b);
+
+ strcpy(a + sizeof(a) - 12 - 9, "N3YhcCqFeID");
+ strcpy(b + sizeof(b) - 12, "L1oK9iKWdmi");
+ test_udev_node_escape_path_one(a, b);
+
+ strcpy(a + sizeof(a) - 12 - 9, "a");
+ strcpy(b + sizeof(b) - 12, "A7oaHBRuuZq");
+ test_udev_node_escape_path_one(a, b);
+
+ a[sizeof(a) - 12 - 9] = '\0';
+ b[sizeof(a) - 12] = '\0';
+ test_udev_node_escape_path_one(a, b);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/udev/test-udev-rule-runner.c b/src/udev/test-udev-rule-runner.c
new file mode 100644
index 0000000..72296b3
--- /dev/null
+++ b/src/udev/test-udev-rule-runner.c
@@ -0,0 +1,178 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2003-2004 Greg Kroah-Hartman <greg@kroah.com>
+***/
+
+#include <errno.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/mount.h>
+#include <sys/signalfd.h>
+#include <unistd.h>
+
+#include "device-private.h"
+#include "fs-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "mkdir-label.h"
+#include "mount-util.h"
+#include "namespace-util.h"
+#include "parse-util.h"
+#include "selinux-util.h"
+#include "signal-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "udev-event.h"
+#include "udev-spawn.h"
+#include "version.h"
+
+static int device_new_from_synthetic_event(sd_device **ret, const char *syspath, const char *action) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ sd_device_action_t a;
+ int r;
+
+ assert(ret);
+ assert(syspath);
+ assert(action);
+
+ a = device_action_from_string(action);
+ if (a < 0)
+ return a;
+
+ r = sd_device_new_from_syspath(&dev, syspath);
+ if (r < 0)
+ return r;
+
+ r = device_read_uevent_file(dev);
+ if (r < 0)
+ return r;
+
+ r = device_set_action(dev, a);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(dev);
+ return 0;
+}
+
+static int fake_filesystems(void) {
+ static const struct fakefs {
+ const char *src;
+ const char *target;
+ const char *error;
+ bool ignore_mount_error;
+ } fakefss[] = {
+ { "tmpfs/sys", "/sys", "Failed to mount test /sys", false },
+ { "tmpfs/dev", "/dev", "Failed to mount test /dev", false },
+ { "run", "/run", "Failed to mount test /run", false },
+ { "run", "/etc/udev/rules.d", "Failed to mount empty /etc/udev/rules.d", true },
+ { "run", UDEVLIBEXECDIR "/rules.d", "Failed to mount empty " UDEVLIBEXECDIR "/rules.d", true },
+ };
+ int r;
+
+ r = detach_mount_namespace();
+ if (r < 0)
+ return log_error_errno(r, "Failed to detach mount namespace: %m");
+
+ for (size_t i = 0; i < ELEMENTSOF(fakefss); i++) {
+ r = mount_nofollow_verbose(fakefss[i].ignore_mount_error ? LOG_NOTICE : LOG_ERR,
+ fakefss[i].src, fakefss[i].target, NULL, MS_BIND, NULL);
+ if (r < 0 && !fakefss[i].ignore_mount_error)
+ return r;
+ }
+
+ return 0;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_(udev_rules_freep) UdevRules *rules = NULL;
+ _cleanup_(udev_event_freep) UdevEvent *event = NULL;
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ const char *devpath, *devname, *action;
+ int r;
+
+ test_setup_logging(LOG_INFO);
+
+ if (!IN_SET(argc, 2, 3, 4))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "This program needs between one and three arguments, %d given", argc - 1);
+
+ r = fake_filesystems();
+ if (r < 0)
+ return r;
+
+ /* Let's make sure the test runs with selinux assumed disabled. */
+#if HAVE_SELINUX
+ fini_selinuxmnt();
+#endif
+ mac_selinux_retest();
+
+ if (argc == 2) {
+ if (!streq(argv[1], "check"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown argument: %s", argv[1]);
+
+ return 0;
+ }
+
+ log_debug("version %s", GIT_VERSION);
+
+ r = mac_init();
+ if (r < 0)
+ return r;
+
+ action = argv[1];
+ devpath = argv[2];
+
+ if (argv[3]) {
+ unsigned us;
+
+ r = safe_atou(argv[3], &us);
+ if (r < 0)
+ return log_error_errno(r, "Invalid delay '%s': %m", argv[3]);
+ usleep_safe(us);
+ }
+
+ assert_se(udev_rules_load(&rules, RESOLVE_NAME_EARLY) == 0);
+
+ const char *syspath = strjoina("/sys", devpath);
+ r = device_new_from_synthetic_event(&dev, syspath, action);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to open device '%s'", devpath);
+
+ assert_se(event = udev_event_new(dev, 0, NULL, log_get_max_level()));
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, SIGHUP, SIGCHLD, -1) >= 0);
+
+ /* do what devtmpfs usually provides us */
+ if (sd_device_get_devname(dev, &devname) >= 0) {
+ const char *subsystem;
+ mode_t mode = 0600;
+
+ if (sd_device_get_subsystem(dev, &subsystem) >= 0 && streq(subsystem, "block"))
+ mode |= S_IFBLK;
+ else
+ mode |= S_IFCHR;
+
+ if (!streq(action, "remove")) {
+ dev_t devnum = makedev(0, 0);
+
+ (void) mkdir_parents_label(devname, 0755);
+ (void) sd_device_get_devnum(dev, &devnum);
+ if (mknod(devname, mode, devnum) < 0)
+ return log_error_errno(errno, "mknod() failed for '%s': %m", devname);
+ } else {
+ if (unlink(devname) < 0)
+ return log_error_errno(errno, "unlink('%s') failed: %m", devname);
+ (void) rmdir_parents(devname, "/dev");
+ }
+ }
+
+ udev_event_execute_rules(event, -1, 3 * USEC_PER_SEC, SIGKILL, NULL, rules);
+ udev_event_execute_run(event, 3 * USEC_PER_SEC, SIGKILL);
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/test-udev-rules.c b/src/udev/test-udev-rules.c
new file mode 100644
index 0000000..b62b08b
--- /dev/null
+++ b/src/udev/test-udev-rules.c
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "string-util.h"
+#include "tests.h"
+#include "udev-rules.h"
+
+static void test_udev_rule_parse_value_one(const char *in, const char *expected_value, int expected_retval) {
+ _cleanup_free_ char *str = NULL;
+ char *value = UINT_TO_PTR(0x12345678U);
+ char *endpos = UINT_TO_PTR(0x87654321U);
+
+ log_info("/* %s (%s, %s, %d) */", __func__, in, strnull(expected_value), expected_retval);
+
+ assert_se(str = strdup(in));
+ assert_se(udev_rule_parse_value(str, &value, &endpos) == expected_retval);
+ if (expected_retval < 0) {
+ /* not modified on failure */
+ assert_se(value == UINT_TO_PTR(0x12345678U));
+ assert_se(endpos == UINT_TO_PTR(0x87654321U));
+ } else {
+ assert_se(streq_ptr(value, expected_value));
+ assert_se(endpos == str + strlen(in));
+ /*
+ * The return value must be terminated by two subsequent NULs
+ * so it could be safely interpreted as nulstr.
+ */
+ assert_se(value[strlen(value) + 1] == '\0');
+ }
+}
+
+TEST(udev_rule_parse_value) {
+ /* input: "valid operand"
+ * parsed: valid operand
+ * use the following command to help generate textual C strings:
+ * python3 -c 'import json; print(json.dumps(input()))' */
+ test_udev_rule_parse_value_one("\"valid operand\"", "valid operand", 0);
+ /* input: "va'l\'id\"op\"erand"
+ * parsed: va'l\'id"op"erand */
+ test_udev_rule_parse_value_one("\"va'l\\'id\\\"op\\\"erand\"", "va'l\\'id\"op\"erand", 0);
+ test_udev_rule_parse_value_one("no quotes", NULL, -EINVAL);
+ test_udev_rule_parse_value_one("\"\\\\a\\b\\x\\y\"", "\\\\a\\b\\x\\y", 0);
+ test_udev_rule_parse_value_one("\"reject\0nul\"", NULL, -EINVAL);
+ /* input: e"" */
+ test_udev_rule_parse_value_one("e\"\"", "", 0);
+ /* input: e"1234" */
+ test_udev_rule_parse_value_one("e\"1234\"", "1234", 0);
+ /* input: e"\"" */
+ test_udev_rule_parse_value_one("e\"\\\"\"", "\"", 0);
+ /* input: e"\ */
+ test_udev_rule_parse_value_one("e\"\\", NULL, -EINVAL);
+ /* input: e"\" */
+ test_udev_rule_parse_value_one("e\"\\\"", NULL, -EINVAL);
+ /* input: e"\\" */
+ test_udev_rule_parse_value_one("e\"\\\\\"", "\\", 0);
+ /* input: e"\\\" */
+ test_udev_rule_parse_value_one("e\"\\\\\\\"", NULL, -EINVAL);
+ /* input: e"\\\"" */
+ test_udev_rule_parse_value_one("e\"\\\\\\\"\"", "\\\"", 0);
+ /* input: e"\\\\" */
+ test_udev_rule_parse_value_one("e\"\\\\\\\\\"", "\\\\", 0);
+ /* input: e"operand with newline\n" */
+ test_udev_rule_parse_value_one("e\"operand with newline\\n\"", "operand with newline\n", 0);
+ /* input: e"single\rcharacter\t\aescape\bsequence" */
+ test_udev_rule_parse_value_one(
+ "e\"single\\rcharacter\\t\\aescape\\bsequence\"", "single\rcharacter\t\aescape\bsequence", 0);
+ /* input: e"reject\invalid escape sequence" */
+ test_udev_rule_parse_value_one("e\"reject\\invalid escape sequence", NULL, -EINVAL);
+ /* input: e"\ */
+ test_udev_rule_parse_value_one("e\"\\", NULL, -EINVAL);
+ /* input: "s\u1d1c\u1d04\u029c \u1d1c\u0274\u026a\u1d04\u1d0f\u1d05\u1d07 \U0001d568\U0001d560\U0001d568" */
+ test_udev_rule_parse_value_one(
+ "e\"s\\u1d1c\\u1d04\\u029c \\u1d1c\\u0274\\u026a\\u1d04\\u1d0f\\u1d05\\u1d07 \\U0001d568\\U0001d560\\U0001d568\"",
+ "s\xe1\xb4\x9c\xe1\xb4\x84\xca\x9c \xe1\xb4\x9c\xc9\xb4\xc9\xaa\xe1\xb4\x84\xe1\xb4\x8f\xe1\xb4\x85\xe1\xb4\x87 \xf0\x9d\x95\xa8\xf0\x9d\x95\xa0\xf0\x9d\x95\xa8",
+ 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/udev/test-udev-spawn.c b/src/udev/test-udev-spawn.c
new file mode 100644
index 0000000..4f43fac
--- /dev/null
+++ b/src/udev/test-udev-spawn.c
@@ -0,0 +1,113 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "path-util.h"
+#include "signal-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "udev-event.h"
+#include "udev-spawn.h"
+
+#define BUF_SIZE 1024
+
+static void test_event_spawn_core(bool with_pidfd, const char *cmd, char *result_buf, size_t buf_size) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ _cleanup_(udev_event_freep) UdevEvent *event = NULL;
+
+ assert_se(setenv("SYSTEMD_PIDFD", yes_no(with_pidfd), 1) >= 0);
+
+ assert_se(sd_device_new_from_syspath(&dev, "/sys/class/net/lo") >= 0);
+ assert_se(event = udev_event_new(dev, 0, NULL, LOG_DEBUG));
+ assert_se(udev_event_spawn(event, 5 * USEC_PER_SEC, SIGKILL, false, cmd, result_buf, buf_size, NULL) == 0);
+
+ assert_se(unsetenv("SYSTEMD_PIDFD") >= 0);
+}
+
+static void test_event_spawn_cat(bool with_pidfd, size_t buf_size) {
+ _cleanup_strv_free_ char **lines = NULL;
+ _cleanup_free_ char *cmd = NULL;
+ char result_buf[BUF_SIZE];
+
+ log_debug("/* %s(%s) */", __func__, yes_no(with_pidfd));
+
+ assert_se(find_executable("cat", &cmd) >= 0);
+ assert_se(strextend_with_separator(&cmd, " ", "/sys/class/net/lo/uevent"));
+
+ test_event_spawn_core(with_pidfd, cmd, result_buf,
+ buf_size >= BUF_SIZE ? BUF_SIZE : buf_size);
+
+ assert_se(lines = strv_split_newlines(result_buf));
+ strv_print(lines);
+
+ if (buf_size >= BUF_SIZE) {
+ assert_se(strv_contains(lines, "INTERFACE=lo"));
+ assert_se(strv_contains(lines, "IFINDEX=1"));
+ }
+}
+
+static void test_event_spawn_self(const char *self, const char *arg, bool with_pidfd) {
+ _cleanup_strv_free_ char **lines = NULL;
+ _cleanup_free_ char *cmd = NULL;
+ char result_buf[BUF_SIZE];
+
+ log_debug("/* %s(%s, %s) */", __func__, arg, yes_no(with_pidfd));
+
+ assert_se(cmd = strjoin(self, " ", arg));
+
+ test_event_spawn_core(with_pidfd, cmd, result_buf, BUF_SIZE);
+
+ assert_se(lines = strv_split_newlines(result_buf));
+ strv_print(lines);
+
+ assert_se(strv_contains(lines, "aaa"));
+ assert_se(strv_contains(lines, "bbb"));
+}
+
+static void test1(void) {
+ fprintf(stdout, "aaa\nbbb");
+ fprintf(stderr, "ccc\nddd");
+}
+
+static void test2(void) {
+ char buf[16384];
+
+ fprintf(stdout, "aaa\nbbb");
+
+ memset(buf, 'a', sizeof(buf) - 1);
+ char_array_0(buf);
+ fputs(buf, stderr);
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_free_ char *self = NULL;
+
+ if (argc > 1) {
+ if (streq(argv[1], "test1"))
+ test1();
+ else if (streq(argv[1], "test2"))
+ test2();
+ else
+ assert_not_reached();
+
+ return 0;
+ }
+
+ test_setup_logging(LOG_DEBUG);
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0);
+
+ test_event_spawn_cat(true, SIZE_MAX);
+ test_event_spawn_cat(false, SIZE_MAX);
+ test_event_spawn_cat(true, 5);
+ test_event_spawn_cat(false, 5);
+
+ assert_se(path_make_absolute_cwd(argv[0], &self) >= 0);
+ path_simplify(self);
+
+ test_event_spawn_self(self, "test1", true);
+ test_event_spawn_self(self, "test1", false);
+
+ test_event_spawn_self(self, "test2", true);
+ test_event_spawn_self(self, "test2", false);
+
+ return 0;
+}
diff --git a/src/udev/udev-builtin-blkid.c b/src/udev/udev-builtin-blkid.c
new file mode 100644
index 0000000..11419a3
--- /dev/null
+++ b/src/udev/udev-builtin-blkid.c
@@ -0,0 +1,474 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * probe disks for filesystems and partitions
+ *
+ * Copyright © 2011 Karel Zak <kzak@redhat.com>
+ */
+
+#if HAVE_VALGRIND_MEMCHECK_H
+#include <valgrind/memcheck.h>
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <linux/loop.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "blkid-util.h"
+#include "device-util.h"
+#include "devnum-util.h"
+#include "efi-loader.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "gpt.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+
+static void print_property(sd_device *dev, bool test, const char *name, const char *value) {
+ char s[256];
+
+ s[0] = '\0';
+
+ if (streq(name, "TYPE")) {
+ udev_builtin_add_property(dev, test, "ID_FS_TYPE", value);
+
+ } else if (streq(name, "USAGE")) {
+ udev_builtin_add_property(dev, test, "ID_FS_USAGE", value);
+
+ } else if (streq(name, "VERSION")) {
+ udev_builtin_add_property(dev, test, "ID_FS_VERSION", value);
+
+ } else if (streq(name, "UUID")) {
+ blkid_safe_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_UUID", s);
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_UUID_ENC", s);
+
+ } else if (streq(name, "UUID_SUB")) {
+ blkid_safe_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_UUID_SUB", s);
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_UUID_SUB_ENC", s);
+
+ } else if (streq(name, "LABEL")) {
+ blkid_safe_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_LABEL", s);
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_LABEL_ENC", s);
+
+ } else if (STR_IN_SET(name, "FSSIZE", "FSLASTBLOCK", "FSBLOCKSIZE")) {
+ strscpyl(s, sizeof(s), "ID_FS_", name + 2, NULL);
+ udev_builtin_add_property(dev, test, s, value);
+
+ } else if (streq(name, "PTTYPE")) {
+ udev_builtin_add_property(dev, test, "ID_PART_TABLE_TYPE", value);
+
+ } else if (streq(name, "PTUUID")) {
+ udev_builtin_add_property(dev, test, "ID_PART_TABLE_UUID", value);
+
+ } else if (streq(name, "PART_ENTRY_NAME")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_PART_ENTRY_NAME", s);
+
+ } else if (streq(name, "PART_ENTRY_TYPE")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_PART_ENTRY_TYPE", s);
+
+ } else if (startswith(name, "PART_ENTRY_")) {
+ strscpyl(s, sizeof(s), "ID_", name, NULL);
+ udev_builtin_add_property(dev, test, s, value);
+
+ } else if (streq(name, "SYSTEM_ID")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_SYSTEM_ID", s);
+
+ } else if (streq(name, "PUBLISHER_ID")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_PUBLISHER_ID", s);
+
+ } else if (streq(name, "APPLICATION_ID")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_APPLICATION_ID", s);
+
+ } else if (streq(name, "BOOT_SYSTEM_ID")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_BOOT_SYSTEM_ID", s);
+
+ } else if (streq(name, "VOLUME_ID")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_VOLUME_ID", s);
+
+ } else if (streq(name, "LOGICAL_VOLUME_ID")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_LOGICAL_VOLUME_ID", s);
+
+ } else if (streq(name, "VOLUME_SET_ID")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_VOLUME_SET_ID", s);
+
+ } else if (streq(name, "DATA_PREPARER_ID")) {
+ blkid_encode_string(value, s, sizeof(s));
+ udev_builtin_add_property(dev, test, "ID_FS_DATA_PREPARER_ID", s);
+ }
+}
+
+static int find_gpt_root(sd_device *dev, blkid_probe pr, bool test) {
+
+#if defined(SD_GPT_ROOT_NATIVE) && ENABLE_EFI
+
+ _cleanup_free_ char *root_label = NULL;
+ bool found_esp_or_xbootldr = false;
+ sd_id128_t root_id = SD_ID128_NULL;
+ int r;
+
+ assert(pr);
+
+ /* Iterate through the partitions on this disk, and see if the UEFI ESP or XBOOTLDR partition we
+ * booted from is on it. If so, find the first root disk, and add a property indicating its partition
+ * UUID. */
+
+ errno = 0;
+ blkid_partlist pl = blkid_probe_get_partitions(pr);
+ if (!pl)
+ return errno_or_else(ENOMEM);
+
+ int nvals = blkid_partlist_numof_partitions(pl);
+ for (int i = 0; i < nvals; i++) {
+ blkid_partition pp;
+ const char *label;
+ sd_id128_t type, id;
+
+ pp = blkid_partlist_get_partition(pl, i);
+ if (!pp)
+ continue;
+
+ r = blkid_partition_get_uuid_id128(pp, &id);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to get partition UUID, ignoring: %m");
+ continue;
+ }
+
+ r = blkid_partition_get_type_id128(pp, &type);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to get partition type UUID, ignoring: %m");
+ continue;
+ }
+
+ label = blkid_partition_get_name(pp); /* returns NULL if empty */
+
+ if (sd_id128_in_set(type, SD_GPT_ESP, SD_GPT_XBOOTLDR)) {
+ sd_id128_t esp_or_xbootldr;
+
+ /* We found an ESP or XBOOTLDR, let's see if it matches the ESP/XBOOTLDR we booted from. */
+
+ r = efi_loader_get_device_part_uuid(&esp_or_xbootldr);
+ if (r < 0)
+ return r;
+
+ if (sd_id128_equal(id, esp_or_xbootldr))
+ found_esp_or_xbootldr = true;
+
+ } else if (sd_id128_equal(type, SD_GPT_ROOT_NATIVE)) {
+ unsigned long long flags;
+
+ flags = blkid_partition_get_flags(pp);
+ if (flags & SD_GPT_FLAG_NO_AUTO)
+ continue;
+
+ /* We found a suitable root partition, let's remember the first one, or the one with
+ * the newest version, as determined by comparing the partition labels. */
+
+ if (sd_id128_is_null(root_id) || strverscmp_improved(label, root_label) > 0) {
+ root_id = id;
+
+ r = free_and_strdup(&root_label, label);
+ if (r < 0)
+ return r;
+ }
+ }
+ }
+
+ /* We found the ESP/XBOOTLDR on this disk, and also found a root partition, nice! Let's export its
+ * UUID */
+ if (found_esp_or_xbootldr && !sd_id128_is_null(root_id))
+ udev_builtin_add_property(dev, test, "ID_PART_GPT_AUTO_ROOT_UUID", SD_ID128_TO_UUID_STRING(root_id));
+#endif
+
+ return 0;
+}
+
+static int probe_superblocks(blkid_probe pr) {
+ struct stat st;
+ int rc;
+
+ /* TODO: Return negative errno. */
+
+ if (fstat(blkid_probe_get_fd(pr), &st))
+ return -errno;
+
+ blkid_probe_enable_partitions(pr, 1);
+
+ if (!S_ISCHR(st.st_mode) &&
+ blkid_probe_get_size(pr) <= 1024 * 1440 &&
+ blkid_probe_is_wholedisk(pr)) {
+ /*
+ * check if the small disk is partitioned, if yes then
+ * don't probe for filesystems.
+ */
+ blkid_probe_enable_superblocks(pr, 0);
+
+ rc = blkid_do_fullprobe(pr);
+ if (rc < 0)
+ return rc; /* -1 = error, 1 = nothing, 0 = success */
+
+ if (blkid_probe_lookup_value(pr, "PTTYPE", NULL, NULL) == 0)
+ return 0; /* partition table detected */
+ }
+
+ blkid_probe_set_partitions_flags(pr, BLKID_PARTS_ENTRY_DETAILS);
+ blkid_probe_enable_superblocks(pr, 1);
+
+ return blkid_do_safeprobe(pr);
+}
+
+static int read_loopback_backing_inode(
+ sd_device *dev,
+ int fd,
+ dev_t *ret_devno,
+ ino_t *ret_inode,
+ char **ret_fname) {
+
+ _cleanup_free_ char *fn = NULL;
+ struct loop_info64 info;
+ const char *name;
+ int r;
+
+ assert(dev);
+ assert(fd >= 0);
+ assert(ret_devno);
+ assert(ret_inode);
+ assert(ret_fname);
+
+ /* Retrieves various fields of the current loopback device backing file, so that we can ultimately
+ * use it to create stable symlinks to loopback block devices, based on what they are backed by. We
+ * pick up inode/device as well as file name field. Note that we pick up the "lo_file_name" field
+ * here, which is an arbitrary free-form string provided by userspace. We do not return the sysfs
+ * attribute loop/backing_file here, because that is directly accessible from udev rules anyway. And
+ * sometimes, depending on context, it's a good thing to return the string userspace can freely pick
+ * over the string automatically generated by the kernel. */
+
+ r = sd_device_get_sysname(dev, &name);
+ if (r < 0)
+ return r;
+
+ if (!startswith(name, "loop"))
+ goto notloop;
+
+ if (ioctl(fd, LOOP_GET_STATUS64, &info) < 0) {
+ if (ERRNO_IS_NOT_SUPPORTED(errno))
+ goto notloop;
+
+ return -errno;
+ }
+
+#if HAVE_VALGRIND_MEMCHECK_H
+ VALGRIND_MAKE_MEM_DEFINED(&info, sizeof(info));
+#endif
+
+ if (isempty((char*) info.lo_file_name) ||
+ strnlen((char*) info.lo_file_name, sizeof(info.lo_file_name)-1) == sizeof(info.lo_file_name)-1)
+ /* Don't pick up file name if it is unset or possibly truncated. (Note: the kernel silently
+ * truncates the string passed from userspace by LOOP_SET_STATUS64 ioctl. See
+ * loop_set_status_from_info() in drivers/block/loop.c. Hence, we can't really know the file
+ * name is truncated if it uses sizeof(info.lo_file_name)-1 as length; it could also mean the
+ * string is just that long and wasn't truncated — but the fact is simply that we cannot know
+ * in that case if it was truncated or not. Thus, we assume the worst and suppress — at least
+ * for now. For shorter strings we know for sure it wasn't truncated, hence that's always
+ * safe.) */
+ fn = NULL;
+ else {
+ fn = memdup_suffix0(info.lo_file_name, sizeof(info.lo_file_name));
+ if (!fn)
+ return -ENOMEM;
+ }
+
+ *ret_inode = info.lo_inode;
+ *ret_devno = info.lo_device;
+ *ret_fname = TAKE_PTR(fn);
+ return 1;
+
+
+notloop:
+ *ret_devno = 0;
+ *ret_inode = 0;
+ *ret_fname = NULL;
+ return 0;
+}
+
+static int builtin_blkid(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ const char *devnode, *root_partition = NULL, *data, *name;
+ _cleanup_(blkid_free_probep) blkid_probe pr = NULL;
+ _cleanup_free_ char *backing_fname = NULL;
+ bool noraid = false, is_gpt = false;
+ _cleanup_close_ int fd = -EBADF;
+ ino_t backing_inode = 0;
+ dev_t backing_devno = 0;
+ int64_t offset = 0;
+ int r;
+
+ static const struct option options[] = {
+ { "offset", required_argument, NULL, 'o' },
+ { "hint", required_argument, NULL, 'H' },
+ { "noraid", no_argument, NULL, 'R' },
+ {}
+ };
+
+ errno = 0;
+ pr = blkid_new_probe();
+ if (!pr)
+ return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to create blkid prober: %m");
+
+ for (;;) {
+ int option;
+
+ option = getopt_long(argc, argv, "o:H:R", options, NULL);
+ if (option == -1)
+ break;
+
+ switch (option) {
+ case 'H':
+#if HAVE_BLKID_PROBE_SET_HINT
+ errno = 0;
+ r = blkid_probe_set_hint(pr, optarg, 0);
+ if (r < 0)
+ return log_device_error_errno(dev, errno_or_else(ENOMEM), "Failed to use '%s' probing hint: %m", optarg);
+ break;
+#else
+ /* Use the hint <name>=<offset> as probing offset for old versions */
+ optarg = strchr(optarg, '=');
+ if (!optarg)
+ /* no value means 0, do nothing for old versions */
+ break;
+ ++optarg;
+ _fallthrough_;
+#endif
+ case 'o':
+ r = safe_atoi64(optarg, &offset);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to parse '%s' as an integer: %m", optarg);
+ if (offset < 0)
+ return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invalid offset %"PRIi64": %m", offset);
+ break;
+ case 'R':
+ noraid = true;
+ break;
+ }
+ }
+
+ blkid_probe_set_superblocks_flags(pr,
+ BLKID_SUBLKS_LABEL | BLKID_SUBLKS_UUID |
+ BLKID_SUBLKS_TYPE | BLKID_SUBLKS_SECTYPE |
+#ifdef BLKID_SUBLKS_FSINFO
+ BLKID_SUBLKS_FSINFO |
+#endif
+ BLKID_SUBLKS_USAGE | BLKID_SUBLKS_VERSION);
+
+ if (noraid)
+ blkid_probe_filter_superblocks_usage(pr, BLKID_FLTR_NOTIN, BLKID_USAGE_RAID);
+
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device name: %m");
+
+ fd = sd_device_open(dev, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0) {
+ bool ignore = ERRNO_IS_DEVICE_ABSENT(fd);
+ log_device_debug_errno(dev, fd, "Failed to open block device %s%s: %m",
+ devnode, ignore ? ", ignoring" : "");
+ return ignore ? 0 : fd;
+ }
+
+ errno = 0;
+ r = blkid_probe_set_device(pr, fd, offset, 0);
+ if (r < 0)
+ return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to set device to blkid prober: %m");
+
+ log_device_debug(dev, "Probe %s with %sraid and offset=%"PRIi64, devnode, noraid ? "no" : "", offset);
+
+ r = probe_superblocks(pr);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to probe superblocks: %m");
+
+ /* If the device is a partition then its parent passed the root partition UUID to the device */
+ (void) sd_device_get_property_value(dev, "ID_PART_GPT_AUTO_ROOT_UUID", &root_partition);
+
+ errno = 0;
+ int nvals = blkid_probe_numof_values(pr);
+ if (nvals < 0)
+ return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to get number of probed values: %m");
+
+ for (int i = 0; i < nvals; i++) {
+ if (blkid_probe_get_value(pr, i, &name, &data, NULL) < 0)
+ continue;
+
+ print_property(dev, test, name, data);
+
+ /* Is this a disk with GPT partition table? */
+ if (streq(name, "PTTYPE") && streq(data, "gpt"))
+ is_gpt = true;
+
+ /* Is this a partition that matches the root partition
+ * property inherited from the parent? */
+ if (root_partition && streq(name, "PART_ENTRY_UUID") && streq(data, root_partition))
+ udev_builtin_add_property(dev, test, "ID_PART_GPT_AUTO_ROOT", "1");
+ }
+
+ if (is_gpt)
+ find_gpt_root(dev, pr, test);
+
+ r = read_loopback_backing_inode(
+ dev,
+ fd,
+ &backing_devno,
+ &backing_inode,
+ &backing_fname);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to read loopback backing inode, ignoring: %m");
+ else if (r > 0) {
+ udev_builtin_add_propertyf(dev, test, "ID_LOOP_BACKING_DEVICE", DEVNUM_FORMAT_STR, DEVNUM_FORMAT_VAL(backing_devno));
+ udev_builtin_add_propertyf(dev, test, "ID_LOOP_BACKING_INODE", "%" PRIu64, (uint64_t) backing_inode);
+
+ if (backing_fname) {
+ /* In the worst case blkid_encode_string() will blow up to 4x the string
+ * length. Hence size the buffer to 4x of the longest string
+ * read_loopback_backing_inode() might return */
+ char encoded[sizeof_field(struct loop_info64, lo_file_name) * 4 + 1];
+
+ assert(strlen(backing_fname) < ELEMENTSOF(encoded) / 4);
+ blkid_encode_string(backing_fname, encoded, ELEMENTSOF(encoded));
+
+ udev_builtin_add_property(dev, test, "ID_LOOP_BACKING_FILENAME", backing_fname);
+ udev_builtin_add_property(dev, test, "ID_LOOP_BACKING_FILENAME_ENC", encoded);
+ }
+ }
+
+ return 0;
+}
+
+const UdevBuiltin udev_builtin_blkid = {
+ .name = "blkid",
+ .cmd = builtin_blkid,
+ .help = "Filesystem and partition probing",
+ .run_once = true,
+};
diff --git a/src/udev/udev-builtin-btrfs.c b/src/udev/udev-builtin-btrfs.c
new file mode 100644
index 0000000..9b12aeb
--- /dev/null
+++ b/src/udev/udev-builtin-btrfs.c
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <linux/btrfs.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+
+#include "device-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+
+static int builtin_btrfs(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ struct btrfs_ioctl_vol_args args = {};
+ _cleanup_close_ int fd = -EBADF;
+ int r;
+
+ if (argc != 3 || !streq(argv[1], "ready"))
+ return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invalid arguments");
+
+ fd = open("/dev/btrfs-control", O_RDWR|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0) {
+ if (ERRNO_IS_DEVICE_ABSENT(errno)) {
+ /* Driver not installed? Then we aren't ready. This is useful in initrds that lack
+ * btrfs.ko. After the host transition (where btrfs.ko will hopefully become
+ * available) the device can be retriggered and will then be considered ready. */
+ udev_builtin_add_property(dev, test, "ID_BTRFS_READY", "0");
+ return 0;
+ }
+
+ return log_device_debug_errno(dev, errno, "Failed to open /dev/btrfs-control: %m");
+ }
+
+ strscpy(args.name, sizeof(args.name), argv[2]);
+ r = ioctl(fd, BTRFS_IOC_DEVICES_READY, &args);
+ if (r < 0)
+ return log_device_debug_errno(dev, errno, "Failed to call BTRFS_IOC_DEVICES_READY: %m");
+
+ udev_builtin_add_property(dev, test, "ID_BTRFS_READY", one_zero(r == 0));
+ return 0;
+}
+
+const UdevBuiltin udev_builtin_btrfs = {
+ .name = "btrfs",
+ .cmd = builtin_btrfs,
+ .help = "btrfs volume management",
+};
diff --git a/src/udev/udev-builtin-hwdb.c b/src/udev/udev-builtin-hwdb.c
new file mode 100644
index 0000000..19e07e7
--- /dev/null
+++ b/src/udev/udev-builtin-hwdb.c
@@ -0,0 +1,227 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <fnmatch.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "sd-hwdb.h"
+
+#include "alloc-util.h"
+#include "device-util.h"
+#include "hwdb-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "udev-builtin.h"
+
+static sd_hwdb *hwdb;
+
+int udev_builtin_hwdb_lookup(sd_device *dev,
+ const char *prefix, const char *modalias,
+ const char *filter, bool test) {
+ _cleanup_free_ char *lookup = NULL;
+ const char *key, *value;
+ int n = 0, r;
+
+ if (!hwdb)
+ return -ENOENT;
+
+ if (prefix) {
+ lookup = strjoin(prefix, modalias);
+ if (!lookup)
+ return -ENOMEM;
+ modalias = lookup;
+ }
+
+ SD_HWDB_FOREACH_PROPERTY(hwdb, modalias, key, value) {
+ if (filter && fnmatch(filter, key, FNM_NOESCAPE) != 0)
+ continue;
+
+ r = udev_builtin_add_property(dev, test, key, value);
+ if (r < 0)
+ return r;
+ n++;
+ }
+ return n;
+}
+
+static const char *modalias_usb(sd_device *dev, char *s, size_t size) {
+ const char *v, *p, *n = NULL;
+ uint16_t vn, pn;
+
+ if (sd_device_get_sysattr_value(dev, "idVendor", &v) < 0)
+ return NULL;
+ if (sd_device_get_sysattr_value(dev, "idProduct", &p) < 0)
+ return NULL;
+ if (safe_atoux16(v, &vn) < 0)
+ return NULL;
+ if (safe_atoux16(p, &pn) < 0)
+ return NULL;
+ (void) sd_device_get_sysattr_value(dev, "product", &n);
+
+ (void) snprintf(s, size, "usb:v%04Xp%04X:%s", vn, pn, strempty(n));
+ return s;
+}
+
+static int udev_builtin_hwdb_search(sd_device *dev, sd_device *srcdev,
+ const char *subsystem, const char *prefix,
+ const char *filter, bool test) {
+ char s[LINE_MAX];
+ bool last = false;
+ int r = 0;
+
+ assert(dev);
+
+ if (!srcdev)
+ srcdev = dev;
+
+ for (sd_device *d = srcdev; d; ) {
+ const char *dsubsys, *devtype, *modalias = NULL;
+
+ if (sd_device_get_subsystem(d, &dsubsys) < 0)
+ goto next;
+
+ /* look only at devices of a specific subsystem */
+ if (subsystem && !streq(dsubsys, subsystem))
+ goto next;
+
+ (void) sd_device_get_property_value(d, "MODALIAS", &modalias);
+
+ if (streq(dsubsys, "usb") &&
+ sd_device_get_devtype(d, &devtype) >= 0 &&
+ streq(devtype, "usb_device")) {
+ /* if the usb_device does not have a modalias, compose one */
+ if (!modalias)
+ modalias = modalias_usb(d, s, sizeof(s));
+
+ /* avoid looking at any parent device, they are usually just a USB hub */
+ last = true;
+ }
+
+ if (!modalias)
+ goto next;
+
+ log_device_debug(dev, "hwdb modalias key: \"%s\"", modalias);
+
+ r = udev_builtin_hwdb_lookup(dev, prefix, modalias, filter, test);
+ if (r > 0)
+ break;
+
+ if (last)
+ break;
+next:
+ if (sd_device_get_parent(d, &d) < 0)
+ break;
+ }
+
+ return r;
+}
+
+static int builtin_hwdb(UdevEvent *event, int argc, char *argv[], bool test) {
+ static const struct option options[] = {
+ { "filter", required_argument, NULL, 'f' },
+ { "device", required_argument, NULL, 'd' },
+ { "subsystem", required_argument, NULL, 's' },
+ { "lookup-prefix", required_argument, NULL, 'p' },
+ {}
+ };
+ const char *filter = NULL;
+ const char *device = NULL;
+ const char *subsystem = NULL;
+ const char *prefix = NULL;
+ _cleanup_(sd_device_unrefp) sd_device *srcdev = NULL;
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ int r;
+
+ if (!hwdb)
+ return -EINVAL;
+
+ for (;;) {
+ int option;
+
+ option = getopt_long(argc, argv, "f:d:s:p:", options, NULL);
+ if (option == -1)
+ break;
+
+ switch (option) {
+ case 'f':
+ filter = optarg;
+ break;
+
+ case 'd':
+ device = optarg;
+ break;
+
+ case 's':
+ subsystem = optarg;
+ break;
+
+ case 'p':
+ prefix = optarg;
+ break;
+ }
+ }
+
+ /* query a specific key given as argument */
+ if (argv[optind]) {
+ r = udev_builtin_hwdb_lookup(dev, prefix, argv[optind], filter, test);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to look up hwdb: %m");
+ if (r == 0)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENODATA), "No entry found from hwdb.");
+ return r;
+ }
+
+ /* read data from another device than the device we will store the data */
+ if (device) {
+ r = sd_device_new_from_device_id(&srcdev, device);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to create sd_device object '%s': %m", device);
+ }
+
+ r = udev_builtin_hwdb_search(dev, srcdev, subsystem, prefix, filter, test);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to look up hwdb: %m");
+ if (r == 0)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENODATA), "No entry found from hwdb.");
+ return r;
+}
+
+/* called at udev startup and reload */
+static int builtin_hwdb_init(void) {
+ int r;
+
+ if (hwdb)
+ return 0;
+
+ r = sd_hwdb_new(&hwdb);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+/* called on udev shutdown and reload request */
+static void builtin_hwdb_exit(void) {
+ hwdb = sd_hwdb_unref(hwdb);
+}
+
+/* called every couple of seconds during event activity; 'true' if config has changed */
+static bool builtin_hwdb_should_reload(void) {
+ if (hwdb_should_reload(hwdb)) {
+ log_debug("hwdb needs reloading.");
+ return true;
+ }
+
+ return false;
+}
+
+const UdevBuiltin udev_builtin_hwdb = {
+ .name = "hwdb",
+ .cmd = builtin_hwdb,
+ .init = builtin_hwdb_init,
+ .exit = builtin_hwdb_exit,
+ .should_reload = builtin_hwdb_should_reload,
+ .help = "Hardware database",
+};
diff --git a/src/udev/udev-builtin-input_id.c b/src/udev/udev-builtin-input_id.c
new file mode 100644
index 0000000..295e8d2
--- /dev/null
+++ b/src/udev/udev-builtin-input_id.c
@@ -0,0 +1,433 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * expose input properties via udev
+ *
+ * Portions Copyright © 2004 David Zeuthen, <david@fubar.dk>
+ * Copyright © 2014 Carlos Garnacho <carlosg@gnome.org>
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <linux/limits.h>
+
+#include "device-util.h"
+#include "fd-util.h"
+#include "missing_input.h"
+#include "parse-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "udev-builtin.h"
+
+/* we must use this kernel-compatible implementation */
+#define BITS_PER_LONG (sizeof(unsigned long) * 8)
+#define NBITS(x) ((((x)-1)/BITS_PER_LONG)+1)
+#define OFF(x) ((x)%BITS_PER_LONG)
+#define BIT(x) (1UL<<OFF(x))
+#define LONG(x) ((x)/BITS_PER_LONG)
+#define test_bit(bit, array) ((array[LONG(bit)] >> OFF(bit)) & 1)
+
+struct range {
+ unsigned start;
+ unsigned end;
+};
+
+/* key code ranges above BTN_MISC (start is inclusive, stop is exclusive) */
+static const struct range high_key_blocks[] = {
+ { KEY_OK, BTN_DPAD_UP },
+ { KEY_ALS_TOGGLE, BTN_TRIGGER_HAPPY }
+};
+
+static int abs_size_mm(const struct input_absinfo *absinfo) {
+ /* Resolution is defined to be in units/mm for ABS_X/Y */
+ return (absinfo->maximum - absinfo->minimum) / absinfo->resolution;
+}
+
+static void extract_info(sd_device *dev, bool test) {
+ char width[DECIMAL_STR_MAX(int)], height[DECIMAL_STR_MAX(int)];
+ struct input_absinfo xabsinfo = {}, yabsinfo = {};
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = sd_device_open(dev, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0)
+ return;
+
+ if (ioctl(fd, EVIOCGABS(ABS_X), &xabsinfo) < 0 ||
+ ioctl(fd, EVIOCGABS(ABS_Y), &yabsinfo) < 0)
+ return;
+
+ if (xabsinfo.resolution <= 0 || yabsinfo.resolution <= 0)
+ return;
+
+ xsprintf(width, "%d", abs_size_mm(&xabsinfo));
+ xsprintf(height, "%d", abs_size_mm(&yabsinfo));
+
+ udev_builtin_add_property(dev, test, "ID_INPUT_WIDTH_MM", width);
+ udev_builtin_add_property(dev, test, "ID_INPUT_HEIGHT_MM", height);
+}
+
+/*
+ * Read a capability attribute and return bitmask.
+ * @param dev sd_device
+ * @param attr sysfs attribute name (e. g. "capabilities/key")
+ * @param bitmask: Output array which has a sizeof of bitmask_size
+ */
+static void get_cap_mask(sd_device *pdev, const char* attr,
+ unsigned long *bitmask, size_t bitmask_size,
+ bool test) {
+ const char *v;
+ char text[4096];
+ unsigned i;
+ char* word;
+ unsigned long val;
+ int r;
+
+ if (sd_device_get_sysattr_value(pdev, attr, &v) < 0)
+ v = "";
+
+ xsprintf(text, "%s", v);
+ log_device_debug(pdev, "%s raw kernel attribute: %s", attr, text);
+
+ memzero(bitmask, bitmask_size);
+ i = 0;
+ while ((word = strrchr(text, ' '))) {
+ r = safe_atolu_full(word+1, 16, &val);
+ if (r < 0)
+ log_device_debug_errno(pdev, r, "Ignoring %s block which failed to parse: %m", attr);
+ else if (i < bitmask_size / sizeof(unsigned long))
+ bitmask[i] = val;
+ else
+ log_device_debug(pdev, "Ignoring %s block %lX which is larger than maximum size", attr, val);
+ *word = '\0';
+ i++;
+ }
+ r = safe_atolu_full(text, 16, &val);
+ if (r < 0)
+ log_device_debug_errno(pdev, r, "Ignoring %s block which failed to parse: %m", attr);
+ else if (i < bitmask_size / sizeof(unsigned long))
+ bitmask[i] = val;
+ else
+ log_device_debug(pdev, "Ignoring %s block %lX which is larger than maximum size", attr, val);
+
+ if (test && DEBUG_LOGGING) {
+ log_device_debug(pdev, "%s decoded bit map:", attr);
+
+ val = bitmask_size / sizeof (unsigned long);
+ /* skip trailing zeros */
+ while (bitmask[val-1] == 0 && val > 0)
+ --val;
+
+ /* IN_SET() cannot be used in assert_cc(). */
+ assert_cc(sizeof(unsigned long) == 4 || sizeof(unsigned long) == 8);
+ for (unsigned long j = 0; j < val; j++)
+ log_device_debug(pdev,
+ sizeof(unsigned long) == 4 ? " bit %4lu: %08lX\n" : " bit %4lu: %016lX\n",
+ j * BITS_PER_LONG, bitmask[j]);
+ }
+}
+
+static struct input_id get_input_id(sd_device *dev) {
+ const char *v;
+ struct input_id id = {};
+
+ if (sd_device_get_sysattr_value(dev, "id/bustype", &v) >= 0)
+ (void) safe_atoux16(v, &id.bustype);
+ if (sd_device_get_sysattr_value(dev, "id/vendor", &v) >= 0)
+ (void) safe_atoux16(v, &id.vendor);
+ if (sd_device_get_sysattr_value(dev, "id/product", &v) >= 0)
+ (void) safe_atoux16(v, &id.product);
+ if (sd_device_get_sysattr_value(dev, "id/version", &v) >= 0)
+ (void) safe_atoux16(v, &id.version);
+
+ return id;
+}
+
+/* pointer devices */
+static bool test_pointers(sd_device *dev,
+ const struct input_id *id,
+ const unsigned long* bitmask_ev,
+ const unsigned long* bitmask_abs,
+ const unsigned long* bitmask_key,
+ const unsigned long* bitmask_rel,
+ const unsigned long* bitmask_props,
+ bool test) {
+ bool has_abs_coordinates = false;
+ bool has_rel_coordinates = false;
+ bool has_mt_coordinates = false;
+ size_t num_joystick_axes = 0;
+ size_t num_joystick_buttons = 0;
+ bool has_pad_buttons = false;
+ bool is_direct = false;
+ bool has_touch = false;
+ bool has_3d_coordinates = false;
+ bool has_keys = false;
+ bool has_stylus = false;
+ bool has_pen = false;
+ bool finger_but_no_pen = false;
+ bool has_mouse_button = false;
+ bool is_mouse = false;
+ bool is_abs_mouse = false;
+ bool is_touchpad = false;
+ bool is_touchscreen = false;
+ bool is_tablet = false;
+ bool is_tablet_pad = false;
+ bool is_joystick = false;
+ bool is_accelerometer = false;
+ bool is_pointing_stick = false;
+ bool has_wheel = false;
+
+ has_keys = test_bit(EV_KEY, bitmask_ev);
+ has_abs_coordinates = test_bit(ABS_X, bitmask_abs) && test_bit(ABS_Y, bitmask_abs);
+ has_3d_coordinates = has_abs_coordinates && test_bit(ABS_Z, bitmask_abs);
+ is_accelerometer = test_bit(INPUT_PROP_ACCELEROMETER, bitmask_props);
+
+ if (!has_keys && has_3d_coordinates)
+ is_accelerometer = true;
+
+ if (is_accelerometer) {
+ udev_builtin_add_property(dev, test, "ID_INPUT_ACCELEROMETER", "1");
+ return true;
+ }
+
+ is_pointing_stick = test_bit(INPUT_PROP_POINTING_STICK, bitmask_props);
+ has_stylus = test_bit(BTN_STYLUS, bitmask_key);
+ has_pen = test_bit(BTN_TOOL_PEN, bitmask_key);
+ finger_but_no_pen = test_bit(BTN_TOOL_FINGER, bitmask_key) && !test_bit(BTN_TOOL_PEN, bitmask_key);
+ for (int button = BTN_MOUSE; button < BTN_JOYSTICK && !has_mouse_button; button++)
+ has_mouse_button = test_bit(button, bitmask_key);
+ has_rel_coordinates = test_bit(EV_REL, bitmask_ev) && test_bit(REL_X, bitmask_rel) && test_bit(REL_Y, bitmask_rel);
+ has_mt_coordinates = test_bit(ABS_MT_POSITION_X, bitmask_abs) && test_bit(ABS_MT_POSITION_Y, bitmask_abs);
+
+ /* unset has_mt_coordinates if devices claims to have all abs axis */
+ if (has_mt_coordinates && test_bit(ABS_MT_SLOT, bitmask_abs) && test_bit(ABS_MT_SLOT - 1, bitmask_abs))
+ has_mt_coordinates = false;
+ is_direct = test_bit(INPUT_PROP_DIRECT, bitmask_props);
+ has_touch = test_bit(BTN_TOUCH, bitmask_key);
+ has_pad_buttons = test_bit(BTN_0, bitmask_key) && test_bit(BTN_1, bitmask_key) && !has_pen;
+ has_wheel = test_bit(EV_REL, bitmask_ev) && (test_bit(REL_WHEEL, bitmask_rel) || test_bit(REL_HWHEEL, bitmask_rel));
+
+ /* joysticks don't necessarily have buttons; e. g.
+ * rudders/pedals are joystick-like, but buttonless; they have
+ * other fancy axes. Others have buttons only but no axes.
+ *
+ * The BTN_JOYSTICK range starts after the mouse range, so a mouse
+ * with more than 16 buttons runs into the joystick range (e.g. Mad
+ * Catz Mad Catz M.M.O.TE). Skip those.
+ */
+ if (!test_bit(BTN_JOYSTICK - 1, bitmask_key)) {
+ for (int button = BTN_JOYSTICK; button < BTN_DIGI; button++)
+ if (test_bit(button, bitmask_key))
+ num_joystick_buttons++;
+ for (int button = BTN_TRIGGER_HAPPY1; button <= BTN_TRIGGER_HAPPY40; button++)
+ if (test_bit(button, bitmask_key))
+ num_joystick_buttons++;
+ for (int button = BTN_DPAD_UP; button <= BTN_DPAD_RIGHT; button++)
+ if (test_bit(button, bitmask_key))
+ num_joystick_buttons++;
+ }
+ for (int axis = ABS_RX; axis < ABS_PRESSURE; axis++)
+ if (test_bit(axis, bitmask_abs))
+ num_joystick_axes++;
+
+ if (has_abs_coordinates) {
+ if (has_stylus || has_pen)
+ is_tablet = true;
+ else if (finger_but_no_pen && !is_direct)
+ is_touchpad = true;
+ else if (has_mouse_button)
+ /* This path is taken by VMware's USB mouse, which has
+ * absolute axes, but no touch/pressure button. */
+ is_abs_mouse = true;
+ else if (has_touch || is_direct)
+ is_touchscreen = true;
+ else if (num_joystick_buttons > 0 || num_joystick_axes > 0)
+ is_joystick = true;
+ } else if (num_joystick_buttons > 0 || num_joystick_axes > 0)
+ is_joystick = true;
+
+ if (has_mt_coordinates) {
+ if (has_stylus || has_pen)
+ is_tablet = true;
+ else if (finger_but_no_pen && !is_direct)
+ is_touchpad = true;
+ else if (has_touch || is_direct)
+ is_touchscreen = true;
+ }
+
+ if (is_tablet && has_pad_buttons)
+ is_tablet_pad = true;
+
+ if (has_pad_buttons && has_wheel && !has_rel_coordinates) {
+ is_tablet = true;
+ is_tablet_pad = true;
+ }
+
+ if (!is_tablet && !is_touchpad && !is_joystick &&
+ has_mouse_button &&
+ (has_rel_coordinates ||
+ !has_abs_coordinates)) /* mouse buttons and no axis */
+ is_mouse = true;
+
+ /* There is no such thing as an i2c mouse */
+ if (is_mouse && id->bustype == BUS_I2C)
+ is_pointing_stick = true;
+
+ /* Joystick un-detection. Some keyboards have random joystick buttons
+ * set. Avoid those being labeled as ID_INPUT_JOYSTICK with some heuristics.
+ * The well-known keys represent a (randomly picked) set of key groups.
+ * A joystick may have one of those but probably not several. And a joystick with less than 2 buttons
+ * or axes is not a joystick either.
+ * libinput uses similar heuristics, any changes here should be added to libinput too.
+ */
+ if (is_joystick) {
+ static const unsigned int well_known_keyboard_keys[] = {
+ KEY_LEFTCTRL, KEY_CAPSLOCK, KEY_NUMLOCK, KEY_INSERT,
+ KEY_MUTE, KEY_CALC, KEY_FILE, KEY_MAIL, KEY_PLAYPAUSE,
+ KEY_BRIGHTNESSDOWN,
+ };
+ size_t num_well_known_keys = 0;
+
+ if (has_keys)
+ for (size_t i = 0; i < ELEMENTSOF(well_known_keyboard_keys); i++)
+ if (test_bit(well_known_keyboard_keys[i], bitmask_key))
+ num_well_known_keys++;
+
+ if (num_well_known_keys >= 4 || num_joystick_buttons + num_joystick_axes < 2) {
+ log_device_debug(dev, "Input device has %zu joystick buttons and %zu axes but also %zu keyboard key sets, "
+ "assuming this is a keyboard, not a joystick.",
+ num_joystick_buttons, num_joystick_axes, num_well_known_keys);
+ is_joystick = false;
+ }
+
+ if (has_wheel && has_pad_buttons) {
+ log_device_debug(dev, "Input device has %zu joystick buttons as well as tablet pad buttons, "
+ "assuming this is a tablet pad, not a joystick.", num_joystick_buttons);
+
+ is_joystick = false;
+ }
+ }
+
+ if (is_pointing_stick)
+ udev_builtin_add_property(dev, test, "ID_INPUT_POINTINGSTICK", "1");
+ if (is_mouse || is_abs_mouse)
+ udev_builtin_add_property(dev, test, "ID_INPUT_MOUSE", "1");
+ if (is_touchpad)
+ udev_builtin_add_property(dev, test, "ID_INPUT_TOUCHPAD", "1");
+ if (is_touchscreen)
+ udev_builtin_add_property(dev, test, "ID_INPUT_TOUCHSCREEN", "1");
+ if (is_joystick)
+ udev_builtin_add_property(dev, test, "ID_INPUT_JOYSTICK", "1");
+ if (is_tablet)
+ udev_builtin_add_property(dev, test, "ID_INPUT_TABLET", "1");
+ if (is_tablet_pad)
+ udev_builtin_add_property(dev, test, "ID_INPUT_TABLET_PAD", "1");
+
+ return is_tablet || is_mouse || is_abs_mouse || is_touchpad || is_touchscreen || is_joystick || is_pointing_stick;
+}
+
+/* key like devices */
+static bool test_key(sd_device *dev,
+ const unsigned long* bitmask_ev,
+ const unsigned long* bitmask_key,
+ bool test) {
+
+ bool found = false;
+
+ /* do we have any KEY_* capability? */
+ if (!test_bit(EV_KEY, bitmask_ev)) {
+ log_device_debug(dev, "test_key: no EV_KEY capability");
+ return false;
+ }
+
+ /* only consider KEY_* here, not BTN_* */
+ for (size_t i = 0; i < BTN_MISC/BITS_PER_LONG && !found; i++) {
+ if (bitmask_key[i])
+ found = true;
+
+ log_device_debug(dev, "test_key: checking bit block %zu for any keys; found=%s",
+ i * BITS_PER_LONG, yes_no(found));
+ }
+ /* If there are no keys in the lower block, check the higher blocks */
+ for (size_t block = 0; block < sizeof(high_key_blocks) / sizeof(struct range) && !found; block++)
+ for (unsigned i = high_key_blocks[block].start; i < high_key_blocks[block].end && !found; i++)
+ if (test_bit(i, bitmask_key)) {
+ log_device_debug(dev, "test_key: Found key %x in high block", i);
+ found = true;
+ }
+
+ if (found)
+ udev_builtin_add_property(dev, test, "ID_INPUT_KEY", "1");
+
+ /* the first 32 bits are ESC, numbers, and Q to D; if we have all of
+ * those, consider it a full keyboard; do not test KEY_RESERVED, though */
+ if (FLAGS_SET(bitmask_key[0], 0xFFFFFFFE)) {
+ udev_builtin_add_property(dev, test, "ID_INPUT_KEYBOARD", "1");
+ return true;
+ }
+
+ return found;
+}
+
+static int builtin_input_id(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *pdev, *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ unsigned long bitmask_ev[NBITS(EV_MAX)];
+ unsigned long bitmask_abs[NBITS(ABS_MAX)];
+ unsigned long bitmask_key[NBITS(KEY_MAX)];
+ unsigned long bitmask_rel[NBITS(REL_MAX)];
+ unsigned long bitmask_props[NBITS(INPUT_PROP_MAX)];
+ const char *sysname;
+ bool is_pointer;
+ bool is_key;
+
+ /* walk up the parental chain until we find the real input device; the
+ * argument is very likely a subdevice of this, like eventN */
+ for (pdev = dev; pdev; ) {
+ const char *s;
+
+ if (sd_device_get_sysattr_value(pdev, "capabilities/ev", &s) >= 0)
+ break;
+
+ if (sd_device_get_parent_with_subsystem_devtype(pdev, "input", NULL, &pdev) >= 0)
+ continue;
+
+ pdev = NULL;
+ break;
+ }
+
+ if (pdev) {
+ struct input_id id = get_input_id(pdev);
+
+ /* Use this as a flag that input devices were detected, so that this
+ * program doesn't need to be called more than once per device */
+ udev_builtin_add_property(dev, test, "ID_INPUT", "1");
+ get_cap_mask(pdev, "capabilities/ev", bitmask_ev, sizeof(bitmask_ev), test);
+ get_cap_mask(pdev, "capabilities/abs", bitmask_abs, sizeof(bitmask_abs), test);
+ get_cap_mask(pdev, "capabilities/rel", bitmask_rel, sizeof(bitmask_rel), test);
+ get_cap_mask(pdev, "capabilities/key", bitmask_key, sizeof(bitmask_key), test);
+ get_cap_mask(pdev, "properties", bitmask_props, sizeof(bitmask_props), test);
+ is_pointer = test_pointers(dev, &id, bitmask_ev, bitmask_abs,
+ bitmask_key, bitmask_rel,
+ bitmask_props, test);
+ is_key = test_key(dev, bitmask_ev, bitmask_key, test);
+ /* Some evdev nodes have only a scrollwheel */
+ if (!is_pointer && !is_key && test_bit(EV_REL, bitmask_ev) &&
+ (test_bit(REL_WHEEL, bitmask_rel) || test_bit(REL_HWHEEL, bitmask_rel)))
+ udev_builtin_add_property(dev, test, "ID_INPUT_KEY", "1");
+ if (test_bit(EV_SW, bitmask_ev))
+ udev_builtin_add_property(dev, test, "ID_INPUT_SWITCH", "1");
+
+ }
+
+ if (sd_device_get_sysname(dev, &sysname) >= 0 &&
+ startswith(sysname, "event"))
+ extract_info(dev, test);
+
+ return 0;
+}
+
+const UdevBuiltin udev_builtin_input_id = {
+ .name = "input_id",
+ .cmd = builtin_input_id,
+ .help = "Input device properties",
+};
diff --git a/src/udev/udev-builtin-keyboard.c b/src/udev/udev-builtin-keyboard.c
new file mode 100644
index 0000000..3903bc4
--- /dev/null
+++ b/src/udev/udev-builtin-keyboard.c
@@ -0,0 +1,252 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <linux/input.h>
+
+#include "device-util.h"
+#include "fd-util.h"
+#include "parse-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+
+static const struct key_name *keyboard_lookup_key(const char *str, GPERF_LEN_TYPE len);
+#include "keyboard-keys-from-name.h"
+
+static int install_force_release(sd_device *dev, const unsigned *release, unsigned release_count) {
+ sd_device *atkbd;
+ const char *cur;
+ char codes[4096];
+ char *s;
+ size_t l;
+ unsigned i;
+ int r;
+
+ assert(dev);
+ assert(release);
+
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "serio", NULL, &atkbd);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get serio parent: %m");
+
+ r = sd_device_get_sysattr_value(atkbd, "force_release", &cur);
+ if (r < 0)
+ return log_device_error_errno(atkbd, r, "Failed to get force-release attribute: %m");
+
+ s = codes;
+ l = sizeof(codes);
+
+ /* copy current content */
+ l = strpcpy(&s, l, cur);
+
+ /* append new codes */
+ for (i = 0; i < release_count; i++)
+ l = strpcpyf(&s, l, ",%u", release[i]);
+
+ log_device_debug(atkbd, "keyboard: updating force-release list with '%s'", codes);
+ r = sd_device_set_sysattr_value(atkbd, "force_release", codes);
+ if (r < 0)
+ return log_device_error_errno(atkbd, r, "Failed to set force-release attribute: %m");
+
+ return 0;
+}
+
+static int map_keycode(sd_device *dev, int fd, int scancode, const char *keycode) {
+ struct {
+ unsigned scan;
+ unsigned key;
+ } map;
+ const struct key_name *k;
+ unsigned keycode_num;
+ int r;
+
+ /* translate identifier to key code */
+ k = keyboard_lookup_key(keycode, strlen(keycode));
+ if (k)
+ keycode_num = k->id;
+ else {
+ /* check if it's a numeric code already */
+ r = safe_atou(keycode, &keycode_num);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to parse key identifier '%s': %m", keycode);
+ }
+
+ map.scan = scancode;
+ map.key = keycode_num;
+
+ log_device_debug(dev, "keyboard: mapping scan code %u (0x%x) to key code %u (0x%x)",
+ map.scan, map.scan, map.key, map.key);
+
+ if (ioctl(fd, EVIOCSKEYCODE, &map) < 0)
+ return log_device_error_errno(dev, errno, "Failed to call EVIOCSKEYCODE with scan code 0x%x, and key code %u: %m", map.scan, map.key);
+
+ return 0;
+}
+
+static const char* parse_token(const char *current, int32_t *val_out) {
+ char *next;
+ int32_t val;
+
+ if (!current)
+ return NULL;
+
+ val = strtol(current, &next, 0);
+ if (*next && *next != ':')
+ return NULL;
+
+ if (next != current)
+ *val_out = val;
+
+ if (*next)
+ next++;
+
+ return next;
+}
+
+static int override_abs(sd_device *dev, int fd, unsigned evcode, const char *value) {
+ struct input_absinfo absinfo;
+ const char *next;
+
+ if (ioctl(fd, EVIOCGABS(evcode), &absinfo) < 0)
+ return log_device_error_errno(dev, errno, "Failed to call EVIOCGABS");
+
+ next = parse_token(value, &absinfo.minimum);
+ next = parse_token(next, &absinfo.maximum);
+ next = parse_token(next, &absinfo.resolution);
+ next = parse_token(next, &absinfo.fuzz);
+ next = parse_token(next, &absinfo.flat);
+ if (!next)
+ return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse EV_ABS override '%s'", value);
+
+ log_device_debug(dev, "keyboard: %x overridden with %"PRIi32"/%"PRIi32"/%"PRIi32"/%"PRIi32"/%"PRIi32,
+ evcode, absinfo.minimum, absinfo.maximum, absinfo.resolution, absinfo.fuzz, absinfo.flat);
+ if (ioctl(fd, EVIOCSABS(evcode), &absinfo) < 0)
+ return log_device_error_errno(dev, errno, "Failed to call EVIOCSABS");
+
+ return 0;
+}
+
+static int set_trackpoint_sensitivity(sd_device *dev, const char *value) {
+ sd_device *pdev;
+ char val_s[DECIMAL_STR_MAX(int)];
+ int r, val_i;
+
+ assert(dev);
+ assert(value);
+
+ /* The sensitivity sysfs attr belongs to the serio parent device */
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "serio", NULL, &pdev);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get serio parent: %m");
+
+ r = safe_atoi(value, &val_i);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to parse POINTINGSTICK_SENSITIVITY '%s': %m", value);
+ else if (val_i < 0 || val_i > 255)
+ return log_device_error_errno(dev, SYNTHETIC_ERRNO(ERANGE), "POINTINGSTICK_SENSITIVITY %d outside range [0..255]", val_i);
+
+ xsprintf(val_s, "%d", val_i);
+
+ r = sd_device_set_sysattr_value(pdev, "sensitivity", val_s);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to write 'sensitivity' attribute: %m");
+
+ return 0;
+}
+
+static int builtin_keyboard(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ unsigned release[1024];
+ unsigned release_count = 0;
+ _cleanup_close_ int fd = -EBADF;
+ const char *node;
+ int has_abs = -1, r;
+
+ r = sd_device_get_devname(dev, &node);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get device name: %m");
+
+ FOREACH_DEVICE_PROPERTY(dev, key, value)
+ if (startswith(key, "KEYBOARD_KEY_")) {
+ const char *keycode = value;
+ unsigned scancode;
+
+ /* KEYBOARD_KEY_<hex scan code>=<key identifier string> */
+ r = safe_atou_full(key + 13, 16, &scancode);
+ if (r < 0) {
+ log_device_warning_errno(dev, r, "Failed to parse scan code from \"%s\", ignoring: %m", key);
+ continue;
+ }
+
+ /* a leading '!' needs a force-release entry */
+ if (keycode[0] == '!') {
+ keycode++;
+
+ release[release_count] = scancode;
+ if (release_count < ELEMENTSOF(release)-1)
+ release_count++;
+
+ if (keycode[0] == '\0')
+ continue;
+ }
+
+ if (fd < 0) {
+ fd = sd_device_open(dev, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0)
+ return log_device_error_errno(dev, fd, "Failed to open device '%s': %m", node);
+ }
+
+ (void) map_keycode(dev, fd, scancode, keycode);
+ } else if (startswith(key, "EVDEV_ABS_")) {
+ unsigned evcode;
+
+ /* EVDEV_ABS_<EV_ABS code>=<min>:<max>:<res>:<fuzz>:<flat> */
+ r = safe_atou_full(key + 10, 16, &evcode);
+ if (r < 0) {
+ log_device_warning_errno(dev, r, "Failed to parse EV_ABS code from \"%s\", ignoring: %m", key);
+ continue;
+ }
+
+ if (fd < 0) {
+ fd = sd_device_open(dev, O_RDWR|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0)
+ return log_device_error_errno(dev, fd, "Failed to open device '%s': %m", node);
+ }
+
+ if (has_abs == -1) {
+ unsigned long bits;
+ int rc;
+
+ rc = ioctl(fd, EVIOCGBIT(0, sizeof(bits)), &bits);
+ if (rc < 0)
+ return log_device_error_errno(dev, errno, "Failed to set EVIOCGBIT");
+
+ has_abs = !!(bits & (1 << EV_ABS));
+ if (!has_abs)
+ log_device_warning(dev, "EVDEV_ABS override set but no EV_ABS present on device");
+ }
+
+ if (!has_abs)
+ continue;
+
+ (void) override_abs(dev, fd, evcode, value);
+ } else if (streq(key, "POINTINGSTICK_SENSITIVITY"))
+ (void) set_trackpoint_sensitivity(dev, value);
+
+ /* install list of force-release codes */
+ if (release_count > 0)
+ (void) install_force_release(dev, release, release_count);
+
+ return 0;
+}
+
+const UdevBuiltin udev_builtin_keyboard = {
+ .name = "keyboard",
+ .cmd = builtin_keyboard,
+ .help = "Keyboard scancode mapping and touchpad/pointingstick characteristics",
+};
diff --git a/src/udev/udev-builtin-kmod.c b/src/udev/udev-builtin-kmod.c
new file mode 100644
index 0000000..3ab5c48
--- /dev/null
+++ b/src/udev/udev-builtin-kmod.c
@@ -0,0 +1,94 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * load kernel modules
+ *
+ * Copyright © 2011 ProFUSION embedded systems
+ */
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "device-util.h"
+#include "module-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "udev-builtin.h"
+
+static struct kmod_ctx *ctx = NULL;
+
+_printf_(6,0) static void udev_kmod_log(void *data, int priority, const char *file, int line, const char *fn, const char *format, va_list args) {
+ log_internalv(priority, 0, file, line, fn, format, args);
+}
+
+static int builtin_kmod(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ int r;
+
+ if (!ctx)
+ return 0;
+
+ if (argc < 2 || !streq(argv[1], "load"))
+ return log_device_warning_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "%s: expected: load [module…]", argv[0]);
+
+ char **modules = strv_skip(argv, 2);
+ if (strv_isempty(modules)) {
+ const char *modalias;
+
+ r = sd_device_get_property_value(dev, "MODALIAS", &modalias);
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to read property \"MODALIAS\".");
+
+ (void) module_load_and_warn(ctx, modalias, /* verbose = */ false);
+ } else
+ STRV_FOREACH(module, modules)
+ (void) module_load_and_warn(ctx, *module, /* verbose = */ false);
+
+ return 0;
+}
+
+/* called at udev startup and reload */
+static int builtin_kmod_init(void) {
+ if (ctx)
+ return 0;
+
+ ctx = kmod_new(NULL, NULL);
+ if (!ctx)
+ return -ENOMEM;
+
+ log_debug("Loading kernel module index.");
+ kmod_set_log_fn(ctx, udev_kmod_log, NULL);
+ kmod_load_resources(ctx);
+ return 0;
+}
+
+/* called on udev shutdown and reload request */
+static void builtin_kmod_exit(void) {
+ log_debug("Unload kernel module index.");
+ ctx = kmod_unref(ctx);
+}
+
+/* called every couple of seconds during event activity; 'true' if config has changed */
+static bool builtin_kmod_should_reload(void) {
+ if (!ctx)
+ return false;
+
+ if (kmod_validate_resources(ctx) != KMOD_RESOURCES_OK) {
+ log_debug("Kernel module index needs reloading.");
+ return true;
+ }
+
+ return false;
+}
+
+const UdevBuiltin udev_builtin_kmod = {
+ .name = "kmod",
+ .cmd = builtin_kmod,
+ .init = builtin_kmod_init,
+ .exit = builtin_kmod_exit,
+ .should_reload = builtin_kmod_should_reload,
+ .help = "Kernel module loader",
+ .run_once = false,
+};
diff --git a/src/udev/udev-builtin-net_driver.c b/src/udev/udev-builtin-net_driver.c
new file mode 100644
index 0000000..f1642a4
--- /dev/null
+++ b/src/udev/udev-builtin-net_driver.c
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "device-util.h"
+#include "errno-util.h"
+#include "ethtool-util.h"
+#include "fd-util.h"
+#include "log.h"
+#include "string-util.h"
+#include "udev-builtin.h"
+
+static int builtin_net_driver_set_driver(UdevEvent *event, int argc, char **argv, bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ _cleanup_close_ int ethtool_fd = -EBADF;
+ _cleanup_free_ char *driver = NULL;
+ const char *sysname;
+ int r;
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to get sysname: %m");
+
+ r = ethtool_get_driver(&ethtool_fd, sysname, &driver);
+ if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) {
+ log_device_debug_errno(dev, r, "Querying driver name via ethtool API is not supported by device '%s', ignoring: %m", sysname);
+ return 0;
+ }
+ if (r == -ENODEV) {
+ log_device_debug_errno(dev, r, "Device already vanished, ignoring.");
+ return 0;
+ }
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to get driver for '%s': %m", sysname);
+
+ return udev_builtin_add_property(event->dev, test, "ID_NET_DRIVER", driver);
+}
+
+const UdevBuiltin udev_builtin_net_driver = {
+ .name = "net_driver",
+ .cmd = builtin_net_driver_set_driver,
+ .help = "Set driver for network device",
+ .run_once = true,
+};
diff --git a/src/udev/udev-builtin-net_id.c b/src/udev/udev-builtin-net_id.c
new file mode 100644
index 0000000..91b4008
--- /dev/null
+++ b/src/udev/udev-builtin-net_id.c
@@ -0,0 +1,1366 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/*
+ * Predictable network interface device names based on:
+ * - firmware/bios-provided index numbers for on-board devices
+ * - firmware-provided pci-express hotplug slot index number
+ * - physical/geographical location of the hardware
+ * - the interface's MAC address
+ *
+ * https://systemd.io/PREDICTABLE_INTERFACE_NAMES
+ *
+ * When the code here is changed, man/systemd.net-naming-scheme.xml must be updated too.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <net/if.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <linux/if.h>
+#include <linux/if_arp.h>
+#include <linux/netdevice.h>
+#include <linux/pci_regs.h>
+
+#include "alloc-util.h"
+#include "chase.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "dirent-util.h"
+#include "ether-addr-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "glyph-util.h"
+#include "netif-naming-scheme.h"
+#include "parse-util.h"
+#include "proc-cmdline.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+
+#define ONBOARD_14BIT_INDEX_MAX ((1U << 14) - 1)
+#define ONBOARD_16BIT_INDEX_MAX ((1U << 16) - 1)
+
+/* skip intermediate virtio devices */
+static sd_device *device_skip_virtio(sd_device *dev) {
+ /* there can only ever be one virtio bus per parent device, so we can
+ * safely ignore any virtio buses. see
+ * http://lists.linuxfoundation.org/pipermail/virtualization/2015-August/030331.html */
+ while (dev) {
+ const char *subsystem;
+
+ if (sd_device_get_subsystem(dev, &subsystem) < 0)
+ break;
+
+ if (!streq(subsystem, "virtio"))
+ break;
+
+ if (sd_device_get_parent(dev, &dev) < 0)
+ return NULL;
+ }
+
+ return dev;
+}
+
+static int get_matching_parent(
+ sd_device *dev,
+ char * const *parent_subsystems,
+ bool skip_virtio,
+ sd_device **ret) {
+
+ sd_device *parent;
+ int r;
+
+ assert(dev);
+
+ r = sd_device_get_parent(dev, &parent);
+ if (r < 0)
+ return r;
+
+ if (skip_virtio) {
+ /* skip virtio subsystem if present */
+ parent = device_skip_virtio(parent);
+ if (!parent)
+ return -ENODEV;
+ }
+
+ if (!strv_isempty(parent_subsystems)) {
+ const char *subsystem;
+
+ /* check if our direct parent is in an expected subsystem. */
+ r = sd_device_get_subsystem(parent, &subsystem);
+ if (r < 0)
+ return r;
+
+ if (!strv_contains(parent_subsystems, subsystem))
+ return -ENODEV;
+ }
+
+ if (ret)
+ *ret = parent;
+
+ return 0;
+}
+
+static int get_first_syspath_component(sd_device *dev, const char *prefix, char **ret) {
+ _cleanup_free_ char *buf = NULL;
+ const char *syspath, *p, *q;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+ assert(ret);
+
+ r = sd_device_get_syspath(dev, &syspath);
+ if (r < 0)
+ return r;
+
+ p = path_startswith(syspath, prefix);
+ if (!p)
+ return -EINVAL;
+
+ r = path_find_first_component(&p, /* accept_dot_dot = */ false, &q);
+ if (r < 0)
+ return r;
+
+ buf = strndup(q, r);
+ if (!buf)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(buf);
+ return r; /* return the length of the string */
+}
+
+static int get_virtfn_info(sd_device *pcidev, sd_device **ret_physfn_pcidev, char **ret_suffix) {
+ _cleanup_(sd_device_unrefp) sd_device *physfn_pcidev = NULL;
+ const char *syspath, *name;
+ int r;
+
+ assert(pcidev);
+ assert(ret_physfn_pcidev);
+ assert(ret_suffix);
+
+ r = sd_device_get_syspath(pcidev, &syspath);
+ if (r < 0)
+ return r;
+
+ /* Get physical function's pci device. */
+ r = sd_device_new_child(&physfn_pcidev, pcidev, "physfn");
+ if (r < 0)
+ return r;
+
+ /* Find the virtual function number by finding the right virtfn link. */
+ FOREACH_DEVICE_CHILD_WITH_SUFFIX(physfn_pcidev, child, name) {
+ const char *n, *s;
+
+ /* Only accepts e.g. virtfn0, virtfn1, and so on. */
+ n = startswith(name, "virtfn");
+ if (isempty(n) || !in_charset(n, DIGITS))
+ continue;
+
+ if (sd_device_get_syspath(child, &s) < 0)
+ continue;
+
+ if (streq(s, syspath)) {
+ char *suffix;
+
+ suffix = strjoin("v", n);
+ if (!suffix)
+ return -ENOMEM;
+
+ *ret_physfn_pcidev = sd_device_ref(physfn_pcidev);
+ *ret_suffix = suffix;
+ return 0;
+ }
+ }
+
+ return -ENOENT;
+}
+
+static int get_dev_port(sd_device *dev, bool fallback_to_dev_id, unsigned *ret) {
+ unsigned v;
+ int r;
+
+ assert(dev);
+ assert(ret);
+
+ /* Get kernel provided port index for the case when multiple ports on a single PCI function. */
+
+ r = device_get_sysattr_unsigned(dev, "dev_port", &v);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ /* Found a positive index. Let's use it. */
+ *ret = v;
+ return 1; /* positive */
+ }
+ assert(v == 0);
+
+ /* With older kernels IP-over-InfiniBand network interfaces sometimes erroneously provide the port
+ * number in the 'dev_id' sysfs attribute instead of 'dev_port', which thus stays initialized as 0. */
+
+ if (fallback_to_dev_id) {
+ unsigned iftype;
+
+ r = device_get_sysattr_unsigned(dev, "type", &iftype);
+ if (r < 0)
+ return r;
+
+ fallback_to_dev_id = (iftype == ARPHRD_INFINIBAND);
+ }
+
+ if (fallback_to_dev_id)
+ return device_get_sysattr_unsigned(dev, "dev_id", ret);
+
+ /* Otherwise, return the original index 0. */
+ *ret = 0;
+ return 0; /* zero */
+}
+
+static int get_port_specifier(sd_device *dev, bool fallback_to_dev_id, char **ret) {
+ const char *phys_port_name;
+ unsigned dev_port;
+ char *buf;
+ int r;
+
+ assert(dev);
+ assert(ret);
+
+ /* First, try to use the kernel provided front panel port name for multiple port PCI device. */
+ r = sd_device_get_sysattr_value(dev, "phys_port_name", &phys_port_name);
+ if (r >= 0 && !isempty(phys_port_name)) {
+ if (naming_scheme_has(NAMING_SR_IOV_R)) {
+ int vf_id = -1;
+
+ /* Check if phys_port_name indicates virtual device representor. */
+ (void) sscanf(phys_port_name, "pf%*uvf%d", &vf_id);
+
+ if (vf_id >= 0) {
+ /* For VF representor append 'r<VF_NUM>'. */
+ if (asprintf(&buf, "r%d", vf_id) < 0)
+ return log_oom_debug();
+
+ *ret = buf;
+ return 1;
+ }
+ }
+
+ /* Otherwise, use phys_port_name as is. */
+ buf = strjoin("n", phys_port_name);
+ if (!buf)
+ return log_oom_debug();
+
+ *ret = buf;
+ return 1;
+ }
+
+ /* Then, try to use the kernel provided port index for the case when multiple ports on a single PCI
+ * function. */
+ r = get_dev_port(dev, fallback_to_dev_id, &dev_port);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device port index: %m");
+ if (r > 0) {
+ assert(dev_port > 0);
+ if (asprintf(&buf, "d%u", dev_port) < 0)
+ return log_oom_debug();
+
+ *ret = buf;
+ return 1;
+ }
+
+ *ret = NULL;
+ return 0;
+}
+
+static bool is_valid_onboard_index(unsigned idx) {
+ /* Some BIOSes report rubbish indexes that are excessively high (2^24-1 is an index VMware likes to
+ * report for example). Let's define a cut-off where we don't consider the index reliable anymore. We
+ * pick some arbitrary cut-off, which is somewhere beyond the realistic number of physical network
+ * interface a system might have. Ideally the kernel would already filter this crap for us, but it
+ * doesn't currently. The initial cut-off value (2^14-1) was too conservative for s390 PCI which
+ * allows for index values up 2^16-1 which is now enabled with the NAMING_16BIT_INDEX naming flag. */
+ return idx <= (naming_scheme_has(NAMING_16BIT_INDEX) ? ONBOARD_16BIT_INDEX_MAX : ONBOARD_14BIT_INDEX_MAX);
+}
+
+static int pci_get_onboard_index(sd_device *dev, unsigned *ret) {
+ unsigned idx;
+ int r;
+
+ assert(dev);
+ assert(ret);
+
+ /* ACPI _DSM — device specific method for naming a PCI or PCI Express device */
+ r = device_get_sysattr_unsigned(dev, "acpi_index", &idx);
+ if (r < 0)
+ /* SMBIOS type 41 — Onboard Devices Extended Information */
+ r = device_get_sysattr_unsigned(dev, "index", &idx);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Could not obtain onboard index: %m");
+
+ if (idx == 0 && !naming_scheme_has(NAMING_ZERO_ACPI_INDEX))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "Naming scheme does not allow onboard index==0.");
+ if (!is_valid_onboard_index(idx))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENOENT),
+ "Not a valid onboard index: %u", idx);
+
+ *ret = idx;
+ return 0;
+}
+
+static int names_pci_onboard(sd_device *dev, sd_device *pci_dev, const char *prefix, const char *suffix, bool test) {
+ _cleanup_free_ char *port = NULL;
+ unsigned idx = 0; /* avoid false maybe-uninitialized warning */
+ int r;
+
+ assert(dev);
+ assert(pci_dev);
+ assert(prefix);
+
+ /* retrieve on-board index number from firmware */
+ r = pci_get_onboard_index(pci_dev, &idx);
+ if (r < 0)
+ return r;
+
+ r = get_port_specifier(dev, /* fallback_to_dev_id = */ false, &port);
+ if (r < 0)
+ return r;
+
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%so%u%s%s", prefix, idx, strempty(port), strempty(suffix)))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_ONBOARD", str);
+
+ log_device_debug(dev, "Onboard index identifier: index=%u port=%s %s %s",
+ idx, strna(port),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), empty_to_na(str));
+
+ return 0;
+}
+
+static int names_pci_onboard_label(sd_device *dev, sd_device *pci_dev, const char *prefix, bool test) {
+ const char *label;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ /* retrieve on-board label from firmware */
+ r = sd_device_get_sysattr_value(pci_dev, "label", &label);
+ if (r < 0)
+ return log_device_debug_errno(pci_dev, r, "Failed to get PCI onboard label: %m");
+
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%s%s",
+ naming_scheme_has(NAMING_LABEL_NOPREFIX) ? "" : prefix,
+ label))
+ udev_builtin_add_property(dev, test, "ID_NET_LABEL_ONBOARD", str);
+
+ log_device_debug(dev, "Onboard label from PCI device: %s", label);
+ return 0;
+}
+
+/* read the 256 bytes PCI configuration space to check the multi-function bit */
+static int is_pci_multifunction(sd_device *dev) {
+ _cleanup_free_ uint8_t *config = NULL;
+ const char *filename, *syspath;
+ size_t len;
+ int r;
+
+ assert(dev);
+
+ r = sd_device_get_syspath(dev, &syspath);
+ if (r < 0)
+ return r;
+
+ filename = strjoina(syspath, "/config");
+ r = read_virtual_file(filename, PCI_HEADER_TYPE + 1, (char **) &config, &len);
+ if (r < 0)
+ return r;
+ if (len < PCI_HEADER_TYPE + 1)
+ return -EINVAL;
+
+#ifndef PCI_HEADER_TYPE_MULTIFUNC
+#define PCI_HEADER_TYPE_MULTIFUNC 0x80
+#endif
+
+ /* bit 0-6 header type, bit 7 multi/single function device */
+ return config[PCI_HEADER_TYPE] & PCI_HEADER_TYPE_MULTIFUNC;
+}
+
+static bool is_pci_ari_enabled(sd_device *dev) {
+ assert(dev);
+
+ return device_get_sysattr_bool(dev, "ari_enabled") > 0;
+}
+
+static bool is_pci_bridge(sd_device *dev) {
+ const char *v, *p;
+
+ assert(dev);
+
+ if (sd_device_get_sysattr_value(dev, "modalias", &v) < 0)
+ return false;
+
+ if (!startswith(v, "pci:"))
+ return false;
+
+ p = strrchr(v, 's');
+ if (!p)
+ return false;
+ if (p[1] != 'c')
+ return false;
+
+ /* PCI device subclass 04 corresponds to PCI bridge */
+ bool b = strneq(p + 2, "04", 2);
+ if (b)
+ log_device_debug(dev, "Device is a PCI bridge.");
+ return b;
+}
+
+static int parse_hotplug_slot_from_function_id(sd_device *dev, int slots_dirfd, uint32_t *ret) {
+ uint64_t function_id;
+ char filename[NAME_MAX+1];
+ const char *attr;
+ int r;
+
+ /* The <sysname>/function_id attribute is unique to the s390 PCI driver. If present, we know that the
+ * slot's directory name for this device is /sys/bus/pci/slots/XXXXXXXX/ where XXXXXXXX is the fixed
+ * length 8 hexadecimal character string representation of function_id. Therefore we can short cut
+ * here and just check for the existence of the slot directory. As this directory has to exist, we're
+ * emitting a debug message for the unlikely case it's not found. Note that the domain part doesn't
+ * belong to the slot name here because there's a 1-to-1 relationship between PCI function and its
+ * hotplug slot. See https://docs.kernel.org/s390/pci.html for more details. */
+
+ assert(dev);
+ assert(slots_dirfd >= 0);
+ assert(ret);
+
+ if (!naming_scheme_has(NAMING_SLOT_FUNCTION_ID)) {
+ *ret = 0;
+ return 0;
+ }
+
+ if (sd_device_get_sysattr_value(dev, "function_id", &attr) < 0) {
+ *ret = 0;
+ return 0;
+ }
+
+ r = safe_atou64(attr, &function_id);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to parse function_id, ignoring: %s", attr);
+
+ if (function_id <= 0 || function_id > UINT32_MAX)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "Invalid function id (0x%"PRIx64"), ignoring.",
+ function_id);
+
+ if (!snprintf_ok(filename, sizeof(filename), "%08"PRIx64, function_id))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENAMETOOLONG),
+ "PCI slot path is too long, ignoring.");
+
+ if (faccessat(slots_dirfd, filename, F_OK, 0) < 0)
+ return log_device_debug_errno(dev, errno, "Cannot access %s under pci slots, ignoring: %m", filename);
+
+ *ret = (uint32_t) function_id;
+ return 1; /* Found. We should ignore domain part. */
+}
+
+static int pci_get_hotplug_slot_from_address(
+ sd_device *dev,
+ sd_device *pci,
+ DIR *dir,
+ uint32_t *ret) {
+
+ const char *sysname;
+ int r;
+
+ assert(dev);
+ assert(pci);
+ assert(dir);
+ assert(ret);
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get sysname: %m");
+
+ rewinddir(dir);
+ FOREACH_DIRENT_ALL(de, dir, break) {
+ _cleanup_free_ char *path = NULL;
+ const char *address;
+ uint32_t slot;
+
+ if (dot_or_dot_dot(de->d_name))
+ continue;
+
+ if (de->d_type != DT_DIR)
+ continue;
+
+ r = safe_atou32(de->d_name, &slot);
+ if (r < 0 || slot <= 0)
+ continue;
+
+ path = path_join("slots", de->d_name, "address");
+ if (!path)
+ return log_oom_debug();
+
+ if (sd_device_get_sysattr_value(pci, path, &address) < 0)
+ continue;
+
+ /* match slot address with device by stripping the function */
+ if (!startswith(sysname, address))
+ continue;
+
+ *ret = slot;
+ return 1; /* found */
+ }
+
+ *ret = 0;
+ return 0; /* not found */
+}
+
+static int pci_get_hotplug_slot(sd_device *dev, uint32_t *ret) {
+ _cleanup_(sd_device_unrefp) sd_device *pci = NULL;
+ _cleanup_closedir_ DIR *dir = NULL;
+ int r;
+
+ assert(dev);
+ assert(ret);
+
+ /* ACPI _SUN — slot user number */
+ r = sd_device_new_from_subsystem_sysname(&pci, "subsystem", "pci");
+ if (r < 0)
+ return log_debug_errno(r, "Failed to create sd_device object for pci subsystem: %m");
+
+ r = device_opendir(pci, "slots", &dir);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Cannot open 'slots' subdirectory: %m");
+
+ for (sd_device *slot_dev = dev; slot_dev; ) {
+ uint32_t slot = 0; /* avoid false maybe-uninitialized warning */
+
+ r = parse_hotplug_slot_from_function_id(slot_dev, dirfd(dir), &slot);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ *ret = slot;
+ return 1; /* domain should be ignored. */
+ }
+
+ r = pci_get_hotplug_slot_from_address(slot_dev, pci, dir, &slot);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ /* We found the match between PCI device and slot. However, we won't use the slot
+ * index if the device is a PCI bridge, because it can have other child devices that
+ * will try to claim the same index and that would create name collision. */
+ if (naming_scheme_has(NAMING_BRIDGE_NO_SLOT) && is_pci_bridge(slot_dev)) {
+ if (naming_scheme_has(NAMING_BRIDGE_MULTIFUNCTION_SLOT) && is_pci_multifunction(dev) <= 0)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ESTALE),
+ "Not using slot information because the PCI device associated with "
+ "the hotplug slot is a bridge and the PCI device has a single function.");
+
+ if (!naming_scheme_has(NAMING_BRIDGE_MULTIFUNCTION_SLOT))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ESTALE),
+ "Not using slot information because the PCI device is a bridge.");
+ }
+
+ *ret = slot;
+ return 0; /* domain can be still used. */
+ }
+
+ if (sd_device_get_parent_with_subsystem_devtype(slot_dev, "pci", NULL, &slot_dev) < 0)
+ break;
+ }
+
+ return -ENOENT;
+}
+
+static int get_pci_slot_specifiers(
+ sd_device *dev,
+ char **ret_domain,
+ char **ret_bus_and_slot,
+ char **ret_func) {
+
+ _cleanup_free_ char *domain_spec = NULL, *bus_and_slot_spec = NULL, *func_spec = NULL;
+ unsigned domain, bus, slot, func;
+ const char *sysname;
+ int r;
+
+ assert(dev);
+ assert(ret_domain);
+ assert(ret_bus_and_slot);
+ assert(ret_func);
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get sysname: %m");
+
+ r = sscanf(sysname, "%x:%x:%x.%u", &domain, &bus, &slot, &func);
+ if (r != 4)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse slot information from PCI device sysname.");
+
+ if (naming_scheme_has(NAMING_NPAR_ARI) &&
+ is_pci_ari_enabled(dev))
+ /* ARI devices support up to 256 functions on a single device ("slot"), and interpret the
+ * traditional 5-bit slot and 3-bit function number as a single 8-bit function number,
+ * where the slot makes up the upper 5 bits. */
+ func += slot * 8;
+
+ if (domain > 0 && asprintf(&domain_spec, "P%u", domain) < 0)
+ return log_oom_debug();
+
+ if (asprintf(&bus_and_slot_spec, "p%us%u", bus, slot) < 0)
+ return log_oom_debug();
+
+ if ((func > 0 || is_pci_multifunction(dev) > 0) &&
+ asprintf(&func_spec, "f%u", func) < 0)
+ return log_oom_debug();
+
+ *ret_domain = TAKE_PTR(domain_spec);
+ *ret_bus_and_slot = TAKE_PTR(bus_and_slot_spec);
+ *ret_func = TAKE_PTR(func_spec);
+ return 0;
+}
+
+static int names_pci_slot(sd_device *dev, sd_device *pci_dev, const char *prefix, const char *suffix, bool test) {
+ _cleanup_free_ char *domain = NULL, *bus_and_slot = NULL, *func = NULL, *port = NULL;
+ uint32_t hotplug_slot = 0; /* avoid false maybe-uninitialized warning */
+ char str[ALTIFNAMSIZ];
+ int r;
+
+ assert(dev);
+ assert(pci_dev);
+ assert(prefix);
+
+ r = get_pci_slot_specifiers(pci_dev, &domain, &bus_and_slot, &func);
+ if (r < 0)
+ return r;
+
+ r = get_port_specifier(dev, /* fallback_to_dev_id = */ true, &port);
+ if (r < 0)
+ return r;
+
+ /* compose a name based on the raw kernel's PCI bus, slot numbers */
+ if (snprintf_ok(str, sizeof str, "%s%s%s%s%s%s",
+ prefix, strempty(domain), bus_and_slot, strempty(func), strempty(port), strempty(suffix)))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+
+ log_device_debug(dev, "PCI path identifier: domain=%s bus_and_slot=%s func=%s port=%s %s %s",
+ strna(domain), bus_and_slot, strna(func), strna(port),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), empty_to_na(str));
+
+ r = pci_get_hotplug_slot(pci_dev, &hotplug_slot);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ /* If the hotplug slot is found through the function ID, then drop the domain from the name.
+ * See comments in parse_hotplug_slot_from_function_id(). */
+ domain = mfree(domain);
+
+ if (snprintf_ok(str, sizeof str, "%s%ss%"PRIu32"%s%s%s",
+ prefix, strempty(domain), hotplug_slot, strempty(func), strempty(port), strempty(suffix)))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str);
+
+ log_device_debug(dev, "Slot identifier: domain=%s slot=%"PRIu32" func=%s port=%s %s %s",
+ strna(domain), hotplug_slot, strna(func), strna(port),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), empty_to_na(str));
+
+ return 0;
+}
+
+static int names_vio(sd_device *dev, const char *prefix, bool test) {
+ _cleanup_free_ char *s = NULL;
+ unsigned slotid;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ /* get ibmveth/ibmvnic slot-based names. */
+
+ /* check if our direct parent is a VIO device with no other bus in-between */
+ if (get_matching_parent(dev, STRV_MAKE("vio"), /* skip_virtio = */ false, NULL) < 0)
+ return 0;
+
+ log_device_debug(dev, "Parent device is in the vio subsystem.");
+
+ /* The devices' $DEVPATH number is tied to (virtual) hardware (slot id
+ * selected in the HMC), thus this provides a reliable naming (e.g.
+ * "/devices/vio/30000002/net/eth1"); we ignore the bus number, as
+ * there should only ever be one bus, and then remove leading zeros. */
+ r = get_first_syspath_component(dev, "/sys/devices/vio/", &s);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get VIO bus ID and slot ID: %m");
+
+ if (r != 8)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "VIO bus ID and slot ID have invalid length: %s", s);
+
+ if (!in_charset(s, HEXDIGITS))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "VIO bus ID and slot ID contain invalid characters: %s", s);
+
+ /* Parse only slot ID (the last 4 hexdigits). */
+ r = safe_atou_full(s + 4, 16, &slotid);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to parse VIO slot from '%s': %m", s);
+
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%sv%u", prefix, slotid))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str);
+ log_device_debug(dev, "Vio slot identifier: slotid=%u %s %s",
+ slotid, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix));
+ return 0;
+}
+
+static int names_platform(sd_device *dev, const char *prefix, bool test) {
+ _cleanup_free_ char *p = NULL;
+ const char *validchars;
+ char *vendor, *model_str, *instance_str;
+ unsigned model, instance;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ /* get ACPI path names for ARM64 platform devices */
+
+ /* check if our direct parent is a platform device with no other bus in-between */
+ if (get_matching_parent(dev, STRV_MAKE("platform"), /* skip_virtio = */ false, NULL) < 0)
+ return 0;
+
+ log_device_debug(dev, "Parent device is in the platform subsystem.");
+
+ r = get_first_syspath_component(dev, "/sys/devices/platform/", &p);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get platform ID: %m");
+
+ /* Platform devices are named after ACPI table match, and instance id
+ * eg. "/sys/devices/platform/HISI00C2:00"
+ * The Vendor (3 or 4 char), followed by hexadecimal model number : instance id. */
+ if (r == 10 && p[7] == ':') {
+ /* 3 char vendor string */
+ vendor = strndupa(p, 3);
+ model_str = strndupa(p + 3, 4);
+ instance_str = strndupa(p + 8, 2);
+ validchars = UPPERCASE_LETTERS;
+ } else if (r == 11 && p[8] == ':') {
+ /* 4 char vendor string */
+ vendor = strndupa(p, 4);
+ model_str = strndupa(p + 4, 4);
+ instance_str = strndupa(p + 9, 2);
+ validchars = UPPERCASE_LETTERS DIGITS;
+ } else
+ return -EOPNOTSUPP;
+
+ if (!in_charset(vendor, validchars))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENOENT),
+ "Platform vendor contains invalid characters: %s", vendor);
+
+ ascii_strlower(vendor);
+
+ r = safe_atou_full(model_str, 16, &model);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to parse model number \"%s\": %m", model_str);
+
+ r = safe_atou_full(instance_str, 16, &instance);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to parse instance id \"%s\": %m", instance_str);
+
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%sa%s%xi%u", prefix, vendor, model, instance))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+ log_device_debug(dev, "Platform identifier: vendor=%s model=%x instance=%u %s %s",
+ vendor, model, instance, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix));
+ return 0;
+}
+
+static int names_devicetree(sd_device *dev, const char *prefix, bool test) {
+ _cleanup_(sd_device_unrefp) sd_device *aliases_dev = NULL, *ofnode_dev = NULL, *devicetree_dev = NULL;
+ const char *ofnode_path, *ofnode_syspath, *devicetree_syspath;
+ sd_device *parent;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ if (!naming_scheme_has(NAMING_DEVICETREE_ALIASES))
+ return 0;
+
+ /* only ethernet supported for now */
+ if (!streq(prefix, "en"))
+ return -EOPNOTSUPP;
+
+ /* check if our direct parent has an of_node */
+ r = sd_device_get_parent(dev, &parent);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get parent device: %m");
+
+ r = sd_device_new_child(&ofnode_dev, parent, "of_node");
+ if (r < 0)
+ return log_device_debug_errno(parent, r, "Failed to get 'of_node' child device: %m");
+
+ r = sd_device_get_syspath(ofnode_dev, &ofnode_syspath);
+ if (r < 0)
+ return log_device_debug_errno(ofnode_dev, r, "Failed to get syspath: %m");
+
+ /* /proc/device-tree should be a symlink to /sys/firmware/devicetree/base. */
+ r = sd_device_new_from_path(&devicetree_dev, "/proc/device-tree");
+ if (r < 0)
+ return log_debug_errno(r, "Failed to create sd-device object from '/proc/device-tree': %m");
+
+ r = sd_device_get_syspath(devicetree_dev, &devicetree_syspath);
+ if (r < 0)
+ return log_device_debug_errno(devicetree_dev, r, "Failed to get syspath: %m");
+
+ /*
+ * Example paths:
+ * devicetree_syspath = /sys/firmware/devicetree/base
+ * ofnode_syspath = /sys/firmware/devicetree/base/soc/ethernet@deadbeef
+ * ofnode_path = soc/ethernet@deadbeef
+ */
+ ofnode_path = path_startswith(ofnode_syspath, devicetree_syspath);
+ if (!ofnode_path)
+ return log_device_debug_errno(ofnode_dev, SYNTHETIC_ERRNO(EINVAL),
+ "The device '%s' is not a child device of '%s': %m",
+ ofnode_syspath, devicetree_syspath);
+
+ /* Get back our leading / to match the contents of the aliases */
+ ofnode_path--;
+ assert(path_is_absolute(ofnode_path));
+
+ r = sd_device_new_child(&aliases_dev, devicetree_dev, "aliases");
+ if (r < 0)
+ return log_device_debug_errno(devicetree_dev, r,
+ "Failed to get 'aliases' child device: %m");
+
+ FOREACH_DEVICE_SYSATTR(aliases_dev, alias) {
+ const char *alias_path, *alias_index, *conflict;
+ unsigned i;
+
+ alias_index = startswith(alias, "ethernet");
+ if (!alias_index)
+ continue;
+
+ if (sd_device_get_sysattr_value(aliases_dev, alias, &alias_path) < 0)
+ continue;
+
+ if (!path_equal(ofnode_path, alias_path))
+ continue;
+
+ /* If there's no index, we default to 0... */
+ if (isempty(alias_index)) {
+ i = 0;
+ conflict = "ethernet0";
+ } else {
+ r = safe_atou(alias_index, &i);
+ if (r < 0)
+ return log_device_debug_errno(dev, r,
+ "Could not get index of alias %s: %m", alias);
+ conflict = "ethernet";
+ }
+
+ /* ...but make sure we don't have an alias conflict */
+ if (i == 0 && sd_device_get_sysattr_value(aliases_dev, conflict, NULL) >= 0)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EEXIST),
+ "Ethernet alias conflict: ethernet and ethernet0 both exist");
+
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%sd%u", prefix, i))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_ONBOARD", str);
+ log_device_debug(dev, "devicetree identifier: alias_index=%u %s \"%s\"",
+ i, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix));
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static int names_pci(sd_device *dev, const char *prefix, bool test) {
+ _cleanup_(sd_device_unrefp) sd_device *physfn_pcidev = NULL;
+ _cleanup_free_ char *virtfn_suffix = NULL;
+ sd_device *parent;
+
+ assert(dev);
+ assert(prefix);
+
+ /* check if our direct parent is a PCI device with no other bus in-between */
+ if (get_matching_parent(dev, STRV_MAKE("pci"), /* skip_virtio = */ true, &parent) < 0)
+ return 0;
+
+ /* If this is an SR-IOV virtual device, get base name using physical device and add virtfn suffix. */
+ if (naming_scheme_has(NAMING_SR_IOV_V) &&
+ get_virtfn_info(parent, &physfn_pcidev, &virtfn_suffix) >= 0)
+ parent = physfn_pcidev;
+ else
+ (void) names_pci_onboard_label(dev, parent, prefix, test);
+
+ (void) names_pci_onboard(dev, parent, prefix, virtfn_suffix, test);
+ (void) names_pci_slot(dev, parent, prefix, virtfn_suffix, test);
+ return 0;
+}
+
+static int get_usb_specifier(sd_device *dev, char **ret) {
+ char *ports, *config, *interf, *s, *buf;
+ const char *sysname;
+ int r;
+
+ assert(dev);
+ assert(ret);
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get sysname: %m");
+
+ /* get USB port number chain, configuration, interface */
+ s = strchr(sysname, '-');
+ if (!s)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "sysname \"%s\" does not have '-' in the expected place.", sysname);
+
+ ports = strdupa_safe(s + 1);
+ s = strchr(ports, ':');
+ if (!s)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "sysname \"%s\" does not have ':' in the expected place.", sysname);
+
+ *s = '\0';
+ config = s + 1;
+ s = strchr(config, '.');
+ if (!s)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "sysname \"%s\" does not have '.' in the expected place.", sysname);
+
+ *s = '\0';
+ interf = s + 1;
+
+ /* prefix every port number in the chain with "u" */
+ string_replace_char(ports, '.', 'u');
+
+ /* suppress the common config == 1 */
+ if (streq(config, "1"))
+ config = NULL;
+
+ /* suppress the interface == 0 */
+ if (streq(interf, "0"))
+ interf = NULL;
+
+ buf = strjoin("u", ports,
+ config ? "c" : "", strempty(config),
+ interf ? "i" : "", strempty(interf));
+ if (!buf)
+ return log_oom_debug();
+
+ log_device_debug(dev, "USB name identifier: ports=%s config=%s interface=%s %s %s",
+ ports, strna(config), strna(interf),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), buf);
+
+ *ret = buf;
+ return 0;
+}
+
+static int names_usb(sd_device *dev, const char *prefix, bool test) {
+ _cleanup_free_ char *suffix = NULL;
+ sd_device *usbdev, *pcidev;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ /* USB device */
+
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface", &usbdev);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Could not find usb parent device: %m");
+
+ r = get_usb_specifier(usbdev, &suffix);
+ if (r < 0)
+ return r;
+
+ /* If the USB bus is on PCI bus, then suffix the USB specifier to the name based on the PCI bus. */
+ r = sd_device_get_parent_with_subsystem_devtype(usbdev, "pci", NULL, &pcidev);
+ if (r >= 0)
+ return names_pci_slot(dev, pcidev, prefix, suffix, test);
+
+ if (r != -ENOENT || !naming_scheme_has(NAMING_USB_HOST))
+ return log_device_debug_errno(usbdev, r, "Failed to get parent PCI bus: %m");
+
+ /* Otherwise, e.g. on-chip asics that have USB ports, use the USB specifier as is. */
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%s%s", prefix, suffix))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+
+ return 0;
+}
+
+static int get_bcma_specifier(sd_device *dev, char **ret) {
+ const char *sysname;
+ char *buf = NULL;
+ unsigned core;
+ int r;
+
+ assert(dev);
+ assert(ret);
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get sysname: %m");
+
+ /* bus num:core num */
+ r = sscanf(sysname, "bcma%*u:%u", &core);
+ if (r != 1)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "Failed to parse bcma device information.");
+
+ /* suppress the common core == 0 */
+ if (core > 0 && asprintf(&buf, "b%u", core) < 0)
+ return log_oom_debug();
+
+ log_device_debug(dev, "BCMA core identifier: core=%u %s \"%s\"",
+ core, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), strna(buf));
+
+ *ret = buf;
+ return 0;
+}
+
+static int names_bcma(sd_device *dev, const char *prefix, bool test) {
+ _cleanup_free_ char *suffix = NULL;
+ sd_device *bcmadev, *pcidev;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "bcma", NULL, &bcmadev);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Could not get bcma parent device: %m");
+
+ r = sd_device_get_parent_with_subsystem_devtype(bcmadev, "pci", NULL, &pcidev);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Could not get pci parent device: %m");
+
+ r = get_bcma_specifier(bcmadev, &suffix);
+ if (r < 0)
+ return r;
+
+ return names_pci_slot(dev, pcidev, prefix, suffix, test);
+}
+
+static int names_ccw(sd_device *dev, const char *prefix, bool test) {
+ sd_device *cdev;
+ const char *bus_id;
+ size_t bus_id_start, bus_id_len;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ /* get path names for Linux on System z network devices */
+
+ if (get_matching_parent(dev, STRV_MAKE("ccwgroup", "ccw"), /* skip_virtio = */ true, &cdev) < 0)
+ return 0;
+
+ log_device_debug(dev, "Device is CCW.");
+
+ /* Retrieve bus-ID of the CCW device. The bus-ID uniquely
+ * identifies the network device on the Linux on System z channel
+ * subsystem. Note that the bus-ID contains lowercase characters.
+ */
+ r = sd_device_get_sysname(cdev, &bus_id);
+ if (r < 0)
+ return log_device_debug_errno(cdev, r, "Failed to get sysname: %m");
+
+ /* Check the length of the bus-ID. Rely on the fact that the kernel provides a correct bus-ID;
+ * alternatively, improve this check and parse and verify each bus-ID part...
+ */
+ bus_id_len = strlen(bus_id);
+ if (!IN_SET(bus_id_len, 8, 9))
+ return log_device_debug_errno(cdev, SYNTHETIC_ERRNO(EINVAL), "Invalid bus_id: %s", bus_id);
+
+ /* Strip leading zeros from the bus id for aesthetic purposes. This
+ * keeps the ccw names stable, yet much shorter in general case of
+ * bus_id 0.0.0600 -> 600. This is similar to e.g. how PCI domain is
+ * not prepended when it is zero. Preserve the last 0 for 0.0.0000.
+ */
+ bus_id_start = strspn(bus_id, ".0");
+ bus_id += bus_id_start < bus_id_len ? bus_id_start : bus_id_len - 1;
+
+ /* Use the CCW bus-ID as network device name */
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%sc%s", prefix, bus_id))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+ log_device_debug(dev, "CCW identifier: ccw_busid=%s %s \"%s\"",
+ bus_id, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix));
+ return 0;
+}
+
+/* IEEE Organizationally Unique Identifier vendor string */
+static int ieee_oui(sd_device *dev, const struct hw_addr_data *hw_addr, bool test) {
+ char str[32];
+
+ assert(dev);
+ assert(hw_addr);
+
+ if (hw_addr->length != 6)
+ return -EOPNOTSUPP;
+
+ /* skip commonly misused 00:00:00 (Xerox) prefix */
+ if (hw_addr->bytes[0] == 0 &&
+ hw_addr->bytes[1] == 0 &&
+ hw_addr->bytes[2] == 0)
+ return -EINVAL;
+
+ xsprintf(str, "OUI:%02X%02X%02X%02X%02X%02X",
+ hw_addr->bytes[0],
+ hw_addr->bytes[1],
+ hw_addr->bytes[2],
+ hw_addr->bytes[3],
+ hw_addr->bytes[4],
+ hw_addr->bytes[5]);
+
+ return udev_builtin_hwdb_lookup(dev, NULL, str, NULL, test);
+}
+
+static int names_mac(sd_device *dev, const char *prefix, bool test) {
+ unsigned iftype, assign_type;
+ struct hw_addr_data hw_addr;
+ const char *s;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ r = device_get_sysattr_unsigned(dev, "type", &iftype);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to read 'type' attribute: %m");
+
+ /* The persistent part of a hardware address of an InfiniBand NIC is 8 bytes long. We cannot
+ * fit this much in an iface name.
+ * TODO: but it can be used as alternative names?? */
+ if (iftype == ARPHRD_INFINIBAND)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Not generating MAC name for infiniband device.");
+
+ /* check for NET_ADDR_PERM, skip random MAC addresses */
+ r = device_get_sysattr_unsigned(dev, "addr_assign_type", &assign_type);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to read/parse addr_assign_type: %m");
+
+ if (assign_type != NET_ADDR_PERM)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL),
+ "addr_assign_type=%u, MAC address is not permanent.", assign_type);
+
+ r = sd_device_get_sysattr_value(dev, "address", &s);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to read 'address' attribute: %m");
+
+ r = parse_hw_addr(s, &hw_addr);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to parse 'address' attribute: %m");
+
+ if (hw_addr.length != 6)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Not generating MAC name for device with MAC address of length %zu.",
+ hw_addr.length);
+
+ char str[ALTIFNAMSIZ];
+ xsprintf(str, "%sx%s", prefix, HW_ADDR_TO_STR_FULL(&hw_addr, HW_ADDR_TO_STRING_NO_COLON));
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_MAC", str);
+ log_device_debug(dev, "MAC address identifier: hw_addr=%s %s %s",
+ HW_ADDR_TO_STR(&hw_addr),
+ special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix));
+
+ (void) ieee_oui(dev, &hw_addr, test);
+ return 0;
+}
+
+static int names_netdevsim(sd_device *dev, const char *prefix, bool test) {
+ sd_device *netdevsimdev;
+ const char *sysnum, *phys_port_name;
+ unsigned addr;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ /* get netdevsim path names */
+
+ if (!naming_scheme_has(NAMING_NETDEVSIM))
+ return 0;
+
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "netdevsim", NULL, &netdevsimdev);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_sysnum(netdevsimdev, &sysnum);
+ if (r < 0)
+ return log_device_debug_errno(netdevsimdev, r, "Failed to get device sysnum: %m");
+
+ r = safe_atou(sysnum, &addr);
+ if (r < 0)
+ return log_device_debug_errno(netdevsimdev, r, "Failed to parse device sysnum: %m");
+
+ r = sd_device_get_sysattr_value(dev, "phys_port_name", &phys_port_name);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get 'phys_port_name' attribute: %m");
+ if (isempty(phys_port_name))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "The 'phys_port_name' attribute is empty.");
+
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%si%un%s", prefix, addr, phys_port_name))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_PATH", str);
+ log_device_debug(dev, "Netdevsim identifier: address=%u, port_name=%s %s %s",
+ addr, phys_port_name, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix));
+ return 0;
+}
+
+static int names_xen(sd_device *dev, const char *prefix, bool test) {
+ _cleanup_free_ char *vif = NULL;
+ const char *p;
+ unsigned id;
+ int r;
+
+ assert(dev);
+ assert(prefix);
+
+ /* get xen vif "slot" based names. */
+
+ if (!naming_scheme_has(NAMING_XEN_VIF))
+ return 0;
+
+ /* check if our direct parent is a Xen VIF device with no other bus in-between */
+ if (get_matching_parent(dev, STRV_MAKE("xen"), /* skip_virtio = */ false, NULL) < 0)
+ return 0;
+
+ /* Use the vif-n name to extract "n" */
+ r = get_first_syspath_component(dev, "/sys/devices/", &vif);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get Xen VIF name: %m");
+
+ p = startswith(vif, "vif-");
+ if (!p)
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invalid vif name: %s: %m", vif);
+
+ r = safe_atou_full(p, SAFE_ATO_REFUSE_PLUS_MINUS | SAFE_ATO_REFUSE_LEADING_ZERO |
+ SAFE_ATO_REFUSE_LEADING_WHITESPACE | 10, &id);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to parse vif index from '%s': %m", p);
+
+ char str[ALTIFNAMSIZ];
+ if (snprintf_ok(str, sizeof str, "%sX%u", prefix, id))
+ udev_builtin_add_property(dev, test, "ID_NET_NAME_SLOT", str);
+ log_device_debug(dev, "Xen identifier: id=%u %s %s",
+ id, special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), str + strlen(prefix));
+ return 0;
+}
+
+static int get_ifname_prefix(sd_device *dev, const char **ret) {
+ unsigned iftype;
+ int r;
+
+ assert(dev);
+ assert(ret);
+
+ r = device_get_sysattr_unsigned(dev, "type", &iftype);
+ if (r < 0)
+ return r;
+
+ /* handle only ARPHRD_ETHER, ARPHRD_SLIP and ARPHRD_INFINIBAND devices */
+ switch (iftype) {
+ case ARPHRD_ETHER: {
+ const char *s = NULL;
+
+ r = sd_device_get_devtype(dev, &s);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ if (streq_ptr(s, "wlan"))
+ *ret = "wl";
+ else if (streq_ptr(s, "wwan"))
+ *ret = "ww";
+ else
+ *ret = "en";
+ return 0;
+ }
+ case ARPHRD_INFINIBAND:
+ if (!naming_scheme_has(NAMING_INFINIBAND))
+ return -EOPNOTSUPP;
+
+ *ret = "ib";
+ return 0;
+
+ case ARPHRD_SLIP:
+ *ret = "sl";
+ return 0;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int device_is_stacked(sd_device *dev) {
+ int ifindex, iflink, r;
+
+ assert(dev);
+
+ r = sd_device_get_ifindex(dev, &ifindex);
+ if (r < 0)
+ return r;
+
+ r = device_get_sysattr_int(dev, "iflink", &iflink);
+ if (r < 0)
+ return r;
+
+ return ifindex != iflink;
+}
+
+static int builtin_net_id(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ const char *prefix;
+ int r;
+
+ /* skip stacked devices, like VLANs, ... */
+ r = device_is_stacked(dev);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to check if the device is stacked: %m");
+ if (r > 0)
+ return 0;
+
+ r = get_ifname_prefix(dev, &prefix);
+ if (r < 0) {
+ log_device_debug_errno(dev, r, "Failed to determine prefix for network interface naming, ignoring: %m");
+ return 0;
+ }
+
+ udev_builtin_add_property(dev, test, "ID_NET_NAMING_SCHEME", naming_scheme()->name);
+
+ (void) names_mac(dev, prefix, test);
+ (void) names_devicetree(dev, prefix, test);
+ (void) names_ccw(dev, prefix, test);
+ (void) names_vio(dev, prefix, test);
+ (void) names_platform(dev, prefix, test);
+ (void) names_netdevsim(dev, prefix, test);
+ (void) names_xen(dev, prefix, test);
+ (void) names_pci(dev, prefix, test);
+ (void) names_usb(dev, prefix, test);
+ (void) names_bcma(dev, prefix, test);
+
+ return 0;
+}
+
+static int builtin_net_id_init(void) {
+ /* Load naming scheme here to suppress log messages in workers. */
+ naming_scheme();
+ return 0;
+}
+
+const UdevBuiltin udev_builtin_net_id = {
+ .name = "net_id",
+ .cmd = builtin_net_id,
+ .init = builtin_net_id_init,
+ .help = "Network device properties",
+};
diff --git a/src/udev/udev-builtin-net_setup_link.c b/src/udev/udev-builtin-net_setup_link.c
new file mode 100644
index 0000000..a308a21
--- /dev/null
+++ b/src/udev/udev-builtin-net_setup_link.c
@@ -0,0 +1,113 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "device-util.h"
+#include "escape.h"
+#include "errno-util.h"
+#include "link-config.h"
+#include "log.h"
+#include "string-util.h"
+#include "strv.h"
+#include "udev-builtin.h"
+
+static LinkConfigContext *ctx = NULL;
+
+static int builtin_net_setup_link(UdevEvent *event, int argc, char **argv, bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ _cleanup_(link_freep) Link *link = NULL;
+ _cleanup_free_ char *joined = NULL;
+ int r;
+
+ if (argc > 1)
+ return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
+
+ r = link_new(ctx, &event->rtnl, dev, &link);
+ if (r == -ENODEV) {
+ log_device_debug_errno(dev, r, "Link vanished while getting information, ignoring.");
+ return 0;
+ }
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to get link information: %m");
+
+ r = link_get_config(ctx, link);
+ if (r < 0) {
+ if (r == -ENOENT) {
+ log_device_debug_errno(dev, r, "No matching link configuration found, ignoring device.");
+ return 0;
+ }
+
+ return log_device_error_errno(dev, r, "Failed to get link config: %m");
+ }
+
+ r = link_apply_config(ctx, &event->rtnl, link);
+ if (r == -ENODEV)
+ log_device_debug_errno(dev, r, "Link vanished while applying configuration, ignoring.");
+ else if (r < 0)
+ log_device_warning_errno(dev, r, "Could not apply link configuration, ignoring: %m");
+
+ udev_builtin_add_property(dev, test, "ID_NET_LINK_FILE", link->config->filename);
+ if (link->new_name)
+ udev_builtin_add_property(dev, test, "ID_NET_NAME", link->new_name);
+
+ event->altnames = TAKE_PTR(link->altnames);
+
+ STRV_FOREACH(d, link->config->dropins) {
+ _cleanup_free_ char *escaped = NULL;
+
+ escaped = xescape(*d, ":");
+ if (!escaped)
+ return log_oom();
+
+ if (!strextend_with_separator(&joined, ":", escaped))
+ return log_oom();
+ }
+
+ udev_builtin_add_property(dev, test, "ID_NET_LINK_FILE_DROPINS", joined);
+
+ return 0;
+}
+
+static int builtin_net_setup_link_init(void) {
+ int r;
+
+ if (ctx)
+ return 0;
+
+ r = link_config_ctx_new(&ctx);
+ if (r < 0)
+ return r;
+
+ r = link_config_load(ctx);
+ if (r < 0)
+ return r;
+
+ log_debug("Created link configuration context.");
+ return 0;
+}
+
+static void builtin_net_setup_link_exit(void) {
+ ctx = link_config_ctx_free(ctx);
+ log_debug("Unloaded link configuration context.");
+}
+
+static bool builtin_net_setup_link_should_reload(void) {
+ if (!ctx)
+ return false;
+
+ if (link_config_should_reload(ctx)) {
+ log_debug("Link configuration context needs reloading.");
+ return true;
+ }
+
+ return false;
+}
+
+const UdevBuiltin udev_builtin_net_setup_link = {
+ .name = "net_setup_link",
+ .cmd = builtin_net_setup_link,
+ .init = builtin_net_setup_link_init,
+ .exit = builtin_net_setup_link_exit,
+ .should_reload = builtin_net_setup_link_should_reload,
+ .help = "Configure network link",
+ .run_once = false,
+};
diff --git a/src/udev/udev-builtin-path_id.c b/src/udev/udev-builtin-path_id.c
new file mode 100644
index 0000000..467c9a6
--- /dev/null
+++ b/src/udev/udev-builtin-path_id.c
@@ -0,0 +1,896 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * compose persistent device path
+ *
+ * Logic based on Hannes Reinecke's shell script.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <linux/usb/ch11.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "device-util.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "sysexits.h"
+#include "udev-builtin.h"
+#include "udev-util.h"
+
+_printf_(2,3)
+static void path_prepend(char **path, const char *fmt, ...) {
+ va_list va;
+ _cleanup_free_ char *pre = NULL;
+ int r;
+
+ va_start(va, fmt);
+ r = vasprintf(&pre, fmt, va);
+ va_end(va);
+ if (r < 0) {
+ log_oom();
+ exit(EX_OSERR);
+ }
+
+ if (*path) {
+ char *new;
+
+ new = strjoin(pre, "-", *path);
+ if (!new) {
+ log_oom();
+ exit(EX_OSERR);
+ }
+
+ free_and_replace(*path, new);
+ } else
+ *path = TAKE_PTR(pre);
+}
+
+/*
+** Linux only supports 32 bit luns.
+** See drivers/scsi/scsi_scan.c::scsilun_to_int() for more details.
+*/
+static int format_lun_number(sd_device *dev, char **path) {
+ const char *sysnum;
+ unsigned long lun;
+ int r;
+
+ r = sd_device_get_sysnum(dev, &sysnum);
+ if (r < 0)
+ return r;
+ if (!sysnum)
+ return -ENOENT;
+
+ r = safe_atolu_full(sysnum, 10, &lun);
+ if (r < 0)
+ return r;
+ if (lun < 256)
+ /* address method 0, peripheral device addressing with bus id of zero */
+ path_prepend(path, "lun-%lu", lun);
+ else
+ /* handle all other lun addressing methods by using a variant of the original lun format */
+ path_prepend(path, "lun-0x%04lx%04lx00000000", lun & 0xffff, (lun >> 16) & 0xffff);
+
+ return 0;
+}
+
+static sd_device *skip_subsystem(sd_device *dev, const char *subsys) {
+ sd_device *parent;
+
+ assert(dev);
+ assert(subsys);
+
+ /* Unlike the function name, this drops multiple parent devices EXCEPT FOR THE LAST ONE.
+ * The last one will be dropped at the end of the loop in builtin_path_id().
+ * E.g.
+ * Input: /sys/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0
+ * Output: /sys/devices/pci0000:00/0000:00:14.0/usb1
+ */
+
+ for (parent = dev; ; ) {
+ const char *subsystem;
+
+ if (sd_device_get_subsystem(parent, &subsystem) < 0)
+ break;
+
+ if (!streq(subsystem, subsys))
+ break;
+
+ dev = parent;
+ if (sd_device_get_parent(dev, &parent) < 0)
+ break;
+ }
+
+ return dev;
+}
+
+static sd_device *handle_scsi_fibre_channel(sd_device *parent, char **path) {
+ sd_device *targetdev;
+ _cleanup_(sd_device_unrefp) sd_device *fcdev = NULL;
+ const char *port, *sysname;
+ _cleanup_free_ char *lun = NULL;
+
+ assert(parent);
+ assert(path);
+
+ if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0)
+ return NULL;
+ if (sd_device_get_sysname(targetdev, &sysname) < 0)
+ return NULL;
+ if (sd_device_new_from_subsystem_sysname(&fcdev, "fc_transport", sysname) < 0)
+ return NULL;
+ if (sd_device_get_sysattr_value(fcdev, "port_name", &port) < 0)
+ return NULL;
+
+ format_lun_number(parent, &lun);
+ path_prepend(path, "fc-%s-%s", port, lun);
+ return parent;
+}
+
+static sd_device *handle_scsi_sas_wide_port(sd_device *parent, char **path) {
+ sd_device *targetdev, *target_parent;
+ _cleanup_(sd_device_unrefp) sd_device *sasdev = NULL;
+ const char *sas_address, *sysname;
+ _cleanup_free_ char *lun = NULL;
+
+ assert(parent);
+ assert(path);
+
+ if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0)
+ return NULL;
+ if (sd_device_get_parent(targetdev, &target_parent) < 0)
+ return NULL;
+ if (sd_device_get_sysname(target_parent, &sysname) < 0)
+ return NULL;
+ if (sd_device_new_from_subsystem_sysname(&sasdev, "sas_device", sysname) < 0)
+ return NULL;
+ if (sd_device_get_sysattr_value(sasdev, "sas_address", &sas_address) < 0)
+ return NULL;
+
+ format_lun_number(parent, &lun);
+ path_prepend(path, "sas-%s-%s", sas_address, lun);
+ return parent;
+}
+
+static sd_device *handle_scsi_sas(sd_device *parent, char **path) {
+ sd_device *targetdev, *target_parent, *port, *expander;
+ _cleanup_(sd_device_unrefp) sd_device *target_sasdev = NULL, *expander_sasdev = NULL, *port_sasdev = NULL;
+ const char *sas_address = NULL;
+ const char *phy_id;
+ const char *phy_count, *sysname;
+ _cleanup_free_ char *lun = NULL;
+
+ assert(parent);
+ assert(path);
+
+ if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_target", &targetdev) < 0)
+ return NULL;
+ if (sd_device_get_parent(targetdev, &target_parent) < 0)
+ return NULL;
+ if (sd_device_get_sysname(target_parent, &sysname) < 0)
+ return NULL;
+ /* Get sas device */
+ if (sd_device_new_from_subsystem_sysname(&target_sasdev, "sas_device", sysname) < 0)
+ return NULL;
+ /* The next parent is sas port */
+ if (sd_device_get_parent(target_parent, &port) < 0)
+ return NULL;
+ if (sd_device_get_sysname(port, &sysname) < 0)
+ return NULL;
+ /* Get port device */
+ if (sd_device_new_from_subsystem_sysname(&port_sasdev, "sas_port", sysname) < 0)
+ return NULL;
+ if (sd_device_get_sysattr_value(port_sasdev, "num_phys", &phy_count) < 0)
+ return NULL;
+
+ /* Check if we are simple disk */
+ if (strncmp(phy_count, "1", 2) != 0)
+ return handle_scsi_sas_wide_port(parent, path);
+
+ /* Get connected phy */
+ if (sd_device_get_sysattr_value(target_sasdev, "phy_identifier", &phy_id) < 0)
+ return NULL;
+
+ /* The port's parent is either hba or expander */
+ if (sd_device_get_parent(port, &expander) < 0)
+ return NULL;
+
+ if (sd_device_get_sysname(expander, &sysname) < 0)
+ return NULL;
+ /* Get expander device */
+ if (sd_device_new_from_subsystem_sysname(&expander_sasdev, "sas_device", sysname) >= 0) {
+ /* Get expander's address */
+ if (sd_device_get_sysattr_value(expander_sasdev, "sas_address", &sas_address) < 0)
+ return NULL;
+ }
+
+ format_lun_number(parent, &lun);
+ if (sas_address)
+ path_prepend(path, "sas-exp%s-phy%s-%s", sas_address, phy_id, lun);
+ else
+ path_prepend(path, "sas-phy%s-%s", phy_id, lun);
+
+ return parent;
+}
+
+static sd_device *handle_scsi_iscsi(sd_device *parent, char **path) {
+ sd_device *transportdev;
+ _cleanup_(sd_device_unrefp) sd_device *sessiondev = NULL, *conndev = NULL;
+ const char *target, *connname, *addr, *port;
+ _cleanup_free_ char *lun = NULL;
+ const char *sysname, *sysnum;
+
+ assert(parent);
+ assert(path);
+
+ /* find iscsi session */
+ for (transportdev = parent; ; ) {
+
+ if (sd_device_get_parent(transportdev, &transportdev) < 0)
+ return NULL;
+ if (sd_device_get_sysname(transportdev, &sysname) < 0)
+ return NULL;
+ if (startswith(sysname, "session"))
+ break;
+ }
+
+ /* find iscsi session device */
+ if (sd_device_new_from_subsystem_sysname(&sessiondev, "iscsi_session", sysname) < 0)
+ return NULL;
+
+ if (sd_device_get_sysattr_value(sessiondev, "targetname", &target) < 0)
+ return NULL;
+
+ if (sd_device_get_sysnum(transportdev, &sysnum) < 0 || !sysnum)
+ return NULL;
+ connname = strjoina("connection", sysnum, ":0");
+ if (sd_device_new_from_subsystem_sysname(&conndev, "iscsi_connection", connname) < 0)
+ return NULL;
+
+ if (sd_device_get_sysattr_value(conndev, "persistent_address", &addr) < 0)
+ return NULL;
+ if (sd_device_get_sysattr_value(conndev, "persistent_port", &port) < 0)
+ return NULL;
+
+ format_lun_number(parent, &lun);
+ path_prepend(path, "ip-%s:%s-iscsi-%s-%s", addr, port, target, lun);
+ return parent;
+}
+
+static sd_device *handle_scsi_ata(sd_device *parent, char **path, char **compat_path) {
+ sd_device *targetdev, *target_parent;
+ _cleanup_(sd_device_unrefp) sd_device *atadev = NULL;
+ const char *port_no, *sysname, *name;
+ unsigned host, bus, target, lun;
+
+ assert(parent);
+ assert(path);
+
+ if (sd_device_get_sysname(parent, &name) < 0)
+ return NULL;
+ if (sscanf(name, "%u:%u:%u:%u", &host, &bus, &target, &lun) != 4)
+ return NULL;
+
+ if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &targetdev) < 0)
+ return NULL;
+
+ if (sd_device_get_parent(targetdev, &target_parent) < 0)
+ return NULL;
+
+ if (sd_device_get_sysname(target_parent, &sysname) < 0)
+ return NULL;
+ if (sd_device_new_from_subsystem_sysname(&atadev, "ata_port", sysname) < 0)
+ return NULL;
+
+ if (sd_device_get_sysattr_value(atadev, "port_no", &port_no) < 0)
+ return NULL;
+
+ if (bus != 0)
+ /* Devices behind port multiplier have a bus != 0 */
+ path_prepend(path, "ata-%s.%u.0", port_no, bus);
+ else
+ /* Master/slave are distinguished by target id */
+ path_prepend(path, "ata-%s.%u", port_no, target);
+
+ /* old compatible persistent link for ATA devices */
+ if (compat_path)
+ path_prepend(compat_path, "ata-%s", port_no);
+
+ return parent;
+}
+
+static sd_device *handle_scsi_default(sd_device *parent, char **path) {
+ sd_device *hostdev;
+ int host, bus, target, lun;
+ const char *name, *base, *pos;
+ _cleanup_closedir_ DIR *dir = NULL;
+ int basenum = -1;
+
+ assert(parent);
+ assert(path);
+
+ if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &hostdev) < 0)
+ return NULL;
+
+ if (sd_device_get_sysname(parent, &name) < 0)
+ return NULL;
+ if (sscanf(name, "%d:%d:%d:%d", &host, &bus, &target, &lun) != 4)
+ return NULL;
+
+ /*
+ * Rebase host offset to get the local relative number
+ *
+ * Note: This is by definition racy, unreliable and too simple.
+ * Please do not copy this model anywhere. It's just a left-over
+ * from the time we had no idea how things should look like in
+ * the end.
+ *
+ * Making assumptions about a global in-kernel counter and use
+ * that to calculate a local offset is a very broken concept. It
+ * can only work as long as things are in strict order.
+ *
+ * The kernel needs to export the instance/port number of a
+ * controller directly, without the need for rebase magic like
+ * this. Manual driver unbind/bind, parallel hotplug/unplug will
+ * get into the way of this "I hope it works" logic.
+ */
+
+ if (sd_device_get_syspath(hostdev, &base) < 0)
+ return NULL;
+ pos = strrchr(base, '/');
+ if (!pos)
+ return NULL;
+
+ base = strndupa_safe(base, pos - base);
+ dir = opendir(base);
+ if (!dir)
+ return NULL;
+
+ FOREACH_DIRENT_ALL(de, dir, break) {
+ unsigned i;
+
+ if (de->d_name[0] == '.')
+ continue;
+ if (!IN_SET(de->d_type, DT_DIR, DT_LNK))
+ continue;
+ if (!startswith(de->d_name, "host"))
+ continue;
+ if (safe_atou_full(&de->d_name[4], 10, &i) < 0)
+ continue;
+ /*
+ * find the smallest number; the host really needs to export its
+ * own instance number per parent device; relying on the global host
+ * enumeration and plainly rebasing the numbers sounds unreliable
+ */
+ if (basenum == -1 || (int) i < basenum)
+ basenum = i;
+ }
+ if (basenum == -1)
+ return hostdev;
+ host -= basenum;
+
+ path_prepend(path, "scsi-%i:%i:%i:%i", host, bus, target, lun);
+ return hostdev;
+}
+
+static sd_device *handle_scsi_hyperv(sd_device *parent, char **path, size_t guid_str_len) {
+ sd_device *hostdev;
+ sd_device *vmbusdev;
+ const char *guid_str;
+ _cleanup_free_ char *lun = NULL;
+ char guid[39];
+
+ assert(parent);
+ assert(path);
+ assert(guid_str_len < sizeof(guid));
+
+ if (sd_device_get_parent_with_subsystem_devtype(parent, "scsi", "scsi_host", &hostdev) < 0)
+ return NULL;
+
+ if (sd_device_get_parent(hostdev, &vmbusdev) < 0)
+ return NULL;
+
+ if (sd_device_get_sysattr_value(vmbusdev, "device_id", &guid_str) < 0)
+ return NULL;
+
+ if (strlen(guid_str) < guid_str_len || guid_str[0] != '{' || guid_str[guid_str_len-1] != '}')
+ return NULL;
+
+ size_t k = 0;
+ for (size_t i = 1; i < guid_str_len-1; i++) {
+ if (guid_str[i] == '-')
+ continue;
+ guid[k++] = guid_str[i];
+ }
+ guid[k] = '\0';
+
+ format_lun_number(parent, &lun);
+ path_prepend(path, "vmbus-%s-%s", guid, lun);
+ return parent;
+}
+
+static sd_device *handle_scsi(sd_device *parent, char **path, char **compat_path, bool *supported_parent) {
+ const char *devtype, *id, *name;
+
+ if (sd_device_get_devtype(parent, &devtype) < 0 ||
+ !streq(devtype, "scsi_device"))
+ return parent;
+
+ /* firewire */
+ if (sd_device_get_sysattr_value(parent, "ieee1394_id", &id) >= 0) {
+ path_prepend(path, "ieee1394-0x%s", id);
+ *supported_parent = true;
+ return skip_subsystem(parent, "scsi");
+ }
+
+ /* scsi sysfs does not have a "subsystem" for the transport */
+ if (sd_device_get_syspath(parent, &name) < 0)
+ return NULL;
+
+ if (strstr(name, "/rport-")) {
+ *supported_parent = true;
+ return handle_scsi_fibre_channel(parent, path);
+ }
+
+ if (strstr(name, "/end_device-")) {
+ *supported_parent = true;
+ return handle_scsi_sas(parent, path);
+ }
+
+ if (strstr(name, "/session")) {
+ *supported_parent = true;
+ return handle_scsi_iscsi(parent, path);
+ }
+
+ if (strstr(name, "/ata"))
+ return handle_scsi_ata(parent, path, compat_path);
+
+ if (strstr(name, "/vmbus_"))
+ return handle_scsi_hyperv(parent, path, 37);
+ else if (strstr(name, "/VMBUS"))
+ return handle_scsi_hyperv(parent, path, 38);
+
+ return handle_scsi_default(parent, path);
+}
+
+static sd_device *handle_cciss(sd_device *parent, char **path) {
+ const char *str;
+ unsigned controller, disk;
+
+ if (sd_device_get_sysname(parent, &str) < 0)
+ return NULL;
+ if (sscanf(str, "c%ud%u%*s", &controller, &disk) != 2)
+ return NULL;
+
+ path_prepend(path, "cciss-disk%u", disk);
+ return skip_subsystem(parent, "cciss");
+}
+
+static void handle_scsi_tape(sd_device *dev, char **path) {
+ const char *name;
+
+ /* must be the last device in the syspath */
+ if (*path)
+ return;
+
+ if (sd_device_get_sysname(dev, &name) < 0)
+ return;
+
+ if (startswith(name, "nst") && strchr("lma", name[3]))
+ path_prepend(path, "nst%c", name[3]);
+ else if (startswith(name, "st") && strchr("lma", name[2]))
+ path_prepend(path, "st%c", name[2]);
+}
+
+static int get_usb_revision(sd_device *dev) {
+ uint8_t protocol;
+ const char *s;
+ int r;
+
+ assert(dev);
+
+ /* Returns usb revision 1, 2, or 3. */
+
+ r = sd_device_get_sysattr_value(dev, "bDeviceProtocol", &s);
+ if (r < 0)
+ return r;
+
+ r = safe_atou8_full(s, 16, &protocol);
+ if (r < 0)
+ return r;
+
+ switch (protocol) {
+ case USB_HUB_PR_HS_NO_TT: /* Full speed hub (USB1) or Hi-speed hub without TT (USB2) */
+
+ /* See speed_show() in drivers/usb/core/sysfs.c of the kernel. */
+ r = sd_device_get_sysattr_value(dev, "speed", &s);
+ if (r < 0)
+ return r;
+
+ if (streq(s, "480"))
+ return 2;
+
+ return 1;
+
+ case USB_HUB_PR_HS_SINGLE_TT: /* Hi-speed hub with single TT */
+ case USB_HUB_PR_HS_MULTI_TT: /* Hi-speed hub with multiple TT */
+ return 2;
+
+ case USB_HUB_PR_SS: /* Super speed hub */
+ return 3;
+
+ default:
+ return -EPROTONOSUPPORT;
+ }
+}
+
+static sd_device *handle_usb(sd_device *parent, char **path) {
+ const char *devtype, *str, *port;
+ int r;
+
+ if (sd_device_get_devtype(parent, &devtype) < 0)
+ return parent;
+ if (!STR_IN_SET(devtype, "usb_interface", "usb_device"))
+ return parent;
+
+ if (sd_device_get_sysname(parent, &str) < 0)
+ return parent;
+ port = strchr(str, '-');
+ if (!port)
+ return parent;
+ port++;
+
+ parent = skip_subsystem(parent, "usb");
+ if (!parent)
+ return NULL;
+
+ /* USB host number may change across reboots (and probably even without reboot). The part after USB
+ * host number is determined by device topology and so does not change. Hence, drop the host number
+ * and always use '0' instead.
+ *
+ * xHCI host controllers may register two (or more?) USB root hubs for USB 2.0 and USB 3.0, and the
+ * sysname, whose host number replaced with 0, of a device under the hubs may conflict with others.
+ * To avoid the conflict, let's include the USB revision of the root hub to the PATH_ID.
+ * See issue https://github.com/systemd/systemd/issues/19406 for more details. */
+ r = get_usb_revision(parent);
+ if (r < 0) {
+ log_device_debug_errno(parent, r, "Failed to get the USB revision number, ignoring: %m");
+ path_prepend(path, "usb-0:%s", port);
+ } else {
+ assert(r > 0);
+ path_prepend(path, "usbv%i-0:%s", r, port);
+ }
+
+ return parent;
+}
+
+static sd_device *handle_bcma(sd_device *parent, char **path) {
+ const char *sysname;
+ unsigned core;
+
+ if (sd_device_get_sysname(parent, &sysname) < 0)
+ return NULL;
+ if (sscanf(sysname, "bcma%*u:%u", &core) != 1)
+ return NULL;
+
+ path_prepend(path, "bcma-%u", core);
+ return parent;
+}
+
+/* Handle devices of AP bus in System z platform. */
+static sd_device *handle_ap(sd_device *parent, char **path) {
+ const char *type, *func;
+
+ assert(parent);
+ assert(path);
+
+ if (sd_device_get_sysattr_value(parent, "type", &type) >= 0 &&
+ sd_device_get_sysattr_value(parent, "ap_functions", &func) >= 0)
+ path_prepend(path, "ap-%s-%s", type, func);
+ else {
+ const char *sysname;
+
+ if (sd_device_get_sysname(parent, &sysname) >= 0)
+ path_prepend(path, "ap-%s", sysname);
+ }
+
+ return skip_subsystem(parent, "ap");
+}
+
+static int find_real_nvme_parent(sd_device *dev, sd_device **ret) {
+ _cleanup_(sd_device_unrefp) sd_device *nvme = NULL;
+ const char *sysname, *end, *devpath;
+ int r;
+
+ /* If the device belongs to "nvme-subsystem" (not to be confused with "nvme"), which happens when
+ * NVMe multipathing is enabled in the kernel (/sys/module/nvme_core/parameters/multipath is Y),
+ * then the syspath is something like the following:
+ * /sys/devices/virtual/nvme-subsystem/nvme-subsys0/nvme0n1
+ * Hence, we need to find the 'real parent' in "nvme" subsystem, e.g,
+ * /sys/devices/pci0000:00/0000:00:1c.4/0000:3c:00.0/nvme/nvme0 */
+
+ assert(dev);
+ assert(ret);
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return r;
+
+ /* The sysname format of nvme block device is nvme%d[c%d]n%d[p%d], e.g. nvme0n1p2 or nvme0c1n2.
+ * (Note, nvme device with 'c' can be ignored, as they are hidden. )
+ * The sysname format of nvme subsystem device is nvme%d.
+ * See nvme_alloc_ns() and nvme_init_ctrl() in drivers/nvme/host/core.c for more details. */
+ end = startswith(sysname, "nvme");
+ if (!end)
+ return -ENXIO;
+
+ end += strspn(end, DIGITS);
+ sysname = strndupa(sysname, end - sysname);
+
+ r = sd_device_new_from_subsystem_sysname(&nvme, "nvme", sysname);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_devpath(nvme, &devpath);
+ if (r < 0)
+ return r;
+
+ /* If the 'real parent' is (still) virtual, e.g. for nvmf disks, refuse to set ID_PATH. */
+ if (path_startswith(devpath, "/devices/virtual/"))
+ return -ENXIO;
+
+ *ret = TAKE_PTR(nvme);
+ return 0;
+}
+
+static void add_id_with_usb_revision(sd_device *dev, bool test, char *path) {
+ char *p;
+ int r;
+
+ assert(dev);
+ assert(path);
+
+ /* When the path contains the USB revision, let's adds ID_PATH_WITH_USB_REVISION property and
+ * drop the version specifier for later use. */
+
+ p = strstrafter(path, "-usbv");
+ if (!p)
+ return;
+ if (!ascii_isdigit(p[0]))
+ return;
+ if (p[1] != '-')
+ return;
+
+ r = udev_builtin_add_property(dev, test, "ID_PATH_WITH_USB_REVISION", path);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to add ID_PATH_WITH_USB_REVISION property, ignoring: %m");
+
+ /* Drop the USB revision specifier for backward compatibility. */
+ memmove(p - 1, p + 1, strlen(p + 1) + 1);
+}
+
+static void add_id_tag(sd_device *dev, bool test, const char *path) {
+ char tag[UDEV_NAME_SIZE];
+ size_t i = 0;
+ int r;
+
+ /* compose valid udev tag name */
+ for (const char *p = path; *p; p++) {
+ if (ascii_isdigit(*p) ||
+ ascii_isalpha(*p) ||
+ *p == '-') {
+ tag[i++] = *p;
+ continue;
+ }
+
+ /* skip all leading '_' */
+ if (i == 0)
+ continue;
+
+ /* avoid second '_' */
+ if (tag[i-1] == '_')
+ continue;
+
+ tag[i++] = '_';
+ }
+ /* strip trailing '_' */
+ while (i > 0 && tag[i-1] == '_')
+ i--;
+ tag[i] = '\0';
+
+ r = udev_builtin_add_property(dev, test, "ID_PATH_TAG", tag);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to add ID_PATH_TAG property, ignoring: %m");
+}
+
+static int builtin_path_id(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ _cleanup_(sd_device_unrefp) sd_device *dev_other_branch = NULL;
+ _cleanup_free_ char *path = NULL, *compat_path = NULL;
+ bool supported_transport = false, supported_parent = false;
+ const char *subsystem;
+ int r;
+
+ /* walk up the chain of devices and compose path */
+ for (sd_device *parent = dev; parent; ) {
+ const char *subsys, *sysname;
+
+ if (sd_device_get_subsystem(parent, &subsys) < 0 ||
+ sd_device_get_sysname(parent, &sysname) < 0) {
+ ;
+ } else if (streq(subsys, "scsi_tape")) {
+ handle_scsi_tape(parent, &path);
+ } else if (streq(subsys, "scsi")) {
+ parent = handle_scsi(parent, &path, &compat_path, &supported_parent);
+ supported_transport = true;
+ } else if (streq(subsys, "cciss")) {
+ parent = handle_cciss(parent, &path);
+ supported_transport = true;
+ } else if (streq(subsys, "usb")) {
+ parent = handle_usb(parent, &path);
+ supported_transport = true;
+ } else if (streq(subsys, "bcma")) {
+ parent = handle_bcma(parent, &path);
+ supported_transport = true;
+ } else if (streq(subsys, "serio")) {
+ const char *sysnum;
+
+ if (sd_device_get_sysnum(parent, &sysnum) >= 0 && sysnum) {
+ path_prepend(&path, "serio-%s", sysnum);
+ parent = skip_subsystem(parent, "serio");
+ }
+ } else if (streq(subsys, "pci")) {
+ path_prepend(&path, "pci-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "pci-%s", sysname);
+ parent = skip_subsystem(parent, "pci");
+ supported_parent = true;
+ } else if (streq(subsys, "platform")) {
+ path_prepend(&path, "platform-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "platform-%s", sysname);
+ parent = skip_subsystem(parent, "platform");
+ supported_transport = true;
+ supported_parent = true;
+ } else if (streq(subsys, "amba")) {
+ path_prepend(&path, "amba-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "amba-%s", sysname);
+ parent = skip_subsystem(parent, "amba");
+ supported_transport = true;
+ supported_parent = true;
+ } else if (streq(subsys, "acpi")) {
+ path_prepend(&path, "acpi-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "acpi-%s", sysname);
+ parent = skip_subsystem(parent, "acpi");
+ supported_parent = true;
+ } else if (streq(subsys, "xen")) {
+ path_prepend(&path, "xen-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "xen-%s", sysname);
+ parent = skip_subsystem(parent, "xen");
+ supported_parent = true;
+ } else if (streq(subsys, "virtio")) {
+ parent = skip_subsystem(parent, "virtio");
+ supported_transport = true;
+ } else if (streq(subsys, "scm")) {
+ path_prepend(&path, "scm-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "scm-%s", sysname);
+ parent = skip_subsystem(parent, "scm");
+ supported_transport = true;
+ supported_parent = true;
+ } else if (streq(subsys, "ccw")) {
+ path_prepend(&path, "ccw-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "ccw-%s", sysname);
+ parent = skip_subsystem(parent, "ccw");
+ supported_transport = true;
+ supported_parent = true;
+ } else if (streq(subsys, "ccwgroup")) {
+ path_prepend(&path, "ccwgroup-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "ccwgroup-%s", sysname);
+ parent = skip_subsystem(parent, "ccwgroup");
+ supported_transport = true;
+ supported_parent = true;
+ } else if (streq(subsys, "ap")) {
+ parent = handle_ap(parent, &path);
+ supported_transport = true;
+ supported_parent = true;
+ } else if (streq(subsys, "iucv")) {
+ path_prepend(&path, "iucv-%s", sysname);
+ if (compat_path)
+ path_prepend(&compat_path, "iucv-%s", sysname);
+ parent = skip_subsystem(parent, "iucv");
+ supported_transport = true;
+ supported_parent = true;
+ } else if (STR_IN_SET(subsys, "nvme", "nvme-subsystem")) {
+ const char *nsid;
+
+ if (sd_device_get_sysattr_value(dev, "nsid", &nsid) >= 0) {
+ path_prepend(&path, "nvme-%s", nsid);
+ if (compat_path)
+ path_prepend(&compat_path, "nvme-%s", nsid);
+
+ if (streq(subsys, "nvme-subsystem")) {
+ r = find_real_nvme_parent(dev, &dev_other_branch);
+ if (r < 0)
+ return r;
+
+ parent = dev_other_branch;
+ }
+
+ parent = skip_subsystem(parent, "nvme");
+ supported_parent = true;
+ supported_transport = true;
+ }
+ } else if (streq(subsys, "spi")) {
+ const char *sysnum;
+
+ if (sd_device_get_sysnum(parent, &sysnum) >= 0 && sysnum) {
+ path_prepend(&path, "cs-%s", sysnum);
+ parent = skip_subsystem(parent, "spi");
+ }
+ }
+
+ if (!parent)
+ break;
+ if (sd_device_get_parent(parent, &parent) < 0)
+ break;
+ }
+
+ if (!path)
+ return -ENOENT;
+
+ /*
+ * Do not return devices with an unknown parent device type. They
+ * might produce conflicting IDs if the parent does not provide a
+ * unique and predictable name.
+ */
+ if (!supported_parent)
+ return -ENOENT;
+
+ /*
+ * Do not return block devices without a well-known transport. Some
+ * devices do not expose their buses and do not provide a unique
+ * and predictable name that way.
+ */
+ if (sd_device_get_subsystem(dev, &subsystem) >= 0 &&
+ streq(subsystem, "block") &&
+ !supported_transport)
+ return -ENOENT;
+
+ add_id_with_usb_revision(dev, test, path);
+
+ r = udev_builtin_add_property(dev, test, "ID_PATH", path);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to add ID_PATH property, ignoring: %m");
+
+ add_id_tag(dev, test, path);
+
+ /*
+ * Compatible link generation for ATA devices
+ * we assign compat_link to the env variable
+ * ID_PATH_ATA_COMPAT
+ */
+ if (compat_path)
+ udev_builtin_add_property(dev, test, "ID_PATH_ATA_COMPAT", compat_path);
+
+ return 0;
+}
+
+const UdevBuiltin udev_builtin_path_id = {
+ .name = "path_id",
+ .cmd = builtin_path_id,
+ .help = "Compose persistent device path",
+ .run_once = true,
+};
diff --git a/src/udev/udev-builtin-uaccess.c b/src/udev/udev-builtin-uaccess.c
new file mode 100644
index 0000000..da42ef5
--- /dev/null
+++ b/src/udev/udev-builtin-uaccess.c
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * manage device node user ACL
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#include "sd-login.h"
+
+#include "device-util.h"
+#include "devnode-acl.h"
+#include "errno-util.h"
+#include "login-util.h"
+#include "log.h"
+#include "udev-builtin.h"
+
+static int builtin_uaccess(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ const char *path = NULL, *seat;
+ bool changed_acl = false;
+ uid_t uid;
+ int r;
+
+ umask(0022);
+
+ /* don't muck around with ACLs when the system is not running systemd */
+ if (!logind_running())
+ return 0;
+
+ r = sd_device_get_devname(dev, &path);
+ if (r < 0) {
+ log_device_error_errno(dev, r, "Failed to get device name: %m");
+ goto finish;
+ }
+
+ if (sd_device_get_property_value(dev, "ID_SEAT", &seat) < 0)
+ seat = "seat0";
+
+ r = sd_seat_get_active(seat, NULL, &uid);
+ if (r < 0) {
+ if (IN_SET(r, -ENXIO, -ENODATA))
+ /* No active session on this seat */
+ r = 0;
+ else
+ log_device_error_errno(dev, r, "Failed to determine active user on seat %s: %m", seat);
+
+ goto finish;
+ }
+
+ r = devnode_acl(path, true, false, 0, true, uid);
+ if (r < 0) {
+ log_device_full_errno(dev, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to apply ACL: %m");
+ goto finish;
+ }
+
+ changed_acl = true;
+ r = 0;
+
+finish:
+ if (path && !changed_acl) {
+ int k;
+
+ /* Better be safe than sorry and reset ACL */
+ k = devnode_acl(path, true, false, 0, false, 0);
+ if (k < 0) {
+ log_device_full_errno(dev, k == -ENOENT ? LOG_DEBUG : LOG_ERR, k, "Failed to apply ACL: %m");
+ RET_GATHER(r, k);
+ }
+ }
+
+ return r;
+}
+
+const UdevBuiltin udev_builtin_uaccess = {
+ .name = "uaccess",
+ .cmd = builtin_uaccess,
+ .help = "Manage device node user ACL",
+};
diff --git a/src/udev/udev-builtin-usb_id.c b/src/udev/udev-builtin-usb_id.c
new file mode 100644
index 0000000..8e83c9c
--- /dev/null
+++ b/src/udev/udev-builtin-usb_id.c
@@ -0,0 +1,489 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * USB device properties and persistent device path
+ *
+ * Copyright (c) 2005 SUSE Linux Products GmbH, Germany
+ * Author: Hannes Reinecke <hare@suse.de>
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "device-nodes.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+#include "udev-util.h"
+
+static void set_usb_iftype(char *to, int if_class_num, size_t len) {
+ const char *type = "generic";
+
+ switch (if_class_num) {
+ case 1:
+ type = "audio";
+ break;
+ case 2: /* CDC-Control */
+ break;
+ case 3:
+ type = "hid";
+ break;
+ case 5: /* Physical */
+ break;
+ case 6:
+ type = "media";
+ break;
+ case 7:
+ type = "printer";
+ break;
+ case 8:
+ type = "storage";
+ break;
+ case 9:
+ type = "hub";
+ break;
+ case 0x0a: /* CDC-Data */
+ break;
+ case 0x0b: /* Chip/Smart Card */
+ break;
+ case 0x0d: /* Content Security */
+ break;
+ case 0x0e:
+ type = "video";
+ break;
+ case 0xdc: /* Diagnostic Device */
+ break;
+ case 0xe0: /* Wireless Controller */
+ break;
+ case 0xfe: /* Application-specific */
+ break;
+ case 0xff: /* Vendor-specific */
+ break;
+ default:
+ break;
+ }
+ strncpy(to, type, len);
+ to[len-1] = '\0';
+}
+
+static int set_usb_mass_storage_ifsubtype(char *to, const char *from, size_t len) {
+ int type_num = 0;
+ const char *type = "generic";
+
+ if (safe_atoi(from, &type_num) >= 0) {
+ switch (type_num) {
+ case 1: /* RBC devices */
+ type = "rbc";
+ break;
+ case 2:
+ type = "atapi";
+ break;
+ case 3:
+ type = "tape";
+ break;
+ case 4: /* UFI */
+ type = "floppy";
+ break;
+ case 6: /* Transparent SPC-2 devices */
+ type = "scsi";
+ break;
+ default:
+ break;
+ }
+ }
+ strscpy(to, len, type);
+ return type_num;
+}
+
+static void set_scsi_type(char *to, const char *from, size_t len) {
+ unsigned type_num;
+ const char *type = "generic";
+
+ if (safe_atou(from, &type_num) >= 0) {
+ switch (type_num) {
+ case 0:
+ case 0xe:
+ type = "disk";
+ break;
+ case 1:
+ type = "tape";
+ break;
+ case 4:
+ case 7:
+ case 0xf:
+ type = "optical";
+ break;
+ case 5:
+ type = "cd";
+ break;
+ default:
+ break;
+ }
+ }
+ strscpy(to, len, type);
+}
+
+#define USB_DT_DEVICE 0x01
+#define USB_DT_INTERFACE 0x04
+
+static int dev_if_packed_info(sd_device *dev, char *ifs_str, size_t len) {
+ _cleanup_close_ int fd = -EBADF;
+ ssize_t size;
+ unsigned char buf[18 + 65535];
+ size_t pos = 0;
+ unsigned strpos = 0;
+ const char *filename, *syspath;
+ int r;
+ struct usb_interface_descriptor {
+ uint8_t bLength;
+ uint8_t bDescriptorType;
+ uint8_t bInterfaceNumber;
+ uint8_t bAlternateSetting;
+ uint8_t bNumEndpoints;
+ uint8_t bInterfaceClass;
+ uint8_t bInterfaceSubClass;
+ uint8_t bInterfaceProtocol;
+ uint8_t iInterface;
+ } _packed_;
+
+ r = sd_device_get_syspath(dev, &syspath);
+ if (r < 0)
+ return r;
+
+ filename = strjoina(syspath, "/descriptors");
+ fd = open(filename, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0)
+ return log_device_debug_errno(dev, errno, "Failed to open \"%s\": %m", filename);
+
+ size = read(fd, buf, sizeof(buf));
+ if (size < 18)
+ return log_device_warning_errno(dev, SYNTHETIC_ERRNO(EIO),
+ "Short read from \"%s\"", filename);
+ assert((size_t) size <= sizeof buf);
+
+ ifs_str[0] = '\0';
+ while (pos + sizeof(struct usb_interface_descriptor) < (size_t) size &&
+ strpos + 7 < len - 2) {
+
+ struct usb_interface_descriptor *desc;
+ char if_str[8];
+
+ desc = (struct usb_interface_descriptor *) (buf + pos);
+ if (desc->bLength < 3)
+ break;
+ if (desc->bLength > size - sizeof(struct usb_interface_descriptor))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EIO),
+ "Corrupt data read from \"%s\"", filename);
+ pos += desc->bLength;
+
+ if (desc->bDescriptorType != USB_DT_INTERFACE)
+ continue;
+
+ if (snprintf(if_str, 8, ":%02x%02x%02x",
+ desc->bInterfaceClass,
+ desc->bInterfaceSubClass,
+ desc->bInterfaceProtocol) != 7)
+ continue;
+
+ if (strstr(ifs_str, if_str))
+ continue;
+
+ memcpy(&ifs_str[strpos], if_str, 8),
+ strpos += 7;
+ }
+
+ if (strpos > 0) {
+ ifs_str[strpos++] = ':';
+ ifs_str[strpos++] = '\0';
+ }
+
+ return 0;
+}
+
+/*
+ * A unique USB identification is generated like this:
+ *
+ * 1.) Get the USB device type from InterfaceClass and InterfaceSubClass
+ * 2.) If the device type is 'Mass-Storage/SPC-2' or 'Mass-Storage/RBC',
+ * use the SCSI vendor and model as USB-Vendor and USB-model.
+ * 3.) Otherwise, use the USB manufacturer and product as
+ * USB-Vendor and USB-model. Any non-printable characters
+ * in those strings will be skipped; a slash '/' will be converted
+ * into a full stop '.'.
+ * 4.) If that fails, too, we will use idVendor and idProduct
+ * as USB-Vendor and USB-model.
+ * 5.) The USB identification is the USB-vendor and USB-model
+ * string concatenated with an underscore '_'.
+ * 6.) If the device supplies a serial number, this number
+ * is concatenated with the identification with an underscore '_'.
+ */
+static int builtin_usb_id(UdevEvent *event, int argc, char *argv[], bool test) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ char vendor_str[64] = "";
+ char vendor_str_enc[256];
+ const char *vendor_id;
+ char model_str[64] = "";
+ char model_str_enc[256];
+ const char *product_id;
+ char serial_str[UDEV_NAME_SIZE] = "";
+ char packed_if_str[UDEV_NAME_SIZE] = "";
+ char revision_str[64] = "";
+ char type_str[64] = "";
+ char instance_str[64] = "";
+ const char *ifnum = NULL;
+ const char *driver = NULL;
+ char serial[256];
+
+ sd_device *dev_interface, *dev_usb;
+ const char *if_class, *if_subclass;
+ unsigned if_class_num;
+ int protocol = 0;
+ size_t l;
+ char *s;
+
+ const char *syspath, *sysname, *devtype, *interface_syspath;
+ int r;
+
+ r = sd_device_get_syspath(dev, &syspath);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return r;
+
+ /* shortcut, if we are called directly for a "usb_device" type */
+ if (sd_device_get_devtype(dev, &devtype) >= 0 && streq(devtype, "usb_device")) {
+ dev_if_packed_info(dev, packed_if_str, sizeof(packed_if_str));
+ dev_usb = dev;
+ goto fallback;
+ }
+
+ /* usb interface directory */
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_interface", &dev_interface);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to access usb_interface: %m");
+
+ r = sd_device_get_syspath(dev_interface, &interface_syspath);
+ if (r < 0)
+ return log_device_debug_errno(dev_interface, r, "Failed to get syspath: %m");
+ (void) sd_device_get_sysattr_value(dev_interface, "bInterfaceNumber", &ifnum);
+ (void) sd_device_get_sysattr_value(dev_interface, "driver", &driver);
+
+ r = sd_device_get_sysattr_value(dev_interface, "bInterfaceClass", &if_class);
+ if (r < 0)
+ return log_device_debug_errno(dev_interface, r, "Failed to get bInterfaceClass attribute: %m");
+
+ r = safe_atou_full(if_class, 16, &if_class_num);
+ if (r < 0)
+ return log_device_debug_errno(dev_interface, r, "Failed to parse if_class: %m");
+ if (if_class_num == 8) {
+ /* mass storage */
+ if (sd_device_get_sysattr_value(dev_interface, "bInterfaceSubClass", &if_subclass) >= 0)
+ protocol = set_usb_mass_storage_ifsubtype(type_str, if_subclass, sizeof(type_str)-1);
+ } else
+ set_usb_iftype(type_str, if_class_num, sizeof(type_str)-1);
+
+ log_device_debug(dev_interface, "if_class:%u protocol:%i", if_class_num, protocol);
+
+ /* usb device directory */
+ r = sd_device_get_parent_with_subsystem_devtype(dev_interface, "usb", "usb_device", &dev_usb);
+ if (r < 0)
+ return log_device_debug_errno(dev_interface, r, "Failed to find parent 'usb' device");
+
+ /* all interfaces of the device in a single string */
+ dev_if_packed_info(dev_usb, packed_if_str, sizeof(packed_if_str));
+
+ /* mass storage : SCSI or ATAPI */
+ if (IN_SET(protocol, 6, 2)) {
+ sd_device *dev_scsi;
+ const char *scsi_sysname, *scsi_model, *scsi_vendor, *scsi_type, *scsi_rev;
+ int host, bus, target, lun;
+
+ /* get scsi device */
+ r = sd_device_get_parent_with_subsystem_devtype(dev, "scsi", "scsi_device", &dev_scsi);
+ if (r < 0) {
+ log_device_debug_errno(dev, r, "Unable to find parent SCSI device");
+ goto fallback;
+ }
+ if (sd_device_get_sysname(dev_scsi, &scsi_sysname) < 0)
+ goto fallback;
+ if (sscanf(scsi_sysname, "%d:%d:%d:%d", &host, &bus, &target, &lun) != 4) {
+ log_device_debug(dev_scsi, "Invalid SCSI device");
+ goto fallback;
+ }
+
+ /* Generic SPC-2 device */
+ r = sd_device_get_sysattr_value(dev_scsi, "vendor", &scsi_vendor);
+ if (r < 0) {
+ log_device_debug_errno(dev_scsi, r, "Failed to get SCSI vendor attribute: %m");
+ goto fallback;
+ }
+ encode_devnode_name(scsi_vendor, vendor_str_enc, sizeof(vendor_str_enc));
+ udev_replace_whitespace(scsi_vendor, vendor_str, sizeof(vendor_str)-1);
+ udev_replace_chars(vendor_str, NULL);
+
+ r = sd_device_get_sysattr_value(dev_scsi, "model", &scsi_model);
+ if (r < 0) {
+ log_device_debug_errno(dev_scsi, r, "Failed to get SCSI model attribute: %m");
+ goto fallback;
+ }
+ encode_devnode_name(scsi_model, model_str_enc, sizeof(model_str_enc));
+ udev_replace_whitespace(scsi_model, model_str, sizeof(model_str)-1);
+ udev_replace_chars(model_str, NULL);
+
+ r = sd_device_get_sysattr_value(dev_scsi, "type", &scsi_type);
+ if (r < 0) {
+ log_device_debug_errno(dev_scsi, r, "Failed to get SCSI type attribute: %m");
+ goto fallback;
+ }
+ set_scsi_type(type_str, scsi_type, sizeof(type_str)-1);
+
+ r = sd_device_get_sysattr_value(dev_scsi, "rev", &scsi_rev);
+ if (r < 0) {
+ log_device_debug_errno(dev_scsi, r, "Failed to get SCSI revision attribute: %m");
+ goto fallback;
+ }
+ udev_replace_whitespace(scsi_rev, revision_str, sizeof(revision_str)-1);
+ udev_replace_chars(revision_str, NULL);
+
+ /*
+ * some broken devices have the same identifiers
+ * for all luns, export the target:lun number
+ */
+ sprintf(instance_str, "%d:%d", target, lun);
+ }
+
+fallback:
+ r = sd_device_get_sysattr_value(dev_usb, "idVendor", &vendor_id);
+ if (r < 0)
+ return log_device_debug_errno(dev_usb, r, "Failed to get idVendor attribute: %m");
+
+ r = sd_device_get_sysattr_value(dev_usb, "idProduct", &product_id);
+ if (r < 0)
+ return log_device_debug_errno(dev_usb, r, "Failed to get idProduct attribute: %m");
+
+ /* fall back to USB vendor & device */
+ if (vendor_str[0] == '\0') {
+ const char *usb_vendor;
+
+ if (sd_device_get_sysattr_value(dev_usb, "manufacturer", &usb_vendor) < 0)
+ usb_vendor = vendor_id;
+ encode_devnode_name(usb_vendor, vendor_str_enc, sizeof(vendor_str_enc));
+ udev_replace_whitespace(usb_vendor, vendor_str, sizeof(vendor_str)-1);
+ udev_replace_chars(vendor_str, NULL);
+ }
+
+ if (model_str[0] == '\0') {
+ const char *usb_model;
+
+ if (sd_device_get_sysattr_value(dev_usb, "product", &usb_model) < 0)
+ usb_model = product_id;
+ encode_devnode_name(usb_model, model_str_enc, sizeof(model_str_enc));
+ udev_replace_whitespace(usb_model, model_str, sizeof(model_str)-1);
+ udev_replace_chars(model_str, NULL);
+ }
+
+ if (revision_str[0] == '\0') {
+ const char *usb_rev;
+
+ if (sd_device_get_sysattr_value(dev_usb, "bcdDevice", &usb_rev) >= 0) {
+ udev_replace_whitespace(usb_rev, revision_str, sizeof(revision_str)-1);
+ udev_replace_chars(revision_str, NULL);
+ }
+ }
+
+ if (serial_str[0] == '\0') {
+ const char *usb_serial;
+
+ if (sd_device_get_sysattr_value(dev_usb, "serial", &usb_serial) >= 0) {
+ /* http://msdn.microsoft.com/en-us/library/windows/hardware/gg487321.aspx */
+ for (const unsigned char *p = (unsigned char*) usb_serial; *p != '\0'; p++)
+ if (*p < 0x20 || *p > 0x7f || *p == ',') {
+ usb_serial = NULL;
+ break;
+ }
+
+ if (usb_serial) {
+ udev_replace_whitespace(usb_serial, serial_str, sizeof(serial_str)-1);
+ udev_replace_chars(serial_str, NULL);
+ }
+ }
+ }
+
+ s = serial;
+ l = strpcpyl(&s, sizeof(serial), vendor_str, "_", model_str, NULL);
+ if (!isempty(serial_str))
+ l = strpcpyl(&s, l, "_", serial_str, NULL);
+
+ if (!isempty(instance_str))
+ strpcpyl(&s, l, "-", instance_str, NULL);
+
+ if (sd_device_get_property_value(dev, "ID_BUS", NULL) >= 0)
+ log_device_debug(dev, "ID_BUS property is already set, setting only properties prefixed with \"ID_USB_\".");
+ else {
+ udev_builtin_add_property(dev, test, "ID_BUS", "usb");
+
+ udev_builtin_add_property(dev, test, "ID_MODEL", model_str);
+ udev_builtin_add_property(dev, test, "ID_MODEL_ENC", model_str_enc);
+ udev_builtin_add_property(dev, test, "ID_MODEL_ID", product_id);
+
+ udev_builtin_add_property(dev, test, "ID_SERIAL", serial);
+ if (!isempty(serial_str))
+ udev_builtin_add_property(dev, test, "ID_SERIAL_SHORT", serial_str);
+
+ udev_builtin_add_property(dev, test, "ID_VENDOR", vendor_str);
+ udev_builtin_add_property(dev, test, "ID_VENDOR_ENC", vendor_str_enc);
+ udev_builtin_add_property(dev, test, "ID_VENDOR_ID", vendor_id);
+
+ udev_builtin_add_property(dev, test, "ID_REVISION", revision_str);
+
+ if (!isempty(type_str))
+ udev_builtin_add_property(dev, test, "ID_TYPE", type_str);
+
+ if (!isempty(instance_str))
+ udev_builtin_add_property(dev, test, "ID_INSTANCE", instance_str);
+ }
+
+ /* Also export the same values in the above by prefixing ID_USB_. */
+ udev_builtin_add_property(dev, test, "ID_USB_MODEL", model_str);
+ udev_builtin_add_property(dev, test, "ID_USB_MODEL_ENC", model_str_enc);
+ udev_builtin_add_property(dev, test, "ID_USB_MODEL_ID", product_id);
+ udev_builtin_add_property(dev, test, "ID_USB_SERIAL", serial);
+ if (!isempty(serial_str))
+ udev_builtin_add_property(dev, test, "ID_USB_SERIAL_SHORT", serial_str);
+
+ udev_builtin_add_property(dev, test, "ID_USB_VENDOR", vendor_str);
+ udev_builtin_add_property(dev, test, "ID_USB_VENDOR_ENC", vendor_str_enc);
+ udev_builtin_add_property(dev, test, "ID_USB_VENDOR_ID", vendor_id);
+
+ udev_builtin_add_property(dev, test, "ID_USB_REVISION", revision_str);
+
+ if (!isempty(type_str))
+ udev_builtin_add_property(dev, test, "ID_USB_TYPE", type_str);
+
+ if (!isempty(instance_str))
+ udev_builtin_add_property(dev, test, "ID_USB_INSTANCE", instance_str);
+
+ if (!isempty(packed_if_str))
+ udev_builtin_add_property(dev, test, "ID_USB_INTERFACES", packed_if_str);
+ if (ifnum)
+ udev_builtin_add_property(dev, test, "ID_USB_INTERFACE_NUM", ifnum);
+ if (driver)
+ udev_builtin_add_property(dev, test, "ID_USB_DRIVER", driver);
+ return 0;
+}
+
+const UdevBuiltin udev_builtin_usb_id = {
+ .name = "usb_id",
+ .cmd = builtin_usb_id,
+ .help = "USB device properties",
+ .run_once = true,
+};
diff --git a/src/udev/udev-builtin.c b/src/udev/udev-builtin.c
new file mode 100644
index 0000000..bcc2018
--- /dev/null
+++ b/src/udev/udev-builtin.c
@@ -0,0 +1,156 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <stdio.h>
+
+#include "device-private.h"
+#include "device-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "udev-builtin.h"
+
+static bool initialized;
+
+static const UdevBuiltin *const builtins[_UDEV_BUILTIN_MAX] = {
+#if HAVE_BLKID
+ [UDEV_BUILTIN_BLKID] = &udev_builtin_blkid,
+#endif
+ [UDEV_BUILTIN_BTRFS] = &udev_builtin_btrfs,
+ [UDEV_BUILTIN_HWDB] = &udev_builtin_hwdb,
+ [UDEV_BUILTIN_INPUT_ID] = &udev_builtin_input_id,
+ [UDEV_BUILTIN_KEYBOARD] = &udev_builtin_keyboard,
+#if HAVE_KMOD
+ [UDEV_BUILTIN_KMOD] = &udev_builtin_kmod,
+#endif
+ [UDEV_BUILTIN_NET_DRIVER] = &udev_builtin_net_driver,
+ [UDEV_BUILTIN_NET_ID] = &udev_builtin_net_id,
+ [UDEV_BUILTIN_NET_LINK] = &udev_builtin_net_setup_link,
+ [UDEV_BUILTIN_PATH_ID] = &udev_builtin_path_id,
+ [UDEV_BUILTIN_USB_ID] = &udev_builtin_usb_id,
+#if HAVE_ACL
+ [UDEV_BUILTIN_UACCESS] = &udev_builtin_uaccess,
+#endif
+};
+
+void udev_builtin_init(void) {
+ if (initialized)
+ return;
+
+ for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++)
+ if (builtins[i] && builtins[i]->init)
+ builtins[i]->init();
+
+ initialized = true;
+}
+
+void udev_builtin_exit(void) {
+ if (!initialized)
+ return;
+
+ for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++)
+ if (builtins[i] && builtins[i]->exit)
+ builtins[i]->exit();
+
+ initialized = false;
+}
+
+bool udev_builtin_should_reload(void) {
+ for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++)
+ if (builtins[i] && builtins[i]->should_reload && builtins[i]->should_reload())
+ return true;
+ return false;
+}
+
+void udev_builtin_list(void) {
+ for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++)
+ if (builtins[i])
+ fprintf(stderr, " %-14s %s\n", builtins[i]->name, builtins[i]->help);
+}
+
+const char *udev_builtin_name(UdevBuiltinCommand cmd) {
+ assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX);
+
+ if (!builtins[cmd])
+ return NULL;
+
+ return builtins[cmd]->name;
+}
+
+bool udev_builtin_run_once(UdevBuiltinCommand cmd) {
+ assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX);
+
+ if (!builtins[cmd])
+ return false;
+
+ return builtins[cmd]->run_once;
+}
+
+UdevBuiltinCommand udev_builtin_lookup(const char *command) {
+ size_t n;
+
+ assert(command);
+
+ command += strspn(command, WHITESPACE);
+ n = strcspn(command, WHITESPACE);
+ for (UdevBuiltinCommand i = 0; i < _UDEV_BUILTIN_MAX; i++)
+ if (builtins[i] && strneq(builtins[i]->name, command, n))
+ return i;
+
+ return _UDEV_BUILTIN_INVALID;
+}
+
+int udev_builtin_run(UdevEvent *event, UdevBuiltinCommand cmd, const char *command, bool test) {
+ _cleanup_strv_free_ char **argv = NULL;
+ int r;
+
+ assert(event);
+ assert(event->dev);
+ assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX);
+ assert(command);
+
+ if (!builtins[cmd])
+ return -EOPNOTSUPP;
+
+ r = strv_split_full(&argv, command, NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX | EXTRACT_RETAIN_ESCAPE);
+ if (r < 0)
+ return r;
+
+ /* we need '0' here to reset the internal state */
+ optind = 0;
+ return builtins[cmd]->cmd(event, strv_length(argv), argv, test);
+}
+
+int udev_builtin_add_property(sd_device *dev, bool test, const char *key, const char *val) {
+ int r;
+
+ assert(dev);
+ assert(key);
+
+ r = device_add_property(dev, key, val);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to add property '%s%s%s'",
+ key, val ? "=" : "", strempty(val));
+
+ if (test)
+ printf("%s=%s\n", key, strempty(val));
+
+ return 0;
+}
+
+int udev_builtin_add_propertyf(sd_device *dev, bool test, const char *key, const char *valf, ...) {
+ _cleanup_free_ char *val = NULL;
+ va_list ap;
+ int r;
+
+ assert(dev);
+ assert(key);
+ assert(valf);
+
+ va_start(ap, valf);
+ r = vasprintf(&val, valf, ap);
+ va_end(ap);
+ if (r < 0)
+ return log_oom_debug();
+
+ return udev_builtin_add_property(dev, test, key, val);
+}
diff --git a/src/udev/udev-builtin.h b/src/udev/udev-builtin.h
new file mode 100644
index 0000000..fcd41d6
--- /dev/null
+++ b/src/udev/udev-builtin.h
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sd-device.h"
+#include "sd-netlink.h"
+
+#include "macro.h"
+#include "udev-event.h"
+
+typedef enum UdevBuiltinCommand {
+#if HAVE_BLKID
+ UDEV_BUILTIN_BLKID,
+#endif
+ UDEV_BUILTIN_BTRFS,
+ UDEV_BUILTIN_HWDB,
+ UDEV_BUILTIN_INPUT_ID,
+ UDEV_BUILTIN_KEYBOARD,
+#if HAVE_KMOD
+ UDEV_BUILTIN_KMOD,
+#endif
+ UDEV_BUILTIN_NET_DRIVER,
+ UDEV_BUILTIN_NET_ID,
+ UDEV_BUILTIN_NET_LINK,
+ UDEV_BUILTIN_PATH_ID,
+ UDEV_BUILTIN_USB_ID,
+#if HAVE_ACL
+ UDEV_BUILTIN_UACCESS,
+#endif
+ _UDEV_BUILTIN_MAX,
+ _UDEV_BUILTIN_INVALID = -EINVAL,
+} UdevBuiltinCommand;
+
+typedef struct UdevBuiltin {
+ const char *name;
+ int (*cmd)(UdevEvent *event, int argc, char *argv[], bool test);
+ const char *help;
+ int (*init)(void);
+ void (*exit)(void);
+ bool (*should_reload)(void);
+ bool run_once;
+} UdevBuiltin;
+
+#define UDEV_BUILTIN_CMD_TO_PTR(u) \
+ ({ \
+ UdevBuiltinCommand _u = (u); \
+ _u < 0 ? NULL : (void*)(intptr_t) (_u + 1); \
+ })
+
+#define PTR_TO_UDEV_BUILTIN_CMD(p) \
+ ({ \
+ void *_p = (p); \
+ _p && (intptr_t)(_p) <= _UDEV_BUILTIN_MAX ? \
+ (UdevBuiltinCommand)((intptr_t)_p - 1) : _UDEV_BUILTIN_INVALID; \
+ })
+
+#if HAVE_BLKID
+extern const UdevBuiltin udev_builtin_blkid;
+#endif
+extern const UdevBuiltin udev_builtin_btrfs;
+extern const UdevBuiltin udev_builtin_hwdb;
+extern const UdevBuiltin udev_builtin_input_id;
+extern const UdevBuiltin udev_builtin_keyboard;
+#if HAVE_KMOD
+extern const UdevBuiltin udev_builtin_kmod;
+#endif
+extern const UdevBuiltin udev_builtin_net_driver;
+extern const UdevBuiltin udev_builtin_net_id;
+extern const UdevBuiltin udev_builtin_net_setup_link;
+extern const UdevBuiltin udev_builtin_path_id;
+extern const UdevBuiltin udev_builtin_usb_id;
+#if HAVE_ACL
+extern const UdevBuiltin udev_builtin_uaccess;
+#endif
+
+void udev_builtin_init(void);
+void udev_builtin_exit(void);
+UdevBuiltinCommand udev_builtin_lookup(const char *command);
+const char *udev_builtin_name(UdevBuiltinCommand cmd);
+bool udev_builtin_run_once(UdevBuiltinCommand cmd);
+int udev_builtin_run(UdevEvent *event, UdevBuiltinCommand cmd, const char *command, bool test);
+void udev_builtin_list(void);
+bool udev_builtin_should_reload(void);
+int udev_builtin_add_property(sd_device *dev, bool test, const char *key, const char *val);
+int udev_builtin_add_propertyf(sd_device *dev, bool test, const char *key, const char *valf, ...) _printf_(4, 5);
+int udev_builtin_hwdb_lookup(sd_device *dev, const char *prefix, const char *modalias,
+ const char *filter, bool test);
diff --git a/src/udev/udev-ctrl.c b/src/udev/udev-ctrl.c
new file mode 100644
index 0000000..2871634
--- /dev/null
+++ b/src/udev/udev-ctrl.c
@@ -0,0 +1,353 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <poll.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "iovec-util.h"
+#include "socket-util.h"
+#include "strxcpyx.h"
+#include "udev-ctrl.h"
+
+/* wire protocol magic must match */
+#define UDEV_CTRL_MAGIC 0xdead1dea
+
+typedef struct UdevCtrlMessageWire {
+ char version[16];
+ unsigned magic;
+ UdevCtrlMessageType type;
+ UdevCtrlMessageValue value;
+} UdevCtrlMessageWire;
+
+struct UdevCtrl {
+ unsigned n_ref;
+ int sock;
+ int sock_connect;
+ union sockaddr_union saddr;
+ socklen_t addrlen;
+ bool bound;
+ bool connected;
+ sd_event *event;
+ sd_event_source *event_source;
+ sd_event_source *event_source_connect;
+ udev_ctrl_handler_t callback;
+ void *userdata;
+};
+
+int udev_ctrl_new_from_fd(UdevCtrl **ret, int fd) {
+ _cleanup_close_ int sock = -EBADF;
+ UdevCtrl *uctrl;
+
+ assert(ret);
+
+ if (fd < 0) {
+ sock = socket(AF_UNIX, SOCK_SEQPACKET|SOCK_NONBLOCK|SOCK_CLOEXEC, 0);
+ if (sock < 0)
+ return log_error_errno(errno, "Failed to create socket: %m");
+ }
+
+ uctrl = new(UdevCtrl, 1);
+ if (!uctrl)
+ return -ENOMEM;
+
+ *uctrl = (UdevCtrl) {
+ .n_ref = 1,
+ .sock = fd >= 0 ? fd : TAKE_FD(sock),
+ .sock_connect = -EBADF,
+ .bound = fd >= 0,
+ };
+
+ uctrl->saddr.un = (struct sockaddr_un) {
+ .sun_family = AF_UNIX,
+ .sun_path = "/run/udev/control",
+ };
+
+ uctrl->addrlen = SOCKADDR_UN_LEN(uctrl->saddr.un);
+
+ *ret = TAKE_PTR(uctrl);
+ return 0;
+}
+
+int udev_ctrl_enable_receiving(UdevCtrl *uctrl) {
+ assert(uctrl);
+
+ if (uctrl->bound)
+ return 0;
+
+ (void) sockaddr_un_unlink(&uctrl->saddr.un);
+ if (bind(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen) < 0)
+ return log_error_errno(errno, "Failed to bind udev control socket: %m");
+
+ if (listen(uctrl->sock, 0) < 0)
+ return log_error_errno(errno, "Failed to listen udev control socket: %m");
+
+ uctrl->bound = true;
+ return 0;
+}
+
+static void udev_ctrl_disconnect(UdevCtrl *uctrl) {
+ if (!uctrl)
+ return;
+
+ uctrl->event_source_connect = sd_event_source_unref(uctrl->event_source_connect);
+ uctrl->sock_connect = safe_close(uctrl->sock_connect);
+}
+
+static UdevCtrl *udev_ctrl_free(UdevCtrl *uctrl) {
+ assert(uctrl);
+
+ udev_ctrl_disconnect(uctrl);
+
+ sd_event_source_unref(uctrl->event_source);
+ safe_close(uctrl->sock);
+
+ sd_event_unref(uctrl->event);
+ return mfree(uctrl);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(UdevCtrl, udev_ctrl, udev_ctrl_free);
+
+int udev_ctrl_attach_event(UdevCtrl *uctrl, sd_event *event) {
+ int r;
+
+ assert_return(uctrl, -EINVAL);
+ assert_return(!uctrl->event, -EBUSY);
+
+ if (event)
+ uctrl->event = sd_event_ref(event);
+ else {
+ r = sd_event_default(&uctrl->event);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+sd_event_source *udev_ctrl_get_event_source(UdevCtrl *uctrl) {
+ assert(uctrl);
+
+ return uctrl->event_source;
+}
+
+static void udev_ctrl_disconnect_and_listen_again(UdevCtrl *uctrl) {
+ udev_ctrl_disconnect(uctrl);
+ udev_ctrl_unref(uctrl);
+ (void) sd_event_source_set_enabled(uctrl->event_source, SD_EVENT_ON);
+ /* We don't return NULL here because uctrl is not freed */
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(UdevCtrl*, udev_ctrl_disconnect_and_listen_again, NULL);
+
+static int udev_ctrl_connection_event_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ _cleanup_(udev_ctrl_disconnect_and_listen_againp) UdevCtrl *uctrl = NULL;
+ UdevCtrlMessageWire msg_wire;
+ struct iovec iov = IOVEC_MAKE(&msg_wire, sizeof(UdevCtrlMessageWire));
+ CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control;
+ struct msghdr smsg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ };
+ struct ucred *cred;
+ ssize_t size;
+
+ assert(userdata);
+
+ /* When UDEV_CTRL_EXIT is received, manager unref udev_ctrl object.
+ * To avoid the object freed, let's increment the refcount. */
+ uctrl = udev_ctrl_ref(userdata);
+
+ size = next_datagram_size_fd(fd);
+ if (size < 0)
+ return log_error_errno(size, "Failed to get size of message: %m");
+ if (size == 0)
+ return 0; /* Client disconnects? */
+
+ size = recvmsg_safe(fd, &smsg, 0);
+ if (size == -EINTR)
+ return 0;
+ if (size < 0)
+ return log_error_errno(size, "Failed to receive ctrl message: %m");
+
+ cmsg_close_all(&smsg);
+
+ cred = CMSG_FIND_DATA(&smsg, SOL_SOCKET, SCM_CREDENTIALS, struct ucred);
+ if (!cred) {
+ log_error("No sender credentials received, ignoring message");
+ return 0;
+ }
+
+ if (cred->uid != 0) {
+ log_error("Invalid sender uid "UID_FMT", ignoring message", cred->uid);
+ return 0;
+ }
+
+ if (msg_wire.magic != UDEV_CTRL_MAGIC) {
+ log_error("Message magic 0x%08x doesn't match, ignoring message", msg_wire.magic);
+ return 0;
+ }
+
+ if (msg_wire.type == _UDEV_CTRL_END_MESSAGES)
+ return 0;
+
+ if (uctrl->callback)
+ (void) uctrl->callback(uctrl, msg_wire.type, &msg_wire.value, uctrl->userdata);
+
+ /* Do not disconnect and wait for next message. */
+ uctrl = udev_ctrl_unref(uctrl);
+ return 0;
+}
+
+static int udev_ctrl_event_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ UdevCtrl *uctrl = ASSERT_PTR(userdata);
+ _cleanup_close_ int sock = -EBADF;
+ struct ucred ucred;
+ int r;
+
+ sock = accept4(fd, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK);
+ if (sock < 0) {
+ if (ERRNO_IS_ACCEPT_AGAIN(errno))
+ return 0;
+
+ return log_error_errno(errno, "Failed to accept ctrl connection: %m");
+ }
+
+ /* check peer credential of connection */
+ r = getpeercred(sock, &ucred);
+ if (r < 0) {
+ log_error_errno(r, "Failed to receive credentials of ctrl connection: %m");
+ return 0;
+ }
+
+ if (ucred.uid > 0) {
+ log_error("Invalid sender uid "UID_FMT", closing connection", ucred.uid);
+ return 0;
+ }
+
+ /* enable receiving of the sender credentials in the messages */
+ r = setsockopt_int(sock, SOL_SOCKET, SO_PASSCRED, true);
+ if (r < 0)
+ log_warning_errno(r, "Failed to set SO_PASSCRED, ignoring: %m");
+
+ r = sd_event_add_io(uctrl->event, &uctrl->event_source_connect, sock, EPOLLIN, udev_ctrl_connection_event_handler, uctrl);
+ if (r < 0) {
+ log_error_errno(r, "Failed to create event source for udev control connection: %m");
+ return 0;
+ }
+
+ (void) sd_event_source_set_description(uctrl->event_source_connect, "udev-ctrl-connection");
+
+ /* Do not accept multiple connection. */
+ (void) sd_event_source_set_enabled(uctrl->event_source, SD_EVENT_OFF);
+
+ uctrl->sock_connect = TAKE_FD(sock);
+ return 0;
+}
+
+int udev_ctrl_start(UdevCtrl *uctrl, udev_ctrl_handler_t callback, void *userdata) {
+ int r;
+
+ assert(uctrl);
+
+ if (!uctrl->event) {
+ r = udev_ctrl_attach_event(uctrl, NULL);
+ if (r < 0)
+ return r;
+ }
+
+ r = udev_ctrl_enable_receiving(uctrl);
+ if (r < 0)
+ return r;
+
+ uctrl->callback = callback;
+ uctrl->userdata = userdata;
+
+ r = sd_event_add_io(uctrl->event, &uctrl->event_source, uctrl->sock, EPOLLIN, udev_ctrl_event_handler, uctrl);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(uctrl->event_source, "udev-ctrl");
+
+ return 0;
+}
+
+int udev_ctrl_send(UdevCtrl *uctrl, UdevCtrlMessageType type, const void *data) {
+ UdevCtrlMessageWire ctrl_msg_wire = {
+ .version = "udev-" STRINGIFY(PROJECT_VERSION),
+ .magic = UDEV_CTRL_MAGIC,
+ .type = type,
+ };
+
+ if (type == UDEV_CTRL_SET_ENV) {
+ assert(data);
+ strscpy(ctrl_msg_wire.value.buf, sizeof(ctrl_msg_wire.value.buf), data);
+ } else if (IN_SET(type, UDEV_CTRL_SET_LOG_LEVEL, UDEV_CTRL_SET_CHILDREN_MAX))
+ ctrl_msg_wire.value.intval = PTR_TO_INT(data);
+
+ if (!uctrl->connected) {
+ if (connect(uctrl->sock, &uctrl->saddr.sa, uctrl->addrlen) < 0)
+ return -errno;
+ uctrl->connected = true;
+ }
+
+ if (send(uctrl->sock, &ctrl_msg_wire, sizeof(ctrl_msg_wire), 0) < 0)
+ return -errno;
+
+ return 0;
+}
+
+int udev_ctrl_wait(UdevCtrl *uctrl, usec_t timeout) {
+ _cleanup_(sd_event_source_disable_unrefp) sd_event_source *source_io = NULL, *source_timeout = NULL;
+ int r;
+
+ assert(uctrl);
+
+ if (uctrl->sock < 0)
+ return 0;
+ if (!uctrl->connected)
+ return 0;
+
+ r = udev_ctrl_send(uctrl, _UDEV_CTRL_END_MESSAGES, NULL);
+ if (r < 0)
+ return r;
+
+ if (timeout == 0)
+ return 0;
+
+ if (!uctrl->event) {
+ r = udev_ctrl_attach_event(uctrl, NULL);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_event_add_io(uctrl->event, &source_io, uctrl->sock, EPOLLIN, NULL, INT_TO_PTR(0));
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(source_io, "udev-ctrl-wait-io");
+
+ if (timeout != USEC_INFINITY) {
+ r = sd_event_add_time_relative(
+ uctrl->event, &source_timeout, CLOCK_BOOTTIME,
+ timeout,
+ 0, NULL, INT_TO_PTR(-ETIMEDOUT));
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(source_timeout, "udev-ctrl-wait-timeout");
+ }
+
+ return sd_event_loop(uctrl->event);
+}
diff --git a/src/udev/udev-ctrl.h b/src/udev/udev-ctrl.h
new file mode 100644
index 0000000..11fc0b6
--- /dev/null
+++ b/src/udev/udev-ctrl.h
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "sd-event.h"
+
+#include "macro.h"
+#include "time-util.h"
+
+typedef struct UdevCtrl UdevCtrl;
+
+typedef enum UdevCtrlMessageType {
+ _UDEV_CTRL_END_MESSAGES,
+ UDEV_CTRL_SET_LOG_LEVEL,
+ UDEV_CTRL_STOP_EXEC_QUEUE,
+ UDEV_CTRL_START_EXEC_QUEUE,
+ UDEV_CTRL_RELOAD,
+ UDEV_CTRL_SET_ENV,
+ UDEV_CTRL_SET_CHILDREN_MAX,
+ UDEV_CTRL_PING,
+ UDEV_CTRL_EXIT,
+} UdevCtrlMessageType;
+
+typedef union UdevCtrlMessageValue {
+ int intval;
+ char buf[256];
+} UdevCtrlMessageValue;
+
+typedef int (*udev_ctrl_handler_t)(UdevCtrl *udev_ctrl, UdevCtrlMessageType type,
+ const UdevCtrlMessageValue *value, void *userdata);
+
+int udev_ctrl_new_from_fd(UdevCtrl **ret, int fd);
+static inline int udev_ctrl_new(UdevCtrl **ret) {
+ return udev_ctrl_new_from_fd(ret, -1);
+}
+
+int udev_ctrl_enable_receiving(UdevCtrl *uctrl);
+UdevCtrl *udev_ctrl_ref(UdevCtrl *uctrl);
+UdevCtrl *udev_ctrl_unref(UdevCtrl *uctrl);
+int udev_ctrl_attach_event(UdevCtrl *uctrl, sd_event *event);
+int udev_ctrl_start(UdevCtrl *uctrl, udev_ctrl_handler_t callback, void *userdata);
+sd_event_source *udev_ctrl_get_event_source(UdevCtrl *uctrl);
+
+int udev_ctrl_wait(UdevCtrl *uctrl, usec_t timeout);
+
+int udev_ctrl_send(UdevCtrl *uctrl, UdevCtrlMessageType type, const void *data);
+static inline int udev_ctrl_send_set_log_level(UdevCtrl *uctrl, int priority) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_SET_LOG_LEVEL, INT_TO_PTR(priority));
+}
+
+static inline int udev_ctrl_send_stop_exec_queue(UdevCtrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_STOP_EXEC_QUEUE, NULL);
+}
+
+static inline int udev_ctrl_send_start_exec_queue(UdevCtrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_START_EXEC_QUEUE, NULL);
+}
+
+static inline int udev_ctrl_send_reload(UdevCtrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_RELOAD, NULL);
+}
+
+static inline int udev_ctrl_send_set_env(UdevCtrl *uctrl, const char *key) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_SET_ENV, key);
+}
+
+static inline int udev_ctrl_send_set_children_max(UdevCtrl *uctrl, int count) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_SET_CHILDREN_MAX, INT_TO_PTR(count));
+}
+
+static inline int udev_ctrl_send_ping(UdevCtrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_PING, NULL);
+}
+
+static inline int udev_ctrl_send_exit(UdevCtrl *uctrl) {
+ return udev_ctrl_send(uctrl, UDEV_CTRL_EXIT, NULL);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(UdevCtrl*, udev_ctrl_unref);
diff --git a/src/udev/udev-event.c b/src/udev/udev-event.c
new file mode 100644
index 0000000..ed22c8b
--- /dev/null
+++ b/src/udev/udev-event.c
@@ -0,0 +1,411 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "alloc-util.h"
+#include "device-internal.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "fs-util.h"
+#include "netif-naming-scheme.h"
+#include "netlink-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "udev-event.h"
+#include "udev-node.h"
+#include "udev-trace.h"
+#include "udev-util.h"
+#include "udev-watch.h"
+#include "user-util.h"
+
+UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl, int log_level) {
+ UdevEvent *event;
+
+ assert(dev);
+
+ event = new(UdevEvent, 1);
+ if (!event)
+ return NULL;
+
+ *event = (UdevEvent) {
+ .dev = sd_device_ref(dev),
+ .birth_usec = now(CLOCK_MONOTONIC),
+ .exec_delay_usec = exec_delay_usec,
+ .rtnl = sd_netlink_ref(rtnl),
+ .uid = UID_INVALID,
+ .gid = GID_INVALID,
+ .mode = MODE_INVALID,
+ .log_level_was_debug = log_level == LOG_DEBUG,
+ .default_log_level = log_level,
+ };
+
+ return event;
+}
+
+UdevEvent *udev_event_free(UdevEvent *event) {
+ if (!event)
+ return NULL;
+
+ sd_device_unref(event->dev);
+ sd_device_unref(event->dev_db_clone);
+ sd_netlink_unref(event->rtnl);
+ ordered_hashmap_free_free_key(event->run_list);
+ ordered_hashmap_free_free_free(event->seclabel_list);
+ free(event->program_result);
+ free(event->name);
+ strv_free(event->altnames);
+
+ return mfree(event);
+}
+
+static int device_rename(sd_device *device, const char *name) {
+ _cleanup_free_ char *new_syspath = NULL;
+ const char *s;
+ int r;
+
+ assert(device);
+ assert(name);
+
+ if (!filename_is_valid(name))
+ return -EINVAL;
+
+ r = sd_device_get_syspath(device, &s);
+ if (r < 0)
+ return r;
+
+ r = path_extract_directory(s, &new_syspath);
+ if (r < 0)
+ return r;
+
+ if (!path_extend(&new_syspath, name))
+ return -ENOMEM;
+
+ if (!path_is_safe(new_syspath))
+ return -EINVAL;
+
+ /* At the time this is called, the renamed device may not exist yet. Hence, we cannot validate
+ * the new syspath. */
+ r = device_set_syspath(device, new_syspath, /* verify = */ false);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_property_value(device, "INTERFACE", &s);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ /* like DEVPATH_OLD, INTERFACE_OLD is not saved to the db, but only stays around for the current event */
+ r = device_add_property_internal(device, "INTERFACE_OLD", s);
+ if (r < 0)
+ return r;
+
+ return device_add_property_internal(device, "INTERFACE", name);
+}
+
+static int rename_netif(UdevEvent *event) {
+ _cleanup_free_ char *old_syspath = NULL, *old_sysname = NULL;
+ const char *s;
+ sd_device *dev;
+ int ifindex, r;
+
+ assert(event);
+
+ if (!event->name)
+ return 0; /* No new name is requested. */
+
+ dev = ASSERT_PTR(event->dev);
+
+ r = sd_device_get_ifindex(dev, &ifindex);
+ if (r == -ENOENT)
+ return 0; /* Device is not a network interface. */
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to get ifindex: %m");
+
+ if (naming_scheme_has(NAMING_REPLACE_STRICTLY) &&
+ !ifname_valid(event->name)) {
+ log_device_warning(dev, "Invalid network interface name, ignoring: %s", event->name);
+ return 0;
+ }
+
+ r = sd_device_get_sysname(dev, &s);
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to get sysname: %m");
+
+ if (streq(event->name, s))
+ return 0; /* The interface name is already requested name. */
+
+ old_sysname = strdup(s);
+ if (!old_sysname)
+ return -ENOMEM;
+
+ r = sd_device_get_syspath(dev, &s);
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to get syspath: %m");
+
+ old_syspath = strdup(s);
+ if (!old_syspath)
+ return -ENOMEM;
+
+ r = device_rename(dev, event->name);
+ if (r < 0) {
+ /* Here and below, use dev_db_clone for logging, otherwise, logged message is prefixed with
+ * the new interface name, and e.g. 'networkctl status INTERFACE' does not show the message. */
+ log_device_warning_errno(event->dev_db_clone, r,
+ "Failed to update properties with new name '%s': %m", event->name);
+ goto revert;
+ }
+
+ /* Set ID_RENAMING boolean property here. It will be dropped when the corresponding move uevent is processed. */
+ r = device_add_property(dev, "ID_RENAMING", "1");
+ if (r < 0) {
+ log_device_warning_errno(event->dev_db_clone, r, "Failed to add 'ID_RENAMING' property: %m");
+ goto revert;
+ }
+
+ /* Also set ID_RENAMING boolean property to cloned sd_device object and save it to database
+ * before calling rtnl_set_link_name(). Otherwise, clients (e.g., systemd-networkd) may receive
+ * RTM_NEWLINK netlink message before the database is updated. */
+ r = device_add_property(event->dev_db_clone, "ID_RENAMING", "1");
+ if (r < 0) {
+ log_device_warning_errno(event->dev_db_clone, r, "Failed to add 'ID_RENAMING' property: %m");
+ goto revert;
+ }
+
+ r = device_update_db(event->dev_db_clone);
+ if (r < 0) {
+ log_device_debug_errno(event->dev_db_clone, r, "Failed to update database under /run/udev/data/: %m");
+ goto revert;
+ }
+
+ r = rtnl_set_link_name(&event->rtnl, ifindex, event->name, event->altnames);
+ if (r < 0) {
+ if (r == -EBUSY) {
+ log_device_info(event->dev_db_clone,
+ "Network interface '%s' is already up, cannot rename to '%s'.",
+ old_sysname, event->name);
+ r = 0;
+ } else
+ log_device_error_errno(event->dev_db_clone, r,
+ "Failed to rename network interface %i from '%s' to '%s': %m",
+ ifindex, old_sysname, event->name);
+ goto revert;
+ }
+
+ log_device_debug(dev, "Network interface %i is renamed from '%s' to '%s'", ifindex, old_sysname, event->name);
+ return 1;
+
+revert:
+ /* Restore 'dev_db_clone' */
+ (void) device_add_property(event->dev_db_clone, "ID_RENAMING", NULL);
+ (void) device_update_db(event->dev_db_clone);
+
+ /* Restore 'dev' */
+ (void) device_set_syspath(dev, old_syspath, /* verify = */ false);
+ if (sd_device_get_property_value(dev, "INTERFACE_OLD", &s) >= 0) {
+ (void) device_add_property_internal(dev, "INTERFACE", s);
+ (void) device_add_property_internal(dev, "INTERFACE_OLD", NULL);
+ }
+ (void) device_add_property(dev, "ID_RENAMING", NULL);
+
+ return r;
+}
+
+static int assign_altnames(UdevEvent *event) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ int ifindex, r;
+ const char *s;
+
+ if (strv_isempty(event->altnames))
+ return 0;
+
+ r = sd_device_get_ifindex(dev, &ifindex);
+ if (r == -ENOENT)
+ return 0; /* Device is not a network interface. */
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to get ifindex: %m");
+
+ r = sd_device_get_sysname(dev, &s);
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to get sysname: %m");
+
+ /* Filter out the current interface name. */
+ strv_remove(event->altnames, s);
+
+ r = rtnl_append_link_alternative_names(&event->rtnl, ifindex, event->altnames);
+ if (r < 0)
+ log_device_full_errno(dev, r == -EOPNOTSUPP ? LOG_DEBUG : LOG_WARNING, r,
+ "Could not set AlternativeName= or apply AlternativeNamesPolicy=, ignoring: %m");
+
+ return 0;
+}
+
+static int update_devnode(UdevEvent *event) {
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ int r;
+
+ r = sd_device_get_devnum(dev, NULL);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get devnum: %m");
+
+ if (!uid_is_valid(event->uid)) {
+ r = device_get_devnode_uid(dev, &event->uid);
+ if (r < 0 && r != -ENOENT)
+ return log_device_error_errno(dev, r, "Failed to get devnode UID: %m");
+ }
+
+ if (!gid_is_valid(event->gid)) {
+ r = device_get_devnode_gid(dev, &event->gid);
+ if (r < 0 && r != -ENOENT)
+ return log_device_error_errno(dev, r, "Failed to get devnode GID: %m");
+ }
+
+ if (event->mode == MODE_INVALID) {
+ r = device_get_devnode_mode(dev, &event->mode);
+ if (r < 0 && r != -ENOENT)
+ return log_device_error_errno(dev, r, "Failed to get devnode mode: %m");
+ }
+
+ bool apply_mac = device_for_action(dev, SD_DEVICE_ADD);
+
+ r = udev_node_apply_permissions(dev, apply_mac, event->mode, event->uid, event->gid, event->seclabel_list);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to apply devnode permissions: %m");
+
+ return udev_node_update(dev, event->dev_db_clone);
+}
+
+static int event_execute_rules_on_remove(
+ UdevEvent *event,
+ int inotify_fd,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list,
+ UdevRules *rules) {
+
+ sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ int r;
+
+ r = device_read_db_internal(dev, true);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to read database under /run/udev/data/: %m");
+
+ r = device_tag_index(dev, NULL, false);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to remove corresponding tag files under /run/udev/tag/, ignoring: %m");
+
+ r = device_delete_db(dev);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to delete database under /run/udev/data/, ignoring: %m");
+
+ r = udev_watch_end(inotify_fd, dev);
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Failed to remove inotify watch, ignoring: %m");
+
+ r = udev_rules_apply_to_event(rules, event, timeout_usec, timeout_signal, properties_list);
+
+ if (sd_device_get_devnum(dev, NULL) >= 0)
+ (void) udev_node_remove(dev);
+
+ return r;
+}
+
+static int copy_all_tags(sd_device *d, sd_device *s) {
+ int r;
+
+ assert(d);
+
+ if (!s)
+ return 0;
+
+ FOREACH_DEVICE_TAG(s, tag) {
+ r = device_add_tag(d, tag, false);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int udev_event_execute_rules(
+ UdevEvent *event,
+ int inotify_fd, /* This may be negative */
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list,
+ UdevRules *rules) {
+
+ sd_device_action_t action;
+ sd_device *dev;
+ int r;
+
+ dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ assert(rules);
+
+ r = sd_device_get_action(dev, &action);
+ if (r < 0)
+ return log_device_error_errno(dev, r, "Failed to get ACTION: %m");
+
+ if (action == SD_DEVICE_REMOVE)
+ return event_execute_rules_on_remove(event, inotify_fd, timeout_usec, timeout_signal, properties_list, rules);
+
+ /* Disable watch during event processing. */
+ r = udev_watch_end(inotify_fd, dev);
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Failed to remove inotify watch, ignoring: %m");
+
+ r = device_clone_with_db(dev, &event->dev_db_clone);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to clone sd_device object: %m");
+
+ r = copy_all_tags(dev, event->dev_db_clone);
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Failed to copy all tags from old database entry, ignoring: %m");
+
+ /* Drop previously added property for safety to make IMPORT{db}="ID_RENAMING" not work. This is
+ * mostly for 'move' uevent, but let's do unconditionally. Why? If a network interface is renamed in
+ * initrd, then udevd may lose the 'move' uevent during switching root. Usually, we do not set the
+ * persistent flag for network interfaces, but user may set it. Just for safety. */
+ r = device_add_property(event->dev_db_clone, "ID_RENAMING", NULL);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to remove 'ID_RENAMING' property: %m");
+
+ DEVICE_TRACE_POINT(rules_start, dev);
+
+ r = udev_rules_apply_to_event(rules, event, timeout_usec, timeout_signal, properties_list);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to apply udev rules: %m");
+
+ DEVICE_TRACE_POINT(rules_finished, dev);
+
+ if (action == SD_DEVICE_ADD) {
+ r = rename_netif(event);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ (void) assign_altnames(event);
+ }
+
+ r = update_devnode(event);
+ if (r < 0)
+ return r;
+
+ /* preserve old, or get new initialization timestamp */
+ r = device_ensure_usec_initialized(dev, event->dev_db_clone);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to set initialization timestamp: %m");
+
+ /* (re)write database file */
+ r = device_tag_index(dev, event->dev_db_clone, true);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to update tags under /run/udev/tag/: %m");
+
+ r = device_update_db(dev);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to update database under /run/udev/data/: %m");
+
+ device_set_is_initialized(dev);
+
+ return 0;
+}
diff --git a/src/udev/udev-event.h b/src/udev/udev-event.h
new file mode 100644
index 0000000..6b94fd0
--- /dev/null
+++ b/src/udev/udev-event.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+/*
+ * Copyright © 2003 Greg Kroah-Hartman <greg@kroah.com>
+ */
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "sd-device.h"
+#include "sd-netlink.h"
+
+#include "hashmap.h"
+#include "macro.h"
+#include "time-util.h"
+#include "udev-rules.h"
+#include "user-util.h"
+
+typedef struct UdevEvent {
+ sd_device *dev;
+ sd_device *dev_parent;
+ sd_device *dev_db_clone;
+ char *name;
+ char **altnames;
+ char *program_result;
+ mode_t mode;
+ uid_t uid;
+ gid_t gid;
+ OrderedHashmap *seclabel_list;
+ OrderedHashmap *run_list;
+ usec_t exec_delay_usec;
+ usec_t birth_usec;
+ sd_netlink *rtnl;
+ unsigned builtin_run;
+ unsigned builtin_ret;
+ UdevRuleEscapeType esc:8;
+ bool inotify_watch;
+ bool inotify_watch_final;
+ bool group_final;
+ bool owner_final;
+ bool mode_final;
+ bool name_final;
+ bool devlink_final;
+ bool run_final;
+ bool log_level_was_debug;
+ int default_log_level;
+} UdevEvent;
+
+UdevEvent *udev_event_new(sd_device *dev, usec_t exec_delay_usec, sd_netlink *rtnl, int log_level);
+UdevEvent *udev_event_free(UdevEvent *event);
+DEFINE_TRIVIAL_CLEANUP_FUNC(UdevEvent*, udev_event_free);
+
+int udev_event_execute_rules(
+ UdevEvent *event,
+ int inotify_fd,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list,
+ UdevRules *rules);
diff --git a/src/udev/udev-format.c b/src/udev/udev-format.c
new file mode 100644
index 0000000..05ed9fd
--- /dev/null
+++ b/src/udev/udev-format.c
@@ -0,0 +1,550 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "device-util.h"
+#include "errno-util.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-event.h"
+#include "udev-format.h"
+#include "udev-util.h"
+
+typedef enum {
+ FORMAT_SUBST_DEVNODE,
+ FORMAT_SUBST_ATTR,
+ FORMAT_SUBST_ENV,
+ FORMAT_SUBST_KERNEL,
+ FORMAT_SUBST_KERNEL_NUMBER,
+ FORMAT_SUBST_DRIVER,
+ FORMAT_SUBST_DEVPATH,
+ FORMAT_SUBST_ID,
+ FORMAT_SUBST_MAJOR,
+ FORMAT_SUBST_MINOR,
+ FORMAT_SUBST_RESULT,
+ FORMAT_SUBST_PARENT,
+ FORMAT_SUBST_NAME,
+ FORMAT_SUBST_LINKS,
+ FORMAT_SUBST_ROOT,
+ FORMAT_SUBST_SYS,
+ _FORMAT_SUBST_TYPE_MAX,
+ _FORMAT_SUBST_TYPE_INVALID = -EINVAL,
+} FormatSubstitutionType;
+
+struct subst_map_entry {
+ const char *name;
+ const char fmt;
+ FormatSubstitutionType type;
+};
+
+static const struct subst_map_entry map[] = {
+ { .name = "devnode", .fmt = 'N', .type = FORMAT_SUBST_DEVNODE },
+ { .name = "tempnode", .fmt = 'N', .type = FORMAT_SUBST_DEVNODE }, /* deprecated */
+ { .name = "attr", .fmt = 's', .type = FORMAT_SUBST_ATTR },
+ { .name = "sysfs", .fmt = 's', .type = FORMAT_SUBST_ATTR }, /* deprecated */
+ { .name = "env", .fmt = 'E', .type = FORMAT_SUBST_ENV },
+ { .name = "kernel", .fmt = 'k', .type = FORMAT_SUBST_KERNEL },
+ { .name = "number", .fmt = 'n', .type = FORMAT_SUBST_KERNEL_NUMBER },
+ { .name = "driver", .fmt = 'd', .type = FORMAT_SUBST_DRIVER },
+ { .name = "devpath", .fmt = 'p', .type = FORMAT_SUBST_DEVPATH },
+ { .name = "id", .fmt = 'b', .type = FORMAT_SUBST_ID },
+ { .name = "major", .fmt = 'M', .type = FORMAT_SUBST_MAJOR },
+ { .name = "minor", .fmt = 'm', .type = FORMAT_SUBST_MINOR },
+ { .name = "result", .fmt = 'c', .type = FORMAT_SUBST_RESULT },
+ { .name = "parent", .fmt = 'P', .type = FORMAT_SUBST_PARENT },
+ { .name = "name", .fmt = 'D', .type = FORMAT_SUBST_NAME },
+ { .name = "links", .fmt = 'L', .type = FORMAT_SUBST_LINKS },
+ { .name = "root", .fmt = 'r', .type = FORMAT_SUBST_ROOT },
+ { .name = "sys", .fmt = 'S', .type = FORMAT_SUBST_SYS },
+};
+
+static const char *format_type_to_string(FormatSubstitutionType t) {
+ for (size_t i = 0; i < ELEMENTSOF(map); i++)
+ if (map[i].type == t)
+ return map[i].name;
+ return NULL;
+}
+
+static char format_type_to_char(FormatSubstitutionType t) {
+ for (size_t i = 0; i < ELEMENTSOF(map); i++)
+ if (map[i].type == t)
+ return map[i].fmt;
+ return '\0';
+}
+
+static int get_subst_type(const char **str, bool strict, FormatSubstitutionType *ret_type, char ret_attr[static UDEV_PATH_SIZE]) {
+ const char *p = *str, *q = NULL;
+ size_t i;
+
+ assert(str);
+ assert(*str);
+ assert(ret_type);
+ assert(ret_attr);
+
+ if (*p == '$') {
+ p++;
+ if (*p == '$') {
+ *str = p;
+ return 0;
+ }
+ for (i = 0; i < ELEMENTSOF(map); i++)
+ if ((q = startswith(p, map[i].name)))
+ break;
+ } else if (*p == '%') {
+ p++;
+ if (*p == '%') {
+ *str = p;
+ return 0;
+ }
+
+ for (i = 0; i < ELEMENTSOF(map); i++)
+ if (*p == map[i].fmt) {
+ q = p + 1;
+ break;
+ }
+ } else
+ return 0;
+ if (!q)
+ /* When 'strict' flag is set, then '$' and '%' must be escaped. */
+ return strict ? -EINVAL : 0;
+
+ if (*q == '{') {
+ const char *start, *end;
+ size_t len;
+
+ start = q + 1;
+ end = strchr(start, '}');
+ if (!end)
+ return -EINVAL;
+
+ len = end - start;
+ if (len == 0 || len >= UDEV_PATH_SIZE)
+ return -EINVAL;
+
+ strnscpy(ret_attr, UDEV_PATH_SIZE, start, len);
+ q = end + 1;
+ } else
+ *ret_attr = '\0';
+
+ *str = q;
+ *ret_type = map[i].type;
+ return 1;
+}
+
+static int safe_atou_optional_plus(const char *s, unsigned *ret) {
+ const char *p;
+ int r;
+
+ assert(s);
+ assert(ret);
+
+ /* Returns 1 if plus, 0 if no plus, negative on error */
+
+ p = endswith(s, "+");
+ if (p)
+ s = strndupa_safe(s, p - s);
+
+ r = safe_atou(s, ret);
+ if (r < 0)
+ return r;
+
+ return !!p;
+}
+
+static ssize_t udev_event_subst_format(
+ UdevEvent *event,
+ FormatSubstitutionType type,
+ const char *attr,
+ char *dest,
+ size_t l,
+ Hashmap *global_props,
+ bool *ret_truncated) {
+
+ sd_device *parent, *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
+ const char *val = NULL;
+ bool truncated = false;
+ char *s = dest;
+ int r;
+
+ switch (type) {
+ case FORMAT_SUBST_DEVPATH:
+ r = sd_device_get_devpath(dev, &val);
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_KERNEL:
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_KERNEL_NUMBER:
+ r = sd_device_get_sysnum(dev, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_ID:
+ if (!event->dev_parent)
+ goto null_terminate;
+ r = sd_device_get_sysname(event->dev_parent, &val);
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_DRIVER:
+ if (!event->dev_parent)
+ goto null_terminate;
+ r = sd_device_get_driver(event->dev_parent, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_MAJOR:
+ case FORMAT_SUBST_MINOR: {
+ dev_t devnum;
+
+ r = sd_device_get_devnum(dev, &devnum);
+ if (r < 0 && r != -ENOENT)
+ return r;
+ strpcpyf_full(&s, l, &truncated, "%u", r < 0 ? 0 : type == FORMAT_SUBST_MAJOR ? major(devnum) : minor(devnum));
+ break;
+ }
+ case FORMAT_SUBST_RESULT: {
+ unsigned index = 0; /* 0 means whole string */
+ bool has_plus;
+
+ if (!event->program_result)
+ goto null_terminate;
+
+ if (!isempty(attr)) {
+ r = safe_atou_optional_plus(attr, &index);
+ if (r < 0)
+ return r;
+
+ has_plus = r;
+ }
+
+ if (index == 0)
+ strpcpy_full(&s, l, event->program_result, &truncated);
+ else {
+ const char *start, *p;
+ unsigned i;
+
+ p = skip_leading_chars(event->program_result, NULL);
+
+ for (i = 1; i < index; i++) {
+ while (*p && !strchr(WHITESPACE, *p))
+ p++;
+ p = skip_leading_chars(p, NULL);
+ if (*p == '\0')
+ break;
+ }
+ if (i != index) {
+ log_device_debug(dev, "requested part of result string not found");
+ goto null_terminate;
+ }
+
+ start = p;
+ /* %c{2+} copies the whole string from the second part on */
+ if (has_plus)
+ strpcpy_full(&s, l, start, &truncated);
+ else {
+ while (*p && !strchr(WHITESPACE, *p))
+ p++;
+ strnpcpy_full(&s, l, start, p - start, &truncated);
+ }
+ }
+ break;
+ }
+ case FORMAT_SUBST_ATTR: {
+ char vbuf[UDEV_NAME_SIZE];
+ int count;
+ bool t;
+
+ if (isempty(attr))
+ return -EINVAL;
+
+ /* try to read the value specified by "[dmi/id]product_name" */
+ if (udev_resolve_subsys_kernel(attr, vbuf, sizeof(vbuf), true) == 0)
+ val = vbuf;
+
+ /* try to read the attribute the device */
+ if (!val)
+ (void) sd_device_get_sysattr_value(dev, attr, &val);
+
+ /* try to read the attribute of the parent device, other matches have selected */
+ if (!val && event->dev_parent && event->dev_parent != dev)
+ (void) sd_device_get_sysattr_value(event->dev_parent, attr, &val);
+
+ if (!val)
+ goto null_terminate;
+
+ /* strip trailing whitespace, and replace unwanted characters */
+ if (val != vbuf)
+ strscpy_full(vbuf, sizeof(vbuf), val, &truncated);
+ delete_trailing_chars(vbuf, NULL);
+ count = udev_replace_chars(vbuf, UDEV_ALLOWED_CHARS_INPUT);
+ if (count > 0)
+ log_device_debug(dev, "%i character(s) replaced", count);
+ strpcpy_full(&s, l, vbuf, &t);
+ truncated = truncated || t;
+ break;
+ }
+ case FORMAT_SUBST_PARENT:
+ r = sd_device_get_parent(dev, &parent);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ r = sd_device_get_devname(parent, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val + STRLEN("/dev/"), &truncated);
+ break;
+ case FORMAT_SUBST_DEVNODE:
+ r = sd_device_get_devname(dev, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ case FORMAT_SUBST_NAME:
+ if (event->name)
+ strpcpy_full(&s, l, event->name, &truncated);
+ else if (sd_device_get_devname(dev, &val) >= 0)
+ strpcpy_full(&s, l, val + STRLEN("/dev/"), &truncated);
+ else {
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ }
+ break;
+ case FORMAT_SUBST_LINKS:
+ FOREACH_DEVICE_DEVLINK(dev, link) {
+ if (s == dest)
+ strpcpy_full(&s, l, link + STRLEN("/dev/"), &truncated);
+ else
+ strpcpyl_full(&s, l, &truncated, " ", link + STRLEN("/dev/"), NULL);
+ if (truncated)
+ break;
+ }
+ if (s == dest)
+ goto null_terminate;
+ break;
+ case FORMAT_SUBST_ROOT:
+ strpcpy_full(&s, l, "/dev", &truncated);
+ break;
+ case FORMAT_SUBST_SYS:
+ strpcpy_full(&s, l, "/sys", &truncated);
+ break;
+ case FORMAT_SUBST_ENV:
+ if (isempty(attr))
+ return -EINVAL;
+ r = device_get_property_value_with_fallback(dev, attr, global_props, &val);
+ if (r == -ENOENT)
+ goto null_terminate;
+ if (r < 0)
+ return r;
+ strpcpy_full(&s, l, val, &truncated);
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ if (ret_truncated)
+ *ret_truncated = truncated;
+
+ return s - dest;
+
+null_terminate:
+ if (ret_truncated)
+ *ret_truncated = truncated;
+
+ *s = '\0';
+ return 0;
+}
+
+size_t udev_event_apply_format(
+ UdevEvent *event,
+ const char *src,
+ char *dest,
+ size_t size,
+ bool replace_whitespace,
+ Hashmap *global_props,
+ bool *ret_truncated) {
+
+ bool truncated = false;
+ const char *s = ASSERT_PTR(src);
+ int r;
+
+ assert(event);
+ assert(event->dev);
+ assert(dest);
+ assert(size > 0);
+
+ while (*s) {
+ FormatSubstitutionType type;
+ char attr[UDEV_PATH_SIZE];
+ ssize_t subst_len;
+ bool t;
+
+ r = get_subst_type(&s, false, &type, attr);
+ if (r < 0) {
+ log_device_warning_errno(event->dev, r, "Invalid format string, ignoring: %s", src);
+ break;
+ } else if (r == 0) {
+ if (size < 2) {
+ /* need space for this char and the terminating NUL */
+ truncated = true;
+ break;
+ }
+ *dest++ = *s++;
+ size--;
+ continue;
+ }
+
+ subst_len = udev_event_subst_format(event, type, attr, dest, size, global_props, &t);
+ if (subst_len < 0) {
+ log_device_warning_errno(event->dev, subst_len,
+ "Failed to substitute variable '$%s' or apply format '%%%c', ignoring: %m",
+ format_type_to_string(type), format_type_to_char(type));
+ break;
+ }
+
+ truncated = truncated || t;
+
+ /* FORMAT_SUBST_RESULT handles spaces itself */
+ if (replace_whitespace && type != FORMAT_SUBST_RESULT)
+ /* udev_replace_whitespace can replace in-place,
+ * and does nothing if subst_len == 0 */
+ subst_len = udev_replace_whitespace(dest, dest, subst_len);
+
+ dest += subst_len;
+ size -= subst_len;
+ }
+
+ assert(size >= 1);
+
+ if (ret_truncated)
+ *ret_truncated = truncated;
+
+ *dest = '\0';
+ return size;
+}
+
+int udev_check_format(const char *value, size_t *offset, const char **hint) {
+ FormatSubstitutionType type;
+ const char *s = value;
+ char attr[UDEV_PATH_SIZE];
+ int r;
+
+ while (*s) {
+ r = get_subst_type(&s, true, &type, attr);
+ if (r < 0) {
+ if (offset)
+ *offset = s - value;
+ if (hint)
+ *hint = "invalid substitution type";
+ return r;
+ } else if (r == 0) {
+ s++;
+ continue;
+ }
+
+ if (IN_SET(type, FORMAT_SUBST_ATTR, FORMAT_SUBST_ENV) && isempty(attr)) {
+ if (offset)
+ *offset = s - value;
+ if (hint)
+ *hint = "attribute value missing";
+ return -EINVAL;
+ }
+
+ if (type == FORMAT_SUBST_RESULT && !isempty(attr)) {
+ unsigned i;
+
+ r = safe_atou_optional_plus(attr, &i);
+ if (r < 0) {
+ if (offset)
+ *offset = s - value;
+ if (hint)
+ *hint = "attribute value not a valid number";
+ return r;
+ }
+ }
+ }
+
+ return 0;
+}
+
+int udev_resolve_subsys_kernel(const char *string, char *result, size_t maxsize, bool read_value) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ _cleanup_free_ char *temp = NULL;
+ char *subsys, *sysname, *attr;
+ const char *val;
+ int r;
+
+ assert(string);
+ assert(result);
+
+ /* handle "[<SUBSYSTEM>/<KERNEL>]<attribute>" format */
+
+ if (string[0] != '[')
+ return -EINVAL;
+
+ temp = strdup(string);
+ if (!temp)
+ return -ENOMEM;
+
+ subsys = &temp[1];
+
+ sysname = strchr(subsys, '/');
+ if (!sysname)
+ return -EINVAL;
+ sysname[0] = '\0';
+ sysname = &sysname[1];
+
+ attr = strchr(sysname, ']');
+ if (!attr)
+ return -EINVAL;
+ attr[0] = '\0';
+ attr = &attr[1];
+ if (attr[0] == '/')
+ attr = &attr[1];
+ if (attr[0] == '\0')
+ attr = NULL;
+
+ if (read_value && !attr)
+ return -EINVAL;
+
+ r = sd_device_new_from_subsystem_sysname(&dev, subsys, sysname);
+ if (r < 0)
+ return r;
+
+ if (read_value) {
+ r = sd_device_get_sysattr_value(dev, attr, &val);
+ if (r < 0 && !ERRNO_IS_PRIVILEGE(r) && r != -ENOENT)
+ return r;
+ if (r >= 0)
+ strscpy(result, maxsize, val);
+ else
+ result[0] = '\0';
+ log_debug("value '[%s/%s]%s' is '%s'", subsys, sysname, attr, result);
+ } else {
+ r = sd_device_get_syspath(dev, &val);
+ if (r < 0)
+ return r;
+
+ strscpyl(result, maxsize, val, attr ? "/" : NULL, attr ?: NULL, NULL);
+ log_debug("path '[%s/%s]%s' is '%s'", subsys, sysname, strempty(attr), result);
+ }
+ return 0;
+}
diff --git a/src/udev/udev-format.h b/src/udev/udev-format.h
new file mode 100644
index 0000000..92fef9b
--- /dev/null
+++ b/src/udev/udev-format.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+
+typedef struct UdevEvent UdevEvent;
+
+#define UDEV_ALLOWED_CHARS_INPUT "/ $%?,"
+
+size_t udev_event_apply_format(
+ UdevEvent *event,
+ const char *src,
+ char *dest,
+ size_t size,
+ bool replace_whitespace,
+ Hashmap *global_props,
+ bool *ret_truncated);
+int udev_check_format(const char *value, size_t *offset, const char **hint);
+
+int udev_resolve_subsys_kernel(const char *string, char *result, size_t maxsize, bool read_value);
diff --git a/src/udev/udev-manager.c b/src/udev/udev-manager.c
new file mode 100644
index 0000000..8077e51
--- /dev/null
+++ b/src/udev/udev-manager.c
@@ -0,0 +1,1352 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "blockdev-util.h"
+#include "cgroup-util.h"
+#include "common-signal.h"
+#include "cpu-set-util.h"
+#include "daemon-util.h"
+#include "device-monitor-private.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "errno-list.h"
+#include "event-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "hashmap.h"
+#include "inotify-util.h"
+#include "iovec-util.h"
+#include "limits-util.h"
+#include "list.h"
+#include "mkdir.h"
+#include "process-util.h"
+#include "selinux-util.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "string-util.h"
+#include "syslog-util.h"
+#include "udev-builtin.h"
+#include "udev-ctrl.h"
+#include "udev-event.h"
+#include "udev-manager.h"
+#include "udev-node.h"
+#include "udev-spawn.h"
+#include "udev-trace.h"
+#include "udev-util.h"
+#include "udev-watch.h"
+#include "udev-worker.h"
+
+#define WORKER_NUM_MAX UINT64_C(2048)
+
+#define EVENT_RETRY_INTERVAL_USEC (200 * USEC_PER_MSEC)
+#define EVENT_RETRY_TIMEOUT_USEC (3 * USEC_PER_MINUTE)
+
+typedef enum EventState {
+ EVENT_UNDEF,
+ EVENT_QUEUED,
+ EVENT_RUNNING,
+} EventState;
+
+typedef struct Event {
+ Manager *manager;
+ Worker *worker;
+ EventState state;
+
+ sd_device *dev;
+
+ sd_device_action_t action;
+ uint64_t seqnum;
+ uint64_t blocker_seqnum;
+ const char *id;
+ const char *devpath;
+ const char *devpath_old;
+ const char *devnode;
+
+ /* Used when the device is locked by another program. */
+ usec_t retry_again_next_usec;
+ usec_t retry_again_timeout_usec;
+ sd_event_source *retry_event_source;
+
+ sd_event_source *timeout_warning_event;
+ sd_event_source *timeout_event;
+
+ LIST_FIELDS(Event, event);
+} Event;
+
+typedef enum WorkerState {
+ WORKER_UNDEF,
+ WORKER_RUNNING,
+ WORKER_IDLE,
+ WORKER_KILLED,
+ WORKER_KILLING,
+} WorkerState;
+
+typedef struct Worker {
+ Manager *manager;
+ pid_t pid;
+ sd_event_source *child_event_source;
+ sd_device_monitor *monitor;
+ WorkerState state;
+ Event *event;
+} Worker;
+
+static Event *event_free(Event *event) {
+ if (!event)
+ return NULL;
+
+ assert(event->manager);
+
+ LIST_REMOVE(event, event->manager->events, event);
+ sd_device_unref(event->dev);
+
+ sd_event_source_unref(event->retry_event_source);
+ sd_event_source_unref(event->timeout_warning_event);
+ sd_event_source_unref(event->timeout_event);
+
+ if (event->worker)
+ event->worker->event = NULL;
+
+ return mfree(event);
+}
+
+static void event_queue_cleanup(Manager *manager, EventState match_state) {
+ LIST_FOREACH(event, event, manager->events) {
+ if (match_state != EVENT_UNDEF && match_state != event->state)
+ continue;
+
+ event_free(event);
+ }
+}
+
+static Worker *worker_free(Worker *worker) {
+ if (!worker)
+ return NULL;
+
+ if (worker->manager)
+ hashmap_remove(worker->manager->workers, PID_TO_PTR(worker->pid));
+
+ sd_event_source_unref(worker->child_event_source);
+ sd_device_monitor_unref(worker->monitor);
+ event_free(worker->event);
+
+ return mfree(worker);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Worker*, worker_free);
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(worker_hash_op, void, trivial_hash_func, trivial_compare_func, Worker, worker_free);
+
+Manager* manager_free(Manager *manager) {
+ if (!manager)
+ return NULL;
+
+ udev_builtin_exit();
+
+ hashmap_free_free_free(manager->properties);
+ udev_rules_free(manager->rules);
+
+ hashmap_free(manager->workers);
+ event_queue_cleanup(manager, EVENT_UNDEF);
+
+ safe_close(manager->inotify_fd);
+ safe_close_pair(manager->worker_watch);
+
+ sd_device_monitor_unref(manager->monitor);
+ udev_ctrl_unref(manager->ctrl);
+
+ sd_event_source_unref(manager->inotify_event);
+ sd_event_source_unref(manager->kill_workers_event);
+ sd_event_source_unref(manager->memory_pressure_event_source);
+ sd_event_source_unref(manager->sigrtmin18_event_source);
+ sd_event_unref(manager->event);
+
+ free(manager->cgroup);
+ return mfree(manager);
+}
+
+static int on_sigchld(sd_event_source *s, const siginfo_t *si, void *userdata);
+
+static int worker_new(Worker **ret, Manager *manager, sd_device_monitor *worker_monitor, pid_t pid) {
+ _cleanup_(worker_freep) Worker *worker = NULL;
+ int r;
+
+ assert(ret);
+ assert(manager);
+ assert(worker_monitor);
+ assert(pid > 1);
+
+ /* close monitor, but keep address around */
+ device_monitor_disconnect(worker_monitor);
+
+ worker = new(Worker, 1);
+ if (!worker)
+ return -ENOMEM;
+
+ *worker = (Worker) {
+ .monitor = sd_device_monitor_ref(worker_monitor),
+ .pid = pid,
+ };
+
+ r = sd_event_add_child(manager->event, &worker->child_event_source, pid, WEXITED, on_sigchld, worker);
+ if (r < 0)
+ return r;
+
+ r = hashmap_ensure_put(&manager->workers, &worker_hash_op, PID_TO_PTR(pid), worker);
+ if (r < 0)
+ return r;
+
+ worker->manager = manager;
+
+ *ret = TAKE_PTR(worker);
+ return 0;
+}
+
+static void manager_kill_workers(Manager *manager, bool force) {
+ Worker *worker;
+
+ assert(manager);
+
+ HASHMAP_FOREACH(worker, manager->workers) {
+ if (worker->state == WORKER_KILLED)
+ continue;
+
+ if (worker->state == WORKER_RUNNING && !force) {
+ worker->state = WORKER_KILLING;
+ continue;
+ }
+
+ worker->state = WORKER_KILLED;
+ (void) kill(worker->pid, SIGTERM);
+ }
+}
+
+static void manager_exit(Manager *manager) {
+ assert(manager);
+
+ manager->exit = true;
+
+ (void) sd_notify(/* unset= */ false, NOTIFY_STOPPING);
+
+ /* close sources of new events and discard buffered events */
+ manager->ctrl = udev_ctrl_unref(manager->ctrl);
+
+ manager->inotify_event = sd_event_source_disable_unref(manager->inotify_event);
+ manager->inotify_fd = safe_close(manager->inotify_fd);
+
+ manager->monitor = sd_device_monitor_unref(manager->monitor);
+
+ /* discard queued events and kill workers */
+ event_queue_cleanup(manager, EVENT_QUEUED);
+ manager_kill_workers(manager, true);
+}
+
+static void notify_ready(Manager *manager) {
+ int r;
+
+ assert(manager);
+
+ r = sd_notifyf(/* unset= */ false,
+ "READY=1\n"
+ "STATUS=Processing with %u children at max", manager->children_max);
+ if (r < 0)
+ log_warning_errno(r, "Failed to send readiness notification, ignoring: %m");
+}
+
+/* reload requested, HUP signal received, rules changed, builtin changed */
+static void manager_reload(Manager *manager, bool force) {
+ _cleanup_(udev_rules_freep) UdevRules *rules = NULL;
+ usec_t now_usec;
+ int r;
+
+ assert(manager);
+
+ assert_se(sd_event_now(manager->event, CLOCK_MONOTONIC, &now_usec) >= 0);
+ if (!force && now_usec < usec_add(manager->last_usec, 3 * USEC_PER_SEC))
+ /* check for changed config, every 3 seconds at most */
+ return;
+ manager->last_usec = now_usec;
+
+ /* Reload SELinux label database, to make the child inherit the up-to-date database. */
+ mac_selinux_maybe_reload();
+
+ /* Nothing changed. It is not necessary to reload. */
+ if (!udev_rules_should_reload(manager->rules) && !udev_builtin_should_reload()) {
+
+ if (!force)
+ return;
+
+ /* If we eat this up, then tell our service manager to just continue */
+ (void) sd_notifyf(/* unset= */ false,
+ "RELOADING=1\n"
+ "STATUS=Skipping configuration reloading, nothing changed.\n"
+ "MONOTONIC_USEC=" USEC_FMT, now(CLOCK_MONOTONIC));
+ } else {
+ (void) sd_notifyf(/* unset= */ false,
+ "RELOADING=1\n"
+ "STATUS=Flushing configuration...\n"
+ "MONOTONIC_USEC=" USEC_FMT, now(CLOCK_MONOTONIC));
+
+ manager_kill_workers(manager, false);
+
+ udev_builtin_exit();
+ udev_builtin_init();
+
+ r = udev_rules_load(&rules, manager->resolve_name_timing);
+ if (r < 0)
+ log_warning_errno(r, "Failed to read udev rules, using the previously loaded rules, ignoring: %m");
+ else
+ udev_rules_free_and_replace(manager->rules, rules);
+ }
+
+ notify_ready(manager);
+}
+
+static int on_kill_workers_event(sd_event_source *s, uint64_t usec, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+
+ log_debug("Cleanup idle workers");
+ manager_kill_workers(manager, false);
+
+ return 1;
+}
+
+static int on_event_timeout(sd_event_source *s, uint64_t usec, void *userdata) {
+ Event *event = ASSERT_PTR(userdata);
+
+ assert(event->manager);
+ assert(event->worker);
+
+ kill_and_sigcont(event->worker->pid, event->manager->timeout_signal);
+ event->worker->state = WORKER_KILLED;
+
+ log_device_error(event->dev, "Worker ["PID_FMT"] processing SEQNUM=%"PRIu64" killed", event->worker->pid, event->seqnum);
+
+ return 1;
+}
+
+static int on_event_timeout_warning(sd_event_source *s, uint64_t usec, void *userdata) {
+ Event *event = ASSERT_PTR(userdata);
+
+ assert(event->worker);
+
+ log_device_warning(event->dev, "Worker ["PID_FMT"] processing SEQNUM=%"PRIu64" is taking a long time", event->worker->pid, event->seqnum);
+
+ return 1;
+}
+
+static void worker_attach_event(Worker *worker, Event *event) {
+ Manager *manager;
+ sd_event *e;
+
+ assert(worker);
+ assert(worker->manager);
+ assert(event);
+ assert(!event->worker);
+ assert(!worker->event);
+
+ worker->state = WORKER_RUNNING;
+ worker->event = event;
+ event->state = EVENT_RUNNING;
+ event->worker = worker;
+
+ manager = worker->manager;
+ e = manager->event;
+
+ (void) sd_event_add_time_relative(e, &event->timeout_warning_event, CLOCK_MONOTONIC,
+ udev_warn_timeout(manager->timeout_usec), USEC_PER_SEC,
+ on_event_timeout_warning, event);
+
+ (void) sd_event_add_time_relative(e, &event->timeout_event, CLOCK_MONOTONIC,
+ manager->timeout_usec, USEC_PER_SEC,
+ on_event_timeout, event);
+}
+
+static int worker_spawn(Manager *manager, Event *event) {
+ _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *worker_monitor = NULL;
+ Worker *worker;
+ pid_t pid;
+ int r;
+
+ /* listen for new events */
+ r = device_monitor_new_full(&worker_monitor, MONITOR_GROUP_NONE, -1);
+ if (r < 0)
+ return r;
+
+ (void) sd_device_monitor_set_description(worker_monitor, "worker");
+
+ /* allow the main daemon netlink address to send devices to the worker */
+ r = device_monitor_allow_unicast_sender(worker_monitor, manager->monitor);
+ if (r < 0)
+ return log_error_errno(r, "Worker: Failed to set unicast sender: %m");
+
+ r = device_monitor_enable_receiving(worker_monitor);
+ if (r < 0)
+ return log_error_errno(r, "Worker: Failed to enable receiving of device: %m");
+
+ r = safe_fork("(udev-worker)", FORK_DEATHSIG_SIGTERM, &pid);
+ if (r < 0) {
+ event->state = EVENT_QUEUED;
+ return log_error_errno(r, "Failed to fork() worker: %m");
+ }
+ if (r == 0) {
+ _cleanup_(udev_worker_done) UdevWorker w = {
+ .monitor = TAKE_PTR(worker_monitor),
+ .properties = TAKE_PTR(manager->properties),
+ .rules = TAKE_PTR(manager->rules),
+ .pipe_fd = TAKE_FD(manager->worker_watch[WRITE_END]),
+ .inotify_fd = TAKE_FD(manager->inotify_fd),
+ .exec_delay_usec = manager->exec_delay_usec,
+ .timeout_usec = manager->timeout_usec,
+ .timeout_signal = manager->timeout_signal,
+ .log_level = manager->log_level,
+ .blockdev_read_only = manager->blockdev_read_only,
+ };
+
+ /* Worker process */
+ r = udev_worker_main(&w, event->dev);
+ log_close();
+ _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
+ }
+
+ r = worker_new(&worker, manager, worker_monitor, pid);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create worker object: %m");
+
+ worker_attach_event(worker, event);
+
+ log_device_debug(event->dev, "Worker ["PID_FMT"] is forked for processing SEQNUM=%"PRIu64".", pid, event->seqnum);
+ return 0;
+}
+
+static int event_run(Event *event) {
+ static bool log_children_max_reached = true;
+ Manager *manager;
+ Worker *worker;
+ int r;
+
+ assert(event);
+ assert(event->manager);
+
+ log_device_uevent(event->dev, "Device ready for processing");
+
+ (void) event_source_disable(event->retry_event_source);
+
+ manager = event->manager;
+ HASHMAP_FOREACH(worker, manager->workers) {
+ if (worker->state != WORKER_IDLE)
+ continue;
+
+ r = device_monitor_send_device(manager->monitor, worker->monitor, event->dev);
+ if (r < 0) {
+ log_device_error_errno(event->dev, r, "Worker ["PID_FMT"] did not accept message, killing the worker: %m",
+ worker->pid);
+ (void) kill(worker->pid, SIGKILL);
+ worker->state = WORKER_KILLED;
+ continue;
+ }
+ worker_attach_event(worker, event);
+ return 1; /* event is now processing. */
+ }
+
+ if (hashmap_size(manager->workers) >= manager->children_max) {
+ /* Avoid spamming the debug logs if the limit is already reached and
+ * many events still need to be processed */
+ if (log_children_max_reached && manager->children_max > 1) {
+ log_debug("Maximum number (%u) of children reached.", hashmap_size(manager->workers));
+ log_children_max_reached = false;
+ }
+ return 0; /* no free worker */
+ }
+
+ /* Re-enable the debug message for the next batch of events */
+ log_children_max_reached = true;
+
+ /* start new worker and pass initial device */
+ r = worker_spawn(manager, event);
+ if (r < 0)
+ return r;
+
+ return 1; /* event is now processing. */
+}
+
+bool devpath_conflict(const char *a, const char *b) {
+ /* This returns true when two paths are equivalent, or one is a child of another. */
+
+ if (!a || !b)
+ return false;
+
+ for (; *a != '\0' && *b != '\0'; a++, b++)
+ if (*a != *b)
+ return false;
+
+ return *a == '/' || *b == '/' || *a == *b;
+}
+
+static int event_is_blocked(Event *event) {
+ Event *loop_event = NULL;
+ int r;
+
+ /* lookup event for identical, parent, child device */
+
+ assert(event);
+ assert(event->manager);
+ assert(event->blocker_seqnum <= event->seqnum);
+
+ if (event->retry_again_next_usec > 0) {
+ usec_t now_usec;
+
+ r = sd_event_now(event->manager->event, CLOCK_BOOTTIME, &now_usec);
+ if (r < 0)
+ return r;
+
+ if (event->retry_again_next_usec > now_usec)
+ return true;
+ }
+
+ if (event->blocker_seqnum == event->seqnum)
+ /* we have checked previously and no blocker found */
+ return false;
+
+ LIST_FOREACH(event, e, event->manager->events) {
+ loop_event = e;
+
+ /* we already found a later event, earlier cannot block us, no need to check again */
+ if (loop_event->seqnum < event->blocker_seqnum)
+ continue;
+
+ /* event we checked earlier still exists, no need to check again */
+ if (loop_event->seqnum == event->blocker_seqnum)
+ return true;
+
+ /* found ourself, no later event can block us */
+ if (loop_event->seqnum >= event->seqnum)
+ goto no_blocker;
+
+ /* found event we have not checked */
+ break;
+ }
+
+ assert(loop_event);
+ assert(loop_event->seqnum > event->blocker_seqnum &&
+ loop_event->seqnum < event->seqnum);
+
+ /* check if queue contains events we depend on */
+ LIST_FOREACH(event, e, loop_event) {
+ loop_event = e;
+
+ /* found ourself, no later event can block us */
+ if (loop_event->seqnum >= event->seqnum)
+ goto no_blocker;
+
+ if (streq_ptr(loop_event->id, event->id))
+ break;
+
+ if (devpath_conflict(event->devpath, loop_event->devpath) ||
+ devpath_conflict(event->devpath, loop_event->devpath_old) ||
+ devpath_conflict(event->devpath_old, loop_event->devpath))
+ break;
+
+ if (event->devnode && streq_ptr(event->devnode, loop_event->devnode))
+ break;
+ }
+
+ assert(loop_event);
+
+ log_device_debug(event->dev, "SEQNUM=%" PRIu64 " blocked by SEQNUM=%" PRIu64,
+ event->seqnum, loop_event->seqnum);
+
+ event->blocker_seqnum = loop_event->seqnum;
+ return true;
+
+no_blocker:
+ event->blocker_seqnum = event->seqnum;
+ return false;
+}
+
+static int event_queue_start(Manager *manager) {
+ int r;
+
+ assert(manager);
+
+ if (!manager->events || manager->exit || manager->stop_exec_queue)
+ return 0;
+
+ /* To make the stack directory /run/udev/links cleaned up later. */
+ manager->udev_node_needs_cleanup = true;
+
+ r = event_source_disable(manager->kill_workers_event);
+ if (r < 0)
+ log_warning_errno(r, "Failed to disable event source for cleaning up idle workers, ignoring: %m");
+
+ manager_reload(manager, /* force = */ false);
+
+ LIST_FOREACH(event, event, manager->events) {
+ if (event->state != EVENT_QUEUED)
+ continue;
+
+ /* do not start event if parent or child event is still running or queued */
+ r = event_is_blocked(event);
+ if (r > 0)
+ continue;
+ if (r < 0)
+ log_device_warning_errno(event->dev, r,
+ "Failed to check dependencies for event (SEQNUM=%"PRIu64", ACTION=%s), "
+ "assuming there is no blocking event, ignoring: %m",
+ event->seqnum,
+ strna(device_action_to_string(event->action)));
+
+ r = event_run(event);
+ if (r <= 0) /* 0 means there are no idle workers. Let's escape from the loop. */
+ return r;
+ }
+
+ return 0;
+}
+
+static int on_event_retry(sd_event_source *s, uint64_t usec, void *userdata) {
+ /* This does nothing. The on_post() callback will start the event if there exists an idle worker. */
+ return 1;
+}
+
+static int event_requeue(Event *event) {
+ usec_t now_usec;
+ int r;
+
+ assert(event);
+ assert(event->manager);
+ assert(event->manager->event);
+
+ event->timeout_warning_event = sd_event_source_disable_unref(event->timeout_warning_event);
+ event->timeout_event = sd_event_source_disable_unref(event->timeout_event);
+
+ /* add a short delay to suppress busy loop */
+ r = sd_event_now(event->manager->event, CLOCK_BOOTTIME, &now_usec);
+ if (r < 0)
+ return log_device_warning_errno(event->dev, r,
+ "Failed to get current time, "
+ "skipping event (SEQNUM=%"PRIu64", ACTION=%s): %m",
+ event->seqnum, strna(device_action_to_string(event->action)));
+
+ if (event->retry_again_timeout_usec > 0 && event->retry_again_timeout_usec <= now_usec)
+ return log_device_warning_errno(event->dev, SYNTHETIC_ERRNO(ETIMEDOUT),
+ "The underlying block device is locked by a process more than %s, "
+ "skipping event (SEQNUM=%"PRIu64", ACTION=%s).",
+ FORMAT_TIMESPAN(EVENT_RETRY_TIMEOUT_USEC, USEC_PER_MINUTE),
+ event->seqnum, strna(device_action_to_string(event->action)));
+
+ event->retry_again_next_usec = usec_add(now_usec, EVENT_RETRY_INTERVAL_USEC);
+ if (event->retry_again_timeout_usec == 0)
+ event->retry_again_timeout_usec = usec_add(now_usec, EVENT_RETRY_TIMEOUT_USEC);
+
+ r = event_reset_time_relative(event->manager->event, &event->retry_event_source,
+ CLOCK_MONOTONIC, EVENT_RETRY_INTERVAL_USEC, 0,
+ on_event_retry, NULL,
+ 0, "retry-event", true);
+ if (r < 0)
+ return log_device_warning_errno(event->dev, r, "Failed to reset timer event source for retrying event, "
+ "skipping event (SEQNUM=%"PRIu64", ACTION=%s): %m",
+ event->seqnum, strna(device_action_to_string(event->action)));
+
+ if (event->worker && event->worker->event == event)
+ event->worker->event = NULL;
+ event->worker = NULL;
+
+ event->state = EVENT_QUEUED;
+ return 0;
+}
+
+static int event_queue_assume_block_device_unlocked(Manager *manager, sd_device *dev) {
+ const char *devname;
+ int r;
+
+ /* When a new event for a block device is queued or we get an inotify event, assume that the
+ * device is not locked anymore. The assumption may not be true, but that should not cause any
+ * issues, as in that case events will be requeued soon. */
+
+ r = udev_get_whole_disk(dev, NULL, &devname);
+ if (r <= 0)
+ return r;
+
+ LIST_FOREACH(event, event, manager->events) {
+ const char *event_devname;
+
+ if (event->state != EVENT_QUEUED)
+ continue;
+
+ if (event->retry_again_next_usec == 0)
+ continue;
+
+ if (udev_get_whole_disk(event->dev, NULL, &event_devname) <= 0)
+ continue;
+
+ if (!streq(devname, event_devname))
+ continue;
+
+ event->retry_again_next_usec = 0;
+ }
+
+ return 0;
+}
+
+static int event_queue_insert(Manager *manager, sd_device *dev) {
+ const char *devpath, *devpath_old = NULL, *id = NULL, *devnode = NULL;
+ sd_device_action_t action;
+ uint64_t seqnum;
+ Event *event;
+ int r;
+
+ assert(manager);
+ assert(dev);
+
+ /* We only accepts devices received by device monitor. */
+ r = sd_device_get_seqnum(dev, &seqnum);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_action(dev, &action);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_devpath(dev, &devpath);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_property_value(dev, "DEVPATH_OLD", &devpath_old);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ r = device_get_device_id(dev, &id);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0 && r != -ENOENT)
+ return r;
+
+ event = new(Event, 1);
+ if (!event)
+ return -ENOMEM;
+
+ *event = (Event) {
+ .manager = manager,
+ .dev = sd_device_ref(dev),
+ .seqnum = seqnum,
+ .action = action,
+ .id = id,
+ .devpath = devpath,
+ .devpath_old = devpath_old,
+ .devnode = devnode,
+ .state = EVENT_QUEUED,
+ };
+
+ if (!manager->events) {
+ r = touch("/run/udev/queue");
+ if (r < 0)
+ log_warning_errno(r, "Failed to touch /run/udev/queue, ignoring: %m");
+ }
+
+ LIST_APPEND(event, manager->events, event);
+
+ log_device_uevent(dev, "Device is queued");
+
+ return 0;
+}
+
+static int on_uevent(sd_device_monitor *monitor, sd_device *dev, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+ int r;
+
+ DEVICE_TRACE_POINT(kernel_uevent_received, dev);
+
+ device_ensure_usec_initialized(dev, NULL);
+
+ r = event_queue_insert(manager, dev);
+ if (r < 0) {
+ log_device_error_errno(dev, r, "Failed to insert device into event queue: %m");
+ return 1;
+ }
+
+ (void) event_queue_assume_block_device_unlocked(manager, dev);
+
+ return 1;
+}
+
+static int on_worker(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+
+ for (;;) {
+ EventResult result;
+ struct iovec iovec = IOVEC_MAKE(&result, sizeof(result));
+ CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control;
+ struct msghdr msghdr = {
+ .msg_iov = &iovec,
+ .msg_iovlen = 1,
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ };
+ ssize_t size;
+ struct ucred *ucred;
+ Worker *worker;
+
+ size = recvmsg_safe(fd, &msghdr, MSG_DONTWAIT);
+ if (size == -EINTR)
+ continue;
+ if (size == -EAGAIN)
+ /* nothing more to read */
+ break;
+ if (size < 0)
+ return log_error_errno(size, "Failed to receive message: %m");
+
+ cmsg_close_all(&msghdr);
+
+ if (size != sizeof(result)) {
+ log_warning("Ignoring worker message with invalid size %zi bytes", size);
+ continue;
+ }
+
+ ucred = CMSG_FIND_DATA(&msghdr, SOL_SOCKET, SCM_CREDENTIALS, struct ucred);
+ if (!ucred || ucred->pid <= 0) {
+ log_warning("Ignoring worker message without valid PID");
+ continue;
+ }
+
+ /* lookup worker who sent the signal */
+ worker = hashmap_get(manager->workers, PID_TO_PTR(ucred->pid));
+ if (!worker) {
+ log_debug("Worker ["PID_FMT"] returned, but is no longer tracked", ucred->pid);
+ continue;
+ }
+
+ if (worker->state == WORKER_KILLING) {
+ worker->state = WORKER_KILLED;
+ (void) kill(worker->pid, SIGTERM);
+ } else if (worker->state != WORKER_KILLED)
+ worker->state = WORKER_IDLE;
+
+ /* worker returned */
+ if (result == EVENT_RESULT_TRY_AGAIN &&
+ event_requeue(worker->event) < 0)
+ udev_broadcast_result(manager->monitor, worker->event->dev, -ETIMEDOUT);
+
+ /* When event_requeue() succeeds, worker->event is NULL, and event_free() handles NULL gracefully. */
+ event_free(worker->event);
+ }
+
+ return 1;
+}
+
+static void manager_set_default_children_max(Manager *manager) {
+ uint64_t cpu_limit, mem_limit, cpu_count = 1;
+ int r;
+
+ assert(manager);
+
+ if (manager->children_max != 0)
+ return;
+
+ r = cpus_in_affinity_mask();
+ if (r < 0)
+ log_warning_errno(r, "Failed to determine number of local CPUs, ignoring: %m");
+ else
+ cpu_count = r;
+
+ cpu_limit = cpu_count * 2 + 16;
+ mem_limit = MAX(physical_memory() / (128*1024*1024), UINT64_C(10));
+
+ manager->children_max = MIN3(cpu_limit, mem_limit, WORKER_NUM_MAX);
+ log_debug("Set children_max to %u", manager->children_max);
+}
+
+/* receive the udevd message from userspace */
+static int on_ctrl_msg(UdevCtrl *uctrl, UdevCtrlMessageType type, const UdevCtrlMessageValue *value, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+ int r;
+
+ assert(value);
+
+ switch (type) {
+ case UDEV_CTRL_SET_LOG_LEVEL:
+ if ((value->intval & LOG_PRIMASK) != value->intval) {
+ log_debug("Received invalid udev control message (SET_LOG_LEVEL, %i), ignoring.", value->intval);
+ break;
+ }
+
+ log_debug("Received udev control message (SET_LOG_LEVEL), setting log_level=%i", value->intval);
+
+ r = log_get_max_level();
+ if (r == value->intval)
+ break;
+
+ log_set_max_level(value->intval);
+ manager->log_level = value->intval;
+ manager_kill_workers(manager, false);
+ break;
+ case UDEV_CTRL_STOP_EXEC_QUEUE:
+ log_debug("Received udev control message (STOP_EXEC_QUEUE)");
+ manager->stop_exec_queue = true;
+ break;
+ case UDEV_CTRL_START_EXEC_QUEUE:
+ log_debug("Received udev control message (START_EXEC_QUEUE)");
+ manager->stop_exec_queue = false;
+ /* It is not necessary to call event_queue_start() here, as it will be called in on_post() if necessary. */
+ break;
+ case UDEV_CTRL_RELOAD:
+ log_debug("Received udev control message (RELOAD)");
+ manager_reload(manager, /* force = */ true);
+ break;
+ case UDEV_CTRL_SET_ENV: {
+ _unused_ _cleanup_free_ char *old_val = NULL, *old_key = NULL;
+ _cleanup_free_ char *key = NULL, *val = NULL;
+ const char *eq;
+
+ eq = strchr(value->buf, '=');
+ if (!eq) {
+ log_error("Invalid key format '%s'", value->buf);
+ return 1;
+ }
+
+ key = strndup(value->buf, eq - value->buf);
+ if (!key) {
+ log_oom();
+ return 1;
+ }
+
+ old_val = hashmap_remove2(manager->properties, key, (void **) &old_key);
+
+ r = hashmap_ensure_allocated(&manager->properties, &string_hash_ops);
+ if (r < 0) {
+ log_oom();
+ return 1;
+ }
+
+ eq++;
+ if (isempty(eq))
+ log_debug("Received udev control message (ENV), unsetting '%s'", key);
+ else {
+ val = strdup(eq);
+ if (!val) {
+ log_oom();
+ return 1;
+ }
+
+ log_debug("Received udev control message (ENV), setting '%s=%s'", key, val);
+
+ r = hashmap_put(manager->properties, key, val);
+ if (r < 0) {
+ log_oom();
+ return 1;
+ }
+ }
+
+ key = val = NULL;
+ manager_kill_workers(manager, false);
+ break;
+ }
+ case UDEV_CTRL_SET_CHILDREN_MAX:
+ if (value->intval < 0) {
+ log_debug("Received invalid udev control message (SET_MAX_CHILDREN, %i), ignoring.", value->intval);
+ return 0;
+ }
+
+ log_debug("Received udev control message (SET_MAX_CHILDREN), setting children_max=%i", value->intval);
+ manager->children_max = value->intval;
+
+ /* When 0 is specified, determine the maximum based on the system resources. */
+ manager_set_default_children_max(manager);
+
+ notify_ready(manager);
+ break;
+ case UDEV_CTRL_PING:
+ log_debug("Received udev control message (PING)");
+ break;
+ case UDEV_CTRL_EXIT:
+ log_debug("Received udev control message (EXIT)");
+ manager_exit(manager);
+ break;
+ default:
+ log_debug("Received unknown udev control message, ignoring");
+ }
+
+ return 1;
+}
+
+static int synthesize_change_one(sd_device *dev, sd_device *target) {
+ int r;
+
+ if (DEBUG_LOGGING) {
+ const char *syspath = NULL;
+ (void) sd_device_get_syspath(target, &syspath);
+ log_device_debug(dev, "device is closed, synthesising 'change' on %s", strna(syspath));
+ }
+
+ r = sd_device_trigger(target, SD_DEVICE_CHANGE);
+ if (r < 0)
+ return log_device_debug_errno(target, r, "Failed to trigger 'change' uevent: %m");
+
+ DEVICE_TRACE_POINT(synthetic_change_event, dev);
+
+ return 0;
+}
+
+static int synthesize_change(sd_device *dev) {
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ bool part_table_read;
+ const char *sysname;
+ int r, k;
+
+ r = sd_device_get_sysname(dev, &sysname);
+ if (r < 0)
+ return r;
+
+ if (startswith(sysname, "dm-") || block_device_is_whole_disk(dev) <= 0)
+ return synthesize_change_one(dev, dev);
+
+ r = blockdev_reread_partition_table(dev);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to re-read partition table, ignoring: %m");
+ part_table_read = r >= 0;
+
+ /* search for partitions */
+ r = partition_enumerator_new(dev, &e);
+ if (r < 0)
+ return r;
+
+ /* We have partitions and re-read the table, the kernel already sent out a "change"
+ * event for the disk, and "remove/add" for all partitions. */
+ if (part_table_read && sd_device_enumerator_get_device_first(e))
+ return 0;
+
+ /* We have partitions but re-reading the partition table did not work, synthesize
+ * "change" for the disk and all partitions. */
+ r = synthesize_change_one(dev, dev);
+ FOREACH_DEVICE(e, d) {
+ k = synthesize_change_one(dev, d);
+ if (k < 0 && r >= 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int on_inotify(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+ union inotify_event_buffer buffer;
+ ssize_t l;
+ int r;
+
+ l = read(fd, &buffer, sizeof(buffer));
+ if (l < 0) {
+ if (ERRNO_IS_TRANSIENT(errno))
+ return 0;
+
+ return log_error_errno(errno, "Failed to read inotify fd: %m");
+ }
+
+ FOREACH_INOTIFY_EVENT_WARN(e, buffer, l) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ const char *devnode;
+
+ /* Do not handle IN_IGNORED here. Especially, do not try to call udev_watch_end() from the
+ * main process. Otherwise, the pair of the symlinks may become inconsistent, and several
+ * garbage may remain. The old symlinks are removed by a worker that processes the
+ * corresponding 'remove' uevent;
+ * udev_event_execute_rules() -> event_execute_rules_on_remove() -> udev_watch_end(). */
+
+ if (!FLAGS_SET(e->mask, IN_CLOSE_WRITE))
+ continue;
+
+ r = device_new_from_watch_handle(&dev, e->wd);
+ if (r < 0) {
+ /* Device may be removed just after closed. */
+ log_debug_errno(r, "Failed to create sd_device object from watch handle, ignoring: %m");
+ continue;
+ }
+
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0) {
+ /* Also here, device may be already removed. */
+ log_device_debug_errno(dev, r, "Failed to get device node, ignoring: %m");
+ continue;
+ }
+
+ log_device_debug(dev, "Received inotify event for %s.", devnode);
+
+ (void) event_queue_assume_block_device_unlocked(manager, dev);
+ (void) synthesize_change(dev);
+ }
+
+ return 0;
+}
+
+static int on_sigterm(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+
+ manager_exit(manager);
+
+ return 1;
+}
+
+static int on_sighup(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+
+ manager_reload(manager, /* force = */ true);
+
+ return 1;
+}
+
+static int on_sigchld(sd_event_source *s, const siginfo_t *si, void *userdata) {
+ Worker *worker = ASSERT_PTR(userdata);
+ Manager *manager = ASSERT_PTR(worker->manager);
+ sd_device *dev = worker->event ? ASSERT_PTR(worker->event->dev) : NULL;
+ EventResult result;
+
+ assert(si);
+
+ switch (si->si_code) {
+ case CLD_EXITED:
+ if (si->si_status == 0)
+ log_device_debug(dev, "Worker ["PID_FMT"] exited.", si->si_pid);
+ else
+ log_device_warning(dev, "Worker ["PID_FMT"] exited with return code %i.",
+ si->si_pid, si->si_status);
+ result = EVENT_RESULT_EXIT_STATUS_BASE + si->si_status;
+ break;
+
+ case CLD_KILLED:
+ case CLD_DUMPED:
+ log_device_warning(dev, "Worker ["PID_FMT"] terminated by signal %i (%s).",
+ si->si_pid, si->si_status, signal_to_string(si->si_status));
+ result = EVENT_RESULT_SIGNAL_BASE + si->si_status;
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (result != EVENT_RESULT_SUCCESS && dev) {
+ /* delete state from disk */
+ device_delete_db(dev);
+ device_tag_index(dev, NULL, false);
+
+ /* Forward kernel event to libudev listeners */
+ udev_broadcast_result(manager->monitor, dev, result);
+ }
+
+ worker_free(worker);
+
+ return 1;
+}
+
+static int on_post(sd_event_source *s, void *userdata) {
+ Manager *manager = ASSERT_PTR(userdata);
+
+ if (manager->events) {
+ /* Try to process pending events if idle workers exist. Why is this necessary?
+ * When a worker finished an event and became idle, even if there was a pending event,
+ * the corresponding device might have been locked and the processing of the event
+ * delayed for a while, preventing the worker from processing the event immediately.
+ * Now, the device may be unlocked. Let's try again! */
+ event_queue_start(manager);
+ return 1;
+ }
+
+ /* There are no queued events. Let's remove /run/udev/queue and clean up the idle processes. */
+
+ if (unlink("/run/udev/queue") < 0) {
+ if (errno != ENOENT)
+ log_warning_errno(errno, "Failed to unlink /run/udev/queue, ignoring: %m");
+ } else
+ log_debug("No events are queued, removing /run/udev/queue.");
+
+ if (!hashmap_isempty(manager->workers)) {
+ /* There are idle workers */
+ (void) event_reset_time_relative(manager->event, &manager->kill_workers_event,
+ CLOCK_MONOTONIC, 3 * USEC_PER_SEC, USEC_PER_SEC,
+ on_kill_workers_event, manager,
+ 0, "kill-workers-event", false);
+ return 1;
+ }
+
+ /* There are no idle workers. */
+
+ if (manager->udev_node_needs_cleanup) {
+ (void) udev_node_cleanup();
+ manager->udev_node_needs_cleanup = false;
+ }
+
+ if (manager->exit)
+ return sd_event_exit(manager->event, 0);
+
+ if (manager->cgroup)
+ /* cleanup possible left-over processes in our cgroup */
+ (void) cg_kill(manager->cgroup, SIGKILL, CGROUP_IGNORE_SELF, /* set=*/ NULL, /* kill_log= */ NULL, /* userdata= */ NULL);
+
+ return 1;
+}
+
+Manager* manager_new(void) {
+ Manager *manager;
+
+ manager = new(Manager, 1);
+ if (!manager)
+ return NULL;
+
+ *manager = (Manager) {
+ .inotify_fd = -EBADF,
+ .worker_watch = EBADF_PAIR,
+ .log_level = LOG_INFO,
+ .resolve_name_timing = RESOLVE_NAME_EARLY,
+ .timeout_usec = 180 * USEC_PER_SEC,
+ .timeout_signal = SIGKILL,
+ };
+
+ return manager;
+}
+
+int manager_init(Manager *manager, int fd_ctrl, int fd_uevent) {
+ _cleanup_free_ char *cgroup = NULL;
+ int r;
+
+ assert(manager);
+
+ r = udev_ctrl_new_from_fd(&manager->ctrl, fd_ctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize udev control socket: %m");
+
+ r = udev_ctrl_enable_receiving(manager->ctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind udev control socket: %m");
+
+ r = device_monitor_new_full(&manager->monitor, MONITOR_GROUP_KERNEL, fd_uevent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize device monitor: %m");
+
+ (void) sd_device_monitor_set_description(manager->monitor, "manager");
+
+ r = device_monitor_enable_receiving(manager->monitor);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind netlink socket: %m");
+
+ manager->log_level = log_get_max_level();
+
+ r = cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, 0, &cgroup);
+ if (r < 0)
+ log_debug_errno(r, "Failed to get cgroup, ignoring: %m");
+ else if (endswith(cgroup, "/udev")) { /* If we are in a subcgroup /udev/ we assume it was delegated to us */
+ log_debug("Running in delegated subcgroup '%s'.", cgroup);
+ manager->cgroup = TAKE_PTR(cgroup);
+ }
+
+ return 0;
+}
+
+int manager_main(Manager *manager) {
+ int fd_worker, r;
+
+ manager_set_default_children_max(manager);
+
+ /* unnamed socket from workers to the main daemon */
+ r = socketpair(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0, manager->worker_watch);
+ if (r < 0)
+ return log_error_errno(errno, "Failed to create socketpair for communicating with workers: %m");
+
+ fd_worker = manager->worker_watch[READ_END];
+
+ r = setsockopt_int(fd_worker, SOL_SOCKET, SO_PASSCRED, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable SO_PASSCRED: %m");
+
+ manager->inotify_fd = inotify_init1(IN_CLOEXEC);
+ if (manager->inotify_fd < 0)
+ return log_error_errno(errno, "Failed to create inotify descriptor: %m");
+
+ udev_watch_restore(manager->inotify_fd);
+
+ /* block and listen to all signals on signalfd */
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, SIGHUP, SIGCHLD, SIGRTMIN+18, -1) >= 0);
+
+ r = sd_event_default(&manager->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ r = sd_event_add_signal(manager->event, NULL, SIGINT, on_sigterm, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create SIGINT event source: %m");
+
+ r = sd_event_add_signal(manager->event, NULL, SIGTERM, on_sigterm, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create SIGTERM event source: %m");
+
+ r = sd_event_add_signal(manager->event, NULL, SIGHUP, on_sighup, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create SIGHUP event source: %m");
+
+ r = sd_event_set_watchdog(manager->event, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create watchdog event source: %m");
+
+ r = udev_ctrl_attach_event(manager->ctrl, manager->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach event to udev control: %m");
+
+ r = udev_ctrl_start(manager->ctrl, on_ctrl_msg, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start udev control: %m");
+
+ /* This needs to be after the inotify and uevent handling, to make sure
+ * that the ping is send back after fully processing the pending uevents
+ * (including the synthetic ones we may create due to inotify events).
+ */
+ r = sd_event_source_set_priority(udev_ctrl_get_event_source(manager->ctrl), SD_EVENT_PRIORITY_IDLE);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set IDLE event priority for udev control event source: %m");
+
+ r = sd_event_add_io(manager->event, &manager->inotify_event, manager->inotify_fd, EPOLLIN, on_inotify, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create inotify event source: %m");
+
+ r = sd_device_monitor_attach_event(manager->monitor, manager->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach event to device monitor: %m");
+
+ r = sd_device_monitor_start(manager->monitor, on_uevent, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+
+ r = sd_event_add_io(manager->event, NULL, fd_worker, EPOLLIN, on_worker, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create worker event source: %m");
+
+ r = sd_event_add_post(manager->event, NULL, on_post, manager);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create post event source: %m");
+
+ /* Eventually, we probably want to do more here on memory pressure, for example, kill idle workers immediately */
+ r = sd_event_add_memory_pressure(manager->event, &manager->memory_pressure_event_source, NULL, NULL);
+ if (r < 0)
+ log_full_errno(ERRNO_IS_NOT_SUPPORTED(r) || ERRNO_IS_PRIVILEGE(r) || (r == -EHOSTDOWN) ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to allocate memory pressure watch, ignoring: %m");
+
+ r = sd_event_add_signal(manager->event, &manager->memory_pressure_event_source, SIGRTMIN+18, sigrtmin18_handler, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate SIGRTMIN+18 event source, ignoring: %m");
+
+ manager->last_usec = now(CLOCK_MONOTONIC);
+
+ udev_builtin_init();
+
+ r = udev_rules_load(&manager->rules, manager->resolve_name_timing);
+ if (r < 0)
+ return log_error_errno(r, "Failed to read udev rules: %m");
+
+ r = udev_rules_apply_static_dev_perms(manager->rules);
+ if (r < 0)
+ log_warning_errno(r, "Failed to apply permissions on static device nodes, ignoring: %m");
+
+ notify_ready(manager);
+
+ r = sd_event_loop(manager->event);
+ if (r < 0)
+ log_error_errno(r, "Event loop failed: %m");
+
+ (void) sd_notify(/* unset= */ false, NOTIFY_STOPPING);
+ return r;
+}
diff --git a/src/udev/udev-manager.h b/src/udev/udev-manager.h
new file mode 100644
index 0000000..afbc67f
--- /dev/null
+++ b/src/udev/udev-manager.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sd-device.h"
+#include "sd-event.h"
+
+#include "hashmap.h"
+#include "macro.h"
+#include "time-util.h"
+#include "udev-ctrl.h"
+#include "udev-rules.h"
+
+typedef struct Event Event;
+typedef struct Worker Worker;
+
+typedef struct Manager {
+ sd_event *event;
+ Hashmap *workers;
+ LIST_HEAD(Event, events);
+ char *cgroup;
+ int log_level;
+
+ UdevRules *rules;
+ Hashmap *properties;
+
+ sd_device_monitor *monitor;
+ UdevCtrl *ctrl;
+ int worker_watch[2];
+
+ /* used by udev-watch */
+ int inotify_fd;
+ sd_event_source *inotify_event;
+
+ sd_event_source *kill_workers_event;
+
+ sd_event_source *memory_pressure_event_source;
+ sd_event_source *sigrtmin18_event_source;
+
+ usec_t last_usec;
+
+ ResolveNameTiming resolve_name_timing;
+ unsigned children_max;
+ usec_t exec_delay_usec;
+ usec_t timeout_usec;
+ int timeout_signal;
+ bool blockdev_read_only;
+
+ bool udev_node_needs_cleanup;
+ bool stop_exec_queue;
+ bool exit;
+} Manager;
+
+Manager* manager_new(void);
+Manager* manager_free(Manager *manager);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
+
+int manager_init(Manager *manager, int fd_ctrl, int fd_uevent);
+int manager_main(Manager *manager);
+
+bool devpath_conflict(const char *a, const char *b);
diff --git a/src/udev/udev-node.c b/src/udev/udev-node.c
new file mode 100644
index 0000000..e12c26c
--- /dev/null
+++ b/src/udev/udev-node.c
@@ -0,0 +1,790 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <sys/file.h>
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "devnum-util.h"
+#include "dirent-util.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "hexdecoct.h"
+#include "label-util.h"
+#include "mkdir-label.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "selinux-util.h"
+#include "smack-util.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "udev-node.h"
+#include "user-util.h"
+
+#define UDEV_NODE_HASH_KEY SD_ID128_MAKE(b9,6a,f1,ce,40,31,44,1a,9e,19,ec,8b,ae,f3,e3,2f)
+
+int udev_node_cleanup(void) {
+ _cleanup_closedir_ DIR *dir = NULL;
+
+ /* This must not be called when any workers exist. It would cause a race between mkdir() called
+ * by stack_directory_lock() and unlinkat() called by this. */
+
+ dir = opendir("/run/udev/links");
+ if (!dir) {
+ if (errno == ENOENT)
+ return 0;
+
+ return log_debug_errno(errno, "Failed to open directory '/run/udev/links', ignoring: %m");
+ }
+
+ FOREACH_DIRENT_ALL(de, dir, break) {
+ _cleanup_free_ char *lockfile = NULL;
+
+ if (de->d_name[0] == '.')
+ continue;
+
+ if (de->d_type != DT_DIR)
+ continue;
+
+ /* As commented in the above, this is called when no worker exists, hence the file is not
+ * locked. On a later uevent, the lock file will be created if necessary. So, we can safely
+ * remove the file now. */
+ lockfile = path_join(de->d_name, ".lock");
+ if (!lockfile)
+ return log_oom_debug();
+
+ if (unlinkat(dirfd(dir), lockfile, 0) < 0 && errno != ENOENT) {
+ log_debug_errno(errno, "Failed to remove '/run/udev/links/%s', ignoring: %m", lockfile);
+ continue;
+ }
+
+ if (unlinkat(dirfd(dir), de->d_name, AT_REMOVEDIR) < 0 && errno != ENOTEMPTY)
+ log_debug_errno(errno, "Failed to remove '/run/udev/links/%s', ignoring: %m", de->d_name);
+ }
+
+ return 0;
+}
+
+static int node_symlink(sd_device *dev, const char *devnode, const char *slink) {
+ struct stat st;
+ int r;
+
+ assert(dev);
+ assert(slink);
+
+ if (!devnode) {
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device node: %m");
+ }
+
+ if (lstat(slink, &st) >= 0) {
+ if (!S_ISLNK(st.st_mode))
+ return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EEXIST),
+ "Conflicting inode '%s' found, symlink to '%s' will not be created.",
+ slink, devnode);
+ } else if (errno != ENOENT)
+ return log_device_debug_errno(dev, errno, "Failed to lstat() '%s': %m", slink);
+
+ r = mkdir_parents_label(slink, 0755);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to create parent directory of '%s': %m", slink);
+
+ /* use relative link */
+ r = symlink_atomic_full_label(devnode, slink, /* make_relative = */ true);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to create symlink '%s' to '%s': %m", slink, devnode);
+
+ log_device_debug(dev, "Successfully created symlink '%s' to '%s'", slink, devnode);
+ return 0;
+}
+
+static int stack_directory_read_one(int dirfd, const char *id, char **devnode, int *priority) {
+ _cleanup_free_ char *buf = NULL;
+ int tmp_prio, r;
+
+ assert(dirfd >= 0);
+ assert(id);
+ assert(priority);
+
+ /* This reads priority and device node from the symlink under /run/udev/links (or udev database).
+ * If 'devnode' is NULL, obtained priority is always set to '*priority'. If 'devnode' is non-NULL,
+ * this updates '*devnode' and '*priority'. */
+
+ /* First, let's try to read the entry with the new format, which should replace the old format pretty
+ * quickly. */
+ r = readlinkat_malloc(dirfd, id, &buf);
+ if (r >= 0) {
+ char *colon;
+
+ /* With the new format, the devnode and priority can be obtained from symlink itself. */
+
+ colon = strchr(buf, ':');
+ if (!colon || colon == buf)
+ return -EINVAL;
+
+ *colon = '\0';
+
+ /* Of course, this check is racy, but it is not necessary to be perfect. Even if the device
+ * node will be removed after this check, we will receive 'remove' uevent, and the invalid
+ * symlink will be removed during processing the event. The check is just for shortening the
+ * timespan that the symlink points to a non-existing device node. */
+ if (access(colon + 1, F_OK) < 0)
+ return -ENODEV;
+
+ r = safe_atoi(buf, &tmp_prio);
+ if (r < 0)
+ return r;
+
+ if (!devnode)
+ goto finalize;
+
+ if (*devnode && tmp_prio <= *priority)
+ return 0; /* Unchanged */
+
+ r = free_and_strdup(devnode, colon + 1);
+ if (r < 0)
+ return r;
+
+ } else if (r == -EINVAL) { /* Not a symlink ? try the old format */
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ const char *val;
+
+ /* Old format. The devnode and priority must be obtained from uevent and udev database. */
+
+ r = sd_device_new_from_device_id(&dev, id);
+ if (r < 0)
+ return r;
+
+ r = device_get_devlink_priority(dev, &tmp_prio);
+ if (r < 0)
+ return r;
+
+ if (!devnode)
+ goto finalize;
+
+ if (*devnode && tmp_prio <= *priority)
+ return 0; /* Unchanged */
+
+ r = sd_device_get_devname(dev, &val);
+ if (r < 0)
+ return r;
+
+ r = free_and_strdup(devnode, val);
+ if (r < 0)
+ return r;
+
+ } else
+ return r == -ENOENT ? -ENODEV : r;
+
+finalize:
+ *priority = tmp_prio;
+ return 1; /* Updated */
+}
+
+static int stack_directory_find_prioritized_devnode(sd_device *dev, int dirfd, bool add, char **ret) {
+ _cleanup_closedir_ DIR *dir = NULL;
+ _cleanup_free_ char *devnode = NULL;
+ int r, priority;
+ const char *id;
+
+ assert(dev);
+ assert(dirfd >= 0);
+ assert(ret);
+
+ /* Find device node of device with highest priority. This returns 1 if a device found, 0 if no
+ * device found, or a negative errno on error. */
+
+ if (add) {
+ const char *n;
+
+ r = device_get_devlink_priority(dev, &priority);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_devname(dev, &n);
+ if (r < 0)
+ return r;
+
+ devnode = strdup(n);
+ if (!devnode)
+ return -ENOMEM;
+ }
+
+ dir = xopendirat(dirfd, ".", O_NOFOLLOW);
+ if (!dir)
+ return -errno;
+
+ r = device_get_device_id(dev, &id);
+ if (r < 0)
+ return r;
+
+ FOREACH_DIRENT(de, dir, break) {
+
+ /* skip ourself */
+ if (streq(de->d_name, id))
+ continue;
+
+ r = stack_directory_read_one(dirfd, de->d_name, &devnode, &priority);
+ if (r < 0 && r != -ENODEV)
+ log_debug_errno(r, "Failed to read '%s', ignoring: %m", de->d_name);
+ }
+
+ *ret = TAKE_PTR(devnode);
+ return !!*ret;
+}
+
+static int stack_directory_update(sd_device *dev, int fd, bool add) {
+ const char *id;
+ int r;
+
+ assert(dev);
+ assert(fd >= 0);
+
+ r = device_get_device_id(dev, &id);
+ if (r < 0)
+ return r;
+
+ if (add) {
+ _cleanup_free_ char *data = NULL, *buf = NULL;
+ const char *devname;
+ int priority;
+
+ r = sd_device_get_devname(dev, &devname);
+ if (r < 0)
+ return r;
+
+ r = device_get_devlink_priority(dev, &priority);
+ if (r < 0)
+ return r;
+
+ if (asprintf(&data, "%i:%s", priority, devname) < 0)
+ return -ENOMEM;
+
+ if (readlinkat_malloc(fd, id, &buf) >= 0 && streq(buf, data))
+ return 0; /* Unchanged. */
+
+ (void) unlinkat(fd, id, 0);
+
+ if (symlinkat(data, fd, id) < 0)
+ return -errno;
+
+ } else {
+ if (unlinkat(fd, id, 0) < 0) {
+ if (errno == ENOENT)
+ return 0; /* Unchanged. */
+ return -errno;
+ }
+ }
+
+ return 1; /* Updated. */
+}
+
+size_t udev_node_escape_path(const char *src, char *dest, size_t size) {
+ size_t i, j;
+ uint64_t h;
+
+ assert(src);
+ assert(dest);
+ assert(size >= 12);
+
+ for (i = 0, j = 0; src[i] != '\0'; i++) {
+ if (src[i] == '/') {
+ if (j+4 >= size - 12 + 1)
+ goto toolong;
+ memcpy(&dest[j], "\\x2f", 4);
+ j += 4;
+ } else if (src[i] == '\\') {
+ if (j+4 >= size - 12 + 1)
+ goto toolong;
+ memcpy(&dest[j], "\\x5c", 4);
+ j += 4;
+ } else {
+ if (j+1 >= size - 12 + 1)
+ goto toolong;
+ dest[j] = src[i];
+ j++;
+ }
+ }
+ dest[j] = '\0';
+ return j;
+
+toolong:
+ /* If the input path is too long to encode as a filename, then let's suffix with a string
+ * generated from the hash of the path. */
+
+ h = siphash24_string(src, UDEV_NODE_HASH_KEY.bytes);
+
+ for (unsigned k = 0; k <= 10; k++)
+ dest[size - k - 2] = urlsafe_base64char((h >> (k * 6)) & 63);
+
+ dest[size - 1] = '\0';
+ return size - 1;
+}
+
+static int stack_directory_get_name(const char *slink, char **ret) {
+ _cleanup_free_ char *s = NULL, *dirname = NULL;
+ char name_enc[NAME_MAX+1];
+ const char *name;
+ int r;
+
+ assert(slink);
+ assert(ret);
+
+ r = path_simplify_alloc(slink, &s);
+ if (r < 0)
+ return r;
+
+ if (!path_is_normalized(s))
+ return -EINVAL;
+
+ name = path_startswith(s, "/dev");
+ if (empty_or_root(name))
+ return -EINVAL;
+
+ udev_node_escape_path(name, name_enc, sizeof(name_enc));
+
+ dirname = path_join("/run/udev/links", name_enc);
+ if (!dirname)
+ return -ENOMEM;
+
+ *ret = TAKE_PTR(dirname);
+ return 0;
+}
+
+static int stack_directory_open(sd_device *dev, const char *slink, int *ret_dirfd, int *ret_lockfd) {
+ _cleanup_close_ int dirfd = -EBADF, lockfd = -EBADF;
+ _cleanup_free_ char *dirname = NULL;
+ int r;
+
+ assert(dev);
+ assert(slink);
+ assert(ret_dirfd);
+ assert(ret_lockfd);
+
+ r = stack_directory_get_name(slink, &dirname);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to build stack directory name for '%s': %m", slink);
+
+ r = mkdir_parents(dirname, 0755);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to create stack directory '%s': %m", dirname);
+
+ dirfd = open_mkdir_at(AT_FDCWD, dirname, O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_RDONLY, 0755);
+ if (dirfd < 0)
+ return log_device_debug_errno(dev, dirfd, "Failed to open stack directory '%s': %m", dirname);
+
+ lockfd = openat(dirfd, ".lock", O_CLOEXEC | O_NOFOLLOW | O_RDONLY | O_CREAT, 0600);
+ if (lockfd < 0)
+ return log_device_debug_errno(dev, errno, "Failed to create lock file for stack directory '%s': %m", dirname);
+
+ if (flock(lockfd, LOCK_EX) < 0)
+ return log_device_debug_errno(dev, errno, "Failed to place a lock on lock file for %s: %m", dirname);
+
+ *ret_dirfd = TAKE_FD(dirfd);
+ *ret_lockfd = TAKE_FD(lockfd);
+ return 0;
+}
+
+static int node_get_current(const char *slink, int dirfd, char **ret_id, int *ret_prio) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ _cleanup_free_ char *id_dup = NULL;
+ const char *id;
+ int r;
+
+ assert(slink);
+ assert(dirfd >= 0);
+ assert(ret_id);
+
+ r = sd_device_new_from_devname(&dev, slink);
+ if (r < 0)
+ return r;
+
+ r = device_get_device_id(dev, &id);
+ if (r < 0)
+ return r;
+
+ id_dup = strdup(id);
+ if (!id_dup)
+ return -ENOMEM;
+
+ if (ret_prio) {
+ r = stack_directory_read_one(dirfd, id, NULL, ret_prio);
+ if (r < 0)
+ return r;
+ }
+
+ *ret_id = TAKE_PTR(id_dup);
+ return 0;
+}
+
+static int link_update(sd_device *dev, const char *slink, bool add) {
+ _cleanup_free_ char *current_id = NULL, *devnode = NULL;
+ _cleanup_close_ int dirfd = -EBADF, lockfd = -EBADF;
+ int r, current_prio;
+
+ assert(dev);
+ assert(slink);
+
+ r = stack_directory_open(dev, slink, &dirfd, &lockfd);
+ if (r < 0)
+ return r;
+
+ r = node_get_current(slink, dirfd, &current_id, add ? &current_prio : NULL);
+ if (r < 0 && !ERRNO_IS_DEVICE_ABSENT(r))
+ return log_device_debug_errno(dev, r, "Failed to get the current device node priority for '%s': %m", slink);
+
+ r = stack_directory_update(dev, dirfd, add);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to update stack directory for '%s': %m", slink);
+
+ if (current_id) {
+ const char *id;
+
+ r = device_get_device_id(dev, &id);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device id: %m");
+
+ if (add) {
+ int prio;
+
+ r = device_get_devlink_priority(dev, &prio);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get devlink priority: %m");
+
+ if (streq(current_id, id)) {
+ if (current_prio <= prio)
+ /* The devlink is ours and already exists, and the new priority is
+ * equal or higher than the previous. Hence, it is not necessary to
+ * recreate it. */
+ return 0;
+
+ /* The devlink priority is downgraded. Another device may have a higher
+ * priority now. Let's find the device node with the highest priority. */
+ } else {
+ if (current_prio > prio)
+ /* The devlink with a higher priority already exists and is owned by
+ * another device. Hence, it is not necessary to recreate it. */
+ return 0;
+
+ /* This device has the equal or a higher priority than the current. Let's
+ * create the devlink to our device node. */
+ return node_symlink(dev, NULL, slink);
+ }
+
+ } else {
+ if (!streq(current_id, id))
+ /* The devlink already exists and is owned by another device. Hence, it is
+ * not necessary to recreate it. */
+ return 0;
+
+ /* The current devlink is ours, and the target device will be removed. Hence, we need
+ * to search the device that has the highest priority. and update the devlink. */
+ }
+ } else {
+ /* The requested devlink does not exist, or the target device does not exist and the devlink
+ * points to a non-existing device. Let's search the device that has the highest priority,
+ * and update the devlink. */
+ ;
+ }
+
+ r = stack_directory_find_prioritized_devnode(dev, dirfd, add, &devnode);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to determine device node with the highest priority for '%s': %m", slink);
+ if (r > 0)
+ return node_symlink(dev, devnode, slink);
+
+ log_device_debug(dev, "No reference left for '%s', removing", slink);
+
+ if (unlink(slink) < 0 && errno != ENOENT)
+ log_device_debug_errno(dev, errno, "Failed to remove '%s', ignoring: %m", slink);
+
+ (void) rmdir_parents(slink, "/dev");
+
+ return 0;
+}
+
+static int device_get_devpath_by_devnum(sd_device *dev, char **ret) {
+ const char *subsystem;
+ dev_t devnum;
+ int r;
+
+ assert(dev);
+ assert(ret);
+
+ r = sd_device_get_subsystem(dev, &subsystem);
+ if (r < 0)
+ return r;
+
+ r = sd_device_get_devnum(dev, &devnum);
+ if (r < 0)
+ return r;
+
+ return device_path_make_major_minor(streq(subsystem, "block") ? S_IFBLK : S_IFCHR, devnum, ret);
+}
+
+int udev_node_update(sd_device *dev, sd_device *dev_old) {
+ _cleanup_free_ char *filename = NULL;
+ int r;
+
+ assert(dev);
+ assert(dev_old);
+
+ /* update possible left-over symlinks */
+ FOREACH_DEVICE_DEVLINK(dev_old, devlink) {
+ /* check if old link name still belongs to this device */
+ if (device_has_devlink(dev, devlink))
+ continue;
+
+ log_device_debug(dev,
+ "Removing/updating old device symlink '%s', which is no longer belonging to this device.",
+ devlink);
+
+ r = link_update(dev, devlink, /* add = */ false);
+ if (r < 0)
+ log_device_warning_errno(dev, r,
+ "Failed to remove/update device symlink '%s', ignoring: %m",
+ devlink);
+ }
+
+ /* create/update symlinks, add symlinks to name index */
+ FOREACH_DEVICE_DEVLINK(dev, devlink) {
+ r = link_update(dev, devlink, /* add = */ true);
+ if (r < 0)
+ log_device_warning_errno(dev, r,
+ "Failed to create/update device symlink '%s', ignoring: %m",
+ devlink);
+ }
+
+ r = device_get_devpath_by_devnum(dev, &filename);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device path: %m");
+
+ /* always add /dev/{block,char}/$major:$minor */
+ r = node_symlink(dev, NULL, filename);
+ if (r < 0)
+ return log_device_warning_errno(dev, r, "Failed to create device symlink '%s': %m", filename);
+
+ return 0;
+}
+
+int udev_node_remove(sd_device *dev) {
+ _cleanup_free_ char *filename = NULL;
+ int r;
+
+ assert(dev);
+
+ /* remove/update symlinks, remove symlinks from name index */
+ FOREACH_DEVICE_DEVLINK(dev, devlink) {
+ r = link_update(dev, devlink, /* add = */ false);
+ if (r < 0)
+ log_device_warning_errno(dev, r,
+ "Failed to remove/update device symlink '%s', ignoring: %m",
+ devlink);
+ }
+
+ r = device_get_devpath_by_devnum(dev, &filename);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device path: %m");
+
+ /* remove /dev/{block,char}/$major:$minor */
+ if (unlink(filename) < 0 && errno != ENOENT)
+ return log_device_debug_errno(dev, errno, "Failed to remove '%s': %m", filename);
+
+ return 0;
+}
+
+static int udev_node_apply_permissions_impl(
+ sd_device *dev, /* can be NULL, only used for logging. */
+ int node_fd,
+ const char *devnode,
+ bool apply_mac,
+ mode_t mode,
+ uid_t uid,
+ gid_t gid,
+ OrderedHashmap *seclabel_list) {
+
+ bool apply_mode, apply_uid, apply_gid;
+ struct stat stats;
+ int r;
+
+ assert(node_fd >= 0);
+ assert(devnode);
+
+ if (fstat(node_fd, &stats) < 0)
+ return log_device_debug_errno(dev, errno, "cannot stat() node %s: %m", devnode);
+
+ /* If group is set, but mode is not set, "upgrade" mode for the group. */
+ if (mode == MODE_INVALID && gid_is_valid(gid) && gid > 0)
+ mode = 0660;
+
+ apply_mode = mode != MODE_INVALID && (stats.st_mode & 0777) != (mode & 0777);
+ apply_uid = uid_is_valid(uid) && stats.st_uid != uid;
+ apply_gid = gid_is_valid(gid) && stats.st_gid != gid;
+
+ if (apply_mode || apply_uid || apply_gid || apply_mac) {
+ bool selinux = false, smack = false;
+ const char *name, *label;
+
+ if (apply_mode || apply_uid || apply_gid) {
+ log_device_debug(dev, "Setting permissions %s, uid=" UID_FMT ", gid=" GID_FMT ", mode=%#o",
+ devnode,
+ uid_is_valid(uid) ? uid : stats.st_uid,
+ gid_is_valid(gid) ? gid : stats.st_gid,
+ mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777);
+
+ r = fchmod_and_chown(node_fd, mode, uid, gid);
+ if (r < 0)
+ log_device_full_errno(dev, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r,
+ "Failed to set owner/mode of %s to uid=" UID_FMT
+ ", gid=" GID_FMT ", mode=%#o: %m",
+ devnode,
+ uid_is_valid(uid) ? uid : stats.st_uid,
+ gid_is_valid(gid) ? gid : stats.st_gid,
+ mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777);
+ } else
+ log_device_debug(dev, "Preserve permissions of %s, uid=" UID_FMT ", gid=" GID_FMT ", mode=%#o",
+ devnode,
+ uid_is_valid(uid) ? uid : stats.st_uid,
+ gid_is_valid(gid) ? gid : stats.st_gid,
+ mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777);
+
+ /* apply SECLABEL{$module}=$label */
+ ORDERED_HASHMAP_FOREACH_KEY(label, name, seclabel_list) {
+ int q;
+
+ if (streq(name, "selinux")) {
+ selinux = true;
+
+ q = mac_selinux_apply_fd(node_fd, devnode, label);
+ if (q < 0)
+ log_device_full_errno(dev, q == -ENOENT ? LOG_DEBUG : LOG_ERR, q,
+ "SECLABEL: failed to set SELinux label '%s': %m", label);
+ else
+ log_device_debug(dev, "SECLABEL: set SELinux label '%s'", label);
+
+ } else if (streq(name, "smack")) {
+ smack = true;
+
+ q = mac_smack_apply_fd(node_fd, SMACK_ATTR_ACCESS, label);
+ if (q < 0)
+ log_device_full_errno(dev, q == -ENOENT ? LOG_DEBUG : LOG_ERR, q,
+ "SECLABEL: failed to set SMACK label '%s': %m", label);
+ else
+ log_device_debug(dev, "SECLABEL: set SMACK label '%s'", label);
+
+ } else
+ log_device_error(dev, "SECLABEL: unknown subsystem, ignoring '%s'='%s'", name, label);
+ }
+
+ /* set the defaults */
+ if (!selinux)
+ (void) mac_selinux_fix_full(node_fd, NULL, devnode, LABEL_IGNORE_ENOENT);
+ if (!smack)
+ (void) mac_smack_apply_fd(node_fd, SMACK_ATTR_ACCESS, NULL);
+ }
+
+ /* always update timestamp when we re-use the node, like on media change events */
+ r = futimens_opath(node_fd, NULL);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to adjust timestamp of node %s: %m", devnode);
+
+ return 0;
+}
+
+int udev_node_apply_permissions(
+ sd_device *dev,
+ bool apply_mac,
+ mode_t mode,
+ uid_t uid,
+ gid_t gid,
+ OrderedHashmap *seclabel_list) {
+
+ const char *devnode;
+ _cleanup_close_ int node_fd = -EBADF;
+ int r;
+
+ assert(dev);
+
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get devname: %m");
+
+ node_fd = sd_device_open(dev, O_PATH|O_CLOEXEC);
+ if (node_fd < 0) {
+ if (ERRNO_IS_DEVICE_ABSENT(node_fd)) {
+ log_device_debug_errno(dev, node_fd, "Device node %s is missing, skipping handling.", devnode);
+ return 0; /* This is necessarily racey, so ignore missing the device */
+ }
+
+ return log_device_debug_errno(dev, node_fd, "Cannot open node %s: %m", devnode);
+ }
+
+ return udev_node_apply_permissions_impl(dev, node_fd, devnode, apply_mac, mode, uid, gid, seclabel_list);
+}
+
+int static_node_apply_permissions(
+ const char *name,
+ mode_t mode,
+ uid_t uid,
+ gid_t gid,
+ char **tags) {
+
+ _cleanup_free_ char *unescaped_filename = NULL;
+ _cleanup_close_ int node_fd = -EBADF;
+ const char *devnode;
+ struct stat stats;
+ int r;
+
+ assert(name);
+
+ if (uid == UID_INVALID && gid == GID_INVALID && mode == MODE_INVALID && !tags)
+ return 0;
+
+ devnode = strjoina("/dev/", name);
+
+ node_fd = open(devnode, O_PATH|O_CLOEXEC);
+ if (node_fd < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to open %s: %m", devnode);
+ return 0;
+ }
+
+ if (fstat(node_fd, &stats) < 0)
+ return log_error_errno(errno, "Failed to stat %s: %m", devnode);
+
+ if (!S_ISBLK(stats.st_mode) && !S_ISCHR(stats.st_mode)) {
+ log_warning("%s is neither block nor character device, ignoring.", devnode);
+ return 0;
+ }
+
+ if (!strv_isempty(tags)) {
+ unescaped_filename = xescape(name, "/.");
+ if (!unescaped_filename)
+ return log_oom();
+ }
+
+ /* export the tags to a directory as symlinks, allowing otherwise dead nodes to be tagged */
+ STRV_FOREACH(t, tags) {
+ _cleanup_free_ char *p = NULL;
+
+ p = path_join("/run/udev/static_node-tags/", *t, unescaped_filename);
+ if (!p)
+ return log_oom();
+
+ r = mkdir_parents(p, 0755);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create parent directory for %s: %m", p);
+
+ r = symlink(devnode, p);
+ if (r < 0 && errno != EEXIST)
+ return log_error_errno(errno, "Failed to create symlink %s -> %s: %m", p, devnode);
+ }
+
+ return udev_node_apply_permissions_impl(NULL, node_fd, devnode, false, mode, uid, gid, NULL);
+}
diff --git a/src/udev/udev-node.h b/src/udev/udev-node.h
new file mode 100644
index 0000000..0c545e4
--- /dev/null
+++ b/src/udev/udev-node.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include "sd-device.h"
+
+#include "hashmap.h"
+
+int udev_node_apply_permissions(
+ sd_device *dev,
+ bool apply_mac,
+ mode_t mode,
+ uid_t uid,
+ gid_t gid,
+ OrderedHashmap *seclabel_list);
+int static_node_apply_permissions(
+ const char *name,
+ mode_t mode,
+ uid_t uid,
+ gid_t gid,
+ char **tags);
+
+int udev_node_remove(sd_device *dev);
+int udev_node_update(sd_device *dev, sd_device *dev_old);
+int udev_node_cleanup(void);
+
+size_t udev_node_escape_path(const char *src, char *dest, size_t size);
diff --git a/src/udev/udev-rules.c b/src/udev/udev-rules.c
new file mode 100644
index 0000000..5f12002
--- /dev/null
+++ b/src/udev/udev-rules.c
@@ -0,0 +1,2965 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <ctype.h>
+
+#include "alloc-util.h"
+#include "architecture.h"
+#include "conf-files.h"
+#include "conf-parser.h"
+#include "confidential-virt.h"
+#include "constants.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "dirent-util.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "glob-util.h"
+#include "list.h"
+#include "mkdir.h"
+#include "netif-naming-scheme.h"
+#include "nulstr-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "proc-cmdline.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "strxcpyx.h"
+#include "sysctl-util.h"
+#include "syslog-util.h"
+#include "udev-builtin.h"
+#include "udev-event.h"
+#include "udev-format.h"
+#include "udev-node.h"
+#include "udev-rules.h"
+#include "udev-spawn.h"
+#include "udev-trace.h"
+#include "udev-util.h"
+#include "user-util.h"
+#include "virt.h"
+
+#define RULES_DIRS ((const char* const*) CONF_PATHS_STRV("udev/rules.d"))
+
+typedef enum {
+ OP_MATCH, /* == */
+ OP_NOMATCH, /* != */
+ OP_ADD, /* += */
+ OP_REMOVE, /* -= */
+ OP_ASSIGN, /* = */
+ OP_ASSIGN_FINAL, /* := */
+ _OP_TYPE_MAX,
+ _OP_TYPE_INVALID = -EINVAL,
+} UdevRuleOperatorType;
+
+typedef enum {
+ MATCH_TYPE_EMPTY, /* empty string */
+ MATCH_TYPE_PLAIN, /* no special characters */
+ MATCH_TYPE_PLAIN_WITH_EMPTY, /* no special characters with empty string, e.g., "|foo" */
+ MATCH_TYPE_GLOB, /* shell globs ?,*,[] */
+ MATCH_TYPE_GLOB_WITH_EMPTY, /* shell globs ?,*,[] with empty string, e.g., "|foo*" */
+ MATCH_TYPE_SUBSYSTEM, /* "subsystem", "bus", or "class" */
+ _MATCH_TYPE_MAX,
+ _MATCH_TYPE_INVALID = -EINVAL,
+} UdevRuleMatchType;
+
+typedef enum {
+ SUBST_TYPE_PLAIN, /* no substitution */
+ SUBST_TYPE_FORMAT, /* % or $ */
+ SUBST_TYPE_SUBSYS, /* "[<SUBSYSTEM>/<KERNEL>]<attribute>" format */
+ _SUBST_TYPE_MAX,
+ _SUBST_TYPE_INVALID = -EINVAL,
+} UdevRuleSubstituteType;
+
+typedef enum {
+ /* lvalues which take match or nomatch operator */
+ TK_M_ACTION, /* string, device_get_action() */
+ TK_M_DEVPATH, /* path, sd_device_get_devpath() */
+ TK_M_KERNEL, /* string, sd_device_get_sysname() */
+ TK_M_DEVLINK, /* strv, sd_device_get_devlink_first(), sd_device_get_devlink_next() */
+ TK_M_NAME, /* string, name of network interface */
+ TK_M_ENV, /* string, device property, takes key through attribute */
+ TK_M_CONST, /* string, system-specific hard-coded constant */
+ TK_M_TAG, /* strv, sd_device_get_tag_first(), sd_device_get_tag_next() */
+ TK_M_SUBSYSTEM, /* string, sd_device_get_subsystem() */
+ TK_M_DRIVER, /* string, sd_device_get_driver() */
+ TK_M_ATTR, /* string, takes filename through attribute, sd_device_get_sysattr_value(), udev_resolve_subsys_kernel(), etc. */
+ TK_M_SYSCTL, /* string, takes kernel parameter through attribute */
+
+ /* matches parent parameters */
+ TK_M_PARENTS_KERNEL, /* string */
+ TK_M_PARENTS_SUBSYSTEM, /* string */
+ TK_M_PARENTS_DRIVER, /* string */
+ TK_M_PARENTS_ATTR, /* string */
+ TK_M_PARENTS_TAG, /* strv */
+
+ TK_M_TEST, /* path, optionally mode_t can be specified by attribute, test the existence of a file */
+ TK_M_PROGRAM, /* string, execute a program */
+ TK_M_IMPORT_FILE, /* path */
+ TK_M_IMPORT_PROGRAM, /* string, import properties from the result of program */
+ TK_M_IMPORT_BUILTIN, /* string, import properties from the result of built-in command */
+ TK_M_IMPORT_DB, /* string, import properties from database */
+ TK_M_IMPORT_CMDLINE, /* string, kernel command line */
+ TK_M_IMPORT_PARENT, /* string, parent property */
+ TK_M_RESULT, /* string, result of TK_M_PROGRAM */
+
+#define _TK_M_MAX (TK_M_RESULT + 1)
+#define _TK_A_MIN _TK_M_MAX
+
+ /* lvalues which take one of assign operators */
+ TK_A_OPTIONS_STRING_ESCAPE_NONE, /* no argument */
+ TK_A_OPTIONS_STRING_ESCAPE_REPLACE, /* no argument */
+ TK_A_OPTIONS_DB_PERSIST, /* no argument */
+ TK_A_OPTIONS_INOTIFY_WATCH, /* boolean */
+ TK_A_OPTIONS_DEVLINK_PRIORITY, /* int */
+ TK_A_OPTIONS_LOG_LEVEL, /* string of log level or "reset" */
+ TK_A_OWNER, /* user name */
+ TK_A_GROUP, /* group name */
+ TK_A_MODE, /* mode string */
+ TK_A_OWNER_ID, /* uid_t */
+ TK_A_GROUP_ID, /* gid_t */
+ TK_A_MODE_ID, /* mode_t */
+ TK_A_TAG, /* string */
+ TK_A_OPTIONS_STATIC_NODE, /* device path, /dev/... */
+ TK_A_SECLABEL, /* string with attribute */
+ TK_A_ENV, /* string with attribute */
+ TK_A_NAME, /* ifname */
+ TK_A_DEVLINK, /* string */
+ TK_A_ATTR, /* string with attribute */
+ TK_A_SYSCTL, /* string with attribute */
+ TK_A_RUN_BUILTIN, /* string */
+ TK_A_RUN_PROGRAM, /* string */
+
+ _TK_TYPE_MAX,
+ _TK_TYPE_INVALID = -EINVAL,
+} UdevRuleTokenType;
+
+typedef enum {
+ LINE_HAS_NAME = 1 << 0, /* has NAME= */
+ LINE_HAS_DEVLINK = 1 << 1, /* has SYMLINK=, OWNER=, GROUP= or MODE= */
+ LINE_HAS_STATIC_NODE = 1 << 2, /* has OPTIONS=static_node */
+ LINE_HAS_GOTO = 1 << 3, /* has GOTO= */
+ LINE_HAS_LABEL = 1 << 4, /* has LABEL= */
+ LINE_UPDATE_SOMETHING = 1 << 5, /* has other TK_A_* or TK_M_IMPORT tokens */
+ LINE_IS_REFERENCED = 1 << 6, /* is referenced by GOTO */
+} UdevRuleLineType;
+
+typedef struct UdevRuleFile UdevRuleFile;
+typedef struct UdevRuleLine UdevRuleLine;
+typedef struct UdevRuleToken UdevRuleToken;
+
+struct UdevRuleToken {
+ UdevRuleTokenType type:8;
+ UdevRuleOperatorType op:8;
+ UdevRuleMatchType match_type:8;
+ UdevRuleSubstituteType attr_subst_type:7;
+ bool attr_match_remove_trailing_whitespace:1;
+ const char *value;
+ void *data;
+
+ UdevRuleLine *rule_line;
+ LIST_FIELDS(UdevRuleToken, tokens);
+};
+
+struct UdevRuleLine {
+ char *line;
+ unsigned line_number;
+ UdevRuleLineType type;
+
+ const char *label;
+ const char *goto_label;
+ UdevRuleLine *goto_line;
+
+ UdevRuleFile *rule_file;
+ LIST_HEAD(UdevRuleToken, tokens);
+ LIST_FIELDS(UdevRuleLine, rule_lines);
+};
+
+struct UdevRuleFile {
+ char *filename;
+ unsigned issues; /* used by "udevadm verify" */
+
+ UdevRules *rules;
+ LIST_HEAD(UdevRuleLine, rule_lines);
+ LIST_FIELDS(UdevRuleFile, rule_files);
+};
+
+struct UdevRules {
+ ResolveNameTiming resolve_name_timing;
+ Hashmap *known_users;
+ Hashmap *known_groups;
+ Hashmap *stats_by_path;
+ LIST_HEAD(UdevRuleFile, rule_files);
+};
+
+#define LINE_GET_RULES(line) \
+ ASSERT_PTR(ASSERT_PTR(ASSERT_PTR(line)->rule_file)->rules)
+
+/*** Logging helpers ***/
+
+#define log_udev_rule_internal(device, file, line_nr, level, error, fmt, ...) \
+ ({ \
+ int _lv = (level); \
+ sd_device *_dev = (device); \
+ UdevRuleFile *_f = (file); \
+ const char *_n = _f ? _f->filename : NULL; \
+ \
+ if (!_dev && _f) \
+ _f->issues |= (1U << _lv); \
+ \
+ log_device_full_errno_zerook( \
+ _dev, _lv, error, "%s:%u " fmt, \
+ strna(_n), line_nr, \
+ ##__VA_ARGS__); \
+ })
+
+/* Mainly used when applying tokens to the event device. */
+#define log_event_full_errno_zerook(device, token, ...) \
+ ({ \
+ UdevRuleToken *_t = (token); \
+ UdevRuleLine *_l = _t ? _t->rule_line : NULL; \
+ \
+ log_udev_rule_internal( \
+ device, \
+ _l ? _l->rule_file : NULL, \
+ _l ? _l->line_number : 0, \
+ __VA_ARGS__); \
+ })
+
+#define log_event_full_errno(device, token, level, error, ...) \
+ ({ \
+ int _error = (error); \
+ ASSERT_NON_ZERO(_error); \
+ log_event_full_errno_zerook( \
+ device, token, level, _error, ##__VA_ARGS__); \
+ })
+
+#define log_event_full(device, token, level, ...) (void) log_event_full_errno_zerook(device, token, level, 0, __VA_ARGS__)
+
+#define log_event_debug(device, token, ...) log_event_full(device, token, LOG_DEBUG, __VA_ARGS__)
+#define log_event_info(device, token, ...) log_event_full(device, token, LOG_INFO, __VA_ARGS__)
+#define log_event_notice(device, token, ...) log_event_full(device, token, LOG_NOTICE, __VA_ARGS__)
+#define log_event_warning(device, token, ...) log_event_full(device, token, LOG_WARNING, __VA_ARGS__)
+#define log_event_error(device, token, ...) log_event_full(device, token, LOG_ERR, __VA_ARGS__)
+
+#define log_event_debug_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_DEBUG, error, __VA_ARGS__)
+#define log_event_info_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_INFO, error, __VA_ARGS__)
+#define log_event_notice_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_NOTICE, error, __VA_ARGS__)
+#define log_event_warning_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_WARNING, error, __VA_ARGS__)
+#define log_event_error_errno(device, token, error, ...) log_event_full_errno(device, token, LOG_ERR, error, __VA_ARGS__)
+
+/* Mainly used when parsing .rules files. */
+#define log_file_full_errno_zerook(...) \
+ log_udev_rule_internal(NULL, __VA_ARGS__)
+
+#define log_file_error(file, line_nr, ...) \
+ log_file_full_errno_zerook(file, line_nr, LOG_ERR, 0, __VA_ARGS__)
+
+#define log_line_full_errno_zerook(line, ...) \
+ ({ \
+ UdevRuleLine *_l = (line); \
+ log_file_full_errno_zerook( \
+ _l ? _l->rule_file : NULL, \
+ _l ? _l->line_number : 0, \
+ __VA_ARGS__); \
+ })
+
+#define log_line_full_errno(line, level, error, ...) \
+ ({ \
+ int _error = (error); \
+ ASSERT_NON_ZERO(_error); \
+ log_line_full_errno_zerook( \
+ line, level, _error, ##__VA_ARGS__); \
+ })
+
+#define log_line_full(line, level, ...) (void) log_line_full_errno_zerook(line, level, 0, __VA_ARGS__)
+
+#define log_line_debug(line, ...) log_line_full(line, LOG_DEBUG, __VA_ARGS__)
+#define log_line_info(line, ...) log_line_full(line, LOG_INFO, __VA_ARGS__)
+#define log_line_notice(line, ...) log_line_full(line, LOG_NOTICE, __VA_ARGS__)
+#define log_line_warning(line, ...) log_line_full(line, LOG_WARNING, __VA_ARGS__)
+#define log_line_error(line, ...) log_line_full(line, LOG_ERR, __VA_ARGS__)
+
+#define log_line_debug_errno(line, error, ...) log_line_full_errno(line, LOG_DEBUG, error, __VA_ARGS__)
+#define log_line_info_errno(line, error, ...) log_line_full_errno(line, LOG_INFO, error, __VA_ARGS__)
+#define log_line_notice_errno(line, error, ...) log_line_full_errno(line, LOG_NOTICE, error, __VA_ARGS__)
+#define log_line_warning_errno(line, error, ...) log_line_full_errno(line, LOG_WARNING, error, __VA_ARGS__)
+#define log_line_error_errno(line, error, ...) log_line_full_errno(line, LOG_ERR, error, __VA_ARGS__)
+
+#define _log_line_invalid_token(line, key, type) \
+ log_line_error_errno(line, SYNTHETIC_ERRNO(EINVAL), \
+ "Invalid %s for %s.", type, key)
+
+#define log_line_invalid_op(line, key) _log_line_invalid_token(line, key, "operator")
+#define log_line_invalid_attr(line, key) _log_line_invalid_token(line, key, "attribute")
+
+#define log_line_invalid_attr_format(line, key, attr, offset, hint) \
+ log_line_error_errno(line, SYNTHETIC_ERRNO(EINVAL), \
+ "Invalid attribute \"%s\" for %s (char %zu: %s), ignoring.", \
+ attr, key, offset, hint)
+#define log_line_invalid_value(line, key, value, offset, hint) \
+ log_line_error_errno(line, SYNTHETIC_ERRNO(EINVAL), \
+ "Invalid value \"%s\" for %s (char %zu: %s), ignoring.", \
+ value, key, offset, hint)
+
+static void log_unknown_owner(sd_device *dev, UdevRuleLine *line, int error, const char *entity, const char *name) {
+ assert(line);
+ ASSERT_NON_ZERO(error);
+
+ if (IN_SET(abs(error), ENOENT, ESRCH))
+ log_udev_rule_internal(dev, line->rule_file, line->line_number, LOG_ERR, error,
+ "Unknown %s '%s', ignoring.", entity, name);
+ else
+ log_udev_rule_internal(dev, line->rule_file, line->line_number, LOG_ERR, error,
+ "Failed to resolve %s '%s', ignoring: %m", entity, name);
+}
+
+static void log_event_truncated(
+ sd_device *dev,
+ UdevRuleToken *token,
+ const char *what,
+ const char *format,
+ const char *key,
+ bool is_match) {
+
+ if (is_match)
+ log_event_debug(dev, token,
+ "The %s is truncated while substituting into '%s', "
+ "assuming the %s key does not match.",
+ what, format, key);
+ else
+ log_event_warning(dev, token,
+ "The %s is truncated while substituting into '%s', "
+ "refusing to apply the %s key.",
+ what, format, key);
+}
+
+/*** Other functions ***/
+
+static UdevRuleToken *udev_rule_token_free(UdevRuleToken *token) {
+ if (!token)
+ return NULL;
+
+ if (token->rule_line)
+ LIST_REMOVE(tokens, token->rule_line->tokens, token);
+
+ return mfree(token);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRuleToken*, udev_rule_token_free);
+
+static void udev_rule_line_clear_tokens(UdevRuleLine *rule_line) {
+ assert(rule_line);
+
+ LIST_FOREACH(tokens, i, rule_line->tokens)
+ udev_rule_token_free(i);
+}
+
+static UdevRuleLine *udev_rule_line_free(UdevRuleLine *rule_line) {
+ if (!rule_line)
+ return NULL;
+
+ udev_rule_line_clear_tokens(rule_line);
+
+ if (rule_line->rule_file)
+ LIST_REMOVE(rule_lines, rule_line->rule_file->rule_lines, rule_line);
+
+ free(rule_line->line);
+ return mfree(rule_line);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRuleLine*, udev_rule_line_free);
+
+static UdevRuleFile *udev_rule_file_free(UdevRuleFile *rule_file) {
+ if (!rule_file)
+ return NULL;
+
+ LIST_FOREACH(rule_lines, i, rule_file->rule_lines)
+ udev_rule_line_free(i);
+
+ if (rule_file->rules)
+ LIST_REMOVE(rule_files, rule_file->rules->rule_files, rule_file);
+
+ free(rule_file->filename);
+ return mfree(rule_file);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRuleFile*, udev_rule_file_free);
+
+UdevRules *udev_rules_free(UdevRules *rules) {
+ if (!rules)
+ return NULL;
+
+ LIST_FOREACH(rule_files, i, rules->rule_files)
+ udev_rule_file_free(i);
+
+ hashmap_free_free_key(rules->known_users);
+ hashmap_free_free_key(rules->known_groups);
+ hashmap_free(rules->stats_by_path);
+ return mfree(rules);
+}
+
+static int rule_resolve_user(UdevRuleLine *rule_line, const char *name, uid_t *ret) {
+ Hashmap **known_users = &LINE_GET_RULES(rule_line)->known_users;
+ _cleanup_free_ char *n = NULL;
+ uid_t uid;
+ void *val;
+ int r;
+
+ assert(name);
+ assert(ret);
+
+ val = hashmap_get(*known_users, name);
+ if (val) {
+ *ret = PTR_TO_UID(val);
+ return 0;
+ }
+
+ r = get_user_creds(&name, &uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
+ if (r < 0) {
+ log_unknown_owner(NULL, rule_line, r, "user", name);
+ *ret = UID_INVALID;
+ return 0;
+ }
+
+ n = strdup(name);
+ if (!n)
+ return -ENOMEM;
+
+ r = hashmap_ensure_put(known_users, &string_hash_ops, n, UID_TO_PTR(uid));
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(n);
+ *ret = uid;
+ return 0;
+}
+
+static int rule_resolve_group(UdevRuleLine *rule_line, const char *name, gid_t *ret) {
+ Hashmap **known_groups = &LINE_GET_RULES(rule_line)->known_groups;
+ _cleanup_free_ char *n = NULL;
+ gid_t gid;
+ void *val;
+ int r;
+
+ assert(name);
+ assert(ret);
+
+ val = hashmap_get(*known_groups, name);
+ if (val) {
+ *ret = PTR_TO_GID(val);
+ return 0;
+ }
+
+ r = get_group_creds(&name, &gid, USER_CREDS_ALLOW_MISSING);
+ if (r < 0) {
+ log_unknown_owner(NULL, rule_line, r, "group", name);
+ *ret = GID_INVALID;
+ return 0;
+ }
+
+ n = strdup(name);
+ if (!n)
+ return -ENOMEM;
+
+ r = hashmap_ensure_put(known_groups, &string_hash_ops, n, GID_TO_PTR(gid));
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(n);
+ *ret = gid;
+ return 0;
+}
+
+static UdevRuleSubstituteType rule_get_substitution_type(const char *str) {
+ assert(str);
+
+ if (str[0] == '[')
+ return SUBST_TYPE_SUBSYS;
+ if (strchr(str, '%') || strchr(str, '$'))
+ return SUBST_TYPE_FORMAT;
+ return SUBST_TYPE_PLAIN;
+}
+
+static bool type_has_nulstr_value(UdevRuleTokenType type) {
+ return type < TK_M_TEST || type == TK_M_RESULT;
+}
+
+static int rule_line_add_token(UdevRuleLine *rule_line, UdevRuleTokenType type, UdevRuleOperatorType op, char *value, void *data) {
+ _cleanup_(udev_rule_token_freep) UdevRuleToken *token = NULL;
+ UdevRuleMatchType match_type = _MATCH_TYPE_INVALID;
+ UdevRuleSubstituteType subst_type = _SUBST_TYPE_INVALID;
+ bool remove_trailing_whitespace = false;
+ size_t len;
+
+ assert(rule_line);
+ assert(type >= 0 && type < _TK_TYPE_MAX);
+ assert(op >= 0 && op < _OP_TYPE_MAX);
+
+ if (type < _TK_M_MAX) {
+ assert(value);
+ assert(IN_SET(op, OP_MATCH, OP_NOMATCH));
+
+ if (type == TK_M_SUBSYSTEM && STR_IN_SET(value, "subsystem", "bus", "class"))
+ match_type = MATCH_TYPE_SUBSYSTEM;
+ else if (isempty(value))
+ match_type = MATCH_TYPE_EMPTY;
+ else if (streq(value, "?*")) {
+ /* Convert KEY=="?*" -> KEY!="" */
+ match_type = MATCH_TYPE_EMPTY;
+ op = op == OP_MATCH ? OP_NOMATCH : OP_MATCH;
+ } else if (string_is_glob(value))
+ match_type = MATCH_TYPE_GLOB;
+ else
+ match_type = MATCH_TYPE_PLAIN;
+
+ if (type_has_nulstr_value(type)) {
+ /* Convert value string to nulstr. */
+ bool bar = true, empty = false;
+ char *a, *b;
+
+ for (a = b = value; *a != '\0'; a++) {
+ if (*a != '|') {
+ *b++ = *a;
+ bar = false;
+ } else {
+ if (bar)
+ empty = true;
+ else
+ *b++ = '\0';
+ bar = true;
+ }
+ }
+ *b = '\0';
+
+ /* Make sure the value is end, so NULSTR_FOREACH can read correct match */
+ if (b < a)
+ b[1] = '\0';
+
+ if (bar)
+ empty = true;
+
+ if (empty) {
+ if (match_type == MATCH_TYPE_GLOB)
+ match_type = MATCH_TYPE_GLOB_WITH_EMPTY;
+ if (match_type == MATCH_TYPE_PLAIN)
+ match_type = MATCH_TYPE_PLAIN_WITH_EMPTY;
+ }
+ }
+ }
+
+ if (IN_SET(type, TK_M_ATTR, TK_M_PARENTS_ATTR)) {
+ assert(value);
+ assert(data);
+
+ len = strlen(value);
+ if (len > 0 && !isspace(value[len - 1]))
+ remove_trailing_whitespace = true;
+
+ subst_type = rule_get_substitution_type(data);
+ }
+
+ token = new(UdevRuleToken, 1);
+ if (!token)
+ return -ENOMEM;
+
+ *token = (UdevRuleToken) {
+ .type = type,
+ .op = op,
+ .value = value,
+ .data = data,
+ .match_type = match_type,
+ .attr_subst_type = subst_type,
+ .attr_match_remove_trailing_whitespace = remove_trailing_whitespace,
+ .rule_line = rule_line,
+ };
+
+ LIST_APPEND(tokens, rule_line->tokens, token);
+
+ if (token->type == TK_A_NAME)
+ SET_FLAG(rule_line->type, LINE_HAS_NAME, true);
+
+ else if (IN_SET(token->type, TK_A_DEVLINK,
+ TK_A_OWNER, TK_A_GROUP, TK_A_MODE,
+ TK_A_OWNER_ID, TK_A_GROUP_ID, TK_A_MODE_ID))
+ SET_FLAG(rule_line->type, LINE_HAS_DEVLINK, true);
+
+ else if (token->type == TK_A_OPTIONS_STATIC_NODE)
+ SET_FLAG(rule_line->type, LINE_HAS_STATIC_NODE, true);
+
+ else if (token->type >= _TK_A_MIN ||
+ IN_SET(token->type, TK_M_PROGRAM,
+ TK_M_IMPORT_FILE, TK_M_IMPORT_PROGRAM, TK_M_IMPORT_BUILTIN,
+ TK_M_IMPORT_DB, TK_M_IMPORT_CMDLINE, TK_M_IMPORT_PARENT))
+ SET_FLAG(rule_line->type, LINE_UPDATE_SOMETHING, true);
+
+ TAKE_PTR(token);
+ return 0;
+}
+
+static void check_value_format_and_warn(UdevRuleLine *line, const char *key, const char *value, bool nonempty) {
+ size_t offset;
+ const char *hint;
+
+ if (nonempty && isempty(value))
+ log_line_invalid_value(line, key, value, (size_t) 0, "empty value");
+ else if (udev_check_format(value, &offset, &hint) < 0)
+ log_line_invalid_value(line, key, value, offset + 1, hint);
+}
+
+static int check_attr_format_and_warn(UdevRuleLine *line, const char *key, const char *value) {
+ size_t offset;
+ const char *hint;
+
+ if (isempty(value))
+ return log_line_invalid_attr(line, key);
+ if (udev_check_format(value, &offset, &hint) < 0)
+ log_line_invalid_attr_format(line, key, value, offset + 1, hint);
+ return 0;
+}
+
+static int parse_token(UdevRuleLine *rule_line, const char *key, char *attr, UdevRuleOperatorType op, char *value) {
+ ResolveNameTiming resolve_name_timing = LINE_GET_RULES(rule_line)->resolve_name_timing;
+ bool is_match = IN_SET(op, OP_MATCH, OP_NOMATCH);
+ int r;
+
+ assert(key);
+ assert(value);
+
+ if (streq(key, "ACTION")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_ACTION, op, value, NULL);
+ } else if (streq(key, "DEVPATH")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_DEVPATH, op, value, NULL);
+ } else if (streq(key, "KERNEL")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_KERNEL, op, value, NULL);
+ } else if (streq(key, "SYMLINK")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match) {
+ check_value_format_and_warn(rule_line, key, value, false);
+ r = rule_line_add_token(rule_line, TK_A_DEVLINK, op, value, NULL);
+ } else
+ r = rule_line_add_token(rule_line, TK_M_DEVLINK, op, value, NULL);
+ } else if (streq(key, "NAME")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (op == OP_ADD) {
+ log_line_warning(rule_line, "%s key takes '==', '!=', '=', or ':=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ if (streq(value, "%k"))
+ return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL),
+ "Ignoring NAME=\"%%k\", as it will take no effect.");
+ if (isempty(value))
+ return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL),
+ "Ignoring NAME=\"\", as udev will not delete any network interfaces.");
+ check_value_format_and_warn(rule_line, key, value, false);
+
+ r = rule_line_add_token(rule_line, TK_A_NAME, op, value, NULL);
+ } else
+ r = rule_line_add_token(rule_line, TK_M_NAME, op, value, NULL);
+ } else if (streq(key, "ENV")) {
+ if (isempty(attr))
+ return log_line_invalid_attr(rule_line, key);
+ if (op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (op == OP_ASSIGN_FINAL) {
+ log_line_warning(rule_line, "%s key takes '==', '!=', '=', or '+=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ if (STR_IN_SET(attr,
+ "ACTION", "DEVLINKS", "DEVNAME", "DEVPATH", "DEVTYPE", "DRIVER",
+ "IFINDEX", "MAJOR", "MINOR", "SEQNUM", "SUBSYSTEM", "TAGS"))
+ return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL),
+ "Invalid ENV attribute. '%s' cannot be set.", attr);
+
+ check_value_format_and_warn(rule_line, key, value, false);
+
+ r = rule_line_add_token(rule_line, TK_A_ENV, op, value, attr);
+ } else
+ r = rule_line_add_token(rule_line, TK_M_ENV, op, value, attr);
+ } else if (streq(key, "CONST")) {
+ if (isempty(attr) || !STR_IN_SET(attr, "arch", "virt"))
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+ r = rule_line_add_token(rule_line, TK_M_CONST, op, value, attr);
+ } else if (streq(key, "TAG")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (op == OP_ASSIGN_FINAL) {
+ log_line_warning(rule_line, "%s key takes '==', '!=', '=', or '+=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ check_value_format_and_warn(rule_line, key, value, true);
+
+ r = rule_line_add_token(rule_line, TK_A_TAG, op, value, NULL);
+ } else
+ r = rule_line_add_token(rule_line, TK_M_TAG, op, value, NULL);
+ } else if (streq(key, "SUBSYSTEM")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ if (STR_IN_SET(value, "bus", "class"))
+ log_line_warning(rule_line, "\"%s\" must be specified as \"subsystem\".", value);
+
+ r = rule_line_add_token(rule_line, TK_M_SUBSYSTEM, op, value, NULL);
+ } else if (streq(key, "DRIVER")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_DRIVER, op, value, NULL);
+ } else if (streq(key, "ATTR")) {
+ r = check_attr_format_and_warn(rule_line, key, attr);
+ if (r < 0)
+ return r;
+ if (op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (IN_SET(op, OP_ADD, OP_ASSIGN_FINAL)) {
+ log_line_warning(rule_line, "%s key takes '==', '!=', or '=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ check_value_format_and_warn(rule_line, key, value, false);
+ r = rule_line_add_token(rule_line, TK_A_ATTR, op, value, attr);
+ } else
+ r = rule_line_add_token(rule_line, TK_M_ATTR, op, value, attr);
+ } else if (streq(key, "SYSCTL")) {
+ r = check_attr_format_and_warn(rule_line, key, attr);
+ if (r < 0)
+ return r;
+ if (op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (IN_SET(op, OP_ADD, OP_ASSIGN_FINAL)) {
+ log_line_warning(rule_line, "%s key takes '==', '!=', or '=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (!is_match) {
+ check_value_format_and_warn(rule_line, key, value, false);
+ r = rule_line_add_token(rule_line, TK_A_SYSCTL, op, value, attr);
+ } else
+ r = rule_line_add_token(rule_line, TK_M_SYSCTL, op, value, attr);
+ } else if (streq(key, "KERNELS")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_PARENTS_KERNEL, op, value, NULL);
+ } else if (streq(key, "SUBSYSTEMS")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_PARENTS_SUBSYSTEM, op, value, NULL);
+ } else if (streq(key, "DRIVERS")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_PARENTS_DRIVER, op, value, NULL);
+ } else if (streq(key, "ATTRS")) {
+ r = check_attr_format_and_warn(rule_line, key, attr);
+ if (r < 0)
+ return r;
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ if (startswith(attr, "device/"))
+ log_line_warning(rule_line, "'device' link may not be available in future kernels.");
+ if (strstr(attr, "../"))
+ log_line_warning(rule_line, "Direct reference to parent sysfs directory, may break in future kernels.");
+
+ r = rule_line_add_token(rule_line, TK_M_PARENTS_ATTR, op, value, attr);
+ } else if (streq(key, "TAGS")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_PARENTS_TAG, op, value, NULL);
+ } else if (streq(key, "TEST")) {
+ mode_t mode = MODE_INVALID;
+
+ if (!isempty(attr)) {
+ r = parse_mode(attr, &mode);
+ if (r < 0)
+ return log_line_error_errno(rule_line, r, "Failed to parse mode '%s': %m", attr);
+ }
+ check_value_format_and_warn(rule_line, key, value, true);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_TEST, op, value, MODE_TO_PTR(mode));
+ } else if (streq(key, "PROGRAM")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ check_value_format_and_warn(rule_line, key, value, true);
+ if (op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (!is_match)
+ op = OP_MATCH;
+
+ r = rule_line_add_token(rule_line, TK_M_PROGRAM, op, value, NULL);
+ } else if (streq(key, "IMPORT")) {
+ if (isempty(attr))
+ return log_line_invalid_attr(rule_line, key);
+ check_value_format_and_warn(rule_line, key, value, true);
+ if (op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (!is_match)
+ op = OP_MATCH;
+
+ if (streq(attr, "file"))
+ r = rule_line_add_token(rule_line, TK_M_IMPORT_FILE, op, value, NULL);
+ else if (streq(attr, "program")) {
+ UdevBuiltinCommand cmd;
+
+ cmd = udev_builtin_lookup(value);
+ if (cmd >= 0) {
+ log_line_debug(rule_line, "Found builtin command '%s' for %s, replacing attribute.", value, key);
+ r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd));
+ } else
+ r = rule_line_add_token(rule_line, TK_M_IMPORT_PROGRAM, op, value, NULL);
+ } else if (streq(attr, "builtin")) {
+ UdevBuiltinCommand cmd;
+
+ cmd = udev_builtin_lookup(value);
+ if (cmd < 0)
+ return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL),
+ "Unknown builtin command: %s", value);
+ r = rule_line_add_token(rule_line, TK_M_IMPORT_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd));
+ } else if (streq(attr, "db"))
+ r = rule_line_add_token(rule_line, TK_M_IMPORT_DB, op, value, NULL);
+ else if (streq(attr, "cmdline"))
+ r = rule_line_add_token(rule_line, TK_M_IMPORT_CMDLINE, op, value, NULL);
+ else if (streq(attr, "parent"))
+ r = rule_line_add_token(rule_line, TK_M_IMPORT_PARENT, op, value, NULL);
+ else
+ return log_line_invalid_attr(rule_line, key);
+ } else if (streq(key, "RESULT")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (!is_match)
+ return log_line_invalid_op(rule_line, key);
+
+ r = rule_line_add_token(rule_line, TK_M_RESULT, op, value, NULL);
+ } else if (streq(key, "OPTIONS")) {
+ char *tmp;
+
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (is_match || op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (op == OP_ADD)
+ op = OP_ASSIGN;
+
+ if (streq(value, "string_escape=none"))
+ r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_NONE, op, NULL, NULL);
+ else if (streq(value, "string_escape=replace"))
+ r = rule_line_add_token(rule_line, TK_A_OPTIONS_STRING_ESCAPE_REPLACE, op, NULL, NULL);
+ else if (streq(value, "db_persist"))
+ r = rule_line_add_token(rule_line, TK_A_OPTIONS_DB_PERSIST, op, NULL, NULL);
+ else if (streq(value, "watch"))
+ r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(1));
+ else if (streq(value, "nowatch"))
+ r = rule_line_add_token(rule_line, TK_A_OPTIONS_INOTIFY_WATCH, op, NULL, INT_TO_PTR(0));
+ else if ((tmp = startswith(value, "static_node=")))
+ r = rule_line_add_token(rule_line, TK_A_OPTIONS_STATIC_NODE, op, tmp, NULL);
+ else if ((tmp = startswith(value, "link_priority="))) {
+ int prio;
+
+ r = safe_atoi(tmp, &prio);
+ if (r < 0)
+ return log_line_error_errno(rule_line, r, "Failed to parse link priority '%s': %m", tmp);
+ r = rule_line_add_token(rule_line, TK_A_OPTIONS_DEVLINK_PRIORITY, op, NULL, INT_TO_PTR(prio));
+ } else if ((tmp = startswith(value, "log_level="))) {
+ int level;
+
+ if (streq(tmp, "reset"))
+ level = -1;
+ else {
+ level = log_level_from_string(tmp);
+ if (level < 0)
+ return log_line_error_errno(rule_line, level, "Failed to parse log level '%s': %m", tmp);
+ }
+ r = rule_line_add_token(rule_line, TK_A_OPTIONS_LOG_LEVEL, op, NULL, INT_TO_PTR(level));
+ } else {
+ log_line_warning(rule_line, "Invalid value for OPTIONS key, ignoring: '%s'", value);
+ return 0;
+ }
+ } else if (streq(key, "OWNER")) {
+ uid_t uid;
+
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (is_match || op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (op == OP_ADD) {
+ log_line_warning(rule_line, "%s key takes '=' or ':=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (parse_uid(value, &uid) >= 0)
+ r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid));
+ else if (resolve_name_timing == RESOLVE_NAME_EARLY &&
+ rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) {
+ r = rule_resolve_user(rule_line, value, &uid);
+ if (r < 0)
+ return log_line_error_errno(rule_line, r, "Failed to resolve user name '%s': %m", value);
+
+ r = rule_line_add_token(rule_line, TK_A_OWNER_ID, op, NULL, UID_TO_PTR(uid));
+ } else if (resolve_name_timing != RESOLVE_NAME_NEVER) {
+ check_value_format_and_warn(rule_line, key, value, true);
+ r = rule_line_add_token(rule_line, TK_A_OWNER, op, value, NULL);
+ } else {
+ log_line_debug(rule_line, "User name resolution is disabled, ignoring %s=\"%s\".", key, value);
+ return 0;
+ }
+ } else if (streq(key, "GROUP")) {
+ gid_t gid;
+
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (is_match || op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (op == OP_ADD) {
+ log_line_warning(rule_line, "%s key takes '=' or ':=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (parse_gid(value, &gid) >= 0)
+ r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid));
+ else if (resolve_name_timing == RESOLVE_NAME_EARLY &&
+ rule_get_substitution_type(value) == SUBST_TYPE_PLAIN) {
+ r = rule_resolve_group(rule_line, value, &gid);
+ if (r < 0)
+ return log_line_error_errno(rule_line, r, "Failed to resolve group name '%s': %m", value);
+
+ r = rule_line_add_token(rule_line, TK_A_GROUP_ID, op, NULL, GID_TO_PTR(gid));
+ } else if (resolve_name_timing != RESOLVE_NAME_NEVER) {
+ check_value_format_and_warn(rule_line, key, value, true);
+ r = rule_line_add_token(rule_line, TK_A_GROUP, op, value, NULL);
+ } else {
+ log_line_debug(rule_line, "Resolving group name is disabled, ignoring GROUP=\"%s\".", value);
+ return 0;
+ }
+ } else if (streq(key, "MODE")) {
+ mode_t mode;
+
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (is_match || op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (op == OP_ADD) {
+ log_line_warning(rule_line, "%s key takes '=' or ':=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ if (parse_mode(value, &mode) >= 0)
+ r = rule_line_add_token(rule_line, TK_A_MODE_ID, op, NULL, MODE_TO_PTR(mode));
+ else {
+ check_value_format_and_warn(rule_line, key, value, true);
+ r = rule_line_add_token(rule_line, TK_A_MODE, op, value, NULL);
+ }
+ } else if (streq(key, "SECLABEL")) {
+ if (isempty(attr))
+ return log_line_invalid_attr(rule_line, key);
+ check_value_format_and_warn(rule_line, key, value, true);
+ if (is_match || op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ if (op == OP_ASSIGN_FINAL) {
+ log_line_warning(rule_line, "%s key takes '=' or '+=' operator, assuming '='.", key);
+ op = OP_ASSIGN;
+ }
+
+ r = rule_line_add_token(rule_line, TK_A_SECLABEL, op, value, attr);
+ } else if (streq(key, "RUN")) {
+ if (is_match || op == OP_REMOVE)
+ return log_line_invalid_op(rule_line, key);
+ check_value_format_and_warn(rule_line, key, value, true);
+ if (!attr || streq(attr, "program"))
+ r = rule_line_add_token(rule_line, TK_A_RUN_PROGRAM, op, value, NULL);
+ else if (streq(attr, "builtin")) {
+ UdevBuiltinCommand cmd;
+
+ cmd = udev_builtin_lookup(value);
+ if (cmd < 0)
+ return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL),
+ "Unknown builtin command '%s', ignoring.", value);
+ r = rule_line_add_token(rule_line, TK_A_RUN_BUILTIN, op, value, UDEV_BUILTIN_CMD_TO_PTR(cmd));
+ } else
+ return log_line_invalid_attr(rule_line, key);
+ } else if (streq(key, "GOTO")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (op != OP_ASSIGN)
+ return log_line_invalid_op(rule_line, key);
+ if (FLAGS_SET(rule_line->type, LINE_HAS_GOTO)) {
+ log_line_warning(rule_line, "Contains multiple GOTO keys, ignoring GOTO=\"%s\".", value);
+ return 0;
+ }
+
+ rule_line->goto_label = value;
+ SET_FLAG(rule_line->type, LINE_HAS_GOTO, true);
+ return 1;
+ } else if (streq(key, "LABEL")) {
+ if (attr)
+ return log_line_invalid_attr(rule_line, key);
+ if (op != OP_ASSIGN)
+ return log_line_invalid_op(rule_line, key);
+ if (FLAGS_SET(rule_line->type, LINE_HAS_LABEL))
+ log_line_warning(rule_line, "Contains multiple LABEL keys, ignoring LABEL=\"%s\".",
+ rule_line->label);
+
+ rule_line->label = value;
+ SET_FLAG(rule_line->type, LINE_HAS_LABEL, true);
+ return 1;
+ } else
+ return log_line_error_errno(rule_line, SYNTHETIC_ERRNO(EINVAL), "Invalid key '%s'.", key);
+ if (r < 0)
+ return log_oom();
+
+ return 1;
+}
+
+static UdevRuleOperatorType parse_operator(const char *op) {
+ assert(op);
+
+ if (startswith(op, "=="))
+ return OP_MATCH;
+ if (startswith(op, "!="))
+ return OP_NOMATCH;
+ if (startswith(op, "+="))
+ return OP_ADD;
+ if (startswith(op, "-="))
+ return OP_REMOVE;
+ if (startswith(op, "="))
+ return OP_ASSIGN;
+ if (startswith(op, ":="))
+ return OP_ASSIGN_FINAL;
+
+ return _OP_TYPE_INVALID;
+}
+
+static void check_token_delimiters(UdevRuleLine *rule_line, const char *line) {
+ assert(rule_line);
+
+ size_t n_comma = 0;
+ bool ws_before_comma = false, ws_after_comma = false;
+ const char *p;
+
+ for (p = line; !isempty(p); ++p) {
+ if (*p == ',')
+ ++n_comma;
+ else if (strchr(WHITESPACE, *p)) {
+ if (n_comma > 0)
+ ws_after_comma = true;
+ else
+ ws_before_comma = true;
+ } else
+ break;
+ }
+
+ if (line == rule_line->line) {
+ /* this is the first token of the rule */
+ if (n_comma > 0)
+ log_line_notice(rule_line, "style: stray leading comma.");
+ } else if (isempty(p)) {
+ /* there are no more tokens in the rule */
+ if (n_comma > 0)
+ log_line_notice(rule_line, "style: stray trailing comma.");
+ } else {
+ /* single comma is expected */
+ if (n_comma == 0)
+ log_line_notice(rule_line, "style: a comma between tokens is expected.");
+ else if (n_comma > 1)
+ log_line_notice(rule_line, "style: more than one comma between tokens.");
+
+ /* whitespace after comma is expected */
+ if (n_comma > 0) {
+ if (ws_before_comma)
+ log_line_notice(rule_line, "style: stray whitespace before comma.");
+ if (!ws_after_comma)
+ log_line_notice(rule_line, "style: whitespace after comma is expected.");
+ } else if (!ws_before_comma && !ws_after_comma)
+ log_line_notice(rule_line, "style: whitespace between tokens is expected.");
+ }
+}
+
+int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos) {
+ char *i, *j;
+ bool is_escaped;
+
+ /* value must be double quotated */
+ is_escaped = str[0] == 'e';
+ str += is_escaped;
+ if (str[0] != '"')
+ return -EINVAL;
+
+ if (!is_escaped) {
+ /* unescape double quotation '\"'->'"' */
+ for (j = str, i = str + 1; *i != '"'; i++, j++) {
+ if (*i == '\0')
+ return -EINVAL;
+ if (i[0] == '\\' && i[1] == '"')
+ i++;
+ *j = *i;
+ }
+ j[0] = '\0';
+ /*
+ * The return value must be terminated by two subsequent NULs
+ * so it could be safely interpreted as nulstr.
+ */
+ j[1] = '\0';
+ } else {
+ _cleanup_free_ char *unescaped = NULL;
+ ssize_t l;
+
+ /* find the end position of value */
+ for (i = str + 1; *i != '"'; i++) {
+ if (i[0] == '\\')
+ i++;
+ if (*i == '\0')
+ return -EINVAL;
+ }
+ i[0] = '\0';
+
+ l = cunescape_length(str + 1, i - (str + 1), 0, &unescaped);
+ if (l < 0)
+ return l;
+
+ assert(l <= i - (str + 1));
+ memcpy(str, unescaped, l + 1);
+ /*
+ * The return value must be terminated by two subsequent NULs
+ * so it could be safely interpreted as nulstr.
+ */
+ str[l + 1] = '\0';
+ }
+
+ *ret_value = str;
+ *ret_endpos = i + 1;
+ return 0;
+}
+
+static int parse_line(char **line, char **ret_key, char **ret_attr, UdevRuleOperatorType *ret_op, char **ret_value) {
+ char *key_begin, *key_end, *attr, *tmp;
+ UdevRuleOperatorType op;
+ int r;
+
+ assert(line);
+ assert(*line);
+ assert(ret_key);
+ assert(ret_op);
+ assert(ret_value);
+
+ key_begin = skip_leading_chars(*line, WHITESPACE ",");
+
+ if (isempty(key_begin))
+ return 0;
+
+ for (key_end = key_begin; ; key_end++) {
+ if (key_end[0] == '\0')
+ return -EINVAL;
+ if (strchr(WHITESPACE "={", key_end[0]))
+ break;
+ if (strchr("+-!:", key_end[0]) && key_end[1] == '=')
+ break;
+ }
+ if (key_end[0] == '{') {
+ attr = key_end + 1;
+ tmp = strchr(attr, '}');
+ if (!tmp)
+ return -EINVAL;
+ *tmp++ = '\0';
+ } else {
+ attr = NULL;
+ tmp = key_end;
+ }
+
+ tmp = skip_leading_chars(tmp, NULL);
+ op = parse_operator(tmp);
+ if (op < 0)
+ return -EINVAL;
+
+ key_end[0] = '\0';
+
+ tmp += op == OP_ASSIGN ? 1 : 2;
+ tmp = skip_leading_chars(tmp, NULL);
+ r = udev_rule_parse_value(tmp, ret_value, line);
+ if (r < 0)
+ return r;
+
+ *ret_key = key_begin;
+ *ret_attr = attr;
+ *ret_op = op;
+ return 1;
+}
+
+static void check_tokens_order(UdevRuleLine *rule_line) {
+ bool has_result = false;
+
+ assert(rule_line);
+
+ LIST_FOREACH(tokens, t, rule_line->tokens)
+ if (t->type == TK_M_RESULT)
+ has_result = true;
+ else if (has_result && t->type == TK_M_PROGRAM) {
+ log_line_warning(rule_line, "Reordering RESULT check after PROGRAM assignment.");
+ break;
+ }
+}
+
+static void sort_tokens(UdevRuleLine *rule_line) {
+ assert(rule_line);
+
+ UdevRuleToken *old_tokens = TAKE_PTR(rule_line->tokens);
+
+ while (old_tokens) {
+ UdevRuleToken *min_token = NULL;
+
+ LIST_FOREACH(tokens, t, old_tokens)
+ if (!min_token || min_token->type > t->type)
+ min_token = t;
+
+ LIST_REMOVE(tokens, old_tokens, min_token);
+ LIST_APPEND(tokens, rule_line->tokens, min_token);
+ }
+}
+
+static int rule_add_line(UdevRuleFile *rule_file, const char *line_str, unsigned line_nr, bool extra_checks) {
+ _cleanup_(udev_rule_line_freep) UdevRuleLine *rule_line = NULL;
+ _cleanup_free_ char *line = NULL;
+ char *p;
+ int r;
+
+ assert(rule_file);
+ assert(line_str);
+
+ if (isempty(line_str))
+ return 0;
+
+ line = strdup(line_str);
+ if (!line)
+ return log_oom();
+
+ rule_line = new(UdevRuleLine, 1);
+ if (!rule_line)
+ return log_oom();
+
+ *rule_line = (UdevRuleLine) {
+ .line = TAKE_PTR(line),
+ .line_number = line_nr,
+ .rule_file = rule_file,
+ };
+
+ LIST_APPEND(rule_lines, rule_file->rule_lines, rule_line);
+
+ for (p = rule_line->line; !isempty(p); ) {
+ char *key, *attr, *value;
+ UdevRuleOperatorType op;
+
+ if (extra_checks)
+ check_token_delimiters(rule_line, p);
+
+ r = parse_line(&p, &key, &attr, &op, &value);
+ if (r < 0)
+ return log_line_error_errno(rule_line, r, "Invalid key/value pair, ignoring.");
+ if (r == 0)
+ break;
+
+ r = parse_token(rule_line, key, attr, op, value);
+ if (r < 0)
+ return r;
+ }
+
+ if (rule_line->type == 0) {
+ log_line_warning(rule_line, "The line has no effect, ignoring.");
+ return 0;
+ }
+
+ if (extra_checks)
+ check_tokens_order(rule_line);
+
+ sort_tokens(rule_line);
+ TAKE_PTR(rule_line);
+ return 0;
+}
+
+static void rule_resolve_goto(UdevRuleFile *rule_file) {
+ assert(rule_file);
+
+ /* link GOTOs to LABEL rules in this file to be able to fast-forward */
+ LIST_FOREACH(rule_lines, line, rule_file->rule_lines) {
+ if (!FLAGS_SET(line->type, LINE_HAS_GOTO))
+ continue;
+
+ LIST_FOREACH(rule_lines, i, line->rule_lines_next)
+ if (streq_ptr(i->label, line->goto_label)) {
+ line->goto_line = i;
+ SET_FLAG(i->type, LINE_IS_REFERENCED, true);
+ break;
+ }
+
+ if (!line->goto_line) {
+ log_line_error(line, "GOTO=\"%s\" has no matching label, ignoring.",
+ line->goto_label);
+
+ SET_FLAG(line->type, LINE_HAS_GOTO, false);
+ line->goto_label = NULL;
+
+ if ((line->type & ~(LINE_HAS_LABEL|LINE_IS_REFERENCED)) == 0) {
+ log_line_warning(line, "The line has no effect any more, dropping.");
+ /* LINE_IS_REFERENCED implies LINE_HAS_LABEL */
+ if (line->type & LINE_HAS_LABEL)
+ udev_rule_line_clear_tokens(line);
+ else
+ udev_rule_line_free(line);
+ }
+ }
+ }
+}
+
+static bool token_data_is_string(UdevRuleTokenType type) {
+ return IN_SET(type, TK_M_ENV,
+ TK_M_CONST,
+ TK_M_ATTR,
+ TK_M_SYSCTL,
+ TK_M_PARENTS_ATTR,
+ TK_A_SECLABEL,
+ TK_A_ENV,
+ TK_A_ATTR,
+ TK_A_SYSCTL);
+}
+
+static bool token_type_and_data_eq(const UdevRuleToken *a, const UdevRuleToken *b) {
+ assert(a);
+ assert(b);
+
+ return a->type == b->type &&
+ (token_data_is_string(a->type) ? streq_ptr(a->data, b->data) : (a->data == b->data));
+}
+
+static bool nulstr_eq(const char *a, const char *b) {
+ NULSTR_FOREACH(i, a)
+ if (!nulstr_contains(b, i))
+ return false;
+
+ NULSTR_FOREACH(i, b)
+ if (!nulstr_contains(a, i))
+ return false;
+
+ return true;
+}
+
+static bool token_type_and_value_eq(const UdevRuleToken *a, const UdevRuleToken *b) {
+ assert(a);
+ assert(b);
+
+ if (a->type != b->type ||
+ a->match_type != b->match_type)
+ return false;
+
+ /* token value is ignored for certain match types */
+ if (IN_SET(a->match_type, MATCH_TYPE_EMPTY, MATCH_TYPE_SUBSYSTEM))
+ return true;
+
+ return type_has_nulstr_value(a->type) ? nulstr_eq(a->value, b->value) :
+ streq_ptr(a->value, b->value);
+}
+
+static bool conflicting_op(UdevRuleOperatorType a, UdevRuleOperatorType b) {
+ return (a == OP_MATCH && b == OP_NOMATCH) ||
+ (a == OP_NOMATCH && b == OP_MATCH);
+}
+
+/* test whether all fields besides UdevRuleOperatorType of two tokens match */
+static bool tokens_eq(const UdevRuleToken *a, const UdevRuleToken *b) {
+ assert(a);
+ assert(b);
+
+ return a->attr_subst_type == b->attr_subst_type &&
+ a->attr_match_remove_trailing_whitespace == b->attr_match_remove_trailing_whitespace &&
+ token_type_and_value_eq(a, b) &&
+ token_type_and_data_eq(a, b);
+}
+
+static bool nulstr_tokens_conflict(const UdevRuleToken *a, const UdevRuleToken *b) {
+ assert(a);
+ assert(b);
+
+ if (!(a->type == b->type &&
+ type_has_nulstr_value(a->type) &&
+ a->op == b->op &&
+ a->op == OP_MATCH &&
+ a->match_type == b->match_type &&
+ a->attr_subst_type == b->attr_subst_type &&
+ a->attr_match_remove_trailing_whitespace == b->attr_match_remove_trailing_whitespace &&
+ token_type_and_data_eq(a, b)))
+ return false;
+
+ if (a->match_type == MATCH_TYPE_PLAIN) {
+ NULSTR_FOREACH(i, a->value)
+ if (nulstr_contains(b->value, i))
+ return false;
+ return true;
+ }
+
+ if (a->match_type == MATCH_TYPE_GLOB) {
+ NULSTR_FOREACH(i, a->value) {
+ size_t i_n = strcspn(i, GLOB_CHARS);
+ if (i_n == 0)
+ return false;
+ NULSTR_FOREACH(j, b->value) {
+ size_t j_n = strcspn(j, GLOB_CHARS);
+ if (j_n == 0 || strneq(i, j, MIN(i_n, j_n)))
+ return false;
+ }
+
+ }
+ return true;
+ }
+
+ return false;
+}
+
+static void udev_check_unused_labels(UdevRuleLine *line) {
+ assert(line);
+
+ if (FLAGS_SET(line->type, LINE_HAS_LABEL) &&
+ !FLAGS_SET(line->type, LINE_IS_REFERENCED))
+ log_line_notice(line, "style: LABEL=\"%s\" is unused.", line->label);
+}
+
+static void udev_check_conflicts_duplicates(UdevRuleLine *line) {
+ assert(line);
+
+ bool conflicts = false, duplicates = false;
+
+ LIST_FOREACH(tokens, token, line->tokens)
+ LIST_FOREACH(tokens, i, token->tokens_next) {
+ bool new_conflicts = false, new_duplicates = false;
+
+ if (tokens_eq(token, i)) {
+ if (!duplicates && token->op == i->op)
+ new_duplicates = true;
+ if (!conflicts && conflicting_op(token->op, i->op))
+ new_conflicts = true;
+ } else if (!conflicts && nulstr_tokens_conflict(token, i))
+ new_conflicts = true;
+ else
+ continue;
+
+ if (new_duplicates) {
+ duplicates = new_duplicates;
+ log_line_warning(line, "duplicate expressions.");
+ }
+ if (new_conflicts) {
+ conflicts = new_conflicts;
+ log_line_error(line, "conflicting match expressions, the line has no effect.");
+ }
+ if (conflicts && duplicates)
+ return;
+ }
+}
+
+static void udev_check_rule_line(UdevRuleLine *line) {
+ udev_check_unused_labels(line);
+ udev_check_conflicts_duplicates(line);
+}
+
+int udev_rules_parse_file(UdevRules *rules, const char *filename, bool extra_checks, UdevRuleFile **ret) {
+ _cleanup_(udev_rule_file_freep) UdevRuleFile *rule_file = NULL;
+ _cleanup_free_ char *continuation = NULL, *name = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ bool ignore_line = false;
+ unsigned line_nr = 0;
+ struct stat st;
+ int r;
+
+ assert(rules);
+ assert(filename);
+
+ f = fopen(filename, "re");
+ if (!f) {
+ if (extra_checks)
+ return -errno;
+
+ if (errno == ENOENT)
+ return 0;
+
+ return log_warning_errno(errno, "Failed to open %s, ignoring: %m", filename);
+ }
+
+ if (fstat(fileno(f), &st) < 0)
+ return log_warning_errno(errno, "Failed to stat %s, ignoring: %m", filename);
+
+ if (null_or_empty(&st)) {
+ log_debug("Skipping empty file: %s", filename);
+ if (ret)
+ *ret = NULL;
+ return 0;
+ }
+
+ r = hashmap_put_stats_by_path(&rules->stats_by_path, filename, &st);
+ if (r < 0)
+ return log_warning_errno(errno, "Failed to save stat for %s, ignoring: %m", filename);
+
+ (void) fd_warn_permissions(filename, fileno(f));
+
+ log_debug("Reading rules file: %s", filename);
+
+ name = strdup(filename);
+ if (!name)
+ return log_oom();
+
+ rule_file = new(UdevRuleFile, 1);
+ if (!rule_file)
+ return log_oom();
+
+ *rule_file = (UdevRuleFile) {
+ .filename = TAKE_PTR(name),
+ .rules = rules,
+ };
+
+ LIST_APPEND(rule_files, rules->rule_files, rule_file);
+
+ for (;;) {
+ _cleanup_free_ char *buf = NULL;
+ size_t len;
+ char *line;
+
+ r = read_line(f, UDEV_LINE_SIZE, &buf);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ line_nr++;
+ line = skip_leading_chars(buf, NULL);
+
+ /* Lines beginning with '#' are ignored regardless of line continuation. */
+ if (line[0] == '#')
+ continue;
+
+ len = strlen(line);
+
+ if (continuation && !ignore_line) {
+ if (strlen(continuation) + len >= UDEV_LINE_SIZE)
+ ignore_line = true;
+
+ if (!strextend(&continuation, line))
+ return log_oom();
+
+ if (!ignore_line) {
+ line = continuation;
+ len = strlen(line);
+ }
+ }
+
+ if (len > 0 && line[len - 1] == '\\') {
+ if (ignore_line)
+ continue;
+
+ line[len - 1] = '\0';
+ if (!continuation) {
+ continuation = strdup(line);
+ if (!continuation)
+ return log_oom();
+ }
+
+ continue;
+ }
+
+ if (ignore_line)
+ log_file_error(rule_file, line_nr, "Line is too long, ignored.");
+ else if (len > 0)
+ (void) rule_add_line(rule_file, line, line_nr, extra_checks);
+
+ continuation = mfree(continuation);
+ ignore_line = false;
+ }
+
+ if (continuation)
+ log_file_error(rule_file, line_nr,
+ "Unexpected EOF after line continuation, line ignored.");
+
+ rule_resolve_goto(rule_file);
+
+ if (extra_checks)
+ LIST_FOREACH(rule_lines, line, rule_file->rule_lines)
+ udev_check_rule_line(line);
+
+ if (ret)
+ *ret = rule_file;
+
+ TAKE_PTR(rule_file);
+ return 1;
+}
+
+unsigned udev_rule_file_get_issues(UdevRuleFile *rule_file) {
+ assert(rule_file);
+
+ return rule_file->issues;
+}
+
+UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing) {
+ assert(resolve_name_timing >= 0 && resolve_name_timing < _RESOLVE_NAME_TIMING_MAX);
+
+ UdevRules *rules = new(UdevRules, 1);
+ if (!rules)
+ return NULL;
+
+ *rules = (UdevRules) {
+ .resolve_name_timing = resolve_name_timing,
+ };
+
+ return rules;
+}
+
+int udev_rules_load(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing) {
+ _cleanup_(udev_rules_freep) UdevRules *rules = NULL;
+ _cleanup_strv_free_ char **files = NULL;
+ int r;
+
+ rules = udev_rules_new(resolve_name_timing);
+ if (!rules)
+ return -ENOMEM;
+
+ r = conf_files_list_strv(&files, ".rules", NULL, 0, RULES_DIRS);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to enumerate rules files: %m");
+
+ STRV_FOREACH(f, files) {
+ r = udev_rules_parse_file(rules, *f, /* extra_checks = */ false, NULL);
+ if (r < 0)
+ log_debug_errno(r, "Failed to read rules file %s, ignoring: %m", *f);
+ }
+
+ *ret_rules = TAKE_PTR(rules);
+ return 0;
+}
+
+bool udev_rules_should_reload(UdevRules *rules) {
+ _cleanup_hashmap_free_ Hashmap *stats_by_path = NULL;
+ int r;
+
+ if (!rules)
+ return true;
+
+ r = config_get_stats_by_path(".rules", NULL, 0, RULES_DIRS, /* check_dropins = */ false, &stats_by_path);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to get stats of udev rules, ignoring: %m");
+ return true;
+ }
+
+ if (!stats_by_path_equal(rules->stats_by_path, stats_by_path)) {
+ log_debug("Udev rules need reloading");
+ return true;
+ }
+
+ return false;
+}
+
+static bool token_match_string(UdevRuleToken *token, const char *str) {
+ const char *value;
+ bool match = false;
+
+ assert(token);
+ assert(token->value);
+ assert(token->type < _TK_M_MAX);
+
+ str = strempty(str);
+ value = token->value;
+
+ switch (token->match_type) {
+ case MATCH_TYPE_EMPTY:
+ match = isempty(str);
+ break;
+ case MATCH_TYPE_SUBSYSTEM:
+ match = STR_IN_SET(str, "subsystem", "class", "bus");
+ break;
+ case MATCH_TYPE_PLAIN_WITH_EMPTY:
+ if (isempty(str)) {
+ match = true;
+ break;
+ }
+ _fallthrough_;
+ case MATCH_TYPE_PLAIN:
+ NULSTR_FOREACH(i, value)
+ if (streq(i, str)) {
+ match = true;
+ break;
+ }
+ break;
+ case MATCH_TYPE_GLOB_WITH_EMPTY:
+ if (isempty(str)) {
+ match = true;
+ break;
+ }
+ _fallthrough_;
+ case MATCH_TYPE_GLOB:
+ NULSTR_FOREACH(i, value)
+ if ((fnmatch(i, str, 0) == 0)) {
+ match = true;
+ break;
+ }
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return token->op == (match ? OP_MATCH : OP_NOMATCH);
+}
+
+static bool token_match_attr(UdevRuleToken *token, sd_device *dev, UdevEvent *event) {
+ char nbuf[UDEV_NAME_SIZE], vbuf[UDEV_NAME_SIZE];
+ const char *name, *value;
+ bool truncated;
+
+ assert(token);
+ assert(IN_SET(token->type, TK_M_ATTR, TK_M_PARENTS_ATTR));
+ assert(dev);
+ assert(event);
+
+ name = token->data;
+
+ switch (token->attr_subst_type) {
+ case SUBST_TYPE_FORMAT:
+ (void) udev_event_apply_format(event, name, nbuf, sizeof(nbuf), false, NULL, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "sysfs attribute name", name,
+ token->type == TK_M_ATTR ? "ATTR" : "ATTRS", /* is_match = */ true);
+ return false;
+ }
+
+ name = nbuf;
+ _fallthrough_;
+ case SUBST_TYPE_PLAIN:
+ if (sd_device_get_sysattr_value(dev, name, &value) < 0)
+ return false;
+ break;
+ case SUBST_TYPE_SUBSYS:
+ if (udev_resolve_subsys_kernel(name, vbuf, sizeof(vbuf), true) < 0)
+ return false;
+ value = vbuf;
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ /* remove trailing whitespace, if not asked to match for it */
+ if (token->attr_match_remove_trailing_whitespace) {
+ if (value != vbuf) {
+ strscpy(vbuf, sizeof(vbuf), value);
+ value = vbuf;
+ }
+
+ delete_trailing_chars(vbuf, NULL);
+ }
+
+ return token_match_string(token, value);
+}
+
+static int get_property_from_string(char *line, char **ret_key, char **ret_value) {
+ char *key, *val;
+ size_t len;
+
+ assert(line);
+ assert(ret_key);
+ assert(ret_value);
+
+ /* find key */
+ key = skip_leading_chars(line, NULL);
+
+ /* comment or empty line */
+ if (IN_SET(key[0], '#', '\0')) {
+ *ret_key = *ret_value = NULL;
+ return 0;
+ }
+
+ /* split key/value */
+ val = strchr(key, '=');
+ if (!val)
+ return -EINVAL;
+ *val++ = '\0';
+
+ key = strstrip(key);
+ if (isempty(key))
+ return -EINVAL;
+
+ val = strstrip(val);
+ if (isempty(val))
+ return -EINVAL;
+
+ /* unquote */
+ if (IN_SET(val[0], '"', '\'')) {
+ len = strlen(val);
+ if (len == 1 || val[len-1] != val[0])
+ return -EINVAL;
+ val[len-1] = '\0';
+ val++;
+ }
+
+ *ret_key = key;
+ *ret_value = val;
+ return 1;
+}
+
+static int import_parent_into_properties(sd_device *dev, const char *filter) {
+ sd_device *parent;
+ int r;
+
+ assert(dev);
+ assert(filter);
+
+ r = sd_device_get_parent(dev, &parent);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ FOREACH_DEVICE_PROPERTY(parent, key, val) {
+ if (fnmatch(filter, key, 0) != 0)
+ continue;
+ r = device_add_property(dev, key, val);
+ if (r < 0)
+ return r;
+ }
+
+ return 1;
+}
+
+static int attr_subst_subdir(char attr[static UDEV_PATH_SIZE]) {
+ _cleanup_closedir_ DIR *dir = NULL;
+ char buf[UDEV_PATH_SIZE], *p;
+ const char *tail;
+ size_t len, size;
+ bool truncated;
+
+ assert(attr);
+
+ tail = strstr(attr, "/*/");
+ if (!tail)
+ return 0;
+
+ len = tail - attr + 1; /* include slash at the end */
+ tail += 2; /* include slash at the beginning */
+
+ p = buf;
+ size = sizeof(buf);
+ size -= strnpcpy_full(&p, size, attr, len, &truncated);
+ if (truncated)
+ return -ENOENT;
+
+ dir = opendir(buf);
+ if (!dir)
+ return -errno;
+
+ FOREACH_DIRENT_ALL(de, dir, break) {
+ if (de->d_name[0] == '.')
+ continue;
+
+ strscpyl_full(p, size, &truncated, de->d_name, tail, NULL);
+ if (truncated)
+ continue;
+
+ if (faccessat(dirfd(dir), p, F_OK, 0) < 0)
+ continue;
+
+ strcpy(attr, buf);
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+static size_t udev_replace_ifname(char *str) {
+ size_t replaced = 0;
+
+ assert(str);
+
+ /* See ifname_valid_full(). */
+
+ for (char *p = str; *p != '\0'; p++)
+ if (!ifname_valid_char(*p)) {
+ *p = '_';
+ replaced++;
+ }
+
+ return replaced;
+}
+
+static int udev_rule_apply_token_to_event(
+ UdevRuleToken *token,
+ sd_device *dev,
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list) {
+
+ int r;
+
+ assert(token);
+ assert(dev);
+ assert(event);
+
+ /* This returns the following values:
+ * 0 on the current token does not match the event,
+ * 1 on the current token matches the event, and
+ * negative errno on some critical errors. */
+
+ switch (token->type) {
+ case TK_M_ACTION: {
+ sd_device_action_t a;
+
+ r = sd_device_get_action(dev, &a);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to get uevent action type: %m");
+
+ return token_match_string(token, device_action_to_string(a));
+ }
+ case TK_M_DEVPATH: {
+ const char *val;
+
+ r = sd_device_get_devpath(dev, &val);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to get devpath: %m");
+
+ return token_match_string(token, val);
+ }
+ case TK_M_KERNEL:
+ case TK_M_PARENTS_KERNEL: {
+ const char *val;
+
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to get sysname: %m");
+
+ return token_match_string(token, val);
+ }
+ case TK_M_DEVLINK:
+ FOREACH_DEVICE_DEVLINK(dev, val)
+ if (token_match_string(token, strempty(startswith(val, "/dev/"))) == (token->op == OP_MATCH))
+ return token->op == OP_MATCH;
+ return token->op == OP_NOMATCH;
+ case TK_M_NAME:
+ return token_match_string(token, event->name);
+ case TK_M_ENV: {
+ const char *val = NULL;
+
+ (void) device_get_property_value_with_fallback(dev, token->data, properties_list, &val);
+
+ return token_match_string(token, val);
+ }
+ case TK_M_CONST: {
+ const char *val, *k = token->data;
+
+ if (streq(k, "arch"))
+ val = architecture_to_string(uname_architecture());
+ else if (streq(k, "virt"))
+ val = virtualization_to_string(detect_virtualization());
+ else if (streq(k, "cvm"))
+ val = confidential_virtualization_to_string(detect_confidential_virtualization());
+ else
+ assert_not_reached();
+ return token_match_string(token, val);
+ }
+ case TK_M_TAG:
+ case TK_M_PARENTS_TAG:
+ FOREACH_DEVICE_CURRENT_TAG(dev, val)
+ if (token_match_string(token, val) == (token->op == OP_MATCH))
+ return token->op == OP_MATCH;
+ return token->op == OP_NOMATCH;
+ case TK_M_SUBSYSTEM:
+ case TK_M_PARENTS_SUBSYSTEM: {
+ const char *val;
+
+ r = sd_device_get_subsystem(dev, &val);
+ if (r == -ENOENT)
+ val = NULL;
+ else if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to get subsystem: %m");
+
+ return token_match_string(token, val);
+ }
+ case TK_M_DRIVER:
+ case TK_M_PARENTS_DRIVER: {
+ const char *val;
+
+ r = sd_device_get_driver(dev, &val);
+ if (r == -ENOENT)
+ val = NULL;
+ else if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to get driver: %m");
+
+ return token_match_string(token, val);
+ }
+ case TK_M_ATTR:
+ case TK_M_PARENTS_ATTR:
+ return token_match_attr(token, dev, event);
+ case TK_M_SYSCTL: {
+ _cleanup_free_ char *value = NULL;
+ char buf[UDEV_PATH_SIZE];
+ bool truncated;
+
+ (void) udev_event_apply_format(event, token->data, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "sysctl entry name", token->data, "SYSCTL", /* is_match = */ true);
+ return false;
+ }
+
+ r = sysctl_read(sysctl_normalize(buf), &value);
+ if (r < 0 && r != -ENOENT)
+ return log_event_error_errno(dev, token, r, "Failed to read sysctl '%s': %m", buf);
+
+ return token_match_string(token, strstrip(value));
+ }
+ case TK_M_TEST: {
+ mode_t mode = PTR_TO_MODE(token->data);
+ char buf[UDEV_PATH_SIZE];
+ struct stat statbuf;
+ bool match, truncated;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "file name", token->value, "TEST", /* is_match = */ true);
+ return false;
+ }
+
+ if (!path_is_absolute(buf) &&
+ udev_resolve_subsys_kernel(buf, buf, sizeof(buf), false) < 0) {
+ char tmp[UDEV_PATH_SIZE];
+ const char *val;
+
+ r = sd_device_get_syspath(dev, &val);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to get syspath: %m");
+
+ strscpy_full(tmp, sizeof(tmp), buf, &truncated);
+ assert(!truncated);
+ strscpyl_full(buf, sizeof(buf), &truncated, val, "/", tmp, NULL);
+ if (truncated)
+ return false;
+ }
+
+ r = attr_subst_subdir(buf);
+ if (r == -ENOENT)
+ return token->op == OP_NOMATCH;
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to test for the existence of '%s': %m", buf);
+
+ if (stat(buf, &statbuf) < 0)
+ return token->op == OP_NOMATCH;
+
+ if (mode == MODE_INVALID)
+ return token->op == OP_MATCH;
+
+ match = (statbuf.st_mode & mode) > 0;
+ return token->op == (match ? OP_MATCH : OP_NOMATCH);
+ }
+ case TK_M_PROGRAM: {
+ char buf[UDEV_LINE_SIZE], result[UDEV_LINE_SIZE];
+ bool truncated;
+ size_t count;
+
+ event->program_result = mfree(event->program_result);
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "command", token->value, "PROGRAM", /* is_match = */ true);
+ return false;
+ }
+
+ log_event_debug(dev, token, "Running PROGRAM '%s'", buf);
+
+ r = udev_event_spawn(event, timeout_usec, timeout_signal, true, buf, result, sizeof(result), NULL);
+ if (r != 0) {
+ if (r < 0)
+ log_event_warning_errno(dev, token, r, "Failed to execute \"%s\": %m", buf);
+ else /* returned value is positive when program fails */
+ log_event_debug(dev, token, "Command \"%s\" returned %d (error)", buf, r);
+ return token->op == OP_NOMATCH;
+ }
+
+ delete_trailing_chars(result, "\n");
+ count = udev_replace_chars(result, UDEV_ALLOWED_CHARS_INPUT);
+ if (count > 0)
+ log_event_debug(dev, token,
+ "Replaced %zu character(s) in result of \"%s\"",
+ count, buf);
+
+ event->program_result = strdup(result);
+ return token->op == OP_MATCH;
+ }
+ case TK_M_IMPORT_FILE: {
+ _cleanup_fclose_ FILE *f = NULL;
+ char buf[UDEV_PATH_SIZE];
+ bool truncated;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "file name to be imported", token->value, "IMPORT", /* is_match = */ true);
+ return false;
+ }
+
+ log_event_debug(dev, token, "Importing properties from '%s'", buf);
+
+ f = fopen(buf, "re");
+ if (!f) {
+ if (errno != ENOENT)
+ return log_event_error_errno(dev, token, errno, "Failed to open '%s': %m", buf);
+ return token->op == OP_NOMATCH;
+ }
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ char *key, *value;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0) {
+ log_event_debug_errno(dev, token, r, "Failed to read '%s', ignoring: %m", buf);
+ return token->op == OP_NOMATCH;
+ }
+ if (r == 0)
+ break;
+
+ r = get_property_from_string(line, &key, &value);
+ if (r < 0) {
+ log_event_debug_errno(dev, token, r,
+ "Failed to parse key and value from '%s', ignoring: %m",
+ line);
+ continue;
+ }
+ if (r == 0)
+ continue;
+
+ r = device_add_property(dev, key, value);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r,
+ "Failed to add property %s=%s: %m",
+ key, value);
+ }
+
+ return token->op == OP_MATCH;
+ }
+ case TK_M_IMPORT_PROGRAM: {
+ _cleanup_strv_free_ char **lines = NULL;
+ char buf[UDEV_LINE_SIZE], result[UDEV_LINE_SIZE];
+ bool truncated;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "command", token->value, "IMPORT", /* is_match = */ true);
+ return false;
+ }
+
+ log_event_debug(dev, token, "Importing properties from results of '%s'", buf);
+
+ r = udev_event_spawn(event, timeout_usec, timeout_signal, true, buf, result, sizeof result, &truncated);
+ if (r != 0) {
+ if (r < 0)
+ log_event_warning_errno(dev, token, r, "Failed to execute '%s', ignoring: %m", buf);
+ else /* returned value is positive when program fails */
+ log_event_debug(dev, token, "Command \"%s\" returned %d (error), ignoring", buf, r);
+ return token->op == OP_NOMATCH;
+ }
+
+ if (truncated) {
+ bool found = false;
+
+ /* Drop the last line. */
+ for (char *p = PTR_SUB1(buf + strlen(buf), buf); p; p = PTR_SUB1(p, buf))
+ if (strchr(NEWLINE, *p)) {
+ *p = '\0';
+ found = true;
+ } else if (found)
+ break;
+ }
+
+ r = strv_split_newlines_full(&lines, result, EXTRACT_RETAIN_ESCAPE);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_event_warning_errno(dev, token, r,
+ "Failed to extract lines from result of command \"%s\", ignoring: %m", buf);
+ return false;
+ }
+
+ STRV_FOREACH(line, lines) {
+ char *key, *value;
+
+ r = get_property_from_string(*line, &key, &value);
+ if (r < 0) {
+ log_event_debug_errno(dev, token, r,
+ "Failed to parse key and value from '%s', ignoring: %m",
+ *line);
+ continue;
+ }
+ if (r == 0)
+ continue;
+
+ r = device_add_property(dev, key, value);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r,
+ "Failed to add property %s=%s: %m",
+ key, value);
+ }
+
+ return token->op == OP_MATCH;
+ }
+ case TK_M_IMPORT_BUILTIN: {
+ UdevBuiltinCommand cmd = PTR_TO_UDEV_BUILTIN_CMD(token->data);
+ assert(cmd >= 0 && cmd < _UDEV_BUILTIN_MAX);
+ unsigned mask = 1U << (int) cmd;
+ char buf[UDEV_LINE_SIZE];
+ bool truncated;
+
+ if (udev_builtin_run_once(cmd)) {
+ /* check if we ran already */
+ if (event->builtin_run & mask) {
+ log_event_debug(dev, token, "Skipping builtin '%s' in IMPORT key",
+ udev_builtin_name(cmd));
+ /* return the result from earlier run */
+ return token->op == (event->builtin_ret & mask ? OP_NOMATCH : OP_MATCH);
+ }
+ /* mark as ran */
+ event->builtin_run |= mask;
+ }
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "builtin command", token->value, "IMPORT", /* is_match = */ true);
+ return false;
+ }
+
+ log_event_debug(dev, token, "Importing properties from results of builtin command '%s'", buf);
+
+ r = udev_builtin_run(event, cmd, buf, false);
+ if (r < 0) {
+ /* remember failure */
+ log_event_debug_errno(dev, token, r, "Failed to run builtin '%s': %m", buf);
+ event->builtin_ret |= mask;
+ }
+ return token->op == (r >= 0 ? OP_MATCH : OP_NOMATCH);
+ }
+ case TK_M_IMPORT_DB: {
+ const char *val;
+
+ if (!event->dev_db_clone)
+ return token->op == OP_NOMATCH;
+ r = sd_device_get_property_value(event->dev_db_clone, token->value, &val);
+ if (r == -ENOENT)
+ return token->op == OP_NOMATCH;
+ if (r < 0)
+ return log_event_error_errno(dev, token, r,
+ "Failed to get property '%s' from database: %m",
+ token->value);
+
+ r = device_add_property(dev, token->value, val);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to add property '%s=%s': %m",
+ token->value, val);
+ return token->op == OP_MATCH;
+ }
+ case TK_M_IMPORT_CMDLINE: {
+ _cleanup_free_ char *value = NULL;
+
+ r = proc_cmdline_get_key(token->value, PROC_CMDLINE_VALUE_OPTIONAL|PROC_CMDLINE_IGNORE_EFI_OPTIONS, &value);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r,
+ "Failed to read '%s' option from /proc/cmdline: %m",
+ token->value);
+ if (r == 0)
+ return token->op == OP_NOMATCH;
+
+ r = device_add_property(dev, token->value, value ?: "1");
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to add property '%s=%s': %m",
+ token->value, value ?: "1");
+ return token->op == OP_MATCH;
+ }
+ case TK_M_IMPORT_PARENT: {
+ char buf[UDEV_PATH_SIZE];
+ bool truncated;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "property name", token->value, "IMPORT", /* is_match = */ true);
+ return false;
+ }
+
+ r = import_parent_into_properties(dev, buf);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r,
+ "Failed to import properties '%s' from parent: %m",
+ buf);
+ return token->op == (r > 0 ? OP_MATCH : OP_NOMATCH);
+ }
+ case TK_M_RESULT:
+ return token_match_string(token, event->program_result);
+ case TK_A_OPTIONS_STRING_ESCAPE_NONE:
+ event->esc = ESCAPE_NONE;
+ break;
+ case TK_A_OPTIONS_STRING_ESCAPE_REPLACE:
+ event->esc = ESCAPE_REPLACE;
+ break;
+ case TK_A_OPTIONS_DB_PERSIST:
+ device_set_db_persist(dev);
+ break;
+ case TK_A_OPTIONS_INOTIFY_WATCH:
+ if (event->inotify_watch_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->inotify_watch_final = true;
+
+ event->inotify_watch = token->data;
+ break;
+ case TK_A_OPTIONS_DEVLINK_PRIORITY:
+ device_set_devlink_priority(dev, PTR_TO_INT(token->data));
+ break;
+ case TK_A_OPTIONS_LOG_LEVEL: {
+ int level = PTR_TO_INT(token->data);
+
+ if (level < 0)
+ level = event->default_log_level;
+
+ log_set_max_level(level);
+
+ if (level == LOG_DEBUG && !event->log_level_was_debug) {
+ /* The log level becomes LOG_DEBUG at first time. Let's log basic information. */
+ log_device_uevent(dev, "The log level is changed to 'debug' while processing device");
+ event->log_level_was_debug = true;
+ }
+
+ break;
+ }
+ case TK_A_OWNER: {
+ char owner[UDEV_NAME_SIZE];
+ const char *ow = owner;
+ bool truncated;
+
+ if (event->owner_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->owner_final = true;
+
+ (void) udev_event_apply_format(event, token->value, owner, sizeof(owner), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "user name", token->value, "OWNER", /* is_match = */ false);
+ break;
+ }
+
+ r = get_user_creds(&ow, &event->uid, NULL, NULL, NULL, USER_CREDS_ALLOW_MISSING);
+ if (r < 0)
+ log_unknown_owner(dev, token->rule_line, r, "user", owner);
+ else
+ log_event_debug(dev, token, "OWNER %s(%u)", owner, event->uid);
+ break;
+ }
+ case TK_A_GROUP: {
+ char group[UDEV_NAME_SIZE];
+ const char *gr = group;
+ bool truncated;
+
+ if (event->group_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->group_final = true;
+
+ (void) udev_event_apply_format(event, token->value, group, sizeof(group), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "group name", token->value, "GROUP", /* is_match = */ false);
+ break;
+ }
+
+ r = get_group_creds(&gr, &event->gid, USER_CREDS_ALLOW_MISSING);
+ if (r < 0)
+ log_unknown_owner(dev, token->rule_line, r, "group", group);
+ else
+ log_event_debug(dev, token, "GROUP %s(%u)", group, event->gid);
+ break;
+ }
+ case TK_A_MODE: {
+ char mode_str[UDEV_NAME_SIZE];
+ bool truncated;
+
+ if (event->mode_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->mode_final = true;
+
+ (void) udev_event_apply_format(event, token->value, mode_str, sizeof(mode_str), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "mode", token->value, "MODE", /* is_match = */ false);
+ break;
+ }
+
+ r = parse_mode(mode_str, &event->mode);
+ if (r < 0)
+ log_event_error_errno(dev, token, r, "Failed to parse mode '%s', ignoring: %m", mode_str);
+ else
+ log_event_debug(dev, token, "MODE %#o", event->mode);
+ break;
+ }
+ case TK_A_OWNER_ID:
+ if (event->owner_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->owner_final = true;
+ if (!token->data)
+ break;
+ event->uid = PTR_TO_UID(token->data);
+ log_event_debug(dev, token, "OWNER %u", event->uid);
+ break;
+ case TK_A_GROUP_ID:
+ if (event->group_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->group_final = true;
+ if (!token->data)
+ break;
+ event->gid = PTR_TO_GID(token->data);
+ log_event_debug(dev, token, "GROUP %u", event->gid);
+ break;
+ case TK_A_MODE_ID:
+ if (event->mode_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->mode_final = true;
+ if (!token->data)
+ break;
+ event->mode = PTR_TO_MODE(token->data);
+ log_event_debug(dev, token, "MODE %#o", event->mode);
+ break;
+ case TK_A_SECLABEL: {
+ _cleanup_free_ char *name = NULL, *label = NULL;
+ char label_str[UDEV_LINE_SIZE] = {};
+ bool truncated;
+
+ name = strdup(token->data);
+ if (!name)
+ return log_oom();
+
+ (void) udev_event_apply_format(event, token->value, label_str, sizeof(label_str), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "security label", token->value, "SECLABEL", /* is_match = */ false);
+ break;
+ }
+
+ if (!isempty(label_str))
+ label = strdup(label_str);
+ else
+ label = strdup(token->value);
+ if (!label)
+ return log_oom();
+
+ if (token->op == OP_ASSIGN)
+ ordered_hashmap_clear_free_free(event->seclabel_list);
+
+ r = ordered_hashmap_ensure_put(&event->seclabel_list, NULL, name, label);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to store SECLABEL{%s}='%s': %m", name, label);
+
+ log_event_debug(dev, token, "SECLABEL{%s}='%s'", name, label);
+
+ TAKE_PTR(name);
+ TAKE_PTR(label);
+ break;
+ }
+ case TK_A_ENV: {
+ const char *val, *name = token->data;
+ char value_new[UDEV_NAME_SIZE], *p = value_new;
+ size_t count, l = sizeof(value_new);
+ bool truncated;
+
+ if (isempty(token->value)) {
+ if (token->op == OP_ADD)
+ break;
+ r = device_add_property(dev, name, NULL);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to remove property '%s': %m", name);
+ break;
+ }
+
+ if (token->op == OP_ADD &&
+ device_get_property_value_with_fallback(dev, name, properties_list, &val) >= 0) {
+ l = strpcpyl_full(&p, l, &truncated, val, " ", NULL);
+ if (truncated) {
+ log_event_warning(dev, token,
+ "The buffer for the property '%s' is full, "
+ "refusing to append the new value '%s'.", name, token->value);
+ break;
+ }
+ }
+
+ (void) udev_event_apply_format(event, token->value, p, l, false, properties_list, &truncated);
+ if (truncated) {
+ _cleanup_free_ char *key_with_name = strjoin("ENV{", name, "}");
+ log_event_truncated(dev, token, "property value", token->value,
+ key_with_name ?: "ENV", /* is_match = */ false);
+ break;
+ }
+
+ if (event->esc == ESCAPE_REPLACE) {
+ count = udev_replace_chars(p, NULL);
+ if (count > 0)
+ log_event_debug(dev, token,
+ "Replaced %zu slash(es) from result of ENV{%s}%s=\"%s\"",
+ count, name, token->op == OP_ADD ? "+" : "", token->value);
+ }
+
+ r = device_add_property(dev, name, value_new);
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to add property '%s=%s': %m", name, value_new);
+ break;
+ }
+ case TK_A_TAG: {
+ char buf[UDEV_PATH_SIZE];
+ bool truncated;
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "tag name", token->value, "TAG", /* is_match = */ false);
+ break;
+ }
+
+ if (token->op == OP_ASSIGN)
+ device_cleanup_tags(dev);
+
+ if (token->op == OP_REMOVE)
+ device_remove_tag(dev, buf);
+ else {
+ r = device_add_tag(dev, buf, true);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ log_event_warning_errno(dev, token, r, "Failed to add tag '%s', ignoring: %m", buf);
+ }
+ break;
+ }
+ case TK_A_NAME: {
+ char buf[UDEV_PATH_SIZE];
+ bool truncated;
+ size_t count;
+
+ if (event->name_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->name_final = true;
+
+ if (sd_device_get_ifindex(dev, NULL) < 0) {
+ log_event_error(dev, token,
+ "Only network interfaces can be renamed, ignoring NAME=\"%s\".",
+ token->value);
+ break;
+ }
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "network interface name", token->value, "NAME", /* is_match = */ false);
+ break;
+ }
+
+ if (IN_SET(event->esc, ESCAPE_UNSET, ESCAPE_REPLACE)) {
+ if (naming_scheme_has(NAMING_REPLACE_STRICTLY))
+ count = udev_replace_ifname(buf);
+ else
+ count = udev_replace_chars(buf, "/");
+ if (count > 0)
+ log_event_debug(dev, token,
+ "Replaced %zu character(s) from result of NAME=\"%s\"",
+ count, token->value);
+ }
+ r = free_and_strdup_warn(&event->name, buf);
+ if (r < 0)
+ return r;
+
+ log_event_debug(dev, token, "NAME '%s'", event->name);
+ break;
+ }
+ case TK_A_DEVLINK: {
+ char buf[UDEV_PATH_SIZE];
+ bool truncated;
+ size_t count;
+
+ if (event->devlink_final)
+ break;
+ if (sd_device_get_devnum(dev, NULL) < 0)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->devlink_final = true;
+ if (IN_SET(token->op, OP_ASSIGN, OP_ASSIGN_FINAL))
+ device_cleanup_devlinks(dev);
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf),
+ /* replace_whitespace = */ event->esc != ESCAPE_NONE, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "symbolic link path", token->value, "SYMLINK", /* is_match = */ false);
+ break;
+ }
+
+ /* By default or string_escape=none, allow multiple symlinks separated by spaces. */
+ if (event->esc == ESCAPE_UNSET)
+ count = udev_replace_chars(buf, /* allow = */ "/ ");
+ else if (event->esc == ESCAPE_REPLACE)
+ count = udev_replace_chars(buf, /* allow = */ "/");
+ else
+ count = 0;
+ if (count > 0)
+ log_event_debug(dev, token,
+ "Replaced %zu character(s) from result of SYMLINK=\"%s\"",
+ count, token->value);
+
+ for (const char *p = buf;;) {
+ _cleanup_free_ char *path = NULL;
+
+ r = extract_first_word(&p, &path, NULL, EXTRACT_RETAIN_ESCAPE);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0) {
+ log_warning_errno(r, "Failed to extract first path in SYMLINK=, ignoring: %m");
+ break;
+ }
+ if (r == 0)
+ break;
+
+ if (token->op == OP_REMOVE) {
+ r = device_remove_devlink(dev, path);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ log_event_warning_errno(dev, token, r, "Failed to remove devlink '%s', ignoring: %m", path);
+ else if (r > 0)
+ log_event_debug(dev, token, "Dropped SYMLINK '%s'", path);
+ } else {
+ r = device_add_devlink(dev, path);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ log_event_warning_errno(dev, token, r, "Failed to add devlink '%s', ignoring: %m", path);
+ else if (r > 0)
+ log_event_debug(dev, token, "Added SYMLINK '%s'", path);
+ }
+ }
+ break;
+ }
+ case TK_A_ATTR: {
+ char buf[UDEV_PATH_SIZE], value[UDEV_NAME_SIZE];
+ const char *val, *key_name = token->data;
+ bool truncated;
+
+ if (udev_resolve_subsys_kernel(key_name, buf, sizeof(buf), false) < 0 &&
+ sd_device_get_syspath(dev, &val) >= 0) {
+ strscpyl_full(buf, sizeof(buf), &truncated, val, "/", key_name, NULL);
+ if (truncated) {
+ log_event_warning(dev, token,
+ "The path to the attribute '%s/%s' is too long, refusing to set the attribute.",
+ val, key_name);
+ break;
+ }
+ }
+
+ r = attr_subst_subdir(buf);
+ if (r < 0) {
+ log_event_error_errno(dev, token, r, "Could not find file matches '%s', ignoring: %m", buf);
+ break;
+ }
+ (void) udev_event_apply_format(event, token->value, value, sizeof(value), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "attribute value", token->value, "ATTR", /* is_match = */ false);
+ break;
+ }
+
+ log_event_debug(dev, token, "ATTR '%s' writing '%s'", buf, value);
+ r = write_string_file(buf, value,
+ WRITE_STRING_FILE_VERIFY_ON_FAILURE |
+ WRITE_STRING_FILE_DISABLE_BUFFER |
+ WRITE_STRING_FILE_AVOID_NEWLINE |
+ WRITE_STRING_FILE_VERIFY_IGNORE_NEWLINE);
+ if (r < 0)
+ log_event_error_errno(dev, token, r, "Failed to write ATTR{%s}, ignoring: %m", buf);
+ break;
+ }
+ case TK_A_SYSCTL: {
+ char buf[UDEV_PATH_SIZE], value[UDEV_NAME_SIZE];
+ bool truncated;
+
+ (void) udev_event_apply_format(event, token->data, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "sysctl entry name", token->data, "SYSCTL", /* is_match = */ false);
+ break;
+ }
+
+ (void) udev_event_apply_format(event, token->value, value, sizeof(value), false, properties_list, &truncated);
+ if (truncated) {
+ _cleanup_free_ char *key_with_name = strjoin("SYSCTL{", buf, "}");
+ log_event_truncated(dev, token, "sysctl value", token->value,
+ key_with_name ?: "SYSCTL", /* is_match = */ false);
+ break;
+ }
+
+ sysctl_normalize(buf);
+ log_event_debug(dev, token, "SYSCTL '%s' writing '%s'", buf, value);
+ r = sysctl_write(buf, value);
+ if (r < 0)
+ log_event_error_errno(dev, token, r, "Failed to write SYSCTL{%s}='%s', ignoring: %m", buf, value);
+ break;
+ }
+ case TK_A_RUN_BUILTIN:
+ case TK_A_RUN_PROGRAM: {
+ _cleanup_free_ char *cmd = NULL;
+ char buf[UDEV_LINE_SIZE];
+ bool truncated;
+
+ if (event->run_final)
+ break;
+ if (token->op == OP_ASSIGN_FINAL)
+ event->run_final = true;
+
+ if (IN_SET(token->op, OP_ASSIGN, OP_ASSIGN_FINAL))
+ ordered_hashmap_clear_free_key(event->run_list);
+
+ (void) udev_event_apply_format(event, token->value, buf, sizeof(buf), false, properties_list, &truncated);
+ if (truncated) {
+ log_event_truncated(dev, token, "command", token->value,
+ token->type == TK_A_RUN_BUILTIN ? "RUN{builtin}" : "RUN{program}",
+ /* is_match = */ false);
+ break;
+ }
+
+ cmd = strdup(buf);
+ if (!cmd)
+ return log_oom();
+
+ r = ordered_hashmap_ensure_put(&event->run_list, NULL, cmd, token->data);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_event_error_errno(dev, token, r, "Failed to store command '%s': %m", cmd);
+
+ TAKE_PTR(cmd);
+
+ log_event_debug(dev, token, "RUN '%s'", token->value);
+ break;
+ }
+ case TK_A_OPTIONS_STATIC_NODE:
+ /* do nothing for events. */
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return true;
+}
+
+static bool token_is_for_parents(UdevRuleToken *token) {
+ return token->type >= TK_M_PARENTS_KERNEL && token->type <= TK_M_PARENTS_TAG;
+}
+
+static int udev_rule_apply_parent_token_to_event(
+ UdevRuleToken *head_token,
+ UdevEvent *event,
+ int timeout_signal) {
+
+ int r;
+
+ assert(head_token);
+ assert(event);
+
+ event->dev_parent = ASSERT_PTR(event->dev);
+
+ for (;;) {
+ LIST_FOREACH(tokens, token, head_token) {
+ if (!token_is_for_parents(token))
+ return true; /* All parent tokens match. */
+
+ r = udev_rule_apply_token_to_event(token, event->dev_parent, event, 0, timeout_signal, NULL);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+ }
+ if (r > 0)
+ /* All parent tokens match, and no more token (except for GOTO) in the line. */
+ return true;
+
+ if (sd_device_get_parent(event->dev_parent, &event->dev_parent) < 0) {
+ event->dev_parent = NULL;
+ return false;
+ }
+ }
+}
+
+static int udev_rule_apply_line_to_event(
+ UdevRuleLine *line,
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list,
+ UdevRuleLine **next_line) {
+
+ UdevRuleLineType mask = LINE_HAS_GOTO | LINE_UPDATE_SOMETHING;
+ bool parents_done = false;
+ sd_device_action_t action;
+ int r;
+
+ assert(line);
+ assert(event);
+ assert(next_line);
+
+ r = sd_device_get_action(event->dev, &action);
+ if (r < 0)
+ return r;
+
+ if (action != SD_DEVICE_REMOVE) {
+ if (sd_device_get_devnum(event->dev, NULL) >= 0)
+ mask |= LINE_HAS_DEVLINK;
+
+ if (sd_device_get_ifindex(event->dev, NULL) >= 0)
+ mask |= LINE_HAS_NAME;
+ }
+
+ if ((line->type & mask) == 0)
+ return 0;
+
+ event->esc = ESCAPE_UNSET;
+
+ DEVICE_TRACE_POINT(rules_apply_line, event->dev, line->rule_file->filename, line->line_number);
+
+ LIST_FOREACH(tokens, token, line->tokens) {
+ if (token_is_for_parents(token)) {
+ if (parents_done)
+ continue;
+
+ r = udev_rule_apply_parent_token_to_event(token, event, timeout_signal);
+ if (r <= 0)
+ return r;
+
+ parents_done = true;
+ continue;
+ }
+
+ r = udev_rule_apply_token_to_event(token, event->dev, event, timeout_usec, timeout_signal, properties_list);
+ if (r <= 0)
+ return r;
+ }
+
+ if (line->goto_line)
+ *next_line = line->goto_line; /* update next_line only when the line has GOTO token. */
+
+ return 0;
+}
+
+int udev_rules_apply_to_event(
+ UdevRules *rules,
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list) {
+
+ int r;
+
+ assert(rules);
+ assert(event);
+
+ LIST_FOREACH(rule_files, file, rules->rule_files)
+ LIST_FOREACH_WITH_NEXT(rule_lines, line, next_line, file->rule_lines) {
+ r = udev_rule_apply_line_to_event(line, event, timeout_usec, timeout_signal, properties_list, &next_line);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int udev_rule_line_apply_static_dev_perms(UdevRuleLine *rule_line) {
+ _cleanup_strv_free_ char **tags = NULL;
+ uid_t uid = UID_INVALID;
+ gid_t gid = GID_INVALID;
+ mode_t mode = MODE_INVALID;
+ int r;
+
+ assert(rule_line);
+
+ if (!FLAGS_SET(rule_line->type, LINE_HAS_STATIC_NODE))
+ return 0;
+
+ LIST_FOREACH(tokens, token, rule_line->tokens)
+ if (token->type == TK_A_OWNER_ID)
+ uid = PTR_TO_UID(token->data);
+ else if (token->type == TK_A_GROUP_ID)
+ gid = PTR_TO_GID(token->data);
+ else if (token->type == TK_A_MODE_ID)
+ mode = PTR_TO_MODE(token->data);
+ else if (token->type == TK_A_TAG) {
+ r = strv_extend(&tags, token->value);
+ if (r < 0)
+ return log_oom();
+ } else if (token->type == TK_A_OPTIONS_STATIC_NODE) {
+ r = static_node_apply_permissions(token->value, mode, uid, gid, tags);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int udev_rules_apply_static_dev_perms(UdevRules *rules) {
+ int r;
+
+ assert(rules);
+
+ LIST_FOREACH(rule_files, file, rules->rule_files)
+ LIST_FOREACH(rule_lines, line, file->rule_lines) {
+ r = udev_rule_line_apply_static_dev_perms(line);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static const char* const resolve_name_timing_table[_RESOLVE_NAME_TIMING_MAX] = {
+ [RESOLVE_NAME_NEVER] = "never",
+ [RESOLVE_NAME_LATE] = "late",
+ [RESOLVE_NAME_EARLY] = "early",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(resolve_name_timing, ResolveNameTiming);
diff --git a/src/udev/udev-rules.h b/src/udev/udev-rules.h
new file mode 100644
index 0000000..4352312
--- /dev/null
+++ b/src/udev/udev-rules.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "alloc-util.h"
+#include "hashmap.h"
+#include "time-util.h"
+
+#define UDEV_NAME_SIZE 512
+#define UDEV_PATH_SIZE 1024
+#define UDEV_LINE_SIZE 16384
+
+typedef struct UdevRuleFile UdevRuleFile;
+typedef struct UdevRules UdevRules;
+typedef struct UdevEvent UdevEvent;
+
+typedef enum {
+ ESCAPE_UNSET,
+ ESCAPE_NONE, /* OPTIONS="string_escape=none" */
+ ESCAPE_REPLACE, /* OPTIONS="string_escape=replace" */
+ _ESCAPE_TYPE_MAX,
+ _ESCAPE_TYPE_INVALID = -EINVAL,
+} UdevRuleEscapeType;
+
+typedef enum ResolveNameTiming {
+ RESOLVE_NAME_NEVER,
+ RESOLVE_NAME_LATE,
+ RESOLVE_NAME_EARLY,
+ _RESOLVE_NAME_TIMING_MAX,
+ _RESOLVE_NAME_TIMING_INVALID = -EINVAL,
+} ResolveNameTiming;
+
+int udev_rule_parse_value(char *str, char **ret_value, char **ret_endpos);
+int udev_rules_parse_file(UdevRules *rules, const char *filename, bool extra_checks, UdevRuleFile **ret);
+unsigned udev_rule_file_get_issues(UdevRuleFile *rule_file);
+UdevRules* udev_rules_new(ResolveNameTiming resolve_name_timing);
+int udev_rules_load(UdevRules **ret_rules, ResolveNameTiming resolve_name_timing);
+UdevRules *udev_rules_free(UdevRules *rules);
+DEFINE_TRIVIAL_CLEANUP_FUNC(UdevRules*, udev_rules_free);
+#define udev_rules_free_and_replace(a, b) free_and_replace_full(a, b, udev_rules_free)
+
+bool udev_rules_should_reload(UdevRules *rules);
+int udev_rules_apply_to_event(UdevRules *rules, UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ Hashmap *properties_list);
+int udev_rules_apply_static_dev_perms(UdevRules *rules);
+
+ResolveNameTiming resolve_name_timing_from_string(const char *s) _pure_;
+const char *resolve_name_timing_to_string(ResolveNameTiming i) _const_;
diff --git a/src/udev/udev-spawn.c b/src/udev/udev-spawn.c
new file mode 100644
index 0000000..67a3005
--- /dev/null
+++ b/src/udev/udev-spawn.c
@@ -0,0 +1,355 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "sd-event.h"
+
+#include "device-private.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "signal-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "udev-builtin.h"
+#include "udev-event.h"
+#include "udev-spawn.h"
+#include "udev-trace.h"
+
+typedef struct Spawn {
+ sd_device *device;
+ const char *cmd;
+ pid_t pid;
+ usec_t timeout_warn_usec;
+ usec_t timeout_usec;
+ int timeout_signal;
+ usec_t event_birth_usec;
+ bool accept_failure;
+ int fd_stdout;
+ int fd_stderr;
+ char *result;
+ size_t result_size;
+ size_t result_len;
+ bool truncated;
+} Spawn;
+
+static int on_spawn_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ Spawn *spawn = ASSERT_PTR(userdata);
+ char buf[4096], *p;
+ size_t size;
+ ssize_t l;
+ int r;
+
+ assert(fd == spawn->fd_stdout || fd == spawn->fd_stderr);
+ assert(!spawn->result || spawn->result_len < spawn->result_size);
+
+ if (fd == spawn->fd_stdout && spawn->result) {
+ p = spawn->result + spawn->result_len;
+ size = spawn->result_size - spawn->result_len;
+ } else {
+ p = buf;
+ size = sizeof(buf);
+ }
+
+ l = read(fd, p, size - (p == buf));
+ if (l < 0) {
+ if (errno == EAGAIN)
+ goto reenable;
+
+ log_device_error_errno(spawn->device, errno,
+ "Failed to read stdout of '%s': %m", spawn->cmd);
+
+ return 0;
+ }
+
+ if ((size_t) l == size) {
+ log_device_warning(spawn->device, "Truncating stdout of '%s' up to %zu byte.",
+ spawn->cmd, spawn->result_size);
+ l--;
+ spawn->truncated = true;
+ }
+
+ p[l] = '\0';
+ if (fd == spawn->fd_stdout && spawn->result)
+ spawn->result_len += l;
+
+ /* Log output only if we watch stderr. */
+ if (l > 0 && spawn->fd_stderr >= 0) {
+ _cleanup_strv_free_ char **v = NULL;
+
+ r = strv_split_newlines_full(&v, p, EXTRACT_RETAIN_ESCAPE);
+ if (r < 0)
+ log_device_debug(spawn->device,
+ "Failed to split output from '%s'(%s), ignoring: %m",
+ spawn->cmd, fd == spawn->fd_stdout ? "out" : "err");
+
+ STRV_FOREACH(q, v)
+ log_device_debug(spawn->device, "'%s'(%s) '%s'", spawn->cmd,
+ fd == spawn->fd_stdout ? "out" : "err", *q);
+ }
+
+ if (l == 0 || spawn->truncated)
+ return 0;
+
+reenable:
+ /* Re-enable the event source if we did not encounter EOF */
+
+ r = sd_event_source_set_enabled(s, SD_EVENT_ONESHOT);
+ if (r < 0)
+ log_device_error_errno(spawn->device, r,
+ "Failed to reactivate IO source of '%s'", spawn->cmd);
+ return 0;
+}
+
+static int on_spawn_timeout(sd_event_source *s, uint64_t usec, void *userdata) {
+ Spawn *spawn = ASSERT_PTR(userdata);
+
+ DEVICE_TRACE_POINT(spawn_timeout, spawn->device, spawn->cmd);
+
+ kill_and_sigcont(spawn->pid, spawn->timeout_signal);
+
+ log_device_error(spawn->device, "Spawned process '%s' ["PID_FMT"] timed out after %s, killing",
+ spawn->cmd, spawn->pid,
+ FORMAT_TIMESPAN(spawn->timeout_usec, USEC_PER_SEC));
+
+ return 1;
+}
+
+static int on_spawn_timeout_warning(sd_event_source *s, uint64_t usec, void *userdata) {
+ Spawn *spawn = ASSERT_PTR(userdata);
+
+ log_device_warning(spawn->device, "Spawned process '%s' ["PID_FMT"] is taking longer than %s to complete",
+ spawn->cmd, spawn->pid,
+ FORMAT_TIMESPAN(spawn->timeout_warn_usec, USEC_PER_SEC));
+
+ return 1;
+}
+
+static int on_spawn_sigchld(sd_event_source *s, const siginfo_t *si, void *userdata) {
+ Spawn *spawn = ASSERT_PTR(userdata);
+ int ret = -EIO;
+
+ switch (si->si_code) {
+ case CLD_EXITED:
+ if (si->si_status == 0)
+ log_device_debug(spawn->device, "Process '%s' succeeded.", spawn->cmd);
+ else
+ log_device_full(spawn->device, spawn->accept_failure ? LOG_DEBUG : LOG_WARNING,
+ "Process '%s' failed with exit code %i.", spawn->cmd, si->si_status);
+ ret = si->si_status;
+ break;
+ case CLD_KILLED:
+ case CLD_DUMPED:
+ log_device_error(spawn->device, "Process '%s' terminated by signal %s.", spawn->cmd, signal_to_string(si->si_status));
+ break;
+ default:
+ log_device_error(spawn->device, "Process '%s' failed due to unknown reason.", spawn->cmd);
+ }
+
+ DEVICE_TRACE_POINT(spawn_exit, spawn->device, spawn->cmd);
+
+ sd_event_exit(sd_event_source_get_event(s), ret);
+ return 1;
+}
+
+static int spawn_wait(Spawn *spawn) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_event_source_disable_unrefp) sd_event_source *sigchld_source = NULL;
+ _cleanup_(sd_event_source_disable_unrefp) sd_event_source *stdout_source = NULL;
+ _cleanup_(sd_event_source_disable_unrefp) sd_event_source *stderr_source = NULL;
+ int r;
+
+ assert(spawn);
+
+ r = sd_event_new(&e);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to allocate sd-event object: %m");
+
+ if (spawn->timeout_usec > 0) {
+ usec_t usec, age_usec;
+
+ usec = now(CLOCK_MONOTONIC);
+ age_usec = usec - spawn->event_birth_usec;
+ if (age_usec < spawn->timeout_usec) {
+ if (spawn->timeout_warn_usec > 0 &&
+ spawn->timeout_warn_usec < spawn->timeout_usec &&
+ spawn->timeout_warn_usec > age_usec) {
+ spawn->timeout_warn_usec -= age_usec;
+
+ r = sd_event_add_time(e, NULL, CLOCK_MONOTONIC,
+ usec + spawn->timeout_warn_usec, USEC_PER_SEC,
+ on_spawn_timeout_warning, spawn);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to create timeout warning event source: %m");
+ }
+
+ spawn->timeout_usec -= age_usec;
+
+ r = sd_event_add_time(e, NULL, CLOCK_MONOTONIC,
+ usec + spawn->timeout_usec, USEC_PER_SEC, on_spawn_timeout, spawn);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to create timeout event source: %m");
+ }
+ }
+
+ if (spawn->fd_stdout >= 0) {
+ r = sd_event_add_io(e, &stdout_source, spawn->fd_stdout, EPOLLIN, on_spawn_io, spawn);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to create stdio event source: %m");
+ r = sd_event_source_set_enabled(stdout_source, SD_EVENT_ONESHOT);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to enable stdio event source: %m");
+ }
+
+ if (spawn->fd_stderr >= 0) {
+ r = sd_event_add_io(e, &stderr_source, spawn->fd_stderr, EPOLLIN, on_spawn_io, spawn);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to create stderr event source: %m");
+ r = sd_event_source_set_enabled(stderr_source, SD_EVENT_ONESHOT);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to enable stderr event source: %m");
+ }
+
+ r = sd_event_add_child(e, &sigchld_source, spawn->pid, WEXITED, on_spawn_sigchld, spawn);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to create sigchild event source: %m");
+ /* SIGCHLD should be processed after IO is complete */
+ r = sd_event_source_set_priority(sigchld_source, SD_EVENT_PRIORITY_NORMAL + 1);
+ if (r < 0)
+ return log_device_debug_errno(spawn->device, r, "Failed to set priority to sigchild event source: %m");
+
+ return sd_event_loop(e);
+}
+
+int udev_event_spawn(
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ bool accept_failure,
+ const char *cmd,
+ char *result,
+ size_t result_size,
+ bool *ret_truncated) {
+
+ _cleanup_close_pair_ int outpipe[2] = EBADF_PAIR, errpipe[2] = EBADF_PAIR;
+ _cleanup_strv_free_ char **argv = NULL;
+ char **envp = NULL;
+ Spawn spawn;
+ pid_t pid;
+ int r;
+
+ assert(event);
+ assert(event->dev);
+ assert(result || result_size == 0);
+
+ /* pipes from child to parent */
+ if (result || log_get_max_level() >= LOG_INFO)
+ if (pipe2(outpipe, O_NONBLOCK|O_CLOEXEC) != 0)
+ return log_device_error_errno(event->dev, errno,
+ "Failed to create pipe for command '%s': %m", cmd);
+
+ if (log_get_max_level() >= LOG_INFO)
+ if (pipe2(errpipe, O_NONBLOCK|O_CLOEXEC) != 0)
+ return log_device_error_errno(event->dev, errno,
+ "Failed to create pipe for command '%s': %m", cmd);
+
+ r = strv_split_full(&argv, cmd, NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX | EXTRACT_RETAIN_ESCAPE);
+ if (r < 0)
+ return log_device_error_errno(event->dev, r, "Failed to split command: %m");
+
+ if (isempty(argv[0]))
+ return log_device_error_errno(event->dev, SYNTHETIC_ERRNO(EINVAL),
+ "Invalid command '%s'", cmd);
+
+ /* allow programs in /usr/lib/udev/ to be called without the path */
+ if (!path_is_absolute(argv[0])) {
+ char *program;
+
+ program = path_join(UDEVLIBEXECDIR, argv[0]);
+ if (!program)
+ return log_oom();
+
+ free_and_replace(argv[0], program);
+ }
+
+ r = device_get_properties_strv(event->dev, &envp);
+ if (r < 0)
+ return log_device_error_errno(event->dev, r, "Failed to get device properties");
+
+ log_device_debug(event->dev, "Starting '%s'", cmd);
+
+ r = safe_fork_full("(spawn)",
+ (int[]) { -EBADF, outpipe[WRITE_END], errpipe[WRITE_END] },
+ NULL, 0,
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG|FORK_RLIMIT_NOFILE_SAFE,
+ &pid);
+ if (r < 0)
+ return log_device_error_errno(event->dev, r,
+ "Failed to fork() to execute command '%s': %m", cmd);
+ if (r == 0) {
+ DEVICE_TRACE_POINT(spawn_exec, event->dev, cmd);
+ execve(argv[0], argv, envp);
+ _exit(EXIT_FAILURE);
+ }
+
+ /* parent closed child's ends of pipes */
+ outpipe[WRITE_END] = safe_close(outpipe[WRITE_END]);
+ errpipe[WRITE_END] = safe_close(errpipe[WRITE_END]);
+
+ spawn = (Spawn) {
+ .device = event->dev,
+ .cmd = cmd,
+ .pid = pid,
+ .accept_failure = accept_failure,
+ .timeout_warn_usec = udev_warn_timeout(timeout_usec),
+ .timeout_usec = timeout_usec,
+ .timeout_signal = timeout_signal,
+ .event_birth_usec = event->birth_usec,
+ .fd_stdout = outpipe[READ_END],
+ .fd_stderr = errpipe[READ_END],
+ .result = result,
+ .result_size = result_size,
+ };
+ r = spawn_wait(&spawn);
+ if (r < 0)
+ return log_device_error_errno(event->dev, r,
+ "Failed to wait for spawned command '%s': %m", cmd);
+
+ if (result)
+ result[spawn.result_len] = '\0';
+
+ if (ret_truncated)
+ *ret_truncated = spawn.truncated;
+
+ return r; /* 0 for success, and positive if the program failed */
+}
+
+void udev_event_execute_run(UdevEvent *event, usec_t timeout_usec, int timeout_signal) {
+ const char *command;
+ void *val;
+ int r;
+
+ ORDERED_HASHMAP_FOREACH_KEY(val, command, event->run_list) {
+ UdevBuiltinCommand builtin_cmd = PTR_TO_UDEV_BUILTIN_CMD(val);
+
+ if (builtin_cmd != _UDEV_BUILTIN_INVALID) {
+ log_device_debug(event->dev, "Running built-in command \"%s\"", command);
+ r = udev_builtin_run(event, builtin_cmd, command, false);
+ if (r < 0)
+ log_device_debug_errno(event->dev, r, "Failed to run built-in command \"%s\", ignoring: %m", command);
+ } else {
+ if (event->exec_delay_usec > 0) {
+ log_device_debug(event->dev, "Delaying execution of \"%s\" for %s.",
+ command, FORMAT_TIMESPAN(event->exec_delay_usec, USEC_PER_SEC));
+ (void) usleep_safe(event->exec_delay_usec);
+ }
+
+ log_device_debug(event->dev, "Running command \"%s\"", command);
+
+ r = udev_event_spawn(event, timeout_usec, timeout_signal, false, command, NULL, 0, NULL);
+ if (r < 0)
+ log_device_warning_errno(event->dev, r, "Failed to execute '%s', ignoring: %m", command);
+ else if (r > 0) /* returned value is positive when program fails */
+ log_device_debug(event->dev, "Command \"%s\" returned %d (error), ignoring.", command, r);
+ }
+ }
+}
diff --git a/src/udev/udev-spawn.h b/src/udev/udev-spawn.h
new file mode 100644
index 0000000..5efea2e
--- /dev/null
+++ b/src/udev/udev-spawn.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#include "macro.h"
+#include "time-util.h"
+
+#define READ_END 0
+#define WRITE_END 1
+
+typedef struct UdevEvent UdevEvent;
+
+int udev_event_spawn(
+ UdevEvent *event,
+ usec_t timeout_usec,
+ int timeout_signal,
+ bool accept_failure,
+ const char *cmd,
+ char *result,
+ size_t ressize,
+ bool *ret_truncated);
+void udev_event_execute_run(UdevEvent *event, usec_t timeout_usec, int timeout_signal);
+
+static inline usec_t udev_warn_timeout(usec_t timeout_usec) {
+ return DIV_ROUND_UP(timeout_usec, 3);
+}
diff --git a/src/udev/udev-trace.h b/src/udev/udev-trace.h
new file mode 100644
index 0000000..5e94390
--- /dev/null
+++ b/src/udev/udev-trace.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#if HAVE_SYS_SDT_H
+#define SDT_USE_VARIADIC
+#include <sys/sdt.h>
+
+#include "device-private.h"
+#include "device-util.h"
+#include "errno-util.h"
+
+/* Each trace point can have different number of additional arguments. Note that when the macro is used only
+ * additional arguments are listed in the macro invocation!
+ *
+ * Default arguments for each trace point are as follows:
+ * - arg0 - action
+ * - arg1 - sysname
+ * - arg2 - syspath
+ * - arg3 - subsystem
+ */
+#define DEVICE_TRACE_POINT(name, dev, ...) \
+ do { \
+ PROTECT_ERRNO; \
+ const char *_n = NULL, *_p = NULL, *_s = NULL; \
+ sd_device *_d = (dev); \
+ sd_device_action_t _a = _SD_DEVICE_ACTION_INVALID; \
+ (void) sd_device_get_action(_d, &_a); \
+ (void) sd_device_get_sysname(_d, &_n); \
+ (void) sd_device_get_syspath(_d, &_p); \
+ (void) sd_device_get_subsystem(_d, &_s); \
+ STAP_PROBEV(udev, name, device_action_to_string(_a), _n, _p, _s __VA_OPT__(,) __VA_ARGS__);\
+ } while (false);
+#else
+#define DEVICE_TRACE_POINT(name, dev, ...) ((void) 0)
+#endif
diff --git a/src/udev/udev-watch.c b/src/udev/udev-watch.c
new file mode 100644
index 0000000..58c8279
--- /dev/null
+++ b/src/udev/udev-watch.c
@@ -0,0 +1,260 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © 2009 Canonical Ltd.
+ * Copyright © 2009 Scott James Remnant <scott@netsplit.com>
+ */
+
+#include <sys/inotify.h>
+
+#include "alloc-util.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "mkdir.h"
+#include "parse-util.h"
+#include "rm-rf.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "udev-util.h"
+#include "udev-watch.h"
+
+int device_new_from_watch_handle_at(sd_device **ret, int dirfd, int wd) {
+ char path_wd[STRLEN("/run/udev/watch/") + DECIMAL_STR_MAX(int)];
+ _cleanup_free_ char *id = NULL;
+ int r;
+
+ assert(ret);
+
+ if (wd < 0)
+ return -EBADF;
+
+ if (dirfd >= 0) {
+ xsprintf(path_wd, "%d", wd);
+ r = readlinkat_malloc(dirfd, path_wd, &id);
+ } else {
+ xsprintf(path_wd, "/run/udev/watch/%d", wd);
+ r = readlink_malloc(path_wd, &id);
+ }
+ if (r < 0)
+ return r;
+
+ return sd_device_new_from_device_id(ret, id);
+}
+
+int udev_watch_restore(int inotify_fd) {
+ _cleanup_closedir_ DIR *dir = NULL;
+ int r;
+
+ /* Move any old watches directory out of the way, and then restore the watches. */
+
+ assert(inotify_fd >= 0);
+
+ (void) rm_rf("/run/udev/watch.old", REMOVE_ROOT);
+
+ if (rename("/run/udev/watch", "/run/udev/watch.old") < 0) {
+ if (errno == ENOENT)
+ return 0;
+
+ r = log_warning_errno(errno,
+ "Failed to move watches directory '/run/udev/watch/'. "
+ "Old watches will not be restored: %m");
+ goto finalize;
+ }
+
+ dir = opendir("/run/udev/watch.old");
+ if (!dir) {
+ r = log_warning_errno(errno,
+ "Failed to open old watches directory '/run/udev/watch.old/'. "
+ "Old watches will not be restored: %m");
+ goto finalize;
+ }
+
+ FOREACH_DIRENT_ALL(de, dir, break) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ int wd;
+
+ /* For backward compatibility, read symlink from watch handle to device ID. This is necessary
+ * when udevd is restarted after upgrading from v248 or older. The new format (ID -> wd) was
+ * introduced by e7f781e473f5119bf9246208a6de9f6b76a39c5d (v249). */
+
+ if (dot_or_dot_dot(de->d_name))
+ continue;
+
+ if (safe_atoi(de->d_name, &wd) < 0)
+ continue;
+
+ r = device_new_from_watch_handle_at(&dev, dirfd(dir), wd);
+ if (r < 0) {
+ log_full_errno(r == -ENODEV ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to create sd_device object from saved watch handle '%i', ignoring: %m",
+ wd);
+ continue;
+ }
+
+ (void) udev_watch_begin(inotify_fd, dev);
+ }
+
+ r = 0;
+
+finalize:
+ (void) rm_rf("/run/udev/watch.old", REMOVE_ROOT);
+ return r;
+}
+
+static int udev_watch_clear(sd_device *dev, int dirfd, int *ret_wd) {
+ _cleanup_free_ char *wd_str = NULL, *buf = NULL;
+ const char *id;
+ int wd = -1, r;
+
+ assert(dev);
+ assert(dirfd >= 0);
+
+ r = device_get_device_id(dev, &id);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device ID: %m");
+
+ /* 1. read symlink ID -> wd */
+ r = readlinkat_malloc(dirfd, id, &wd_str);
+ if (r == -ENOENT) {
+ if (ret_wd)
+ *ret_wd = -1;
+ return 0;
+ }
+ if (r < 0) {
+ log_device_debug_errno(dev, r, "Failed to read symlink '/run/udev/watch/%s': %m", id);
+ goto finalize;
+ }
+
+ r = safe_atoi(wd_str, &wd);
+ if (r < 0) {
+ log_device_debug_errno(dev, r, "Failed to parse watch handle from symlink '/run/udev/watch/%s': %m", id);
+ goto finalize;
+ }
+
+ if (wd < 0) {
+ r = log_device_debug_errno(dev, SYNTHETIC_ERRNO(EBADF), "Invalid watch handle %i.", wd);
+ goto finalize;
+ }
+
+ /* 2. read symlink wd -> ID */
+ r = readlinkat_malloc(dirfd, wd_str, &buf);
+ if (r < 0) {
+ log_device_debug_errno(dev, r, "Failed to read symlink '/run/udev/watch/%s': %m", wd_str);
+ goto finalize;
+ }
+
+ /* 3. check if the symlink wd -> ID is owned by the device. */
+ if (!streq(buf, id)) {
+ r = log_device_debug_errno(dev, SYNTHETIC_ERRNO(ENOENT),
+ "Symlink '/run/udev/watch/%s' is owned by another device '%s'.", wd_str, buf);
+ goto finalize;
+ }
+
+ /* 4. remove symlink wd -> ID.
+ * In the above, we already confirmed that the symlink is owned by us. Hence, no other workers remove
+ * the symlink and cannot create a new symlink with the same filename but to a different ID. Hence,
+ * the removal below is safe even the steps in this function are not atomic. */
+ if (unlinkat(dirfd, wd_str, 0) < 0 && errno != ENOENT)
+ log_device_debug_errno(dev, errno, "Failed to remove '/run/udev/watch/%s', ignoring: %m", wd_str);
+
+ if (ret_wd)
+ *ret_wd = wd;
+ r = 0;
+
+finalize:
+ /* 5. remove symlink ID -> wd.
+ * The file is always owned by the device. Hence, it is safe to remove it unconditionally. */
+ if (unlinkat(dirfd, id, 0) < 0 && errno != ENOENT)
+ log_device_debug_errno(dev, errno, "Failed to remove '/run/udev/watch/%s': %m", id);
+
+ return r;
+}
+
+int udev_watch_begin(int inotify_fd, sd_device *dev) {
+ char wd_str[DECIMAL_STR_MAX(int)];
+ _cleanup_close_ int dirfd = -EBADF;
+ const char *devnode, *id;
+ int wd, r;
+
+ assert(inotify_fd >= 0);
+ assert(dev);
+
+ if (device_for_action(dev, SD_DEVICE_REMOVE))
+ return 0;
+
+ r = sd_device_get_devname(dev, &devnode);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device node: %m");
+
+ r = device_get_device_id(dev, &id);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get device ID: %m");
+
+ r = dirfd = open_mkdir_at(AT_FDCWD, "/run/udev/watch", O_CLOEXEC | O_RDONLY, 0755);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to create and open '/run/udev/watch/': %m");
+
+ /* 1. Clear old symlinks */
+ (void) udev_watch_clear(dev, dirfd, NULL);
+
+ /* 2. Add inotify watch */
+ log_device_debug(dev, "Adding watch on '%s'", devnode);
+ wd = inotify_add_watch(inotify_fd, devnode, IN_CLOSE_WRITE);
+ if (wd < 0)
+ return log_device_debug_errno(dev, errno, "Failed to watch device node '%s': %m", devnode);
+
+ xsprintf(wd_str, "%d", wd);
+
+ /* 3. Create new symlinks */
+ if (symlinkat(wd_str, dirfd, id) < 0) {
+ r = log_device_debug_errno(dev, errno, "Failed to create symlink '/run/udev/watch/%s' to '%s': %m", id, wd_str);
+ goto on_failure;
+ }
+
+ if (symlinkat(id, dirfd, wd_str) < 0) {
+ /* Possibly, the watch handle is previously assigned to another device, and udev_watch_end()
+ * is not called for the device yet. */
+ r = log_device_debug_errno(dev, errno, "Failed to create symlink '/run/udev/watch/%s' to '%s': %m", wd_str, id);
+ goto on_failure;
+ }
+
+ return 0;
+
+on_failure:
+ (void) unlinkat(dirfd, id, 0);
+ (void) inotify_rm_watch(inotify_fd, wd);
+ return r;
+}
+
+int udev_watch_end(int inotify_fd, sd_device *dev) {
+ _cleanup_close_ int dirfd = -EBADF;
+ int wd, r;
+
+ assert(dev);
+
+ /* This may be called by 'udevadm test'. In that case, inotify_fd is not initialized. */
+ if (inotify_fd < 0)
+ return 0;
+
+ if (sd_device_get_devname(dev, NULL) < 0)
+ return 0;
+
+ dirfd = RET_NERRNO(open("/run/udev/watch", O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_RDONLY));
+ if (dirfd == -ENOENT)
+ return 0;
+ if (dirfd < 0)
+ return log_device_debug_errno(dev, dirfd, "Failed to open '/run/udev/watch/': %m");
+
+ /* First, clear symlinks. */
+ r = udev_watch_clear(dev, dirfd, &wd);
+ if (r < 0)
+ return r;
+
+ /* Then, remove inotify watch. */
+ log_device_debug(dev, "Removing watch handle %i.", wd);
+ (void) inotify_rm_watch(inotify_fd, wd);
+
+ return 0;
+}
diff --git a/src/udev/udev-watch.h b/src/udev/udev-watch.h
new file mode 100644
index 0000000..c454dee
--- /dev/null
+++ b/src/udev/udev-watch.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "sd-device.h"
+
+int device_new_from_watch_handle_at(sd_device **ret, int dirfd, int wd);
+static inline int device_new_from_watch_handle(sd_device **ret, int wd) {
+ return device_new_from_watch_handle_at(ret, -1, wd);
+}
+
+int udev_watch_restore(int inotify_fd);
+int udev_watch_begin(int inotify_fd, sd_device *dev);
+int udev_watch_end(int inotify_fd, sd_device *dev);
diff --git a/src/udev/udev-worker.c b/src/udev/udev-worker.c
new file mode 100644
index 0000000..53722b2
--- /dev/null
+++ b/src/udev/udev-worker.c
@@ -0,0 +1,352 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <sys/file.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+
+#include "alloc-util.h"
+#include "blockdev-util.h"
+#include "common-signal.h"
+#include "device-monitor-private.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "io-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "signal-util.h"
+#include "string-util.h"
+#include "udev-event.h"
+#include "udev-spawn.h"
+#include "udev-trace.h"
+#include "udev-util.h"
+#include "udev-watch.h"
+#include "udev-worker.h"
+
+void udev_worker_done(UdevWorker *worker) {
+ assert(worker);
+
+ sd_event_unref(worker->event);
+ sd_netlink_unref(worker->rtnl);
+ sd_device_monitor_unref(worker->monitor);
+ hashmap_free(worker->properties);
+ udev_rules_free(worker->rules);
+ safe_close(worker->pipe_fd);
+}
+
+int udev_get_whole_disk(sd_device *dev, sd_device **ret_device, const char **ret_devname) {
+ const char *val;
+ int r;
+
+ assert(dev);
+
+ if (device_for_action(dev, SD_DEVICE_REMOVE))
+ goto irrelevant;
+
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get sysname: %m");
+
+ /* Exclude the following devices:
+ * For "dm-", see the comment added by e918a1b5a94f270186dca59156354acd2a596494.
+ * For "md", see the commit message of 2e5b17d01347d3c3118be2b8ad63d20415dbb1f0,
+ * but not sure the assumption is still valid even when partitions are created on the md
+ * devices, surprisingly which seems to be possible, see PR #22973.
+ * For "drbd", see the commit message of fee854ee8ccde0cd28e0f925dea18cce35f3993d. */
+ if (STARTSWITH_SET(val, "dm-", "md", "drbd"))
+ goto irrelevant;
+
+ r = block_device_get_whole_disk(dev, &dev);
+ if (IN_SET(r,
+ -ENOTBLK, /* The device is not a block device. */
+ -ENODEV /* The whole disk device was not found, it may already be removed. */))
+ goto irrelevant;
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get whole disk device: %m");
+
+ r = sd_device_get_devname(dev, &val);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get devname: %m");
+
+ if (ret_device)
+ *ret_device = dev;
+ if (ret_devname)
+ *ret_devname = val;
+ return 1;
+
+irrelevant:
+ if (ret_device)
+ *ret_device = NULL;
+ if (ret_devname)
+ *ret_devname = NULL;
+ return 0;
+}
+
+static int worker_lock_whole_disk(sd_device *dev, int *ret_fd) {
+ _cleanup_close_ int fd = -EBADF;
+ sd_device *dev_whole_disk;
+ const char *val;
+ int r;
+
+ assert(dev);
+ assert(ret_fd);
+
+ /* Take a shared lock on the device node; this establishes a concept of device "ownership" to
+ * serialize device access. External processes holding an exclusive lock will cause udev to skip the
+ * event handling; in the case udev acquired the lock, the external process can block until udev has
+ * finished its event handling. */
+
+ r = udev_get_whole_disk(dev, &dev_whole_disk, &val);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ goto nolock;
+
+ fd = sd_device_open(dev_whole_disk, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0) {
+ bool ignore = ERRNO_IS_DEVICE_ABSENT(fd);
+
+ log_device_debug_errno(dev, fd, "Failed to open '%s'%s: %m", val, ignore ? ", ignoring" : "");
+ if (!ignore)
+ return fd;
+
+ goto nolock;
+ }
+
+ if (flock(fd, LOCK_SH|LOCK_NB) < 0)
+ return log_device_debug_errno(dev, errno, "Failed to flock(%s): %m", val);
+
+ *ret_fd = TAKE_FD(fd);
+ return 1;
+
+nolock:
+ *ret_fd = -EBADF;
+ return 0;
+}
+
+static int worker_mark_block_device_read_only(sd_device *dev) {
+ _cleanup_close_ int fd = -EBADF;
+ const char *val;
+ int state = 1, r;
+
+ assert(dev);
+
+ /* Do this only once, when the block device is new. If the device is later retriggered let's not
+ * toggle the bit again, so that people can boot up with full read-only mode and then unset the bit
+ * for specific devices only. */
+ if (!device_for_action(dev, SD_DEVICE_ADD))
+ return 0;
+
+ r = sd_device_get_subsystem(dev, &val);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get subsystem: %m");
+
+ if (!streq(val, "block"))
+ return 0;
+
+ r = sd_device_get_sysname(dev, &val);
+ if (r < 0)
+ return log_device_debug_errno(dev, r, "Failed to get sysname: %m");
+
+ /* Exclude synthetic devices for now, this is supposed to be a safety feature to avoid modification
+ * of physical devices, and what sits on top of those doesn't really matter if we don't allow the
+ * underlying block devices to receive changes. */
+ if (STARTSWITH_SET(val, "dm-", "md", "drbd", "loop", "nbd", "zram"))
+ return 0;
+
+ fd = sd_device_open(dev, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0)
+ return log_device_debug_errno(dev, fd, "Failed to open '%s', ignoring: %m", val);
+
+ if (ioctl(fd, BLKROSET, &state) < 0)
+ return log_device_warning_errno(dev, errno, "Failed to mark block device '%s' read-only: %m", val);
+
+ log_device_info(dev, "Successfully marked block device '%s' read-only.", val);
+ return 0;
+}
+
+static int worker_process_device(UdevWorker *worker, sd_device *dev) {
+ _cleanup_(udev_event_freep) UdevEvent *udev_event = NULL;
+ _cleanup_close_ int fd_lock = -EBADF;
+ int r;
+
+ assert(worker);
+ assert(dev);
+
+ log_device_uevent(dev, "Processing device");
+
+ udev_event = udev_event_new(dev, worker->exec_delay_usec, worker->rtnl, worker->log_level);
+ if (!udev_event)
+ return -ENOMEM;
+
+ /* If this is a block device and the device is locked currently via the BSD advisory locks,
+ * someone else is using it exclusively. We don't run our udev rules now to not interfere.
+ * Instead of processing the event, we requeue the event and will try again after a delay.
+ *
+ * The user-facing side of this: https://systemd.io/BLOCK_DEVICE_LOCKING */
+ r = worker_lock_whole_disk(dev, &fd_lock);
+ if (r == -EAGAIN)
+ return EVENT_RESULT_TRY_AGAIN;
+ if (r < 0)
+ return r;
+
+ if (worker->blockdev_read_only)
+ (void) worker_mark_block_device_read_only(dev);
+
+ /* apply rules, create node, symlinks */
+ r = udev_event_execute_rules(
+ udev_event,
+ worker->inotify_fd,
+ worker->timeout_usec,
+ worker->timeout_signal,
+ worker->properties,
+ worker->rules);
+ if (r < 0)
+ return r;
+
+ udev_event_execute_run(udev_event, worker->timeout_usec, worker->timeout_signal);
+
+ if (!worker->rtnl)
+ /* in case rtnl was initialized */
+ worker->rtnl = sd_netlink_ref(udev_event->rtnl);
+
+ if (udev_event->inotify_watch) {
+ r = udev_watch_begin(worker->inotify_fd, dev);
+ if (r < 0 && r != -ENOENT) /* The device may be already removed, ignore -ENOENT. */
+ log_device_warning_errno(dev, r, "Failed to add inotify watch, ignoring: %m");
+ }
+
+ log_device_uevent(dev, "Device processed");
+ return 0;
+}
+
+void udev_broadcast_result(sd_device_monitor *monitor, sd_device *dev, EventResult result) {
+ int r;
+
+ assert(dev);
+
+ /* On exit, manager->monitor is already NULL. */
+ if (!monitor)
+ return;
+
+ if (result != EVENT_RESULT_SUCCESS) {
+ (void) device_add_property(dev, "UDEV_WORKER_FAILED", "1");
+
+ switch (result) {
+ case EVENT_RESULT_NERRNO_MIN ... EVENT_RESULT_NERRNO_MAX: {
+ const char *str;
+
+ (void) device_add_propertyf(dev, "UDEV_WORKER_ERRNO", "%i", -result);
+
+ str = errno_to_name(result);
+ if (str)
+ (void) device_add_property(dev, "UDEV_WORKER_ERRNO_NAME", str);
+ break;
+ }
+ case EVENT_RESULT_EXIT_STATUS_BASE ... EVENT_RESULT_EXIT_STATUS_MAX:
+ (void) device_add_propertyf(dev, "UDEV_WORKER_EXIT_STATUS", "%i", result - EVENT_RESULT_EXIT_STATUS_BASE);
+ break;
+
+ case EVENT_RESULT_TRY_AGAIN:
+ assert_not_reached();
+ break;
+
+ case EVENT_RESULT_SIGNAL_BASE ... EVENT_RESULT_SIGNAL_MAX: {
+ const char *str;
+
+ (void) device_add_propertyf(dev, "UDEV_WORKER_SIGNAL", "%i", result - EVENT_RESULT_SIGNAL_BASE);
+
+ str = signal_to_string(result - EVENT_RESULT_SIGNAL_BASE);
+ if (str)
+ (void) device_add_property(dev, "UDEV_WORKER_SIGNAL_NAME", str);
+ break;
+ }
+ default:
+ log_device_warning(dev, "Unknown event result \"%i\", ignoring.", result);
+ }
+ }
+
+ r = device_monitor_send_device(monitor, NULL, dev);
+ if (r < 0)
+ log_device_warning_errno(dev, r,
+ "Failed to broadcast event to libudev listeners, ignoring: %m");
+}
+
+static int worker_send_result(UdevWorker *worker, EventResult result) {
+ assert(worker);
+ assert(worker->pipe_fd >= 0);
+
+ return loop_write(worker->pipe_fd, &result, sizeof(result));
+}
+
+static int worker_device_monitor_handler(sd_device_monitor *monitor, sd_device *dev, void *userdata) {
+ UdevWorker *worker = ASSERT_PTR(userdata);
+ int r;
+
+ assert(dev);
+
+ r = worker_process_device(worker, dev);
+ if (r == EVENT_RESULT_TRY_AGAIN)
+ /* if we couldn't acquire the flock(), then requeue the event */
+ log_device_debug(dev, "Block device is currently locked, requeueing the event.");
+ else {
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Failed to process device, ignoring: %m");
+
+ /* send processed event back to libudev listeners */
+ udev_broadcast_result(monitor, dev, r);
+ }
+
+ /* send udevd the result of the event execution */
+ r = worker_send_result(worker, r);
+ if (r < 0)
+ log_device_warning_errno(dev, r, "Failed to send signal to main daemon, ignoring: %m");
+
+ /* Reset the log level, as it might be changed by "OPTIONS=log_level=". */
+ log_set_max_level(worker->log_level);
+
+ return 1;
+}
+
+int udev_worker_main(UdevWorker *worker, sd_device *dev) {
+ int r;
+
+ assert(worker);
+ assert(worker->monitor);
+ assert(dev);
+
+ DEVICE_TRACE_POINT(worker_spawned, dev, getpid_cached());
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, -1) >= 0);
+
+ /* Reset OOM score, we only protect the main daemon. */
+ r = set_oom_score_adjust(0);
+ if (r < 0)
+ log_debug_errno(r, "Failed to reset OOM score, ignoring: %m");
+
+ r = sd_event_new(&worker->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate event loop: %m");
+
+ r = sd_event_add_signal(worker->event, NULL, SIGTERM, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set SIGTERM event: %m");
+
+ r = sd_device_monitor_attach_event(worker->monitor, worker->event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach event loop to device monitor: %m");
+
+ r = sd_device_monitor_start(worker->monitor, worker_device_monitor_handler, worker);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+
+ /* Process first device */
+ (void) worker_device_monitor_handler(worker->monitor, dev, worker);
+
+ r = sd_event_loop(worker->event);
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+ return 0;
+}
diff --git a/src/udev/udev-worker.h b/src/udev/udev-worker.h
new file mode 100644
index 0000000..05c319e
--- /dev/null
+++ b/src/udev/udev-worker.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdbool.h>
+
+#include "sd-device.h"
+#include "sd-event.h"
+#include "sd-netlink.h"
+
+#include "errno-list.h"
+#include "hashmap.h"
+#include "time-util.h"
+
+typedef struct UdevRules UdevRules;
+
+typedef struct UdevWorker {
+ sd_event *event;
+ sd_netlink *rtnl;
+ sd_device_monitor *monitor;
+
+ Hashmap *properties;
+ UdevRules *rules;
+
+ int pipe_fd;
+ int inotify_fd; /* Do not close! */
+
+ usec_t exec_delay_usec;
+ usec_t timeout_usec;
+ int timeout_signal;
+ int log_level;
+ bool blockdev_read_only;
+} UdevWorker;
+
+/* passed from worker to main process */
+typedef enum EventResult {
+ EVENT_RESULT_NERRNO_MIN = -ERRNO_MAX,
+ EVENT_RESULT_NERRNO_MAX = -1,
+ EVENT_RESULT_SUCCESS = 0,
+ EVENT_RESULT_EXIT_STATUS_BASE = 0,
+ EVENT_RESULT_EXIT_STATUS_MAX = 255,
+ EVENT_RESULT_TRY_AGAIN = 256, /* when the block device is locked by another process. */
+ EVENT_RESULT_SIGNAL_BASE = 257,
+ EVENT_RESULT_SIGNAL_MAX = EVENT_RESULT_SIGNAL_BASE + _NSIG,
+ _EVENT_RESULT_MAX,
+ _EVENT_RESULT_INVALID = -EINVAL,
+} EventResult;
+
+void udev_worker_done(UdevWorker *worker);
+int udev_worker_main(UdevWorker *worker, sd_device *dev);
+
+void udev_broadcast_result(sd_device_monitor *monitor, sd_device *dev, EventResult result);
+int udev_get_whole_disk(sd_device *dev, sd_device **ret_device, const char **ret_devname);
diff --git a/src/udev/udev.conf b/src/udev/udev.conf
new file mode 100644
index 0000000..07d7f0c
--- /dev/null
+++ b/src/udev/udev.conf
@@ -0,0 +1,11 @@
+# see udev.conf(5) for details
+#
+# udevd is also started in the initrd. When this file is modified you might
+# also want to rebuild the initrd, so that it will include the modified configuration.
+
+#udev_log=info
+#children_max=
+#exec_delay=
+#event_timeout=180
+#timeout_signal=SIGKILL
+#resolve_names=early
diff --git a/src/udev/udev.pc.in b/src/udev/udev.pc.in
new file mode 100644
index 0000000..cbf7693
--- /dev/null
+++ b/src/udev/udev.pc.in
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+Name: udev
+Description: udev
+Version: {{PROJECT_VERSION}}
+
+udev_dir={{UDEVLIBEXECDIR}}
+udevdir=${udev_dir}
diff --git a/src/udev/udevadm-control.c b/src/udev/udevadm-control.c
new file mode 100644
index 0000000..64615f5
--- /dev/null
+++ b/src/udev/udevadm-control.c
@@ -0,0 +1,239 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include <errno.h>
+#include <getopt.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "parse-util.h"
+#include "process-util.h"
+#include "static-destruct.h"
+#include "strv.h"
+#include "syslog-util.h"
+#include "time-util.h"
+#include "udevadm.h"
+#include "udev-ctrl.h"
+#include "virt.h"
+
+static char **arg_env = NULL;
+static usec_t arg_timeout = 60 * USEC_PER_SEC;
+static bool arg_ping = false;
+static bool arg_reload = false;
+static bool arg_exit = false;
+static int arg_max_children = -1;
+static int arg_log_level = -1;
+static int arg_start_exec_queue = -1;
+
+STATIC_DESTRUCTOR_REGISTER(arg_env, strv_freep);
+
+static int help(void) {
+ printf("%s control OPTION\n\n"
+ "Control the udev daemon.\n\n"
+ " -h --help Show this help\n"
+ " -V --version Show package version\n"
+ " -e --exit Instruct the daemon to cleanup and exit\n"
+ " -l --log-level=LEVEL Set the udev log level for the daemon\n"
+ " -s --stop-exec-queue Do not execute events, queue only\n"
+ " -S --start-exec-queue Execute events, flush queue\n"
+ " -R --reload Reload rules and databases\n"
+ " -p --property=KEY=VALUE Set a global property for all events\n"
+ " -m --children-max=N Maximum number of children\n"
+ " --ping Wait for udev to respond to a ping message\n"
+ " -t --timeout=SECONDS Maximum time to block for a reply\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_PING = 0x100,
+ };
+
+ static const struct option options[] = {
+ { "exit", no_argument, NULL, 'e' },
+ { "log-level", required_argument, NULL, 'l' },
+ { "log-priority", required_argument, NULL, 'l' }, /* for backward compatibility */
+ { "stop-exec-queue", no_argument, NULL, 's' },
+ { "start-exec-queue", no_argument, NULL, 'S' },
+ { "reload", no_argument, NULL, 'R' },
+ { "reload-rules", no_argument, NULL, 'R' }, /* alias for -R */
+ { "property", required_argument, NULL, 'p' },
+ { "env", required_argument, NULL, 'p' }, /* alias for -p */
+ { "children-max", required_argument, NULL, 'm' },
+ { "ping", no_argument, NULL, ARG_PING },
+ { "timeout", required_argument, NULL, 't' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ if (argc <= 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "This command expects one or more options.");
+
+ while ((c = getopt_long(argc, argv, "el:sSRp:m:t:Vh", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'e':
+ arg_exit = true;
+ break;
+
+ case 'l':
+ arg_log_level = log_level_from_string(optarg);
+ if (arg_log_level < 0)
+ return log_error_errno(arg_log_level, "Failed to parse log level '%s': %m", optarg);
+ break;
+
+ case 's':
+ arg_start_exec_queue = false;
+ break;
+
+ case 'S':
+ arg_start_exec_queue = true;
+ break;
+
+ case 'R':
+ arg_reload = true;
+ break;
+
+ case 'p':
+ if (!strchr(optarg, '='))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "expect <KEY>=<value> instead of '%s'", optarg);
+
+ r = strv_extend(&arg_env, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extend environment: %m");
+
+ break;
+
+ case 'm': {
+ unsigned i;
+ r = safe_atou(optarg, &i);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse maximum number of children '%s': %m", optarg);
+ arg_max_children = i;
+ break;
+ }
+
+ case ARG_PING:
+ arg_ping = true;
+ break;
+
+ case 't':
+ r = parse_sec(optarg, &arg_timeout);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg);
+ break;
+
+ case 'V':
+ return print_version();
+
+ case 'h':
+ return help();
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (optind < argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Extraneous argument: %s", argv[optind]);
+
+ return 1;
+}
+
+int control_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(udev_ctrl_unrefp) UdevCtrl *uctrl = NULL;
+ int r;
+
+ if (running_in_chroot() > 0) {
+ log_info("Running in chroot, ignoring request.");
+ return 0;
+ }
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = udev_ctrl_new(&uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize udev control: %m");
+
+ if (arg_exit) {
+ r = udev_ctrl_send_exit(uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send exit request: %m");
+ return 0;
+ }
+
+ if (arg_log_level >= 0) {
+ r = udev_ctrl_send_set_log_level(uctrl, arg_log_level);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send request to set log level: %m");
+ }
+
+ if (arg_start_exec_queue == false) {
+ r = udev_ctrl_send_stop_exec_queue(uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send request to stop exec queue: %m");
+ }
+
+ if (arg_start_exec_queue == true) {
+ r = udev_ctrl_send_start_exec_queue(uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send request to start exec queue: %m");
+ }
+
+ if (arg_reload) {
+ r = udev_ctrl_send_reload(uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send reload request: %m");
+ }
+
+ STRV_FOREACH(env, arg_env) {
+ r = udev_ctrl_send_set_env(uctrl, *env);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send request to update environment: %m");
+ }
+
+ if (arg_max_children >= 0) {
+ r = udev_ctrl_send_set_children_max(uctrl, arg_max_children);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send request to set number of children: %m");
+ }
+
+ if (arg_ping) {
+ r = udev_ctrl_send_ping(uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to send a ping message: %m");
+ }
+
+ r = udev_ctrl_wait(uctrl, arg_timeout);
+ if (r < 0)
+ return log_error_errno(r, "Failed to wait for daemon to reply: %m");
+
+ return 0;
+}
diff --git a/src/udev/udevadm-hwdb.c b/src/udev/udevadm-hwdb.c
new file mode 100644
index 0000000..2f5429f
--- /dev/null
+++ b/src/udev/udevadm-hwdb.c
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include "hwdb-util.h"
+#include "udevadm.h"
+
+static const char *arg_test = NULL;
+static const char *arg_root = NULL;
+static const char *arg_hwdb_bin_dir = NULL;
+static bool arg_update = false;
+static bool arg_strict = false;
+
+static int help(void) {
+ printf("%s hwdb [OPTIONS]\n\n"
+ " -h --help Print this message\n"
+ " -V --version Print version of the program\n"
+ " -u --update Update the hardware database\n"
+ " -s --strict When updating, return non-zero exit value on any parsing error\n"
+ " --usr Generate in " UDEVLIBEXECDIR " instead of /etc/udev\n"
+ " -t --test=MODALIAS Query database and print result\n"
+ " -r --root=PATH Alternative root path in the filesystem\n\n"
+ "NOTE:\n"
+ "The sub-command 'hwdb' is deprecated, and is left for backwards compatibility.\n"
+ "Please use systemd-hwdb instead.\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_USR = 0x100,
+ };
+
+ static const struct option options[] = {
+ { "update", no_argument, NULL, 'u' },
+ { "usr", no_argument, NULL, ARG_USR },
+ { "strict", no_argument, NULL, 's' },
+ { "test", required_argument, NULL, 't' },
+ { "root", required_argument, NULL, 'r' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+
+ int c;
+
+ while ((c = getopt_long(argc, argv, "ust:r:Vh", options, NULL)) >= 0)
+ switch (c) {
+ case 'u':
+ arg_update = true;
+ break;
+ case ARG_USR:
+ arg_hwdb_bin_dir = UDEVLIBEXECDIR;
+ break;
+ case 's':
+ arg_strict = true;
+ break;
+ case 't':
+ arg_test = optarg;
+ break;
+ case 'r':
+ arg_root = optarg;
+ break;
+ case 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ return 1;
+}
+
+int hwdb_main(int argc, char *argv[], void *userdata) {
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (!arg_update && !arg_test)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Either --update or --test must be used.");
+
+ log_notice("udevadm hwdb is deprecated. Use systemd-hwdb instead.");
+
+ if (arg_update) {
+ r = hwdb_update(arg_root, arg_hwdb_bin_dir, arg_strict, true);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_test)
+ return hwdb_query(arg_test, NULL);
+
+ return 0;
+}
diff --git a/src/udev/udevadm-info.c b/src/udev/udevadm-info.c
new file mode 100644
index 0000000..4cd9ad4
--- /dev/null
+++ b/src/udev/udevadm-info.c
@@ -0,0 +1,1120 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "sd-device.h"
+
+#include "alloc-util.h"
+#include "device-enumerator-private.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "devnum-util.h"
+#include "dirent-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "glyph-util.h"
+#include "json.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "sort-util.h"
+#include "static-destruct.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "terminal-util.h"
+#include "udev-util.h"
+#include "udevadm.h"
+#include "udevadm-util.h"
+
+typedef enum ActionType {
+ ACTION_QUERY,
+ ACTION_ATTRIBUTE_WALK,
+ ACTION_DEVICE_ID_FILE,
+ ACTION_TREE,
+ ACTION_EXPORT,
+} ActionType;
+
+typedef enum QueryType {
+ QUERY_NAME,
+ QUERY_PATH,
+ QUERY_SYMLINK,
+ QUERY_PROPERTY,
+ QUERY_ALL,
+} QueryType;
+
+static char **arg_properties = NULL;
+static bool arg_root = false;
+static bool arg_export = false;
+static bool arg_value = false;
+static const char *arg_export_prefix = NULL;
+static usec_t arg_wait_for_initialization_timeout = 0;
+PagerFlags arg_pager_flags = 0;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+
+/* Put a limit on --tree descent level to not exhaust our stack */
+#define TREE_DEPTH_MAX 64
+
+static bool skip_attribute(const char *name) {
+ assert(name);
+
+ /* Those are either displayed separately or should not be shown at all. */
+ return STR_IN_SET(name,
+ "uevent",
+ "dev",
+ "modalias",
+ "resource",
+ "driver",
+ "subsystem",
+ "module");
+}
+
+typedef struct SysAttr {
+ const char *name;
+ const char *value;
+} SysAttr;
+
+STATIC_DESTRUCTOR_REGISTER(arg_properties, strv_freep);
+
+static int sysattr_compare(const SysAttr *a, const SysAttr *b) {
+ assert(a);
+ assert(b);
+
+ return strcmp(a->name, b->name);
+}
+
+static int print_all_attributes(sd_device *device, bool is_parent) {
+ _cleanup_free_ SysAttr *sysattrs = NULL;
+ const char *value;
+ size_t n_items = 0;
+ int r;
+
+ assert(device);
+
+ value = NULL;
+ (void) sd_device_get_devpath(device, &value);
+ printf(" looking at %sdevice '%s':\n", is_parent ? "parent " : "", strempty(value));
+
+ value = NULL;
+ (void) sd_device_get_sysname(device, &value);
+ printf(" %s==\"%s\"\n", is_parent ? "KERNELS" : "KERNEL", strempty(value));
+
+ value = NULL;
+ (void) sd_device_get_subsystem(device, &value);
+ printf(" %s==\"%s\"\n", is_parent ? "SUBSYSTEMS" : "SUBSYSTEM", strempty(value));
+
+ value = NULL;
+ (void) sd_device_get_driver(device, &value);
+ printf(" %s==\"%s\"\n", is_parent ? "DRIVERS" : "DRIVER", strempty(value));
+
+ FOREACH_DEVICE_SYSATTR(device, name) {
+ size_t len;
+
+ if (skip_attribute(name))
+ continue;
+
+ r = sd_device_get_sysattr_value(device, name, &value);
+ if (r >= 0) {
+ /* skip any values that look like a path */
+ if (value[0] == '/')
+ continue;
+
+ /* skip nonprintable attributes */
+ len = strlen(value);
+ while (len > 0 && isprint((unsigned char) value[len-1]))
+ len--;
+ if (len > 0)
+ continue;
+
+ } else if (ERRNO_IS_PRIVILEGE(r))
+ value = "(not readable)";
+ else
+ continue;
+
+ if (!GREEDY_REALLOC(sysattrs, n_items + 1))
+ return log_oom();
+
+ sysattrs[n_items] = (SysAttr) {
+ .name = name,
+ .value = value,
+ };
+ n_items++;
+ }
+
+ typesafe_qsort(sysattrs, n_items, sysattr_compare);
+
+ for (size_t i = 0; i < n_items; i++)
+ printf(" %s{%s}==\"%s\"\n", is_parent ? "ATTRS" : "ATTR", sysattrs[i].name, sysattrs[i].value);
+
+ puts("");
+
+ return 0;
+}
+
+static int print_device_chain(sd_device *device) {
+ sd_device *child, *parent;
+ int r;
+
+ assert(device);
+
+ printf("\n"
+ "Udevadm info starts with the device specified by the devpath and then\n"
+ "walks up the chain of parent devices. It prints for every device\n"
+ "found, all possible attributes in the udev rules key format.\n"
+ "A rule to match, can be composed by the attributes of the device\n"
+ "and the attributes from one single parent device.\n"
+ "\n");
+
+ r = print_all_attributes(device, false);
+ if (r < 0)
+ return r;
+
+ for (child = device; sd_device_get_parent(child, &parent) >= 0; child = parent) {
+ r = print_all_attributes(parent, true);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int print_record(sd_device *device, const char *prefix) {
+ const char *str, *subsys;
+ dev_t devnum;
+ uint64_t q;
+ int i, ifi;
+
+ assert(device);
+
+ prefix = strempty(prefix);
+
+ /* We don't show syspath here, because it's identical to devpath (modulo the "/sys" prefix).
+ *
+ * We don't show action/seqnum here because that only makes sense for records synthesized from
+ * uevents, not for those synthesized from database entries.
+ *
+ * We don't show sysattrs here, because they can be expensive and potentially issue expensive driver
+ * IO.
+ *
+ * Coloring: let's be conservative with coloring. Let's use it to group related fields. Right now:
+ *
+ * • white for fields that give the device a name
+ * • green for fields that categorize the device into subsystem/devtype and similar
+ * • cyan for fields about associated device nodes/symlinks/network interfaces and such
+ * • magenta for block device diskseq
+ * • yellow for driver info
+ * • no color for regular properties */
+
+ assert_se(sd_device_get_devpath(device, &str) >= 0);
+ printf("%sP: %s%s%s\n", prefix, ansi_highlight_white(), str, ansi_normal());
+
+ if (sd_device_get_sysname(device, &str) >= 0)
+ printf("%sM: %s%s%s\n", prefix, ansi_highlight_white(), str, ansi_normal());
+
+ if (sd_device_get_sysnum(device, &str) >= 0)
+ printf("%sR: %s%s%s\n", prefix, ansi_highlight_white(), str, ansi_normal());
+
+ if (sd_device_get_subsystem(device, &subsys) >= 0)
+ printf("%sU: %s%s%s\n", prefix, ansi_highlight_green(), subsys, ansi_normal());
+
+ if (sd_device_get_devtype(device, &str) >= 0)
+ printf("%sT: %s%s%s\n", prefix, ansi_highlight_green(), str, ansi_normal());
+
+ if (sd_device_get_devnum(device, &devnum) >= 0)
+ printf("%sD: %s%c %u:%u%s\n",
+ prefix,
+ ansi_highlight_cyan(),
+ streq_ptr(subsys, "block") ? 'b' : 'c', major(devnum), minor(devnum),
+ ansi_normal());
+
+ if (sd_device_get_ifindex(device, &ifi) >= 0)
+ printf("%sI: %s%i%s\n", prefix, ansi_highlight_cyan(), ifi, ansi_normal());
+
+ if (sd_device_get_devname(device, &str) >= 0) {
+ const char *val;
+
+ assert_se(val = path_startswith(str, "/dev/"));
+ printf("%sN: %s%s%s\n", prefix, ansi_highlight_cyan(), val, ansi_normal());
+
+ if (device_get_devlink_priority(device, &i) >= 0)
+ printf("%sL: %s%i%s\n", prefix, ansi_highlight_cyan(), i, ansi_normal());
+
+ FOREACH_DEVICE_DEVLINK(device, link) {
+ assert_se(val = path_startswith(link, "/dev/"));
+ printf("%sS: %s%s%s\n", prefix, ansi_highlight_cyan(), val, ansi_normal());
+ }
+ }
+
+ if (sd_device_get_diskseq(device, &q) >= 0)
+ printf("%sQ: %s%" PRIu64 "%s\n", prefix, ansi_highlight_magenta(), q, ansi_normal());
+
+ if (sd_device_get_driver(device, &str) >= 0)
+ printf("%sV: %s%s%s\n", prefix, ansi_highlight_yellow4(), str, ansi_normal());
+
+ FOREACH_DEVICE_PROPERTY(device, key, val)
+ printf("%sE: %s=%s\n", prefix, key, val);
+
+ if (isempty(prefix))
+ puts("");
+ return 0;
+}
+
+static int record_to_json(sd_device *device, JsonVariant **ret) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ const char *str;
+ int r;
+
+ assert(device);
+ assert(ret);
+
+ /* We don't show any shorthand fields here as done in print_record() except for SYSNAME and SYSNUM as
+ * all the other ones have a matching property which will already be included. */
+
+ if (sd_device_get_sysname(device, &str) >= 0) {
+ r = json_variant_set_field_string(&v, "SYSNAME", str);
+ if (r < 0)
+ return r;
+ }
+
+ if (sd_device_get_sysnum(device, &str) >= 0) {
+ r = json_variant_set_field_string(&v, "SYSNUM", str);
+ if (r < 0)
+ return r;
+ }
+
+ FOREACH_DEVICE_PROPERTY(device, key, val) {
+ r = json_variant_set_field_string(&v, key, val);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+static int stat_device(const char *name, bool export, const char *prefix) {
+ struct stat statbuf;
+
+ assert(name);
+
+ if (stat(name, &statbuf) != 0)
+ return -errno;
+
+ if (export) {
+ if (!prefix)
+ prefix = "INFO_";
+ printf("%sMAJOR=%u\n"
+ "%sMINOR=%u\n",
+ prefix, major(statbuf.st_dev),
+ prefix, minor(statbuf.st_dev));
+ } else
+ printf("%u:%u\n", major(statbuf.st_dev), minor(statbuf.st_dev));
+ return 0;
+}
+
+static int export_devices(sd_device_enumerator *e) {
+ sd_device *d;
+ int r;
+
+ assert(e);
+
+ r = device_enumerator_scan_devices(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to scan devices: %m");
+
+ pager_open(arg_pager_flags);
+
+ FOREACH_DEVICE_AND_SUBSYSTEM(e, d)
+ if (arg_json_format_flags & JSON_FORMAT_OFF)
+ (void) print_record(d, NULL);
+ else {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ r = record_to_json(d, &v);
+ if (r < 0)
+ return r;
+
+ (void) json_variant_dump(v, arg_json_format_flags, stdout, NULL);
+ }
+
+ return 0;
+}
+
+static void cleanup_dir(DIR *dir, mode_t mask, int depth) {
+ assert(dir);
+
+ if (depth <= 0)
+ return;
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ struct stat stats;
+
+ if (dot_or_dot_dot(dent->d_name))
+ continue;
+ if (fstatat(dirfd(dir), dent->d_name, &stats, AT_SYMLINK_NOFOLLOW) < 0)
+ continue;
+ if ((stats.st_mode & mask) != 0)
+ continue;
+ if (S_ISDIR(stats.st_mode)) {
+ _cleanup_closedir_ DIR *subdir = NULL;
+
+ subdir = xopendirat(dirfd(dir), dent->d_name, O_NOFOLLOW);
+ if (!subdir)
+ log_debug_errno(errno, "Failed to open subdirectory '%s', ignoring: %m", dent->d_name);
+ else
+ cleanup_dir(subdir, mask, depth-1);
+
+ (void) unlinkat(dirfd(dir), dent->d_name, AT_REMOVEDIR);
+ } else
+ (void) unlinkat(dirfd(dir), dent->d_name, 0);
+ }
+}
+
+/*
+ * Assume that dir is a directory with file names matching udev data base
+ * entries for devices in /run/udev/data (such as "b8:16"), and removes
+ * all files except those that haven't been deleted in /run/udev/data
+ * (i.e. they were skipped during db cleanup because of the db_persist flag).
+ */
+static void cleanup_dir_after_db_cleanup(DIR *dir, DIR *datadir) {
+ assert(dir);
+ assert(datadir);
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ if (dot_or_dot_dot(dent->d_name))
+ continue;
+
+ if (faccessat(dirfd(datadir), dent->d_name, F_OK, AT_SYMLINK_NOFOLLOW) >= 0)
+ /* The corresponding udev database file still exists.
+ * Assuming the persistent flag is set for the database. */
+ continue;
+
+ (void) unlinkat(dirfd(dir), dent->d_name, 0);
+ }
+}
+
+static void cleanup_dirs_after_db_cleanup(DIR *dir, DIR *datadir) {
+ assert(dir);
+ assert(datadir);
+
+ FOREACH_DIRENT_ALL(dent, dir, break) {
+ struct stat stats;
+
+ if (dot_or_dot_dot(dent->d_name))
+ continue;
+ if (fstatat(dirfd(dir), dent->d_name, &stats, AT_SYMLINK_NOFOLLOW) < 0)
+ continue;
+ if (S_ISDIR(stats.st_mode)) {
+ _cleanup_closedir_ DIR *subdir = NULL;
+
+ subdir = xopendirat(dirfd(dir), dent->d_name, O_NOFOLLOW);
+ if (!subdir)
+ log_debug_errno(errno, "Failed to open subdirectory '%s', ignoring: %m", dent->d_name);
+ else
+ cleanup_dir_after_db_cleanup(subdir, datadir);
+
+ (void) unlinkat(dirfd(dir), dent->d_name, AT_REMOVEDIR);
+ } else
+ (void) unlinkat(dirfd(dir), dent->d_name, 0);
+ }
+}
+
+static void cleanup_db(void) {
+ _cleanup_closedir_ DIR *dir1 = NULL, *dir2 = NULL, *dir3 = NULL, *dir4 = NULL;
+
+ dir1 = opendir("/run/udev/data");
+ if (dir1)
+ cleanup_dir(dir1, S_ISVTX, 1);
+
+ dir2 = opendir("/run/udev/links");
+ if (dir2)
+ cleanup_dirs_after_db_cleanup(dir2, dir1);
+
+ dir3 = opendir("/run/udev/tags");
+ if (dir3)
+ cleanup_dirs_after_db_cleanup(dir3, dir1);
+
+ dir4 = opendir("/run/udev/static_node-tags");
+ if (dir4)
+ cleanup_dir(dir4, 0, 2);
+
+ /* Do not remove /run/udev/watch. It will be handled by udevd well on restart.
+ * And should not be removed by external program when udevd is running. */
+}
+
+static int query_device(QueryType query, sd_device* device) {
+ int r;
+
+ assert(device);
+
+ switch (query) {
+ case QUERY_NAME: {
+ const char *node;
+
+ r = sd_device_get_devname(device, &node);
+ if (r < 0)
+ return log_error_errno(r, "No device node found: %m");
+
+ if (!arg_root)
+ assert_se(node = path_startswith(node, "/dev/"));
+ printf("%s\n", node);
+ return 0;
+ }
+
+ case QUERY_SYMLINK: {
+ const char *prefix = "";
+
+ FOREACH_DEVICE_DEVLINK(device, devlink) {
+ if (!arg_root)
+ assert_se(devlink = path_startswith(devlink, "/dev/"));
+ printf("%s%s", prefix, devlink);
+ prefix = " ";
+ }
+ puts("");
+ return 0;
+ }
+
+ case QUERY_PATH: {
+ const char *devpath;
+
+ r = sd_device_get_devpath(device, &devpath);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get device path: %m");
+
+ printf("%s\n", devpath);
+ return 0;
+ }
+
+ case QUERY_PROPERTY:
+ FOREACH_DEVICE_PROPERTY(device, key, value) {
+ if (arg_properties && !strv_contains(arg_properties, key))
+ continue;
+
+ if (arg_export)
+ printf("%s%s='%s'\n", strempty(arg_export_prefix), key, value);
+ else if (arg_value)
+ printf("%s\n", value);
+ else
+ printf("%s=%s\n", key, value);
+ }
+
+ return 0;
+
+ case QUERY_ALL:
+ if (arg_json_format_flags & JSON_FORMAT_OFF)
+ return print_record(device, NULL);
+ else {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ r = record_to_json(device, &v);
+ if (r < 0)
+ return r;
+
+ (void) json_variant_dump(v, arg_json_format_flags, stdout, NULL);
+ }
+
+ return 0;
+
+ default:
+ assert_not_reached();
+ }
+}
+
+static int help(void) {
+ printf("%s info [OPTIONS] [DEVPATH|FILE]\n\n"
+ "Query sysfs or the udev database.\n\n"
+ " -h --help Print this message\n"
+ " -V --version Print version of the program\n"
+ " -q --query=TYPE Query device information:\n"
+ " name Name of device node\n"
+ " symlink Pointing to node\n"
+ " path sysfs device path\n"
+ " property The device properties\n"
+ " all All values\n"
+ " --property=NAME Show only properties by this name\n"
+ " --value When showing properties, print only their values\n"
+ " -p --path=SYSPATH sysfs device path used for query or attribute walk\n"
+ " -n --name=NAME Node or symlink name used for query or attribute walk\n"
+ " -r --root Prepend dev directory to path names\n"
+ " -a --attribute-walk Print all key matches walking along the chain\n"
+ " of parent devices\n"
+ " -t --tree Show tree of devices\n"
+ " -d --device-id-of-file=FILE Print major:minor of device containing this file\n"
+ " -x --export Export key/value pairs\n"
+ " -P --export-prefix Export the key name with a prefix\n"
+ " -e --export-db Export the content of the udev database\n"
+ " -c --cleanup-db Clean up the udev database\n"
+ " -w --wait-for-initialization[=SECONDS]\n"
+ " Wait for device to be initialized\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --json=pretty|short|off Generate JSON output\n"
+ " --subsystem-match=SUBSYSTEM\n"
+ " Query devices matching a subsystem\n"
+ " --subsystem-nomatch=SUBSYSTEM\n"
+ " Query devices not matching a subsystem\n"
+ " --attr-match=FILE[=VALUE]\n"
+ " Query devices that match an attribute\n"
+ " --attr-nomatch=FILE[=VALUE]\n"
+ " Query devices that do not match an attribute\n"
+ " --property-match=KEY=VALUE\n"
+ " Query devices with matching properties\n"
+ " --tag-match=TAG Query devices with a matching tag\n"
+ " --sysname-match=NAME Query devices with this /sys path\n"
+ " --name-match=NAME Query devices with this /dev name\n"
+ " --parent-match=NAME Query devices with this parent device\n"
+ " --initialized-match Query devices that are already initialized\n"
+ " --initialized-nomatch Query devices that are not initialized yet\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int draw_tree(
+ sd_device *parent,
+ sd_device *const array[], size_t n,
+ const char *prefix,
+ unsigned level);
+
+static int output_tree_device(
+ sd_device *device,
+ const char *str,
+ const char *prefix,
+ bool more,
+ sd_device *const array[], size_t n,
+ unsigned level) {
+
+ _cleanup_free_ char *subprefix = NULL, *subsubprefix = NULL;
+
+ assert(device);
+ assert(str);
+
+ prefix = strempty(prefix);
+
+ printf("%s%s%s\n", prefix, special_glyph(more ? SPECIAL_GLYPH_TREE_BRANCH : SPECIAL_GLYPH_TREE_RIGHT), str);
+
+ subprefix = strjoin(prefix, special_glyph(more ? SPECIAL_GLYPH_TREE_VERTICAL : SPECIAL_GLYPH_TREE_SPACE));
+ if (!subprefix)
+ return log_oom();
+
+ subsubprefix = strjoin(subprefix, special_glyph(SPECIAL_GLYPH_VERTICAL_DOTTED), " ");
+ if (!subsubprefix)
+ return log_oom();
+
+ (void) print_record(device, subsubprefix);
+
+ return draw_tree(device, array, n, subprefix, level + 1);
+}
+
+static int draw_tree(
+ sd_device *parent,
+ sd_device *const array[], size_t n,
+ const char *prefix,
+ unsigned level) {
+
+ const char *parent_path;
+ size_t i = 0;
+ int r;
+
+ if (n == 0)
+ return 0;
+
+ assert(array);
+
+ if (parent) {
+ r = sd_device_get_devpath(parent, &parent_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get sysfs path of parent device: %m");
+ } else
+ parent_path = NULL;
+
+ if (level > TREE_DEPTH_MAX) {
+ log_warning("Eliding tree below '%s', too deep.", strna(parent_path));
+ return 0;
+ }
+
+ while (i < n) {
+ sd_device *device = array[i];
+ const char *device_path, *str;
+ bool more = false;
+ size_t j;
+
+ r = sd_device_get_devpath(device, &device_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get sysfs path of enumerated device: %m");
+
+ /* Scan through the subsequent devices looking children of the device we are looking at. */
+ for (j = i + 1; j < n; j++) {
+ sd_device *next = array[j];
+ const char *next_path;
+
+ r = sd_device_get_devpath(next, &next_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get sysfs of child device: %m");
+
+ if (!path_startswith(next_path, device_path)) {
+ more = !parent_path || path_startswith(next_path, parent_path);
+ break;
+ }
+ }
+
+ /* Determine the string to display for this node. If we are at the top of the tree, the full
+ * device path so far, otherwise just the part suffixing the parent's device path. */
+ str = parent ? ASSERT_PTR(path_startswith(device_path, parent_path)) : device_path;
+
+ r = output_tree_device(device, str, prefix, more, array + i + 1, j - i - 1, level);
+ if (r < 0)
+ return r;
+
+ i = j;
+ }
+
+ return 0;
+}
+
+static int print_tree(sd_device* below) {
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ const char *below_path;
+ sd_device **array;
+ size_t n = 0;
+ int r;
+
+ if (below) {
+ r = sd_device_get_devpath(below, &below_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get sysfs path of device: %m");
+
+ } else
+ below_path = NULL;
+
+ r = sd_device_enumerator_new(&e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate device enumerator: %m");
+
+ if (below) {
+ r = sd_device_enumerator_add_match_parent(e, below);
+ if (r < 0)
+ return log_error_errno(r, "Failed to install parent enumerator match: %m");
+ }
+
+ r = sd_device_enumerator_allow_uninitialized(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable enumeration of uninitialized devices: %m");
+
+ r = device_enumerator_scan_devices_and_subsystems(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to scan for devices and subsystems: %m");
+
+ if (below) {
+ /* This must be called after device_enumerator_scan_devices_and_subsystems(). */
+ r = device_enumerator_add_parent_devices(e, below);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add parent devices: %m");
+ }
+
+ assert_se(array = device_enumerator_get_devices(e, &n));
+
+ if (n == 0) {
+ log_info("No items.");
+ return 0;
+ }
+
+ r = draw_tree(NULL, array, n, NULL, 0);
+ if (r < 0)
+ return r;
+
+ printf("\n%zu items shown.\n", n);
+ return 0;
+}
+
+static int ensure_device_enumerator(sd_device_enumerator **e) {
+ int r;
+
+ assert(e);
+
+ if (*e)
+ return 0;
+
+ r = sd_device_enumerator_new(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create device enumerator: %m");
+
+ r = sd_device_enumerator_allow_uninitialized(*e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allow uninitialized devices: %m");
+
+ return 0;
+}
+
+static int parse_key_value_argument(const char *s, char **key, char **value) {
+ _cleanup_free_ char *k = NULL, *v = NULL;
+ int r;
+
+ assert(s);
+ assert(key);
+ assert(value);
+
+ r = extract_many_words(&s, "=", EXTRACT_DONT_COALESCE_SEPARATORS, &k, &v, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse key/value pair %s: %m", s);
+ if (r < 2)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing '=' in key/value pair %s.", s);
+
+ if (!filename_is_valid(k))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "%s is not a valid key name", k);
+
+ free_and_replace(*key, k);
+ free_and_replace(*value, v);
+ return 0;
+}
+
+int info_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ _cleanup_strv_free_ char **devices = NULL;
+ _cleanup_free_ char *name = NULL;
+ int c, r, ret;
+
+ enum {
+ ARG_PROPERTY = 0x100,
+ ARG_VALUE,
+ ARG_NO_PAGER,
+ ARG_JSON,
+ ARG_SUBSYSTEM_MATCH,
+ ARG_SUBSYSTEM_NOMATCH,
+ ARG_ATTR_MATCH,
+ ARG_ATTR_NOMATCH,
+ ARG_PROPERTY_MATCH,
+ ARG_TAG_MATCH,
+ ARG_SYSNAME_MATCH,
+ ARG_NAME_MATCH,
+ ARG_PARENT_MATCH,
+ ARG_INITIALIZED_MATCH,
+ ARG_INITIALIZED_NOMATCH,
+ };
+
+ static const struct option options[] = {
+ { "attribute-walk", no_argument, NULL, 'a' },
+ { "tree", no_argument, NULL, 't' },
+ { "cleanup-db", no_argument, NULL, 'c' },
+ { "device-id-of-file", required_argument, NULL, 'd' },
+ { "export", no_argument, NULL, 'x' },
+ { "export-db", no_argument, NULL, 'e' },
+ { "export-prefix", required_argument, NULL, 'P' },
+ { "help", no_argument, NULL, 'h' },
+ { "name", required_argument, NULL, 'n' },
+ { "path", required_argument, NULL, 'p' },
+ { "property", required_argument, NULL, ARG_PROPERTY },
+ { "query", required_argument, NULL, 'q' },
+ { "root", no_argument, NULL, 'r' },
+ { "value", no_argument, NULL, ARG_VALUE },
+ { "version", no_argument, NULL, 'V' },
+ { "wait-for-initialization", optional_argument, NULL, 'w' },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "subsystem-match", required_argument, NULL, ARG_SUBSYSTEM_MATCH },
+ { "subsystem-nomatch", required_argument, NULL, ARG_SUBSYSTEM_NOMATCH },
+ { "attr-match", required_argument, NULL, ARG_ATTR_MATCH },
+ { "attr-nomatch", required_argument, NULL, ARG_ATTR_NOMATCH },
+ { "property-match", required_argument, NULL, ARG_PROPERTY_MATCH },
+ { "tag-match", required_argument, NULL, ARG_TAG_MATCH },
+ { "sysname-match", required_argument, NULL, ARG_SYSNAME_MATCH },
+ { "name-match", required_argument, NULL, ARG_NAME_MATCH },
+ { "parent-match", required_argument, NULL, ARG_PARENT_MATCH },
+ { "initialized-match", no_argument, NULL, ARG_INITIALIZED_MATCH },
+ { "initialized-nomatch", no_argument, NULL, ARG_INITIALIZED_NOMATCH },
+ {}
+ };
+
+ ActionType action = ACTION_QUERY;
+ QueryType query = QUERY_ALL;
+
+ while ((c = getopt_long(argc, argv, "atced:n:p:q:rxP:w::Vh", options, NULL)) >= 0)
+ switch (c) {
+ case ARG_PROPERTY:
+ /* Make sure that if the empty property list was specified, we won't show any
+ properties. */
+ if (isempty(optarg) && !arg_properties) {
+ arg_properties = new0(char*, 1);
+ if (!arg_properties)
+ return log_oom();
+ } else {
+ r = strv_split_and_extend(&arg_properties, optarg, ",", true);
+ if (r < 0)
+ return log_oom();
+ }
+ break;
+ case ARG_VALUE:
+ arg_value = true;
+ break;
+ case 'n':
+ case 'p': {
+ const char *prefix = c == 'n' ? "/dev/" : "/sys/";
+ char *path;
+
+ path = path_join(path_startswith(optarg, prefix) ? NULL : prefix, optarg);
+ if (!path)
+ return log_oom();
+
+ r = strv_consume(&devices, path);
+ if (r < 0)
+ return log_oom();
+ break;
+ }
+
+ case 'q':
+ action = ACTION_QUERY;
+ if (streq(optarg, "property") || streq(optarg, "env"))
+ query = QUERY_PROPERTY;
+ else if (streq(optarg, "name"))
+ query = QUERY_NAME;
+ else if (streq(optarg, "symlink"))
+ query = QUERY_SYMLINK;
+ else if (streq(optarg, "path"))
+ query = QUERY_PATH;
+ else if (streq(optarg, "all"))
+ query = QUERY_ALL;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "unknown query type");
+ break;
+ case 'r':
+ arg_root = true;
+ break;
+ case 'd':
+ action = ACTION_DEVICE_ID_FILE;
+ r = free_and_strdup(&name, optarg);
+ if (r < 0)
+ return log_oom();
+ break;
+ case 'a':
+ action = ACTION_ATTRIBUTE_WALK;
+ break;
+ case 't':
+ action = ACTION_TREE;
+ break;
+ case 'e':
+ action = ACTION_EXPORT;
+ break;
+ case 'c':
+ cleanup_db();
+ return 0;
+ case 'x':
+ arg_export = true;
+ break;
+ case 'P':
+ arg_export = true;
+ arg_export_prefix = optarg;
+ break;
+ case 'w':
+ if (optarg) {
+ r = parse_sec(optarg, &arg_wait_for_initialization_timeout);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse timeout value: %m");
+ } else
+ arg_wait_for_initialization_timeout = USEC_INFINITY;
+ break;
+ case 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+ break;
+
+ case ARG_SUBSYSTEM_MATCH:
+ case ARG_SUBSYSTEM_NOMATCH:
+ r = ensure_device_enumerator(&e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_subsystem(e, optarg, c == ARG_SUBSYSTEM_MATCH);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add%s subsystem match '%s': %m",
+ c == ARG_SUBSYSTEM_MATCH ? "" : " negative", optarg);
+
+ break;
+
+ case ARG_ATTR_MATCH:
+ case ARG_ATTR_NOMATCH: {
+ _cleanup_free_ char *k = NULL, *v = NULL;
+
+ r = ensure_device_enumerator(&e);
+ if (r < 0)
+ return r;
+
+ r = parse_key_value_argument(optarg, &k, &v);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_sysattr(e, k, v, c == ARG_ATTR_MATCH);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add%s sysattr match '%s=%s': %m",
+ c == ARG_ATTR_MATCH ? "" : " negative", k, v);
+ break;
+ }
+
+ case ARG_PROPERTY_MATCH: {
+ _cleanup_free_ char *k = NULL, *v = NULL;
+
+ r = ensure_device_enumerator(&e);
+ if (r < 0)
+ return r;
+
+ r = parse_key_value_argument(optarg, &k, &v);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_property_required(e, k, v);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add property match '%s=%s': %m", k, v);
+ break;
+ }
+
+ case ARG_TAG_MATCH:
+ r = ensure_device_enumerator(&e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_tag(e, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add tag match '%s': %m", optarg);
+ break;
+
+ case ARG_SYSNAME_MATCH:
+ r = ensure_device_enumerator(&e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_add_match_sysname(e, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add sysname match '%s': %m", optarg);
+ break;
+
+ case ARG_NAME_MATCH:
+ case ARG_PARENT_MATCH: {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+
+ r = find_device(optarg, c == ARG_NAME_MATCH ? "/dev" : "/sys", &dev);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open the device '%s': %m", optarg);
+
+ r = ensure_device_enumerator(&e);
+ if (r < 0)
+ return r;
+
+ r = device_enumerator_add_match_parent_incremental(e, dev);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add parent match '%s': %m", optarg);
+ break;
+ }
+
+ case ARG_INITIALIZED_MATCH:
+ case ARG_INITIALIZED_NOMATCH:
+ r = ensure_device_enumerator(&e);
+ if (r < 0)
+ return r;
+
+ r = device_enumerator_add_match_is_initialized(e, c == ARG_INITIALIZED_MATCH ? MATCH_INITIALIZED_YES : MATCH_INITIALIZED_NO);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set initialized filter: %m");
+ break;
+
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ if (action == ACTION_DEVICE_ID_FILE) {
+ if (argv[optind])
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Positional arguments are not allowed with -d/--device-id-of-file.");
+ assert(name);
+ return stat_device(name, arg_export, arg_export_prefix);
+ }
+
+ if (action == ACTION_EXPORT) {
+ r = ensure_device_enumerator(&e);
+ if (r < 0)
+ return r;
+
+ return export_devices(e);
+ }
+
+ r = strv_extend_strv(&devices, argv + optind, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build argument list: %m");
+
+ if (action != ACTION_TREE && strv_isempty(devices))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "A device name or path is required");
+ if (IN_SET(action, ACTION_ATTRIBUTE_WALK, ACTION_TREE) && strv_length(devices) > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Only one device may be specified with -a/--attribute-walk and -t/--tree");
+
+ if (arg_export && arg_value)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "-x/--export or -P/--export-prefix cannot be used with --value");
+
+ pager_open(arg_pager_flags);
+
+ if (strv_isempty(devices)) {
+ assert(action == ACTION_TREE);
+ return print_tree(NULL);
+ }
+
+ ret = 0;
+ STRV_FOREACH(p, devices) {
+ _cleanup_(sd_device_unrefp) sd_device *device = NULL;
+
+ r = find_device(*p, NULL, &device);
+ if (r < 0) {
+ if (r == -EINVAL)
+ log_error_errno(r, "Bad argument \"%s\", expected an absolute path in /dev/ or /sys/ or a unit name: %m", *p);
+ else
+ log_error_errno(r, "Unknown device \"%s\": %m", *p);
+
+ if (ret == 0)
+ ret = r;
+ continue;
+ }
+
+ if (arg_wait_for_initialization_timeout > 0) {
+ sd_device *d;
+
+ r = device_wait_for_initialization(
+ device,
+ NULL,
+ arg_wait_for_initialization_timeout,
+ &d);
+ if (r < 0)
+ return r;
+
+ sd_device_unref(device);
+ device = d;
+ }
+
+ if (action == ACTION_QUERY)
+ r = query_device(query, device);
+ else if (action == ACTION_ATTRIBUTE_WALK)
+ r = print_device_chain(device);
+ else if (action == ACTION_TREE)
+ r = print_tree(device);
+ else
+ assert_not_reached();
+ if (r < 0)
+ return r;
+ }
+
+ return ret;
+}
diff --git a/src/udev/udevadm-lock.c b/src/udev/udevadm-lock.c
new file mode 100644
index 0000000..bc2d5e7
--- /dev/null
+++ b/src/udev/udevadm-lock.c
@@ -0,0 +1,306 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <getopt.h>
+#include <stdlib.h>
+#include <sys/file.h>
+#include <unistd.h>
+
+#include "blockdev-util.h"
+#include "btrfs-util.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "fdset.h"
+#include "lock-util.h"
+#include "main-func.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "process-util.h"
+#include "signal-util.h"
+#include "sort-util.h"
+#include "strv.h"
+#include "time-util.h"
+#include "udevadm.h"
+
+static usec_t arg_timeout_usec = USEC_INFINITY;
+static char **arg_devices = NULL;
+static char **arg_backing = NULL;
+static char **arg_cmdline = NULL;
+static bool arg_print = false;
+
+STATIC_DESTRUCTOR_REGISTER(arg_devices, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_backing, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep);
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("udevadm", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...] COMMAND\n"
+ "%s [OPTIONS...] --print\n"
+ "\n%sLock a block device and run a command.%s\n\n"
+ " -h --help Print this message\n"
+ " -V --version Print version of the program\n"
+ " -d --device=DEVICE Block device to lock\n"
+ " -b --backing=FILE File whose backing block device to lock\n"
+ " -t --timeout=SECS Block at most the specified time waiting for lock\n"
+ " -p --print Only show which block device the lock would be taken on\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ link);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "device", required_argument, NULL, 'd' },
+ { "backing", required_argument, NULL, 'b' },
+ { "timeout", required_argument, NULL, 't' },
+ { "print", no_argument, NULL, 'p' },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
+ * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
+ optind = 0;
+ while ((c = getopt_long(argc, argv, arg_print ? "hVd:b:t:p" : "+hVd:b:t:p", options, NULL)) >= 0)
+
+ switch (c) {
+
+ case 'h':
+ return help();
+
+ case 'V':
+ return print_version();
+
+ case 'd':
+ case 'b': {
+ _cleanup_free_ char *s = NULL;
+ char ***l = c == 'd' ? &arg_devices : &arg_backing;
+
+ r = path_make_absolute_cwd(optarg, &s);
+ if (r < 0)
+ return log_error_errno(r, "Failed to make path '%s' absolute: %m", optarg);
+
+ path_simplify(s);
+
+ if (strv_consume(l, TAKE_PTR(s)) < 0)
+ return log_oom();
+
+ strv_uniq(*l);
+ break;
+ }
+
+ case 't':
+ r = parse_sec(optarg, &arg_timeout_usec);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --timeout= parameter: %s", optarg);
+ break;
+
+ case 'p':
+ arg_print = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (arg_print) {
+ if (optind != argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No arguments expected");
+ } else {
+ if (optind + 1 > argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments, command to execute.");
+
+ arg_cmdline = strv_copy(argv + optind);
+ if (!arg_cmdline)
+ return log_oom();
+ }
+
+ if (strv_isempty(arg_devices) && strv_isempty(arg_backing))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No devices to lock specified, refusing.");
+
+ return 1;
+}
+
+static int find_devno(
+ dev_t **devnos,
+ size_t *n_devnos,
+ const char *device,
+ bool backing) {
+
+ dev_t devt;
+ int r;
+
+ assert(devnos);
+ assert(n_devnos);
+ assert(*devnos || *n_devnos == 0);
+ assert(device);
+
+ r = path_get_whole_disk(device, backing, &devt);
+ if (r < 0)
+ return log_error_errno(r, "Failed to find whole block device for '%s': %m", device);
+
+ if (typesafe_bsearch(&devt, *devnos, *n_devnos, devt_compare_func)) {
+ log_debug("Device %u:%u already listed for locking, ignoring.", major(devt), minor(devt));
+ return 0;
+ }
+
+ if (!GREEDY_REALLOC(*devnos, *n_devnos + 1))
+ return log_oom();
+
+ (*devnos)[(*n_devnos)++] = devt;
+
+ /* Immediately sort again, to ensure the binary search above will work for the next device we add */
+ typesafe_qsort(*devnos, *n_devnos, devt_compare_func);
+ return 1;
+}
+
+static int lock_device(
+ const char *path,
+ dev_t devno,
+ usec_t deadline) {
+
+ _cleanup_close_ int fd = -EBADF;
+ struct stat st;
+ int r;
+
+ fd = open(path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open '%s': %m", path);
+
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat '%s': %m", path);
+
+ /* Extra safety: check that the device still refers to what we think it refers to */
+ if (!S_ISBLK(st.st_mode) || st.st_rdev != devno)
+ return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "Path '%s' no longer refers to specified block device %u:%u: %m", path, major(devno), minor(devno));
+
+ r = lock_generic(fd, LOCK_BSD, LOCK_EX|LOCK_NB);
+ if (r < 0) {
+ if (r != -EAGAIN)
+ return log_error_errno(r, "Failed to lock device '%s': %m", path);
+
+ if (deadline == 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Device '%s' is currently locked.", path);
+
+ if (deadline == USEC_INFINITY) {
+ log_info("Device '%s' is currently locked, waiting%s", path, special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ r = lock_generic(fd, LOCK_BSD, LOCK_EX);
+ } else {
+ usec_t left = usec_sub_unsigned(deadline, now(CLOCK_MONOTONIC));
+
+ log_info("Device '%s' is currently locked, waiting %s%s",
+ path, FORMAT_TIMESPAN(left, 0),
+ special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ r = lock_generic_with_timeout(fd, LOCK_BSD, LOCK_EX, left);
+ if (r == -ETIMEDOUT)
+ return log_error_errno(r, "Timeout reached.");
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to lock device '%s': %m", path);
+ }
+
+ log_debug("Successfully locked %s (%u:%u)%s", path, major(devno), minor(devno), special_glyph(SPECIAL_GLYPH_ELLIPSIS));
+
+ return TAKE_FD(fd);
+}
+
+int lock_main(int argc, char *argv[], void *userdata) {
+ _cleanup_fdset_free_ FDSet *fds = NULL;
+ _cleanup_free_ dev_t *devnos = NULL;
+ size_t n_devnos = 0;
+ usec_t deadline;
+ pid_t pid;
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ STRV_FOREACH(i, arg_devices) {
+ r = find_devno(&devnos, &n_devnos, *i, /* backing= */ false);
+ if (r < 0)
+ return r;
+ }
+
+ STRV_FOREACH(i, arg_backing) {
+ r = find_devno(&devnos, &n_devnos, *i, /* backing= */ true);
+ if (r < 0)
+ return r;
+ }
+
+ assert(n_devnos > 0);
+
+ fds = fdset_new();
+ if (!fds)
+ return log_oom();
+
+ if (!timestamp_is_set(arg_timeout_usec))
+ deadline = arg_timeout_usec;
+ else
+ deadline = usec_add(now(CLOCK_MONOTONIC), arg_timeout_usec);
+
+ for (size_t i = 0; i < n_devnos; i++) {
+ _cleanup_free_ char *node = NULL;
+
+ r = devname_from_devnum(S_IFBLK, devnos[i], &node);
+ if (r < 0)
+ return log_error_errno(r, "Failed to format block device path: %m");
+
+ if (arg_print)
+ printf("%s\n", node);
+ else {
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = lock_device(node, devnos[i], deadline);
+ if (fd < 0)
+ return fd;
+
+ r = fdset_consume(fds, TAKE_FD(fd));
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ if (arg_print)
+ return EXIT_SUCCESS;
+
+ /* Ignore SIGINT and allow the forked process to receive it */
+ (void) ignore_signals(SIGINT);
+
+ r = safe_fork("(lock)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_CLOSE_ALL_FDS|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG, &pid);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ /* Child */
+
+ execvp(arg_cmdline[0], arg_cmdline);
+ log_open();
+ log_error_errno(errno, "Failed to execute %s: %m", arg_cmdline[0]);
+ _exit(EXIT_FAILURE);
+ }
+
+ return wait_for_terminate_and_check(arg_cmdline[0], pid, 0);
+}
diff --git a/src/udev/udevadm-monitor.c b/src/udev/udevadm-monitor.c
new file mode 100644
index 0000000..27c4853
--- /dev/null
+++ b/src/udev/udevadm-monitor.c
@@ -0,0 +1,246 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+
+#include "sd-device.h"
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "device-monitor-private.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "hashmap.h"
+#include "set.h"
+#include "signal-util.h"
+#include "string-util.h"
+#include "udevadm.h"
+#include "virt.h"
+#include "time-util.h"
+
+static bool arg_show_property = false;
+static bool arg_print_kernel = false;
+static bool arg_print_udev = false;
+static Set *arg_tag_filter = NULL;
+static Hashmap *arg_subsystem_filter = NULL;
+
+static int device_monitor_handler(sd_device_monitor *monitor, sd_device *device, void *userdata) {
+ sd_device_action_t action = _SD_DEVICE_ACTION_INVALID;
+ const char *devpath = NULL, *subsystem = NULL;
+ MonitorNetlinkGroup group = PTR_TO_INT(userdata);
+ struct timespec ts;
+
+ assert(device);
+ assert(IN_SET(group, MONITOR_GROUP_UDEV, MONITOR_GROUP_KERNEL));
+
+ (void) sd_device_get_action(device, &action);
+ (void) sd_device_get_devpath(device, &devpath);
+ (void) sd_device_get_subsystem(device, &subsystem);
+
+ assert_se(clock_gettime(CLOCK_MONOTONIC, &ts) == 0);
+
+ printf("%-6s[%"PRI_TIME".%06"PRI_NSEC"] %-8s %s (%s)\n",
+ group == MONITOR_GROUP_UDEV ? "UDEV" : "KERNEL",
+ ts.tv_sec, (nsec_t)ts.tv_nsec/1000,
+ strna(device_action_to_string(action)),
+ devpath, subsystem);
+
+ if (arg_show_property) {
+ FOREACH_DEVICE_PROPERTY(device, key, value)
+ printf("%s=%s\n", key, value);
+
+ printf("\n");
+ }
+
+ return 0;
+}
+
+static int setup_monitor(MonitorNetlinkGroup sender, sd_event *event, sd_device_monitor **ret) {
+ _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL;
+ const char *subsystem, *devtype, *tag;
+ int r;
+
+ r = device_monitor_new_full(&monitor, sender, -1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create netlink socket: %m");
+
+ r = sd_device_monitor_attach_event(monitor, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach event: %m");
+
+ HASHMAP_FOREACH_KEY(devtype, subsystem, arg_subsystem_filter) {
+ r = sd_device_monitor_filter_add_match_subsystem_devtype(monitor, subsystem, devtype);
+ if (r < 0)
+ return log_error_errno(r, "Failed to apply subsystem filter '%s%s%s': %m",
+ subsystem, devtype ? "/" : "", strempty(devtype));
+ }
+
+ SET_FOREACH(tag, arg_tag_filter) {
+ r = sd_device_monitor_filter_add_match_tag(monitor, tag);
+ if (r < 0)
+ return log_error_errno(r, "Failed to apply tag filter '%s': %m", tag);
+ }
+
+ r = sd_device_monitor_start(monitor, device_monitor_handler, INT_TO_PTR(sender));
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+
+ (void) sd_device_monitor_set_description(monitor, sender == MONITOR_GROUP_UDEV ? "udev" : "kernel");
+
+ *ret = TAKE_PTR(monitor);
+ return 0;
+}
+
+static int help(void) {
+ printf("%s monitor [OPTIONS]\n\n"
+ "Listen to kernel and udev events.\n\n"
+ " -h --help Show this help\n"
+ " -V --version Show package version\n"
+ " -p --property Print the event properties\n"
+ " -k --kernel Print kernel uevents\n"
+ " -u --udev Print udev events\n"
+ " -s --subsystem-match=SUBSYSTEM[/DEVTYPE] Filter events by subsystem\n"
+ " -t --tag-match=TAG Filter events by tag\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "property", no_argument, NULL, 'p' },
+ { "environment", no_argument, NULL, 'e' }, /* alias for -p */
+ { "kernel", no_argument, NULL, 'k' },
+ { "udev", no_argument, NULL, 'u' },
+ { "subsystem-match", required_argument, NULL, 's' },
+ { "tag-match", required_argument, NULL, 't' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+
+ int r, c;
+
+ while ((c = getopt_long(argc, argv, "pekus:t:Vh", options, NULL)) >= 0)
+ switch (c) {
+ case 'p':
+ case 'e':
+ arg_show_property = true;
+ break;
+ case 'k':
+ arg_print_kernel = true;
+ break;
+ case 'u':
+ arg_print_udev = true;
+ break;
+ case 's': {
+ _cleanup_free_ char *subsystem = NULL, *devtype = NULL;
+ const char *slash;
+
+ slash = strchr(optarg, '/');
+ if (slash) {
+ devtype = strdup(slash + 1);
+ if (!devtype)
+ return -ENOMEM;
+
+ subsystem = strndup(optarg, slash - optarg);
+ } else
+ subsystem = strdup(optarg);
+
+ if (!subsystem)
+ return -ENOMEM;
+
+ r = hashmap_ensure_put(&arg_subsystem_filter, NULL, subsystem, devtype);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(subsystem);
+ TAKE_PTR(devtype);
+ break;
+ }
+ case 't':
+ /* optarg is stored in argv[], so we don't need to copy it */
+ r = set_ensure_put(&arg_tag_filter, &string_hash_ops, optarg);
+ if (r < 0)
+ return r;
+ break;
+
+ case 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ if (!arg_print_kernel && !arg_print_udev) {
+ arg_print_kernel = true;
+ arg_print_udev = true;
+ }
+
+ return 1;
+}
+
+int monitor_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *kernel_monitor = NULL, *udev_monitor = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ goto finalize;
+
+ if (running_in_chroot() > 0) {
+ log_info("Running in chroot, ignoring request.");
+ return 0;
+ }
+
+ /* Callers are expecting to see events as they happen: Line buffering */
+ setlinebuf(stdout);
+
+ r = sd_event_default(&event);
+ if (r < 0) {
+ log_error_errno(r, "Failed to initialize event: %m");
+ goto finalize;
+ }
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, -1) >= 0);
+ (void) sd_event_add_signal(event, NULL, SIGTERM, NULL, NULL);
+ (void) sd_event_add_signal(event, NULL, SIGINT, NULL, NULL);
+
+ printf("monitor will print the received events for:\n");
+ if (arg_print_udev) {
+ r = setup_monitor(MONITOR_GROUP_UDEV, event, &udev_monitor);
+ if (r < 0)
+ goto finalize;
+
+ printf("UDEV - the event which udev sends out after rule processing\n");
+ }
+
+ if (arg_print_kernel) {
+ r = setup_monitor(MONITOR_GROUP_KERNEL, event, &kernel_monitor);
+ if (r < 0)
+ goto finalize;
+
+ printf("KERNEL - the kernel uevent\n");
+ }
+ printf("\n");
+
+ r = sd_event_loop(event);
+ if (r < 0) {
+ log_error_errno(r, "Failed to run event loop: %m");
+ goto finalize;
+ }
+
+ r = 0;
+
+finalize:
+ hashmap_free_free_free(arg_subsystem_filter);
+ set_free(arg_tag_filter);
+
+ return r;
+}
diff --git a/src/udev/udevadm-settle.c b/src/udev/udevadm-settle.c
new file mode 100644
index 0000000..c236a70
--- /dev/null
+++ b/src/udev/udevadm-settle.c
@@ -0,0 +1,252 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © 2009 Canonical Ltd.
+ * Copyright © 2009 Scott James Remnant <scott@netsplit.com>
+ */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "sd-bus.h"
+#include "sd-event.h"
+#include "sd-login.h"
+#include "sd-messages.h"
+
+#include "bus-util.h"
+#include "path-util.h"
+#include "strv.h"
+#include "time-util.h"
+#include "udev-ctrl.h"
+#include "udev-util.h"
+#include "udevadm.h"
+#include "unit-def.h"
+#include "virt.h"
+
+static usec_t arg_timeout_usec = 120 * USEC_PER_SEC;
+static const char *arg_exists = NULL;
+
+static int help(void) {
+ printf("%s settle [OPTIONS]\n\n"
+ "Wait for pending udev events.\n\n"
+ " -h --help Show this help\n"
+ " -V --version Show package version\n"
+ " -t --timeout=SEC Maximum time to wait for events\n"
+ " -E --exit-if-exists=FILE Stop waiting if file exists\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "timeout", required_argument, NULL, 't' },
+ { "exit-if-exists", required_argument, NULL, 'E' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ { "seq-start", required_argument, NULL, 's' }, /* removed */
+ { "seq-end", required_argument, NULL, 'e' }, /* removed */
+ { "quiet", no_argument, NULL, 'q' }, /* removed */
+ {}
+ };
+
+ int c, r;
+
+ while ((c = getopt_long(argc, argv, "t:E:Vhs:e:q", options, NULL)) >= 0) {
+ switch (c) {
+ case 't':
+ r = parse_sec(optarg, &arg_timeout_usec);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg);
+ break;
+ case 'E':
+ if (!path_is_valid(optarg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid path: %s", optarg);
+
+ arg_exists = optarg;
+ break;
+ case 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case 's':
+ case 'e':
+ case 'q':
+ return log_info_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Option -%c no longer supported.",
+ c);
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+ }
+
+ return 1;
+}
+
+static int emit_deprecation_warning(void) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_strv_free_ char **a = NULL;
+ _cleanup_free_ char *unit = NULL;
+ int r;
+
+ r = sd_pid_get_unit(0, &unit);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to determine unit we run in, ignoring: %m");
+ return 0;
+ }
+
+ if (!streq(unit, "systemd-udev-settle.service"))
+ return 0;
+
+ r = bus_connect_system_systemd(&bus);
+ if (r < 0)
+ log_debug_errno(r, "Failed to open connection to systemd, skipping dependency queries: %m");
+ else {
+ _cleanup_strv_free_ char **b = NULL;
+ _cleanup_free_ char *unit_path = NULL;
+
+ unit_path = unit_dbus_path_from_name("systemd-udev-settle.service");
+ if (!unit_path)
+ return -ENOMEM;
+
+ (void) sd_bus_get_property_strv(
+ bus,
+ "org.freedesktop.systemd1",
+ unit_path,
+ "org.freedesktop.systemd1.Unit",
+ "WantedBy",
+ NULL,
+ &a);
+
+ (void) sd_bus_get_property_strv(
+ bus,
+ "org.freedesktop.systemd1",
+ unit_path,
+ "org.freedesktop.systemd1.Unit",
+ "RequiredBy",
+ NULL,
+ &b);
+
+ r = strv_extend_strv(&a, b, true);
+ if (r < 0)
+ return r;
+ }
+
+ if (strv_isempty(a))
+ /* Print a simple message if we cannot determine the dependencies */
+ log_notice("systemd-udev-settle.service is deprecated.");
+ else {
+ /* Print a longer, structured message if we can acquire the dependencies (this should be the
+ * common case). This is hooked up with a catalog entry and everything. */
+ _cleanup_free_ char *t = NULL;
+
+ t = strv_join(a, ", ");
+ if (!t)
+ return -ENOMEM;
+
+ log_struct(LOG_NOTICE,
+ LOG_MESSAGE("systemd-udev-settle.service is deprecated. Please fix %s not to pull it in.", t),
+ "OFFENDING_UNITS=%s", t,
+ "MESSAGE_ID=" SD_MESSAGE_SYSTEMD_UDEV_SETTLE_DEPRECATED_STR);
+ }
+
+ return 0;
+}
+
+static bool check(void) {
+ int r;
+
+ if (arg_exists) {
+ if (access(arg_exists, F_OK) >= 0)
+ return true;
+
+ if (errno != ENOENT)
+ log_warning_errno(errno, "Failed to check the existence of \"%s\", ignoring: %m", arg_exists);
+ }
+
+ /* exit if queue is empty */
+ r = udev_queue_is_empty();
+ if (r < 0)
+ log_warning_errno(r, "Failed to check if udev queue is empty, ignoring: %m");
+
+ return r > 0;
+}
+
+static int on_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) {
+ assert(s);
+
+ if (check())
+ return sd_event_exit(sd_event_source_get_event(s), 0);
+
+ return 0;
+}
+
+int settle_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ if (running_in_chroot() > 0) {
+ log_info("Running in chroot, ignoring request.");
+ return 0;
+ }
+
+ (void) emit_deprecation_warning();
+
+ if (getuid() == 0) {
+ _cleanup_(udev_ctrl_unrefp) UdevCtrl *uctrl = NULL;
+
+ /* guarantee that the udev daemon isn't pre-processing */
+
+ r = udev_ctrl_new(&uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create control socket for udev daemon: %m");
+
+ r = udev_ctrl_send_ping(uctrl);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to connect to udev daemon, ignoring: %m");
+ return 0;
+ }
+
+ r = udev_ctrl_wait(uctrl, MAX(5 * USEC_PER_SEC, arg_timeout_usec));
+ if (r < 0)
+ return log_error_errno(r, "Failed to wait for daemon to reply: %m");
+ } else {
+ /* For non-privileged users, at least check if udevd is running. */
+ if (access("/run/udev/control", F_OK) < 0)
+ return log_error_errno(errno,
+ errno == ENOENT ? "systemd-udevd is not running." :
+ "Failed to check if /run/udev/control exists: %m");
+ }
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get default sd-event object: %m");
+
+ r = sd_event_add_inotify(event, NULL, "/run/udev" , IN_DELETE, on_inotify, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add inotify watch for /run/udev: %m");
+
+ if (arg_timeout_usec != USEC_INFINITY) {
+ r = sd_event_add_time_relative(event, NULL, CLOCK_BOOTTIME, arg_timeout_usec, 0,
+ NULL, INT_TO_PTR(-ETIMEDOUT));
+ if (r < 0)
+ return log_error_errno(r, "Failed to add timer event source: %m");
+ }
+
+ /* Check before entering the event loop, as the udev queue may be already empty. */
+ if (check())
+ return 0;
+
+ r = sd_event_loop(event);
+ if (r == -ETIMEDOUT)
+ return log_error_errno(r, "Timed out for waiting the udev queue being empty.");
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+ return 0;
+}
diff --git a/src/udev/udevadm-test-builtin.c b/src/udev/udevadm-test-builtin.c
new file mode 100644
index 0000000..088b4da
--- /dev/null
+++ b/src/udev/udevadm-test-builtin.c
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "log.h"
+#include "udev-builtin.h"
+#include "udevadm.h"
+#include "udevadm-util.h"
+
+static sd_device_action_t arg_action = SD_DEVICE_ADD;
+static const char *arg_command = NULL;
+static const char *arg_syspath = NULL;
+
+static int help(void) {
+ printf("%s test-builtin [OPTIONS] COMMAND DEVPATH\n\n"
+ "Test a built-in command.\n\n"
+ " -h --help Print this message\n"
+ " -V --version Print version of the program\n\n"
+ " -a --action=ACTION|help Set action string\n"
+ "Commands:\n",
+ program_invocation_short_name);
+
+ udev_builtin_list();
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "action", required_argument, NULL, 'a' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+
+ int r, c;
+
+ while ((c = getopt_long(argc, argv, "a:Vh", options, NULL)) >= 0)
+ switch (c) {
+ case 'a':
+ r = parse_device_action(optarg, &arg_action);
+ if (r < 0)
+ return log_error_errno(r, "Invalid action '%s'", optarg);
+ if (r == 0)
+ return 0;
+ break;
+ case 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ arg_command = argv[optind++];
+ if (!arg_command)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Command missing.");
+
+ arg_syspath = argv[optind++];
+ if (!arg_syspath)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "device is missing.");
+
+ return 1;
+}
+
+int builtin_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(udev_event_freep) UdevEvent *event = NULL;
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ UdevBuiltinCommand cmd;
+ int r;
+
+ log_set_max_level(LOG_DEBUG);
+ log_parse_environment();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ udev_builtin_init();
+
+ cmd = udev_builtin_lookup(arg_command);
+ if (cmd < 0) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown command '%s'", arg_command);
+ goto finish;
+ }
+
+ r = find_device_with_action(arg_syspath, arg_action, &dev);
+ if (r < 0) {
+ log_error_errno(r, "Failed to open device '%s': %m", arg_syspath);
+ goto finish;
+ }
+
+ event = udev_event_new(dev, 0, NULL, LOG_DEBUG);
+ if (!event) {
+ r = log_oom();
+ goto finish;
+ }
+
+ r = udev_builtin_run(event, cmd, arg_command, true);
+ if (r < 0) {
+ log_debug_errno(r, "Builtin command '%s' fails: %m", arg_command);
+ goto finish;
+ }
+
+ r = 0;
+finish:
+ udev_builtin_exit();
+ return r;
+}
diff --git a/src/udev/udevadm-test.c b/src/udev/udevadm-test.c
new file mode 100644
index 0000000..e1afd7d
--- /dev/null
+++ b/src/udev/udevadm-test.c
@@ -0,0 +1,152 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © 2003-2004 Greg Kroah-Hartman <greg@kroah.com>
+ */
+
+#include <errno.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/signalfd.h>
+#include <unistd.h>
+
+#include "sd-device.h"
+
+#include "device-private.h"
+#include "device-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "udev-builtin.h"
+#include "udev-event.h"
+#include "udev-format.h"
+#include "udevadm-util.h"
+#include "udevadm.h"
+
+static sd_device_action_t arg_action = SD_DEVICE_ADD;
+static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY;
+static const char *arg_syspath = NULL;
+
+static int help(void) {
+
+ printf("%s test [OPTIONS] DEVPATH\n\n"
+ "Test an event run.\n\n"
+ " -h --help Show this help\n"
+ " -V --version Show package version\n"
+ " -a --action=ACTION|help Set action string\n"
+ " -N --resolve-names=early|late|never When to resolve names\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "action", required_argument, NULL, 'a' },
+ { "resolve-names", required_argument, NULL, 'N' },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ {}
+ };
+
+ int r, c;
+
+ while ((c = getopt_long(argc, argv, "a:N:Vh", options, NULL)) >= 0)
+ switch (c) {
+ case 'a':
+ r = parse_device_action(optarg, &arg_action);
+ if (r < 0)
+ return log_error_errno(r, "Invalid action '%s'", optarg);
+ if (r == 0)
+ return 0;
+ break;
+ case 'N':
+ arg_resolve_name_timing = resolve_name_timing_from_string(optarg);
+ if (arg_resolve_name_timing < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--resolve-names= must be early, late or never");
+ break;
+ case 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ arg_syspath = argv[optind];
+ if (!arg_syspath)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "syspath parameter missing.");
+
+ return 1;
+}
+
+int test_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(udev_rules_freep) UdevRules *rules = NULL;
+ _cleanup_(udev_event_freep) UdevEvent *event = NULL;
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ const char *cmd;
+ sigset_t mask, sigmask_orig;
+ void *val;
+ int r;
+
+ log_set_max_level(LOG_DEBUG);
+ log_parse_environment();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ printf("This program is for debugging only, it does not run any program\n"
+ "specified by a RUN key. It may show incorrect results, because\n"
+ "some values may be different, or not available at a simulation run.\n"
+ "\n");
+
+ assert_se(sigprocmask(SIG_SETMASK, NULL, &sigmask_orig) >= 0);
+
+ udev_builtin_init();
+
+ r = udev_rules_load(&rules, arg_resolve_name_timing);
+ if (r < 0) {
+ log_error_errno(r, "Failed to read udev rules: %m");
+ goto out;
+ }
+
+ r = find_device_with_action(arg_syspath, arg_action, &dev);
+ if (r < 0) {
+ log_error_errno(r, "Failed to open device '%s': %m", arg_syspath);
+ goto out;
+ }
+
+ /* don't read info from the db */
+ device_seal(dev);
+
+ event = udev_event_new(dev, 0, NULL, LOG_DEBUG);
+
+ assert_se(sigfillset(&mask) >= 0);
+ assert_se(sigprocmask(SIG_SETMASK, &mask, &sigmask_orig) >= 0);
+
+ udev_event_execute_rules(event, -1, 60 * USEC_PER_SEC, SIGKILL, NULL, rules);
+
+ FOREACH_DEVICE_PROPERTY(dev, key, value)
+ printf("%s=%s\n", key, value);
+
+ ORDERED_HASHMAP_FOREACH_KEY(val, cmd, event->run_list) {
+ char program[UDEV_PATH_SIZE];
+ bool truncated;
+
+ (void) udev_event_apply_format(event, cmd, program, sizeof(program), false, NULL, &truncated);
+ if (truncated)
+ log_warning("The command '%s' is truncated while substituting into '%s'.", program, cmd);
+ printf("run: '%s'\n", program);
+ }
+
+ r = 0;
+out:
+ udev_builtin_exit();
+ return r;
+}
diff --git a/src/udev/udevadm-trigger.c b/src/udev/udevadm-trigger.c
new file mode 100644
index 0000000..e0f487d
--- /dev/null
+++ b/src/udev/udevadm-trigger.c
@@ -0,0 +1,569 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+
+#include "sd-device.h"
+#include "sd-event.h"
+
+#include "device-enumerator-private.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "id128-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "set.h"
+#include "static-destruct.h"
+#include "string-util.h"
+#include "strv.h"
+#include "udevadm.h"
+#include "udevadm-util.h"
+#include "udev-ctrl.h"
+#include "virt.h"
+
+static bool arg_verbose = false;
+static bool arg_dry_run = false;
+static bool arg_quiet = false;
+static bool arg_uuid = false;
+static bool arg_settle = false;
+
+static int exec_list(
+ sd_device_enumerator *e,
+ sd_device_action_t action,
+ Set **ret_settle_path_or_ids) {
+
+ _cleanup_set_free_ Set *settle_path_or_ids = NULL;
+ int uuid_supported = -1;
+ const char *action_str;
+ sd_device *d;
+ int r, ret = 0;
+
+ assert(e);
+
+ action_str = device_action_to_string(action);
+
+ FOREACH_DEVICE_AND_SUBSYSTEM(e, d) {
+ sd_id128_t id = SD_ID128_NULL;
+ const char *syspath;
+
+ r = sd_device_get_syspath(d, &syspath);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to get syspath of enumerated devices, ignoring: %m");
+ continue;
+ }
+
+ if (arg_verbose)
+ printf("%s\n", syspath);
+
+ if (arg_dry_run)
+ continue;
+
+ /* Use the UUID mode if the user explicitly asked for it, or if --settle has been specified,
+ * so that we can recognize our own uevent. */
+ r = sd_device_trigger_with_uuid(d, action, (arg_uuid || arg_settle) && uuid_supported != 0 ? &id : NULL);
+ if (r == -EINVAL && !arg_uuid && arg_settle && uuid_supported < 0) {
+ /* If we specified a UUID because of the settling logic, and we got EINVAL this might
+ * be caused by an old kernel which doesn't know the UUID logic (pre-4.13). Let's try
+ * if it works without the UUID logic then. */
+ r = sd_device_trigger(d, action);
+ if (r != -EINVAL)
+ uuid_supported = false; /* dropping the uuid stuff changed the return code,
+ * hence don't bother next time */
+ }
+ if (r < 0) {
+ /* ENOENT may be returned when a device does not have /uevent or is already
+ * removed. Hence, this is logged at debug level and ignored.
+ *
+ * ENODEV may be returned by some buggy device drivers e.g. /sys/devices/vio.
+ * See,
+ * https://github.com/systemd/systemd/issues/13652#issuecomment-535129791 and
+ * https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1845319.
+ * So, this error is ignored, but logged at warning level to encourage people to
+ * fix the driver.
+ *
+ * EROFS is returned when /sys is read only. In that case, all subsequent
+ * writes will also fail, hence return immediately.
+ *
+ * EACCES or EPERM may be returned when this is invoked by non-priviledged user.
+ * We do NOT return immediately, but continue operation and propagate the error.
+ * Why? Some device can be owned by a user, e.g., network devices configured in
+ * a network namespace. See, https://github.com/systemd/systemd/pull/18559 and
+ * https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=ebb4a4bf76f164457184a3f43ebc1552416bc823
+ *
+ * All other errors are logged at error level, but let's continue the operation,
+ * and propagate the error.
+ */
+
+ bool ignore = IN_SET(r, -ENOENT, -ENODEV);
+ int level =
+ arg_quiet ? LOG_DEBUG :
+ r == -ENOENT ? LOG_DEBUG :
+ r == -ENODEV ? LOG_WARNING : LOG_ERR;
+
+ log_device_full_errno(d, level, r,
+ "Failed to write '%s' to '%s/uevent'%s: %m",
+ action_str, syspath, ignore ? ", ignoring" : "");
+
+ if (r == -EROFS)
+ return r;
+ if (ret == 0 && !ignore)
+ ret = r;
+ continue;
+ } else
+ log_device_debug(d, "Triggered device with action '%s'.", action_str);
+
+ if (uuid_supported < 0)
+ uuid_supported = true;
+
+ /* If the user asked for it, write event UUID to stdout */
+ if (arg_uuid)
+ printf(SD_ID128_UUID_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(id));
+
+ if (arg_settle) {
+ if (uuid_supported) {
+ sd_id128_t *dup;
+
+ dup = newdup(sd_id128_t, &id, 1);
+ if (!dup)
+ return log_oom();
+
+ r = set_ensure_consume(&settle_path_or_ids, &id128_hash_ops_free, dup);
+ } else {
+ char *dup;
+
+ dup = strdup(syspath);
+ if (!dup)
+ return log_oom();
+
+ r = set_ensure_consume(&settle_path_or_ids, &path_hash_ops_free, dup);
+ }
+ if (r < 0)
+ return log_oom();
+ }
+ }
+
+ if (ret_settle_path_or_ids)
+ *ret_settle_path_or_ids = TAKE_PTR(settle_path_or_ids);
+
+ return ret;
+}
+
+static int device_monitor_handler(sd_device_monitor *m, sd_device *dev, void *userdata) {
+ Set *settle_path_or_ids = * (Set**) ASSERT_PTR(userdata);
+ const char *syspath;
+ sd_id128_t id;
+ int r;
+
+ assert(dev);
+
+ r = sd_device_get_syspath(dev, &syspath);
+ if (r < 0) {
+ log_device_debug_errno(dev, r, "Failed to get syspath of device event, ignoring: %m");
+ return 0;
+ }
+
+ if (sd_device_get_trigger_uuid(dev, &id) >= 0) {
+ _cleanup_free_ sd_id128_t *saved = NULL;
+
+ saved = set_remove(settle_path_or_ids, &id);
+ if (!saved) {
+ log_device_debug(dev, "Got uevent not matching expected UUID, ignoring.");
+ return 0;
+ }
+ } else {
+ _cleanup_free_ char *saved = NULL;
+
+ saved = set_remove(settle_path_or_ids, syspath);
+ if (!saved) {
+ const char *old_sysname;
+
+ /* When the device is renamed, the new name is broadcast, and the old name is saved
+ * in INTERFACE_OLD.
+ *
+ * TODO: remove support for INTERFACE_OLD when kernel baseline is bumped to 4.13 or
+ * higher. See 1193448cb68e5a90cab027e16a093bbd367e9494.
+ */
+
+ if (sd_device_get_property_value(dev, "INTERFACE_OLD", &old_sysname) >= 0) {
+ _cleanup_free_ char *dir = NULL, *old_syspath = NULL;
+
+ r = path_extract_directory(syspath, &dir);
+ if (r < 0) {
+ log_device_debug_errno(dev, r,
+ "Failed to extract directory from '%s', ignoring: %m",
+ syspath);
+ return 0;
+ }
+
+ old_syspath = path_join(dir, old_sysname);
+ if (!old_syspath) {
+ log_oom_debug();
+ return 0;
+ }
+
+ saved = set_remove(settle_path_or_ids, old_syspath);
+ }
+ }
+ if (!saved) {
+ log_device_debug(dev, "Got uevent for unexpected device, ignoring.");
+ return 0;
+ }
+ }
+
+ if (arg_verbose)
+ printf("settle %s\n", syspath);
+
+ if (arg_uuid)
+ printf("settle " SD_ID128_UUID_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(id));
+
+ if (set_isempty(settle_path_or_ids))
+ return sd_event_exit(sd_device_monitor_get_event(m), 0);
+
+ return 0;
+}
+
+static char* keyval(const char *str, const char **key, const char **val) {
+ char *buf, *pos;
+
+ buf = strdup(str);
+ if (!buf)
+ return NULL;
+
+ pos = strchr(buf, '=');
+ if (pos) {
+ pos[0] = 0;
+ pos++;
+ }
+
+ *key = buf;
+ *val = pos;
+
+ return buf;
+}
+
+static int help(void) {
+ printf("%s trigger [OPTIONS] DEVPATH\n\n"
+ "Request events from the kernel.\n\n"
+ " -h --help Show this help\n"
+ " -V --version Show package version\n"
+ " -v --verbose Print the list of devices while running\n"
+ " -n --dry-run Do not actually trigger the events\n"
+ " -q --quiet Suppress error logging in triggering events\n"
+ " -t --type= Type of events to trigger\n"
+ " devices sysfs devices (default)\n"
+ " subsystems sysfs subsystems and drivers\n"
+ " all sysfs devices, subsystems, and drivers\n"
+ " -c --action=ACTION|help Event action value, default is \"change\"\n"
+ " -s --subsystem-match=SUBSYSTEM Trigger devices from a matching subsystem\n"
+ " -S --subsystem-nomatch=SUBSYSTEM Exclude devices from a matching subsystem\n"
+ " -a --attr-match=FILE[=VALUE] Trigger devices with a matching attribute\n"
+ " -A --attr-nomatch=FILE[=VALUE] Exclude devices with a matching attribute\n"
+ " -p --property-match=KEY=VALUE Trigger devices with a matching property\n"
+ " -g --tag-match=TAG Trigger devices with a matching tag\n"
+ " -y --sysname-match=NAME Trigger devices with this /sys path\n"
+ " --name-match=NAME Trigger devices with this /dev name\n"
+ " -b --parent-match=NAME Trigger devices with that parent device\n"
+ " --initialized-match Trigger devices that are already initialized\n"
+ " --initialized-nomatch Trigger devices that are not initialized yet\n"
+ " -w --settle Wait for the triggered events to complete\n"
+ " --wait-daemon[=SECONDS] Wait for udevd daemon to be initialized\n"
+ " before triggering uevents\n"
+ " --uuid Print synthetic uevent UUID\n"
+ " --prioritized-subsystem=SUBSYSTEM[,SUBSYSTEM…]\n"
+ " Trigger devices from a matching subsystem first\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+int trigger_main(int argc, char *argv[], void *userdata) {
+ enum {
+ ARG_NAME = 0x100,
+ ARG_PING,
+ ARG_UUID,
+ ARG_PRIORITIZED_SUBSYSTEM,
+ ARG_INITIALIZED_MATCH,
+ ARG_INITIALIZED_NOMATCH,
+ };
+
+ static const struct option options[] = {
+ { "verbose", no_argument, NULL, 'v' },
+ { "dry-run", no_argument, NULL, 'n' },
+ { "quiet", no_argument, NULL, 'q' },
+ { "type", required_argument, NULL, 't' },
+ { "action", required_argument, NULL, 'c' },
+ { "subsystem-match", required_argument, NULL, 's' },
+ { "subsystem-nomatch", required_argument, NULL, 'S' },
+ { "attr-match", required_argument, NULL, 'a' },
+ { "attr-nomatch", required_argument, NULL, 'A' },
+ { "property-match", required_argument, NULL, 'p' },
+ { "tag-match", required_argument, NULL, 'g' },
+ { "sysname-match", required_argument, NULL, 'y' },
+ { "name-match", required_argument, NULL, ARG_NAME },
+ { "parent-match", required_argument, NULL, 'b' },
+ { "initialized-match", no_argument, NULL, ARG_INITIALIZED_MATCH },
+ { "initialized-nomatch", no_argument, NULL, ARG_INITIALIZED_NOMATCH },
+ { "settle", no_argument, NULL, 'w' },
+ { "wait-daemon", optional_argument, NULL, ARG_PING },
+ { "version", no_argument, NULL, 'V' },
+ { "help", no_argument, NULL, 'h' },
+ { "uuid", no_argument, NULL, ARG_UUID },
+ { "prioritized-subsystem", required_argument, NULL, ARG_PRIORITIZED_SUBSYSTEM },
+ {}
+ };
+ enum {
+ TYPE_DEVICES,
+ TYPE_SUBSYSTEMS,
+ TYPE_ALL,
+ } device_type = TYPE_DEVICES;
+ sd_device_action_t action = SD_DEVICE_CHANGE;
+ _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL;
+ _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *m = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_set_free_ Set *settle_path_or_ids = NULL;
+ usec_t ping_timeout_usec = 5 * USEC_PER_SEC;
+ bool ping = false;
+ int c, r;
+
+ if (running_in_chroot() > 0) {
+ log_info("Running in chroot, ignoring request.");
+ return 0;
+ }
+
+ r = sd_device_enumerator_new(&e);
+ if (r < 0)
+ return r;
+
+ r = sd_device_enumerator_allow_uninitialized(e);
+ if (r < 0)
+ return r;
+
+ while ((c = getopt_long(argc, argv, "vnqt:c:s:S:a:A:p:g:y:b:wVh", options, NULL)) >= 0) {
+ _cleanup_free_ char *buf = NULL;
+ const char *key, *val;
+
+ switch (c) {
+ case 'v':
+ arg_verbose = true;
+ break;
+ case 'n':
+ arg_dry_run = true;
+ break;
+ case 'q':
+ arg_quiet = true;
+ break;
+ case 't':
+ if (streq(optarg, "devices"))
+ device_type = TYPE_DEVICES;
+ else if (streq(optarg, "subsystems"))
+ device_type = TYPE_SUBSYSTEMS;
+ else if (streq(optarg, "all"))
+ device_type = TYPE_ALL;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown type --type=%s", optarg);
+ break;
+ case 'c':
+ r = parse_device_action(optarg, &action);
+ if (r < 0)
+ return log_error_errno(r, "Unknown action '%s'", optarg);
+ if (r == 0)
+ return 0;
+ break;
+ case 's':
+ r = sd_device_enumerator_add_match_subsystem(e, optarg, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add subsystem match '%s': %m", optarg);
+ break;
+ case 'S':
+ r = sd_device_enumerator_add_match_subsystem(e, optarg, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add negative subsystem match '%s': %m", optarg);
+ break;
+ case 'a':
+ buf = keyval(optarg, &key, &val);
+ if (!buf)
+ return log_oom();
+ r = sd_device_enumerator_add_match_sysattr(e, key, val, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add sysattr match '%s=%s': %m", key, val);
+ break;
+ case 'A':
+ buf = keyval(optarg, &key, &val);
+ if (!buf)
+ return log_oom();
+ r = sd_device_enumerator_add_match_sysattr(e, key, val, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add negative sysattr match '%s=%s': %m", key, val);
+ break;
+ case 'p':
+ buf = keyval(optarg, &key, &val);
+ if (!buf)
+ return log_oom();
+ r = sd_device_enumerator_add_match_property(e, key, val);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add property match '%s=%s': %m", key, val);
+ break;
+ case 'g':
+ r = sd_device_enumerator_add_match_tag(e, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add tag match '%s': %m", optarg);
+ break;
+ case 'y':
+ r = sd_device_enumerator_add_match_sysname(e, optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add sysname match '%s': %m", optarg);
+ break;
+ case 'b': {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+
+ r = find_device(optarg, "/sys", &dev);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open the device '%s': %m", optarg);
+
+ r = device_enumerator_add_match_parent_incremental(e, dev);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add parent match '%s': %m", optarg);
+ break;
+ }
+ case 'w':
+ arg_settle = true;
+ break;
+
+ case ARG_NAME: {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+
+ r = find_device(optarg, "/dev", &dev);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open the device '%s': %m", optarg);
+
+ r = device_enumerator_add_match_parent_incremental(e, dev);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add parent match '%s': %m", optarg);
+ break;
+ }
+
+ case ARG_PING:
+ ping = true;
+ if (optarg) {
+ r = parse_sec(optarg, &ping_timeout_usec);
+ if (r < 0)
+ log_error_errno(r, "Failed to parse timeout value '%s', ignoring: %m", optarg);
+ }
+ break;
+
+ case ARG_UUID:
+ arg_uuid = true;
+ break;
+
+ case ARG_PRIORITIZED_SUBSYSTEM: {
+ _cleanup_strv_free_ char **subsystems = NULL;
+
+ subsystems = strv_split(optarg, ",");
+ if (!subsystems)
+ return log_error_errno(r, "Failed to parse prioritized subsystem '%s': %m", optarg);
+
+ STRV_FOREACH(p, subsystems) {
+ r = device_enumerator_add_prioritized_subsystem(e, *p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add prioritized subsystem '%s': %m", *p);
+ }
+ break;
+ }
+ case ARG_INITIALIZED_MATCH:
+ case ARG_INITIALIZED_NOMATCH:
+ r = device_enumerator_add_match_is_initialized(e, c == ARG_INITIALIZED_MATCH ? MATCH_INITIALIZED_YES : MATCH_INITIALIZED_NO);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set initialized filter: %m");
+ break;
+ case 'V':
+ return print_version();
+ case 'h':
+ return help();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+ }
+
+ if (ping) {
+ _cleanup_(udev_ctrl_unrefp) UdevCtrl *uctrl = NULL;
+
+ r = udev_ctrl_new(&uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize udev control: %m");
+
+ r = udev_ctrl_send_ping(uctrl);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to udev daemon: %m");
+
+ r = udev_ctrl_wait(uctrl, ping_timeout_usec);
+ if (r < 0)
+ return log_error_errno(r, "Failed to wait for daemon to reply: %m");
+ }
+
+ for (; optind < argc; optind++) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+
+ r = find_device(argv[optind], NULL, &dev);
+ if (r < 0)
+ return log_error_errno(r, "Failed to open the device '%s': %m", argv[optind]);
+
+ r = device_enumerator_add_match_parent_incremental(e, dev);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add parent match '%s': %m", argv[optind]);
+ }
+
+ if (arg_settle) {
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get default event: %m");
+
+ r = sd_device_monitor_new(&m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create device monitor object: %m");
+
+ r = sd_device_monitor_attach_event(m, event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach event to device monitor: %m");
+
+ r = sd_device_monitor_start(m, device_monitor_handler, &settle_path_or_ids);
+ if (r < 0)
+ return log_error_errno(r, "Failed to start device monitor: %m");
+ }
+
+ switch (device_type) {
+ case TYPE_SUBSYSTEMS:
+ r = device_enumerator_scan_subsystems(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to scan subsystems: %m");
+ break;
+ case TYPE_DEVICES:
+ r = device_enumerator_scan_devices(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to scan devices: %m");
+ break;
+ case TYPE_ALL:
+ r = device_enumerator_scan_devices_and_subsystems(e);
+ if (r < 0)
+ return log_error_errno(r, "Failed to scan devices and subsystems: %m");
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ r = exec_list(e, action, arg_settle ? &settle_path_or_ids : NULL);
+ if (r < 0)
+ return r;
+
+ if (!set_isempty(settle_path_or_ids)) {
+ r = sd_event_loop(event);
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+ }
+
+ return 0;
+}
diff --git a/src/udev/udevadm-util.c b/src/udev/udevadm-util.c
new file mode 100644
index 0000000..2447eda
--- /dev/null
+++ b/src/udev/udevadm-util.c
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <errno.h>
+
+#include "alloc-util.h"
+#include "bus-error.h"
+#include "bus-util.h"
+#include "device-private.h"
+#include "path-util.h"
+#include "udevadm-util.h"
+#include "unit-name.h"
+
+static int find_device_from_unit(const char *unit_name, sd_device **ret) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *unit_path = NULL, *syspath = NULL;
+ int r;
+
+ if (!unit_name_is_valid(unit_name, UNIT_NAME_PLAIN))
+ return -EINVAL;
+
+ if (unit_name_to_type(unit_name) != UNIT_DEVICE)
+ return -EINVAL;
+
+ r = bus_connect_system_systemd(&bus);
+ if (r < 0) {
+ _cleanup_free_ char *path = NULL;
+
+ log_debug_errno(r, "Failed to open connection to systemd, using unit name as syspath: %m");
+
+ r = unit_name_to_path(unit_name, &path);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to convert \"%s\" to a device path: %m", unit_name);
+
+ return sd_device_new_from_path(ret, path);
+ }
+
+ unit_path = unit_dbus_path_from_name(unit_name);
+ if (!unit_path)
+ return -ENOMEM;
+
+ r = sd_bus_get_property_string(
+ bus,
+ "org.freedesktop.systemd1",
+ unit_path,
+ "org.freedesktop.systemd1.Device",
+ "SysFSPath",
+ &error,
+ &syspath);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to get SysFSPath= dbus property for %s: %s",
+ unit_name, bus_error_message(&error, r));
+
+ return sd_device_new_from_syspath(ret, syspath);
+}
+
+int find_device(const char *id, const char *prefix, sd_device **ret) {
+ assert(id);
+ assert(ret);
+
+ if (sd_device_new_from_path(ret, id) >= 0)
+ return 0;
+
+ if (prefix && !path_startswith(id, prefix)) {
+ _cleanup_free_ char *path = NULL;
+
+ path = path_join(prefix, id);
+ if (!path)
+ return -ENOMEM;
+
+ if (sd_device_new_from_path(ret, path) >= 0)
+ return 0;
+ }
+
+ /* if a path is provided, then it cannot be a unit name. Let's return earlier. */
+ if (is_path(id))
+ return -ENODEV;
+
+ /* Check if the argument looks like a device unit name. */
+ return find_device_from_unit(id, ret);
+}
+
+int find_device_with_action(const char *id, sd_device_action_t action, sd_device **ret) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ int r;
+
+ assert(id);
+ assert(ret);
+ assert(action >= 0 && action < _SD_DEVICE_ACTION_MAX);
+
+ r = find_device(id, "/sys", &dev);
+ if (r < 0)
+ return r;
+
+ r = device_read_uevent_file(dev);
+ if (r < 0)
+ return r;
+
+ r = device_set_action(dev, action);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(dev);
+ return 0;
+}
+
+int parse_device_action(const char *str, sd_device_action_t *action) {
+ sd_device_action_t a;
+
+ assert(str);
+ assert(action);
+
+ if (streq(str, "help")) {
+ dump_device_action_table();
+ return 0;
+ }
+
+ a = device_action_from_string(str);
+ if (a < 0)
+ return a;
+
+ *action = a;
+ return 1;
+}
diff --git a/src/udev/udevadm-util.h b/src/udev/udevadm-util.h
new file mode 100644
index 0000000..7fb4556
--- /dev/null
+++ b/src/udev/udevadm-util.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include "sd-device.h"
+
+int find_device(const char *id, const char *prefix, sd_device **ret);
+int find_device_with_action(const char *id, sd_device_action_t action, sd_device **ret);
+int parse_device_action(const char *str, sd_device_action_t *action);
diff --git a/src/udev/udevadm-verify.c b/src/udev/udevadm-verify.c
new file mode 100644
index 0000000..3220250
--- /dev/null
+++ b/src/udev/udevadm-verify.c
@@ -0,0 +1,236 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "conf-files.h"
+#include "constants.h"
+#include "log.h"
+#include "parse-argument.h"
+#include "pretty-print.h"
+#include "stat-util.h"
+#include "static-destruct.h"
+#include "strv.h"
+#include "udev-rules.h"
+#include "udevadm.h"
+
+static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY;
+static char *arg_root = NULL;
+static bool arg_summary = true;
+static bool arg_style = true;
+
+STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("udevadm", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s verify [OPTIONS] [FILE...]\n"
+ "\n%sVerify udev rules files.%s\n\n"
+ " -h --help Show this help\n"
+ " -V --version Show package version\n"
+ " -N --resolve-names=early|never When to resolve names\n"
+ " --root=PATH Operate on an alternate filesystem root\n"
+ " --no-summary Do not show summary\n"
+ " --no-style Ignore style issues\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ ansi_highlight(),
+ ansi_normal(),
+ link);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_ROOT = 0x100,
+ ARG_NO_SUMMARY,
+ ARG_NO_STYLE,
+ };
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "resolve-names", required_argument, NULL, 'N' },
+ { "root", required_argument, NULL, ARG_ROOT },
+ { "no-summary", no_argument, NULL, ARG_NO_SUMMARY },
+ { "no-style", no_argument, NULL, ARG_NO_STYLE },
+ {}
+ };
+
+ int r, c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ while ((c = getopt_long(argc, argv, "hVN:", options, NULL)) >= 0)
+ switch (c) {
+ case 'h':
+ return help();
+ case 'V':
+ return print_version();
+ case 'N':
+ arg_resolve_name_timing = resolve_name_timing_from_string(optarg);
+ if (arg_resolve_name_timing < 0)
+ return log_error_errno(arg_resolve_name_timing,
+ "--resolve-names= takes \"early\" or \"never\"");
+ /*
+ * In the verifier "late" has the effect of "never",
+ * and "never" would generate irrelevant diagnostics,
+ * so map "never" to "late".
+ */
+ if (arg_resolve_name_timing == RESOLVE_NAME_NEVER)
+ arg_resolve_name_timing = RESOLVE_NAME_LATE;
+ break;
+ case ARG_ROOT:
+ r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root);
+ if (r < 0)
+ return r;
+ break;
+ case ARG_NO_SUMMARY:
+ arg_summary = false;
+ break;
+
+ case ARG_NO_STYLE:
+ arg_style = false;
+ break;
+
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ if (arg_root && optind < argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Combination of --root= and FILEs is not supported.");
+
+ return 1;
+}
+
+static int verify_rules_file(UdevRules *rules, const char *fname) {
+ UdevRuleFile *file;
+ int r;
+
+ r = udev_rules_parse_file(rules, fname, /* extra_checks = */ true, &file);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse rules file %s: %m", fname);
+ if (r == 0) /* empty file. */
+ return 0;
+
+ unsigned issues = udev_rule_file_get_issues(file);
+ unsigned mask = (1U << LOG_ERR) | (1U << LOG_WARNING);
+ if (issues & mask)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: udev rules check failed.", fname);
+
+ if (arg_style && (issues & (1U << LOG_NOTICE)))
+ return log_warning_errno(SYNTHETIC_ERRNO(EINVAL),
+ "%s: udev rules have style issues.", fname);
+
+ return 0;
+}
+
+static int verify_rules_filelist(UdevRules *rules, char **files, size_t *fail_count, size_t *success_count, bool walk_dirs);
+
+static int verify_rules_dir(UdevRules *rules, const char *dir, size_t *fail_count, size_t *success_count) {
+ int r;
+ _cleanup_strv_free_ char **files = NULL;
+
+ assert(rules);
+ assert(dir);
+ assert(fail_count);
+ assert(success_count);
+
+ r = conf_files_list(&files, ".rules", NULL, 0, dir);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate rules files: %m");
+
+ return verify_rules_filelist(rules, files, fail_count, success_count, /* walk_dirs */ false);
+}
+
+static int verify_rules_filelist(UdevRules *rules, char **files, size_t *fail_count, size_t *success_count, bool walk_dirs) {
+ int r, rv = 0;
+
+ assert(rules);
+ assert(files);
+ assert(fail_count);
+ assert(success_count);
+
+ STRV_FOREACH(fp, files) {
+ if (walk_dirs && is_dir(*fp, /* follow = */ true) > 0)
+ r = verify_rules_dir(rules, *fp, fail_count, success_count);
+ else {
+ r = verify_rules_file(rules, *fp);
+ if (r < 0)
+ ++(*fail_count);
+ else
+ ++(*success_count);
+ }
+ if (r < 0 && rv >= 0)
+ rv = r;
+ }
+
+ return rv;
+}
+
+static int verify_rules(UdevRules *rules, char **files) {
+ size_t fail_count = 0, success_count = 0;
+ int r;
+
+ assert(rules);
+ assert(files);
+
+ r = verify_rules_filelist(rules, files, &fail_count, &success_count, /* walk_dirs */ true);
+
+ if (arg_summary)
+ printf("\n%s%zu udev rules files have been checked.%s\n"
+ " Success: %zu\n"
+ "%s Fail: %zu%s\n",
+ ansi_highlight(),
+ fail_count + success_count,
+ ansi_normal(),
+ success_count,
+ fail_count > 0 ? ansi_highlight_red() : "",
+ fail_count,
+ fail_count > 0 ? ansi_normal() : "");
+
+ return r;
+}
+
+int verify_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(udev_rules_freep) UdevRules *rules = NULL;
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ rules = udev_rules_new(arg_resolve_name_timing);
+ if (!rules)
+ return -ENOMEM;
+
+ if (optind == argc) {
+ const char* const* rules_dirs = STRV_MAKE_CONST(CONF_PATHS("udev/rules.d"));
+ _cleanup_strv_free_ char **files = NULL;
+
+ r = conf_files_list_strv(&files, ".rules", arg_root, 0, rules_dirs);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate rules files: %m");
+ if (arg_root && strv_isempty(files))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
+ "No rules files found in %s.", arg_root);
+
+ return verify_rules(rules, files);
+ }
+
+ return verify_rules(rules, strv_skip(argv, optind));
+}
diff --git a/src/udev/udevadm-wait.c b/src/udev/udevadm-wait.c
new file mode 100644
index 0000000..e6620c2
--- /dev/null
+++ b/src/udev/udevadm-wait.c
@@ -0,0 +1,456 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "chase.h"
+#include "device-monitor-private.h"
+#include "device-util.h"
+#include "errno-util.h"
+#include "event-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "inotify-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "static-destruct.h"
+#include "string-table.h"
+#include "strv.h"
+#include "udev-util.h"
+#include "udevadm.h"
+
+typedef enum WaitUntil {
+ WAIT_UNTIL_INITIALIZED,
+ WAIT_UNTIL_ADDED,
+ WAIT_UNTIL_REMOVED,
+ _WAIT_UNTIL_MAX,
+ _WAIT_UNTIL_INVALID = -EINVAL,
+} WaitUntil;
+
+static WaitUntil arg_wait_until = WAIT_UNTIL_INITIALIZED;
+static usec_t arg_timeout_usec = USEC_INFINITY;
+static bool arg_settle = false;
+static char **arg_devices = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(arg_devices, strv_freep);
+
+static const char * const wait_until_table[_WAIT_UNTIL_MAX] = {
+ [WAIT_UNTIL_INITIALIZED] = "initialized",
+ [WAIT_UNTIL_ADDED] = "added",
+ [WAIT_UNTIL_REMOVED] = "removed",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(wait_until, WaitUntil);
+
+static int check_device(const char *path) {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+ int r;
+
+ assert(path);
+
+ if (arg_wait_until == WAIT_UNTIL_REMOVED) {
+ r = laccess(path, F_OK);
+ if (r == -ENOENT)
+ return true;
+ if (r < 0)
+ return r;
+ return false;
+ }
+
+ r = sd_device_new_from_path(&dev, path);
+ if (r == -ENODEV)
+ return false;
+ if (r < 0)
+ return r;
+
+ if (arg_wait_until == WAIT_UNTIL_INITIALIZED)
+ return sd_device_get_is_initialized(dev);
+
+ return true;
+}
+
+static bool check(void) {
+ int r;
+
+ if (arg_settle) {
+ r = udev_queue_is_empty();
+ if (r == 0)
+ return false;
+ if (r < 0)
+ log_warning_errno(r, "Failed to check if udev queue is empty, assuming empty: %m");
+ }
+
+ STRV_FOREACH(p, arg_devices) {
+ r = check_device(*p);
+ if (r <= 0) {
+ if (r < 0)
+ log_warning_errno(r, "Failed to check if device \"%s\" is %s, assuming not %s: %m",
+ *p,
+ wait_until_to_string(arg_wait_until),
+ wait_until_to_string(arg_wait_until));
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static int check_and_exit(sd_event *event) {
+ int r;
+
+ assert(event);
+
+ if (check()) {
+ r = sd_event_exit(event, 0);
+ if (r < 0)
+ return r;
+
+ return 1;
+ }
+
+ return 0;
+}
+
+static int device_monitor_handler(sd_device_monitor *monitor, sd_device *device, void *userdata) {
+ const char *name;
+ int r;
+
+ assert(monitor);
+ assert(device);
+
+ if (device_for_action(device, SD_DEVICE_REMOVE) != (arg_wait_until == WAIT_UNTIL_REMOVED))
+ return 0;
+
+ if (arg_wait_until == WAIT_UNTIL_REMOVED)
+ /* On removed event, the received device may not contain enough information.
+ * Let's unconditionally check all requested devices are removed. */
+ return check_and_exit(sd_device_monitor_get_event(monitor));
+
+ /* For other events, at first check if the received device matches with the requested devices,
+ * to avoid calling check() so many times within a short time. */
+
+ r = sd_device_get_sysname(device, &name);
+ if (r < 0) {
+ log_device_warning_errno(device, r, "Failed to get sysname of received device, ignoring: %m");
+ return 0;
+ }
+
+ STRV_FOREACH(p, arg_devices) {
+ const char *s;
+
+ if (!path_startswith(*p, "/sys"))
+ continue;
+
+ r = path_find_last_component(*p, false, NULL, &s);
+ if (r < 0) {
+ log_warning_errno(r, "Failed to extract filename from \"%s\", ignoring: %m", *p);
+ continue;
+ }
+ if (r == 0)
+ continue;
+
+ if (strneq(s, name, r))
+ return check_and_exit(sd_device_monitor_get_event(monitor));
+ }
+
+ r = sd_device_get_devname(device, &name);
+ if (r < 0) {
+ if (r != -ENOENT)
+ log_device_warning_errno(device, r, "Failed to get devname of received device, ignoring: %m");
+ return 0;
+ }
+
+ if (path_strv_contains(arg_devices, name))
+ return check_and_exit(sd_device_monitor_get_event(monitor));
+
+ FOREACH_DEVICE_DEVLINK(device, link)
+ if (path_strv_contains(arg_devices, link))
+ return check_and_exit(sd_device_monitor_get_event(monitor));
+
+ return 0;
+}
+
+static int setup_monitor(sd_event *event, MonitorNetlinkGroup group, const char *description, sd_device_monitor **ret) {
+ _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL;
+ int r;
+
+ assert(event);
+ assert(ret);
+
+ r = device_monitor_new_full(&monitor, group, /* fd = */ -1);
+ if (r < 0)
+ return r;
+
+ r = sd_device_monitor_attach_event(monitor, event);
+ if (r < 0)
+ return r;
+
+ r = sd_device_monitor_set_description(monitor, description);
+ if (r < 0)
+ return r;
+
+ r = sd_device_monitor_start(monitor, device_monitor_handler, NULL);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(monitor);
+ return 0;
+}
+
+static int on_inotify(sd_event_source *s, const struct inotify_event *event, void *userdata) {
+ return check_and_exit(sd_event_source_get_event(s));
+}
+
+static int setup_inotify(sd_event *event) {
+ _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL;
+ int r;
+
+ assert(event);
+
+ if (!arg_settle)
+ return 0;
+
+ r = sd_event_add_inotify(event, &s, "/run/udev" , IN_CREATE | IN_DELETE, on_inotify, NULL);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_description(s, "inotify-event-source");
+ if (r < 0)
+ return r;
+
+ return sd_event_source_set_floating(s, true);
+}
+
+static int setup_timer(sd_event *event) {
+ _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL;
+ int r;
+
+ assert(event);
+
+ if (arg_timeout_usec == USEC_INFINITY)
+ return 0;
+
+ r = sd_event_add_time_relative(event, &s, CLOCK_BOOTTIME, arg_timeout_usec, 0,
+ NULL, INT_TO_PTR(-ETIMEDOUT));
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_description(s, "timeout-event-source");
+ if (r < 0)
+ return r;
+
+ return sd_event_source_set_floating(s, true);
+}
+
+static int reset_timer(sd_event *e, sd_event_source **s);
+
+static int on_periodic_timer(sd_event_source *s, uint64_t usec, void *userdata) {
+ static unsigned counter = 0;
+ sd_event *e;
+ int r;
+
+ assert(s);
+
+ e = sd_event_source_get_event(s);
+
+ /* Even if all devices exists, we try to wait for uevents to be emitted from kernel. */
+ if (check())
+ counter++;
+ else
+ counter = 0;
+
+ if (counter >= 2) {
+ log_debug("All requested devices popped up without receiving kernel uevents.");
+ return sd_event_exit(e, 0);
+ }
+
+ r = reset_timer(e, &s);
+ if (r < 0)
+ log_warning_errno(r, "Failed to reset periodic timer event source, ignoring: %m");
+
+ return 0;
+}
+
+static int reset_timer(sd_event *e, sd_event_source **s) {
+ return event_reset_time_relative(e, s, CLOCK_BOOTTIME, 250 * USEC_PER_MSEC, 0,
+ on_periodic_timer, NULL, 0, "periodic-timer-event-source", false);
+}
+
+static int setup_periodic_timer(sd_event *event) {
+ _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL;
+ int r;
+
+ assert(event);
+
+ r = reset_timer(event, &s);
+ if (r < 0)
+ return r;
+
+ /* Set the lower priority than device monitor, to make uevents always dispatched first. */
+ r = sd_event_source_set_priority(s, SD_EVENT_PRIORITY_NORMAL + 1);
+ if (r < 0)
+ return r;
+
+ return sd_event_source_set_floating(s, true);
+}
+
+static int help(void) {
+ printf("%s wait [OPTIONS] DEVICE [DEVICE…]\n\n"
+ "Wait for devices or device symlinks being created.\n\n"
+ " -h --help Print this message\n"
+ " -V --version Print version of the program\n"
+ " -t --timeout=SEC Maximum time to wait for the device\n"
+ " --initialized=BOOL Wait for devices being initialized by systemd-udevd\n"
+ " --removed Wait for devices being removed\n"
+ " --settle Also wait for all queued events being processed\n",
+ program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_INITIALIZED = 0x100,
+ ARG_REMOVED,
+ ARG_SETTLE,
+ };
+
+ static const struct option options[] = {
+ { "timeout", required_argument, NULL, 't' },
+ { "initialized", required_argument, NULL, ARG_INITIALIZED },
+ { "removed", no_argument, NULL, ARG_REMOVED },
+ { "settle", no_argument, NULL, ARG_SETTLE },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ {}
+ };
+
+ int c, r;
+
+ while ((c = getopt_long(argc, argv, "t:hV", options, NULL)) >= 0)
+ switch (c) {
+ case 't':
+ r = parse_sec(optarg, &arg_timeout_usec);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse -t/--timeout= parameter: %s", optarg);
+ break;
+
+ case ARG_INITIALIZED:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --initialized= parameter: %s", optarg);
+ arg_wait_until = r ? WAIT_UNTIL_INITIALIZED : WAIT_UNTIL_ADDED;
+ break;
+
+ case ARG_REMOVED:
+ arg_wait_until = WAIT_UNTIL_REMOVED;
+ break;
+
+ case ARG_SETTLE:
+ arg_settle = true;
+ break;
+
+ case 'V':
+ return print_version();
+
+ case 'h':
+ return help();
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (optind >= argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Too few arguments, expected at least one device path or device symlink.");
+
+ arg_devices = strv_copy(argv + optind);
+ if (!arg_devices)
+ return log_oom();
+
+ return 1; /* work to do */
+}
+
+int wait_main(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *udev_monitor = NULL, *kernel_monitor = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ STRV_FOREACH(p, arg_devices) {
+ path_simplify(*p);
+
+ if (!path_is_safe(*p))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Device path cannot contain \"..\".");
+
+ if (!is_device_path(*p))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Specified path \"%s\" does not start with \"/dev/\" or \"/sys/\".", *p);
+ }
+
+ /* Check before configuring event sources, as devices may be already initialized. */
+ if (check())
+ return 0;
+
+ r = sd_event_default(&event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to initialize sd-event: %m");
+
+ r = setup_timer(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up timeout: %m");
+
+ r = setup_inotify(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up inotify: %m");
+
+ r = setup_monitor(event, MONITOR_GROUP_UDEV, "udev-uevent-monitor-event-source", &udev_monitor);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up udev uevent monitor: %m");
+
+ if (arg_wait_until == WAIT_UNTIL_ADDED) {
+ /* If --initialized=no is specified, it is not necessary to wait uevents for the specified
+ * devices to be processed by udevd. Hence, let's listen on the kernel's uevent stream. Then,
+ * we may be able to finish this program earlier when udevd is very busy.
+ * Note, we still need to also setup udev monitor, as this may be invoked with a devlink
+ * (e.g. /dev/disk/by-id/foo). In that case, the devlink may not exist when we received a
+ * uevent from kernel, as the udevd may not finish to process the uevent yet. Hence, we need
+ * to wait until the event is processed by udevd. */
+ r = setup_monitor(event, MONITOR_GROUP_KERNEL, "kernel-uevent-monitor-event-source", &kernel_monitor);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up kernel uevent monitor: %m");
+
+ /* This is a workaround for issues #24360 and #24450.
+ * For some reasons, the kernel sometimes does not emit uevents for loop block device on
+ * attach. Hence, without the periodic timer, no event source for this program will be
+ * triggered, and this will be timed out.
+ * Theoretically, inotify watch may be better, but this program typically expected to run in
+ * a short time. Hence, let's use the simpler periodic timer event source here. */
+ r = setup_periodic_timer(event);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set up periodic timer: %m");
+ }
+
+ /* Check before entering the event loop, as devices may be initialized during setting up event sources. */
+ if (check())
+ return 0;
+
+ r = sd_event_loop(event);
+ if (r == -ETIMEDOUT)
+ return log_error_errno(r, "Timed out for waiting devices being %s.",
+ wait_until_to_string(arg_wait_until));
+ if (r < 0)
+ return log_error_errno(r, "Event loop failed: %m");
+
+ return 0;
+}
diff --git a/src/udev/udevadm.c b/src/udev/udevadm.c
new file mode 100644
index 0000000..687b927
--- /dev/null
+++ b/src/udev/udevadm.c
@@ -0,0 +1,140 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+#include <stddef.h>
+#include <stdio.h>
+
+#include "alloc-util.h"
+#include "main-func.h"
+#include "pretty-print.h"
+#include "process-util.h"
+#include "selinux-util.h"
+#include "string-util.h"
+#include "udev-util.h"
+#include "udevadm.h"
+#include "udevd.h"
+#include "verbs.h"
+
+static int help(void) {
+ static const char *const short_descriptions[][2] = {
+ { "info", "Query sysfs or the udev database" },
+ { "trigger", "Request events from the kernel" },
+ { "settle", "Wait for pending udev events" },
+ { "control", "Control the udev daemon" },
+ { "monitor", "Listen to kernel and udev events" },
+ { "test", "Test an event run" },
+ { "test-builtin", "Test a built-in command" },
+ { "verify", "Verify udev rules files" },
+ { "wait", "Wait for device or device symlink" },
+ { "lock", "Lock a block device" },
+ };
+
+ _cleanup_free_ char *link = NULL;
+ size_t i;
+ int r;
+
+ r = terminal_urlify_man("udevadm", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [--help] [--version] [--debug] COMMAND [COMMAND OPTIONS]\n\n"
+ "Send control commands or test the device manager.\n\n"
+ "Commands:\n",
+ program_invocation_short_name);
+
+ for (i = 0; i < ELEMENTSOF(short_descriptions); i++)
+ printf(" %-12s %s\n", short_descriptions[i][0], short_descriptions[i][1]);
+
+ printf("\nSee the %s for details.\n", link);
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "debug", no_argument, NULL, 'd' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ {}
+ };
+ int c;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
+ * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
+ optind = 0;
+ while ((c = getopt_long(argc, argv, "+dhV", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'd':
+ log_set_max_level(LOG_DEBUG);
+ break;
+
+ case 'h':
+ return help();
+
+ case 'V':
+ return print_version();
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 1; /* work to do */
+}
+
+static int version_main(int argc, char *argv[], void *userdata) {
+ return print_version();
+}
+
+static int help_main(int argc, char *argv[], void *userdata) {
+ return help();
+}
+
+static int udevadm_main(int argc, char *argv[]) {
+ static const Verb verbs[] = {
+ { "info", VERB_ANY, VERB_ANY, 0, info_main },
+ { "trigger", VERB_ANY, VERB_ANY, 0, trigger_main },
+ { "settle", VERB_ANY, VERB_ANY, 0, settle_main },
+ { "control", VERB_ANY, VERB_ANY, 0, control_main },
+ { "monitor", VERB_ANY, VERB_ANY, 0, monitor_main },
+ { "hwdb", VERB_ANY, VERB_ANY, 0, hwdb_main },
+ { "test", VERB_ANY, VERB_ANY, 0, test_main },
+ { "test-builtin", VERB_ANY, VERB_ANY, 0, builtin_main },
+ { "wait", VERB_ANY, VERB_ANY, 0, wait_main },
+ { "lock", VERB_ANY, VERB_ANY, 0, lock_main },
+ { "verify", VERB_ANY, VERB_ANY, 0, verify_main },
+ { "version", VERB_ANY, VERB_ANY, 0, version_main },
+ { "help", VERB_ANY, VERB_ANY, 0, help_main },
+ {}
+ };
+
+ return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ if (invoked_as(argv, "udevd"))
+ return run_udevd(argc, argv);
+
+ udev_parse_config();
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ r = mac_init();
+ if (r < 0)
+ return r;
+
+ return udevadm_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/udev/udevadm.h b/src/udev/udevadm.h
new file mode 100644
index 0000000..7920a70
--- /dev/null
+++ b/src/udev/udevadm.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#include <stdio.h>
+
+#include "macro.h"
+
+int info_main(int argc, char *argv[], void *userdata);
+int trigger_main(int argc, char *argv[], void *userdata);
+int settle_main(int argc, char *argv[], void *userdata);
+int control_main(int argc, char *argv[], void *userdata);
+int monitor_main(int argc, char *argv[], void *userdata);
+int hwdb_main(int argc, char *argv[], void *userdata);
+int test_main(int argc, char *argv[], void *userdata);
+int builtin_main(int argc, char *argv[], void *userdata);
+int verify_main(int argc, char *argv[], void *userdata);
+int wait_main(int argc, char *argv[], void *userdata);
+int lock_main(int argc, char *argv[], void *userdata);
+
+static inline int print_version(void) {
+ /* Dracut relies on the version being a single integer */
+ puts(STRINGIFY(PROJECT_VERSION));
+ return 0;
+}
diff --git a/src/udev/udevd.c b/src/udev/udevd.c
new file mode 100644
index 0000000..2ed4282
--- /dev/null
+++ b/src/udev/udevd.c
@@ -0,0 +1,408 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright © 2004 Chris Friesen <chris_friesen@sympatico.ca>
+ * Copyright © 2009 Canonical Ltd.
+ * Copyright © 2009 Scott James Remnant <scott@netsplit.com>
+ */
+
+#include <getopt.h>
+#include <unistd.h>
+
+#include "sd-daemon.h"
+
+#include "env-file.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "mkdir.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "proc-cmdline.h"
+#include "process-util.h"
+#include "rlimit-util.h"
+#include "selinux-util.h"
+#include "signal-util.h"
+#include "syslog-util.h"
+#include "udev-manager.h"
+#include "udev-util.h"
+#include "udevd.h"
+#include "version.h"
+
+static bool arg_debug = false;
+static int arg_daemonize = false;
+
+static int listen_fds(int *ret_ctrl, int *ret_netlink) {
+ int ctrl_fd = -EBADF, netlink_fd = -EBADF;
+ int fd, n;
+
+ assert(ret_ctrl);
+ assert(ret_netlink);
+
+ n = sd_listen_fds(true);
+ if (n < 0)
+ return n;
+
+ for (fd = SD_LISTEN_FDS_START; fd < n + SD_LISTEN_FDS_START; fd++) {
+ if (sd_is_socket(fd, AF_UNIX, SOCK_SEQPACKET, -1) > 0) {
+ if (ctrl_fd >= 0)
+ return -EINVAL;
+ ctrl_fd = fd;
+ continue;
+ }
+
+ if (sd_is_socket(fd, AF_NETLINK, SOCK_RAW, -1) > 0) {
+ if (netlink_fd >= 0)
+ return -EINVAL;
+ netlink_fd = fd;
+ continue;
+ }
+
+ return -EINVAL;
+ }
+
+ *ret_ctrl = ctrl_fd;
+ *ret_netlink = netlink_fd;
+
+ return 0;
+}
+
+static int manager_parse_udev_config(Manager *manager) {
+ _cleanup_free_ char *log_val = NULL, *children_max = NULL, *exec_delay = NULL,
+ *event_timeout = NULL, *resolve_names = NULL, *timeout_signal = NULL;
+ int r;
+
+ assert(manager);
+
+ r = parse_env_file(NULL, "/etc/udev/udev.conf",
+ "udev_log", &log_val,
+ "children_max", &children_max,
+ "exec_delay", &exec_delay,
+ "event_timeout", &event_timeout,
+ "resolve_names", &resolve_names,
+ "timeout_signal", &timeout_signal);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return r;
+
+ r = udev_set_max_log_level(log_val);
+ if (r < 0)
+ log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r,
+ "Failed to set udev log level '%s', ignoring: %m", log_val);
+
+ if (children_max) {
+ r = safe_atou(children_max, &manager->children_max);
+ if (r < 0)
+ log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r,
+ "Failed to parse children_max=%s, ignoring: %m", children_max);
+ }
+
+ if (exec_delay) {
+ r = parse_sec(exec_delay, &manager->exec_delay_usec);
+ if (r < 0)
+ log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r,
+ "Failed to parse exec_delay=%s, ignoring: %m", exec_delay);
+ }
+
+ if (event_timeout) {
+ r = parse_sec(event_timeout, &manager->timeout_usec);
+ if (r < 0)
+ log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r,
+ "Failed to parse event_timeout=%s, ignoring: %m", event_timeout);
+ }
+
+ if (resolve_names) {
+ ResolveNameTiming t;
+
+ t = resolve_name_timing_from_string(resolve_names);
+ if (t < 0)
+ log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r,
+ "Failed to parse resolve_names=%s, ignoring.", resolve_names);
+ else
+ manager->resolve_name_timing = t;
+ }
+
+ if (timeout_signal) {
+ r = signal_from_string(timeout_signal);
+ if (r < 0)
+ log_syntax(NULL, LOG_WARNING, "/etc/udev/udev.conf", 0, r,
+ "Failed to parse timeout_signal=%s, ignoring: %m", timeout_signal);
+ else
+ manager->timeout_signal = r;
+ }
+
+ return 0;
+}
+
+/*
+ * read the kernel command line, in case we need to get into debug mode
+ * udev.log_level=<level> syslog priority
+ * udev.children_max=<number of workers> events are fully serialized if set to 1
+ * udev.exec_delay=<number of seconds> delay execution of every executed program
+ * udev.event_timeout=<number of seconds> seconds to wait before terminating an event
+ * udev.blockdev_read_only<=bool> mark all block devices read-only when they appear
+ */
+static int parse_proc_cmdline_item(const char *key, const char *value, void *data) {
+ Manager *manager = ASSERT_PTR(data);
+ int r;
+
+ assert(key);
+
+ if (proc_cmdline_key_streq(key, "udev.log_level") ||
+ proc_cmdline_key_streq(key, "udev.log_priority")) { /* kept for backward compatibility */
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ r = log_level_from_string(value);
+ if (r >= 0)
+ log_set_max_level(r);
+
+ } else if (proc_cmdline_key_streq(key, "udev.event_timeout")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ r = parse_sec(value, &manager->timeout_usec);
+
+ } else if (proc_cmdline_key_streq(key, "udev.children_max")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ r = safe_atou(value, &manager->children_max);
+
+ } else if (proc_cmdline_key_streq(key, "udev.exec_delay")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ r = parse_sec(value, &manager->exec_delay_usec);
+
+ } else if (proc_cmdline_key_streq(key, "udev.timeout_signal")) {
+
+ if (proc_cmdline_value_missing(key, value))
+ return 0;
+
+ r = signal_from_string(value);
+ if (r > 0)
+ manager->timeout_signal = r;
+
+ } else if (proc_cmdline_key_streq(key, "udev.blockdev_read_only")) {
+
+ if (!value)
+ manager->blockdev_read_only = true;
+ else {
+ r = parse_boolean(value);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse udev.blockdev-read-only argument, ignoring: %s", value);
+ else
+ manager->blockdev_read_only = r;
+ }
+
+ if (manager->blockdev_read_only)
+ log_notice("All physical block devices will be marked read-only.");
+
+ return 0;
+
+ } else {
+ if (startswith(key, "udev."))
+ log_warning("Unknown udev kernel command line option \"%s\", ignoring.", key);
+
+ return 0;
+ }
+
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse \"%s=%s\", ignoring: %m", key, value);
+
+ return 0;
+}
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("systemd-udevd.service", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%s [OPTIONS...]\n\n"
+ "Rule-based manager for device events and files.\n\n"
+ " -h --help Print this message\n"
+ " -V --version Print version of the program\n"
+ " -d --daemon Detach and run in the background\n"
+ " -D --debug Enable debug output\n"
+ " -c --children-max=INT Set maximum number of workers\n"
+ " -e --exec-delay=SECONDS Seconds to wait before executing RUN=\n"
+ " -t --event-timeout=SECONDS Seconds to wait before terminating an event\n"
+ " -N --resolve-names=early|late|never\n"
+ " When to resolve users and groups\n"
+ "\nSee the %s for details.\n",
+ program_invocation_short_name,
+ link);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[], Manager *manager) {
+ enum {
+ ARG_TIMEOUT_SIGNAL,
+ };
+
+ static const struct option options[] = {
+ { "daemon", no_argument, NULL, 'd' },
+ { "debug", no_argument, NULL, 'D' },
+ { "children-max", required_argument, NULL, 'c' },
+ { "exec-delay", required_argument, NULL, 'e' },
+ { "event-timeout", required_argument, NULL, 't' },
+ { "resolve-names", required_argument, NULL, 'N' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "timeout-signal", required_argument, NULL, ARG_TIMEOUT_SIGNAL },
+ {}
+ };
+
+ int c, r;
+
+ assert(argc >= 0);
+ assert(argv);
+ assert(manager);
+
+ while ((c = getopt_long(argc, argv, "c:de:Dt:N:hV", options, NULL)) >= 0) {
+ switch (c) {
+
+ case 'd':
+ arg_daemonize = true;
+ break;
+ case 'c':
+ r = safe_atou(optarg, &manager->children_max);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse --children-max= value '%s', ignoring: %m", optarg);
+ break;
+ case 'e':
+ r = parse_sec(optarg, &manager->exec_delay_usec);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse --exec-delay= value '%s', ignoring: %m", optarg);
+ break;
+ case ARG_TIMEOUT_SIGNAL:
+ r = signal_from_string(optarg);
+ if (r <= 0)
+ log_warning_errno(r, "Failed to parse --timeout-signal= value '%s', ignoring: %m", optarg);
+ else
+ manager->timeout_signal = r;
+
+ break;
+ case 't':
+ r = parse_sec(optarg, &manager->timeout_usec);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse --event-timeout= value '%s', ignoring: %m", optarg);
+ break;
+ case 'D':
+ arg_debug = true;
+ break;
+ case 'N': {
+ ResolveNameTiming t;
+
+ t = resolve_name_timing_from_string(optarg);
+ if (t < 0)
+ log_warning("Invalid --resolve-names= value '%s', ignoring.", optarg);
+ else
+ manager->resolve_name_timing = t;
+ break;
+ }
+ case 'h':
+ return help();
+ case 'V':
+ printf("%s\n", GIT_VERSION);
+ return 0;
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+
+ }
+ }
+
+ return 1;
+}
+
+int run_udevd(int argc, char *argv[]) {
+ _cleanup_(manager_freep) Manager *manager = NULL;
+ int fd_ctrl = -EBADF, fd_uevent = -EBADF;
+ int r;
+
+ log_set_target(LOG_TARGET_AUTO);
+ log_open();
+
+ manager = manager_new();
+ if (!manager)
+ return log_oom();
+
+ manager_parse_udev_config(manager);
+
+ log_parse_environment();
+ log_open(); /* Done again to update after reading configuration. */
+
+ r = parse_argv(argc, argv, manager);
+ if (r <= 0)
+ return r;
+
+ r = proc_cmdline_parse(parse_proc_cmdline_item, manager, PROC_CMDLINE_STRIP_RD_PREFIX);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m");
+
+ if (arg_debug) {
+ log_set_target(LOG_TARGET_CONSOLE);
+ log_set_max_level(LOG_DEBUG);
+ }
+
+ r = must_be_root();
+ if (r < 0)
+ return r;
+
+ /* set umask before creating any file/directory */
+ umask(022);
+
+ r = mac_init();
+ if (r < 0)
+ return r;
+
+ /* Make sure we can have plenty fds (for example for pidfds) */
+ (void) rlimit_nofile_bump(-1);
+
+ r = RET_NERRNO(mkdir("/run/udev", 0755));
+ if (r < 0 && r != -EEXIST)
+ return log_error_errno(r, "Failed to create /run/udev: %m");
+
+ r = listen_fds(&fd_ctrl, &fd_uevent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to listen on fds: %m");
+
+ r = manager_init(manager, fd_ctrl, fd_uevent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create manager: %m");
+
+ if (arg_daemonize) {
+ pid_t pid;
+
+ log_info("Starting systemd-udevd version " GIT_VERSION);
+
+ /* connect /dev/null to stdin, stdout, stderr */
+ if (log_get_max_level() < LOG_DEBUG) {
+ r = make_null_stdio();
+ if (r < 0)
+ log_warning_errno(r, "Failed to redirect standard streams to /dev/null: %m");
+ }
+
+ pid = fork();
+ if (pid < 0)
+ return log_error_errno(errno, "Failed to fork daemon: %m");
+ if (pid > 0)
+ /* parent */
+ return 0;
+
+ /* child */
+ (void) setsid();
+ }
+
+ return manager_main(manager);
+}
diff --git a/src/udev/udevd.h b/src/udev/udevd.h
new file mode 100644
index 0000000..583e895
--- /dev/null
+++ b/src/udev/udevd.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+int run_udevd(int argc, char *argv[]);
diff --git a/src/udev/v4l_id/v4l_id.c b/src/udev/v4l_id/v4l_id.c
new file mode 100644
index 0000000..30527e9
--- /dev/null
+++ b/src/udev/v4l_id/v4l_id.c
@@ -0,0 +1,113 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (c) 2009 Filippo Argiolas <filippo.argiolas@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details:
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <linux/videodev2.h>
+
+#include "build.h"
+#include "fd-util.h"
+#include "main-func.h"
+
+static const char *arg_device = NULL;
+
+static int parse_argv(int argc, char *argv[]) {
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'v' },
+ {}
+ };
+ int c;
+
+ while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+ switch (c) {
+ case 'h':
+ printf("%s [OPTIONS...] DEVICE\n\n"
+ "Video4Linux device identification.\n\n"
+ " -h --help Show this help text\n"
+ " --version Show package version\n",
+ program_invocation_short_name);
+ return 0;
+ case 'v':
+ return version();
+ case '?':
+ return -EINVAL;
+ default:
+ assert_not_reached();
+ }
+
+ if (!argv[optind])
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "DEVICE argument missing.");
+
+ arg_device = argv[optind];
+ return 1;
+}
+
+static int run(int argc, char *argv[]) {
+ _cleanup_close_ int fd = -EBADF;
+ struct v4l2_capability v2cap;
+ int r;
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ fd = open(arg_device, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0)
+ return log_error_errno(errno, "Failed to open %s: %m", arg_device);
+
+ if (ioctl(fd, VIDIOC_QUERYCAP, &v2cap) == 0) {
+ int capabilities;
+
+ printf("ID_V4L_VERSION=2\n");
+ printf("ID_V4L_PRODUCT=%s\n", v2cap.card);
+ printf("ID_V4L_CAPABILITIES=:");
+
+ if (v2cap.capabilities & V4L2_CAP_DEVICE_CAPS)
+ capabilities = v2cap.device_caps;
+ else
+ capabilities = v2cap.capabilities;
+
+ if ((capabilities & V4L2_CAP_VIDEO_CAPTURE) > 0 ||
+ (capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) > 0)
+ printf("capture:");
+ if ((capabilities & V4L2_CAP_VIDEO_OUTPUT) > 0 ||
+ (capabilities & V4L2_CAP_VIDEO_OUTPUT_MPLANE) > 0)
+ printf("video_output:");
+ if ((capabilities & V4L2_CAP_VIDEO_OVERLAY) > 0)
+ printf("video_overlay:");
+ if ((capabilities & V4L2_CAP_AUDIO) > 0)
+ printf("audio:");
+ if ((capabilities & V4L2_CAP_TUNER) > 0)
+ printf("tuner:");
+ if ((capabilities & V4L2_CAP_RADIO) > 0)
+ printf("radio:");
+ printf("\n");
+ }
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);